This will need some further work on your part to integrate into your code but should give you some ideas. The first step in bootloader will be to include headers related to programming of the FLASH:
#include <avr/boot.h>
#include <avr/pgmspace.h>
A few other defines that help define things later and define some response codes at the end of processing each line are:
#define HEX2DEC(x) (((x < 'A') ? ((x) - 48) : ((x) - 55)))
#define SPM_PAGEMASK ((uint32_t) ~(SPM_PAGESIZE - 1))
enum response_t {RSP_OK, RSP_CHECKSUM_FAIL, RSP_INVALID, RSP_FINISHED};
Then a routine like the following can be used to process the Intel hex format and write to FLASH. It is written for a larger device so handles extended addresses in the hex files so you can remove that for your smaller device to reduce the code size but it won't do any harm to leave in place. You'll also need to add your own UART receive / init code and determine what to do if things time out, I used a watchdog timer in this case.
enum response_t process_line()
{
char c, line_buffer[128], data_buffer[64];
uint8_t line_len = 0, data_len = 0, data_count, line_type, line_pos, data;
uint8_t addrh, addrl, checksum, recv_checksum;
uint16_t addr, extended_addr = 0, i;
static uint32_t full_addr, last_addr = 0xFFFFFFFF;
c = uart_getc();
while (c != '\r')
{
if (c == ':')
line_len = 0;
else if (c == '\n')
;
else if (c == '\0')
;
else if (line_len < sizeof(line_buffer))
line_buffer[line_len++] = c;
c = uart_getc();
}
if (line_len < 2)
return RSP_INVALID;
data_count = (HEX2DEC(line_buffer[0]) << 4) + HEX2DEC(line_buffer[1]);
if (line_len != data_count * 2 + 10)
return RSP_INVALID;
addrh = (HEX2DEC(line_buffer[2]) << 4) + HEX2DEC(line_buffer[3]);
addrl = (HEX2DEC(line_buffer[4]) << 4) + HEX2DEC(line_buffer[5]);
addr = (addrh << 8) + addrl;
line_type = (HEX2DEC(line_buffer[6]) << 4) + HEX2DEC(line_buffer[7]);
line_pos = 8;
checksum = data_count + addrh + addrl + line_type;
for (i=0; i < data_count; i++)
{
data = (HEX2DEC(line_buffer[line_pos]) << 4) + HEX2DEC(line_buffer[line_pos + 1]);
line_pos += 2;
data_buffer[data_len++] = data;
checksum += data;
}
checksum = 0xFF - checksum + 1;
recv_checksum = (HEX2DEC(line_buffer[line_pos]) << 4) + HEX2DEC(line_buffer[line_pos + 1]);
if (checksum != recv_checksum)
return RSP_CHECKSUM_FAIL;
if (line_type == 1)
{
if (last_addr != 0xFFFFFFFF)
{
boot_page_write (last_addr & SPM_PAGEMASK);
boot_spm_busy_wait();
}
return RSP_FINISHED;
}
else if ((line_type == 2) || (line_type == 4))
extended_addr = (data_buffer[0] << 8) + data_buffer[1];
else if (line_type == 0)
{
full_addr = ((uint32_t) extended_addr << 16) + addr;
if ((full_addr & SPM_PAGEMASK) != (last_addr & SPM_PAGEMASK))
{
if (last_addr != 0xFFFFFFFF)
{
boot_page_write (last_addr & SPM_PAGEMASK);
boot_spm_busy_wait();
}
boot_page_erase (full_addr);
boot_spm_busy_wait ();
}
for (i=0; i < data_len; i+=2)
{
uint16_t w = data_buffer[i] + ((uint16_t) data_buffer[i + 1] << 8);
boot_page_fill (full_addr + i, w);
}
last_addr = full_addr;
}
return RSP_OK;
}
It reads an entire hex line and verifies the checksum so check it against the Intel hex format to see how it works in detail before using. From there the general idea is that when it hits a new FLASH page boundary it does an erase but otherwise goes ahead and writes the data.
When the bootloader is entered this is the main code I was using to call it, but once again you'll need to work out what do to when a failure occurs. In my case I was using some custom PC software so sent back a few strings to indicate the status, but for a terminal upload you may just want to set an EEPROM flag depending on whether it was successful and you should boot to it or to show a human friendly message and call the bootloader again:
void enter_loader()
{
enum response_t response = RSP_OK;
uart_puts("+OK/\r\n");
while (response != RSP_FINISHED)
{
response = process_line();
if (response == RSP_OK)
uart_puts("+OK/");
else if (response != RSP_FINISHED)
uart_puts("+FAIL/");
wdt_reset();
}
boot_rww_enable ();
while (1) // Use watchdog to reboot
;
}
Anyway that should give you a good start and the above code has been pretty well tested with some fairly complex firmware. You'll just need a bit of work to write the missing UART and other initialisation code to determine when the bootloader should be called for your application. It may also need some tweaking for a ATmega32A but I think the approach should work pretty well across AVR devices.
The way we do it in one of our products is like this (code is based on IAR compiler, so pragmas might differ for you):
We defined a structure which contains important information of the application part (which is called mainpart in our projects):
#pragma pack(push,1)
struct softwareRevision_t {
uint8_t VersionSystem;
uint8_t VersionFunction;
uint8_t VersionError;
uint8_t VersionCustomer;
uint8_t BuildNumber;
};
#pragma pack(pop)
#pragma pack(push,1)
struct mainInfoStruct_t {
softwareRevision_t softwareRevision;
uint32_t startFunctionAddress;
uint8_t reserve[27];
uint32_t bootControl;
};
#pragma pack(pop)
This contains among other things the uint32_t startFunctionAddress
. Now for this structure to be useful, you have to do several things:
- Initialize it with meaningful values in the application
- Make it
const
so it can be placed in flash
- Tell the linker to place the structure always on the exact same location every time you compile your project
- share the location and structure between application and boot part
In the application part:
To share the location, we created a header-file which is included in both parts with a simple define:
#define MAIN_INFO_MEMORY_POSITION (0x0807FFD4)
In a .cpp
file of our application we create an instance of the structure which is placed accordingly:
#pragma location = MAIN_INFO_MEMORY_POSITION
extern __root const mainInfoStruct_t mainInfoStruct =
{
{
1U, // mainInfo.softwareRevision.VersionSystem
0U, // mainInfo.softwareRevision.VersionFunction
0U, // mainInfo.softwareRevision.VersionError
0U, // mainInfo.softwareRevision.VersionCustomer
0U // mainInfo.softwareRevision.BuildNumber
},
reinterpret_cast<uint32_t>((&_start)), // mainInfo.startFunctionAddress
{0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U,0U}, // reserve
0xF0F0FFFFU // mainInfo.bootControl
};
Several things happen here:
#pragma location = MAIN_INFO_MEMORY_POSITION
tells the linker where to place the next structure.
The __root
tells our linker that this symbol must be kept, even if it is not referenced by the application code (which it isn't). It has to be const
otherwise it would not get placed in flash (if it doesn't end up in flash try adding a static
).
So with this we have done everything in the application.
In the boot part:
The boot part needs access to the application part info structure. We wrapped a class around this whole thing, but basically a pointer will be enough:
volatile mainInfoStruct_t* pMainInfo = reinterpret_cast<mainInfoStruct_t*>(MAIN_INFO_MEMORY_POSITION);
We use volatile
here to make sure, that the compiler will not optimize the access to the flash away. With this you can now jump into the application code with an ugly cast:
(reinterpret_cast<void (*)(void)>(pMainInfo->startFunctionAddress))();
Notes:
In our application we do a CRC check over the whole application part which includes the mainInfoStruct, so we don't jump into nirvana is something went wrong with the update.
The first thing we do in our application is to set the stackpointer to the value it is expected to start with (you can get that from the linker configuration file or your interrupt vector table). Otherwise you might end up with a stack overflow and corrupting your other data.
This is not a minimal example, but might give some hint on what might be useful in a shared structure as well.
We use C++, sorry for the reinterpret_cast
s.
Best Answer
You do not need to relocate the vector table--the toolchain should correctly locate your vector table in your binary--but you do need to tell the MCU where the correct vector table is located, as well as where the stack pointer should start, when you jump from bootloader to application. By default, on ARM cores, the vector table is found at the very beginning of your application binary. The first entry will be the initial stack pointer, and the second entry will be the reset vector (the application entry point). The rest of the entries are defined by the specific ARM architecture as well as the specific implementation.
At startup or a hardware reset, the hardware will initialize the Vector Table Offset Register to 0x00000000, set the stack pointer to the first value in the vector table, and then jump to the location given in the second entry in the table. But when you jump from a bootloader to the application, you have to do each of those things yourself.