Writing A Bootloader





5.00/5 (3 votes)
In this article, I’ll explain what a bootloader is, and how pbldr currently works.
I’ve been working on a simple bootloader for PIC18F devices called pbldr. In this article, I’ll explain what a bootloader is, and how pbldr currently works. Note that this code is experimental, and is intended to show how a bootloader works.
What is a Bootloader?
The simplest bootloader just runs a program: it could be as simple as a single jump instruction that jumps to the program. In embedded systems, bootloaders usually provide a method of flashing new code to the device and initialize the hardware before running the main program. One example of this is the Arduino bootloader, which loads code from the Arduino IDE to the ATmega microcontroller over asynchronous serial (UART). Bootloaders can include other features, such as code decryption and power-on tests of the device.
Bootloaders allow code to be flashed to a microcontroller without specific programming hardware. This allows end-users to upgrade firmware without needing special hardware. It can also simplify firmware updates for installed devices that are difficult to physically connect to. For example, an automotive controller might use Controller Area Network to load new code.
How Does It Work?
A bootloader runs immediately after the device is powered on. It first checks if the user is trying to load new code. If so, it receives the code and loads it into program memory at a specific memory location. Otherwise, it jumps to the start of the user’s program, which has already been loaded at that specific memory location.
Let's look at pbldr, which is currently a (very) minimal example of a bootloader. The full source is available from github.
UART
The UART1
port is used to load programs. There are a few functions that deal with UART
:
/********************
UART 1 Functions
********************/
// initializes UART1 at specified baud rate
void UART1Init(long baud){
RCSTA1bits.SPEN = 1; // enable port
TRISCbits.TRISC7 = 1; // make rx pin an input
RCSTA1bits.CREN = 1; // enable receive
TRISCbits.TRISC6 = 0; // make tx pin an output
TXSTA1bits.TXEN = 1; // enable transmit
TXSTA1bits.SYNC = 0; // use async serial
TXSTA1bits.BRGH = 1; // high speed mode
BAUDCON1bits.BRG16 = 1; // use 16 bit baud rate generator
SPBRG1 = (FCY/baud/4)-1; // set baud rate generator
return;
}
// writes a byte to UART1
void UART1TxByte(char byte)
{
while (!TXSTA1bits.TRMT); // wait until buffer is empty
TXREG1 = byte; // write the byte
return;
}
// reads a byte from UART 1
char UART1RxByte(unsigned int timeout)
{
while (!PIR1bits.RC1IF && timeout > 0) // wait for data to be available
timeout--;
return RCREG1; // return data byte
}
// writes a string from ROM to UART1
void UART1TxROMString(const rom char *str)
{
int i = 0;
while(str[i] != 0){
UART1TxByte(str[i]);
i++;
}
return;
}
// writes a string from RAM to UART1
void UART1TxString(char *str)
{
int i = 0;
while(str[i] != 0){
UART1TxByte(str[i]);
i++;
}
return;
}
These functions are fairly straight forward, and can be fully understood by reading the device datasheet section on the EUSART peripheral. However, it is worth noting the difference between UART1TxString
and UART1TxROMString
. The Microchip C18 compiler will assume variables are in RAM unless the ‘rom
’ keyword is used. The UART1TxString
function will only work when passed a pointer to a string
, and the UART1TxROMString
will only work when passed a string
stored in ROM (including string
literals). In other words, the function calls:
char *str = "test";
UART1TxString(str);
and:
UART1TxROMString("test");
are equivalent.
Program Memory
The next set of functions handles writing to flash memory. In this case, it is used to write to program memory. Program memory must be erased before being written to, and it can only be addressed in 64 byte blocks when erasing and writing.
/************************
Program Memory Functions
*************************/
void FlashErase(long addr)
{
TBLPTR = addr;
EECON1bits.EEPGD = 1; // select program memory
EECON1bits.CFGS = 0; // enable program memory access
EECON1bits.WREN = 1; // enable write access
EECON1bits.FREE = 1; // enable the erase
INTCONbits.GIE = 0; // disable interrupts
// erase sequence
EECON2 = 0x55;
EECON2 = 0xAA;
EECON1bits.WR = 1;
INTCONbits.GIE = 1; // enable interrupts
}
void FlashWrite(long addr, char *data)
{
int i;
FlashErase(addr); // must erase flash before writing
TBLPTR = addr;
// load the table latch with data
for (i = 0; i < 64; i++)
{
TABLAT = data[i]; // copy data from buffer
_asm
TBLWTPOSTINC // increment the table latch
_endasm
}
TBLPTR = addr;
EECON1bits.EEPGD = 1; // select program memory
EECON1bits.CFGS = 0; // enable program memory access
EECON1bits.WREN = 1; // enable write access
INTCONbits.GIE = 0; // disable interrupts
// write sequence
EECON2 = 0x55;
EECON2 = 0xAA;
EECON1bits.WR = 1;
INTCONbits.GIE = 1; // enable interrupts
}
FlashErase
erases 64 bytes of program memory at the specified offset, and FlashWrite
writes 64 bytes at the specified offset. Note the _asm
and _endasm
directives which are used to include assembly code. The TABWTPOSTINC
instruction is used to increment the table write pointer. The sequence for writing and erasing is taken directly from the device datasheet section on flash memory.
Flashing and Running Code
This is the entry point when the device is first powered on.
void main()
{
int i;
long cur_addr = 0x800;
char buf[64];
char done = 0;
UART1Init(115200);
// wait for request to load code
if (UART1RxByte(20000) == 0)
_asm goto 0x800 _endasm // no request, jump to program
UART1TxROMString("OK\n");
for (;;)
{
for (i = 0; i < 64; i++)
{
buf[i] = UART1RxByte(5000);
if (buf[i-3] == 'D' && buf[i-2] == 'O' &&
buf[i-1] == 'N' && buf[i] == 'E')
{
done = 1;
break;
}
}
FlashWrite(cur_addr, buf);
cur_addr += 64;
UART1TxByte('K');
if (done)
break;
}
UART1TxROMString("DONE\n");
_asm goto 0x800 _endasm
}
First, UART1
is initialized and the program waits for a request to load code. If it does not receive this request before the timeout, it will jump to 0×800 in program memory, which is the start of the loaded program.
If it does get a request to load code, it will receive data one byte at a time, and write every 64 bytes to program memory. Once the sequence ‘DONE
’ is received, the write loop ends and the bootloader jumps to the program at 0×800.
Flashing Code
To flash code, connect to the device and send a byte immediately after power on. Then send code one byte at a time, followed by “DONE
”. The device will then start the flashed application. When it resets, it will jump to the application after a short delay.
To get a compiler to generate code for the device, you will need to tell it to put code at the 0×800 offset, otherwise all of the jumps and branches will be incorrect. This can be done with a linker script. I’ll have more about that soon.
Next Steps
This bootloader is very simple and works, but has some issues. Interrupts are not yet supported, the sequence for flashing is not robust, there is are no checksums, etc… This is a first working state of the project. I hope to improve it to address the aforementioned issues and develop support for loading code from other protocols including CAN.
Writing A Bootloader -> Original post.