posted by 블르샤이닝 2012. 5. 3. 15:11
728x90

출처 : http://evenchick.com/articles/writing-a-bootloader/


WRITING A BOOTLOADER

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 is 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.

Lets look at pbldr, which is currently a (very) minimal example of a bootloader. 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.

See part two for some background on interrupts, and getting them to work with a bootloader.


728x90