Example of a Serial bootloader for the K64F platform

Dependencies:   mbed

Introduction

Once in a while on mbed questions arise regarding making your own bootloader. So I decided to make one. Due to the device specific parts in the code this only works on the K64F. Porting it to other Freescale devices should be fairly straightforward. Devices from other manufacturers will require completely different code, although it could still be used as a basic guide.

Disclaimer

Bricking your device!

This rewrites your flash memory, including the security byte which determines among others the access external programmers get, and which can completely block future access.

Additionally this is an example, it is not intended to be used in a mission critical system in a nuclear power plant.

Requirements

I set some requirements for this bootloader. To start with, it should work when build from the online compiler. So no special compiler options to place functions in the correct sections. Even more important, it should be able to load regular programs from the mbed online compiler.

There are different options to get the data to the mbed, but for simplicity the goal is to just use the USBTX/USBRX serial pins. Finally there are some different types of bootloaders, for example they can launch at start up. That is not going to be easy with the requirement to also be able to load regular programs, although it might be possible, but it is not a requirement for here. In principle it is sufficient if the new user program has to call the bootloader, but an alternative is presented which allows a button to be able to enter bootloader mode, regardless of user code (not completely regardless, but it does not need to explictitly add any code for the bootloader).

Bootloader options

When a bootloader recides in the first part of the flash memory, it will run at start up and allows you to always upload a new program, regardless of your old program (it is always possible to brick it). However this means your user program needs to be compiled with an offset, and the online compiler cannot do this.

The alternative is putting the bootloader at the end of the flash memory. From here it can reprogram the first parts of the flash memory, but it will not automatically start up. Still this options has been used here.

The bootloader

The first requirement is placing the functions at the end of the flash memory. The online compiler (and Keil, other compilers will have different options) uses the following syntax to place functions at an absolute location:

__attribute__((section(".ARM.__at_0x10000"))) void bootloader(void)

Here obviously the 0x10000 is the absolute address where the function is placed. You may realise this is in fact not the end of the flash memory on the K64F. You are correct, I randomly choose this and cannot be bothered to change it. It allows 64kB for your user program, which is sufficient for just trying it out.

You do not need to worry about placing it on top of other functions/values: The compiler checks this, and gives a clear error message on the location and sizes of the collision.

It is important to realise that you cannot depend on any functions in the 'regular' area of your flash! This can be reprogrammed to contain any value/data, so every single functions and variable used by your bootloader needs to be specifically placed in your bootloader section. This also means that we cannot use mbed library functions, unless you manage to move the entire mbed library. Which could be done for the C API functions, but for here we use an easier solution, we just code it ourselves :D.

Serial code

Using void setupserial(); the serial peripheral is set up. This assumes the clock is already running correctly, so it depends a bit on the user program, but for any normal mbed user program this will be the case. I started using the K20D50M serial_api.c file from the mbed library, which contains all registers which are being set (The K64F code uses the KSDK drivers, which besides harder to use also cannot be used, because they will not reside in the correct memory area). The code simply hardcodes some register values to be correct for 9600 baud rate at USBTX/USBRX.

void write(char *value); is used to write messages on the serial connection.

Flash programming

Now we can send serial data (and receiving it is also pretty straight forward), we need to be able to program or flash memory. We use this program for this: http://developer.mbed.org/users/Sissors/code/FreescaleIAP/. However also this needs to be placed in the correct flash location. Since I have not been able yet to do it in a nice way in the library, it was done here manually.

To program flash it first needs to be erased. This takes a relative large amount of time, and due to the setup of this bootloader we do not know beforehand how many sectors need to be erased. So it just erases 15 4kB sectors, allowing for a program size of 60kB.

Once erased, the program will wait until the first char arrived. From this moment on it will enter a loop where received chars are placed in a 16-byte buffer. In principle the K64F should be able to program per 8-byte, however this resulted in error statuses from the flash peripheral. Since it works fine with programming per 16-bytes we use this. Every time 16-new bytes have arrived they are programmed into the flash memory, and a counter is increased.

In the loop also a timeout counter runs. Once no new chars have arrived for a period of time it assumes the program has been completely sent. The remaining bytes in the buffer are programmed, and the MCU resets itself, loading automatically the new user program.

Non-Maskable Interrupt

All (mbed) Freescale MCUs have a NMI setup which is called when the correct pin is pulled to zero. This interrupt vector resides in bytes 8-9-10-11, and on the K64F is connected to SW3, which makes it easy to use. The code currently simply replaces those 4 bytes with the location of the bootloader function. This means that regardless of the user program, it will be modified to start the bootloader upon pressing SW3 on your board.

Usage

After loading this program on your K64F the regular way, it will directly start the bootloader. Later on you can enter the bootloader by pressing SW3, or call it from your user code (for this you need to make a function pointer towards the location of the bootloader).

I assume for this Teraterm is used, other terminal programs might have similar options. After confirming you want to start the bootloader by typing 'y', it will erase the current program. Next you need to send the binary intact to the K64F via Teraterm. You can NOT do this via drag and dropping files into it, Teraterm will mutilate them. Instead go to File > Send File, and locate your file. Next make sure the "Binary" option is checked. If everything goes correctly you see a stream '#'s appear in your serial window, followed by a "Done programming!". It resets itself and your bootloaded program should run directly.

The bootloader simply expects the binary data to be transmitted via the Serial connection, so any programs which does so will do.

Remarks and Comments

Since you cannot rely on any functions outside the bootloader, you have to write everything yourself, or move existing libraries one function at a time. This makes it easiest if you stick to simple peripherals, such as SPI or Serial. It might be doable to move USBDevice over, although it would be far from trivial. I wouldn't even consider starting with Ethernet.

But what if you really want to use Ethernet? Let your user program handle it. Downside is of course your user program needs to handle this, and you cannot use any random program. But one example would be loading the new binary via whatever means you want, and storing it on an external memory chip. Then your bootloader program only needs to read it from your memory chip back.

If your code needs to be able to call the bootloader, for example after you loaded it via Ethernet, you need to add a pointer to the bootloader in your code, even though your newly loaded code does not contain the bootloader itself. This can be done using:

void *(*bootloader)(void) = (void *(*)(void))0x10001;

This creates a bootloader point of type void bootloader(void), pointing at a bootloader at 0x10000. If your bootloader function is at another memory location: you need the memory location plus 1. After this your code can call the bootloader simply as a normal function: bootloader();.

If you want it to start always upon reset, you can overwrite the Reset Handler. This is located right before the NMI, at bytes 4-5-6-7. It should work the same way as doing it the NMI way, with two differences. First of all currently the code relies on the clock being set up correctly. That won't be the case. In addition I do not know if it will correctly be able to make new variables on the stack, without code having run to initialize that. (My guess: It won't and you need to do that manually in your code).

The second difference is that you of course still need to be able to run the user code, so it needs to store the old reset vector in another location.

Committer:
Sissors
Date:
Fri Mar 13 21:47:01 2015 +0000
Revision:
5:1345007a5fc3
Parent:
4:8d109a566486
Child:
6:76c37fd3780a
Deleted bit too much in clean up

Who changed what in which revision?

UserRevisionLine numberNew contents of line
Sissors 2:8c44f28c122c 1 #include "mbed.h"
Sissors 2:8c44f28c122c 2 #include "FreescaleIAP.h"
Sissors 2:8c44f28c122c 3
Sissors 2:8c44f28c122c 4 //Could be nicer, but for now just erase all preceding sectors
Sissors 3:8c39a7751758 5 #define NUM_SECTORS 15
Sissors 2:8c44f28c122c 6 #define TIMEOUT 10000000
Sissors 4:8d109a566486 7 #define BUFFER_SIZE 16
Sissors 2:8c44f28c122c 8
Sissors 2:8c44f28c122c 9 void setupserial();
Sissors 2:8c44f28c122c 10 void write(char *value);
Sissors 2:8c44f28c122c 11 char getserial(void);
Sissors 1:782a3ddc329e 12
Sissors 2:8c44f28c122c 13 __attribute__((section(".ARM.__at_0x10000"))) void bootloader(void)
Sissors 2:8c44f28c122c 14 {
Sissors 1:782a3ddc329e 15 setupserial();
Sissors 2:8c44f28c122c 16 write("\n\n\rBootloader\r\n");
Sissors 2:8c44f28c122c 17 write("Continue? (y/n)");
Sissors 4:8d109a566486 18
Sissors 4:8d109a566486 19 //Wait until data arrived, if it is 'y', continue
Sissors 2:8c44f28c122c 20 while(!(UART0->S1 & UART_S1_RDRF_MASK));
Sissors 2:8c44f28c122c 21 if (UART0->D != 'y')
Sissors 2:8c44f28c122c 22 return;
Sissors 4:8d109a566486 23
Sissors 4:8d109a566486 24 //Erase all sectors we use for the user program
Sissors 2:8c44f28c122c 25 write("Erasing sectors!\r\n");
Sissors 2:8c44f28c122c 26 for (int i = 0; i<NUM_SECTORS; i++)
Sissors 2:8c44f28c122c 27 erase_sector(SECTOR_SIZE * i);
Sissors 2:8c44f28c122c 28
Sissors 2:8c44f28c122c 29 write("Done erasing, send file!\r\n");
Sissors 4:8d109a566486 30
Sissors 1:782a3ddc329e 31
Sissors 4:8d109a566486 32 char buffer[BUFFER_SIZE];
Sissors 2:8c44f28c122c 33 uint32_t count = 0;
Sissors 2:8c44f28c122c 34 uint8_t buffercount = 0;
Sissors 2:8c44f28c122c 35 uint32_t timeout = 0;
Sissors 4:8d109a566486 36
Sissors 5:1345007a5fc3 37 //Wait until data is sent
Sissors 5:1345007a5fc3 38 while(!(UART0->S1 & UART_S1_RDRF_MASK));
Sissors 5:1345007a5fc3 39
Sissors 4:8d109a566486 40 //Data receive loop
Sissors 1:782a3ddc329e 41 while(1) {
Sissors 2:8c44f28c122c 42 //Check if there is new data
Sissors 2:8c44f28c122c 43 if (UART0->S1 & UART_S1_RDRF_MASK) {
Sissors 4:8d109a566486 44 //Place data in buffer
Sissors 2:8c44f28c122c 45 buffer[buffercount] = UART0->D;
Sissors 2:8c44f28c122c 46 buffercount++;
Sissors 4:8d109a566486 47
Sissors 4:8d109a566486 48 //Reset timeout
Sissors 4:8d109a566486 49 timeout = 0;
Sissors 2:8c44f28c122c 50
Sissors 4:8d109a566486 51 //We write per BUFFER_SIZE chars
Sissors 4:8d109a566486 52 if (buffercount == BUFFER_SIZE) {
Sissors 4:8d109a566486 53 //NMI Handler is at bytes 8-9-10-11, we overwrite this to point to bootloader function
Sissors 4:8d109a566486 54 if (count == 0) {
Sissors 4:8d109a566486 55 buffer[8] = 0x01;
Sissors 4:8d109a566486 56 buffer[9] = 0x00;
Sissors 4:8d109a566486 57 buffer[10] = 0x01;
Sissors 4:8d109a566486 58 buffer[11] = 0x00;
Sissors 4:8d109a566486 59 }
Sissors 4:8d109a566486 60
Sissors 4:8d109a566486 61 //Program the buffer into the flash memory
Sissors 4:8d109a566486 62 if (program_flash(count, buffer, BUFFER_SIZE) != 0) {
Sissors 2:8c44f28c122c 63 write("Error!\r\n");
Sissors 2:8c44f28c122c 64 break;
Sissors 2:8c44f28c122c 65 }
Sissors 4:8d109a566486 66
Sissors 4:8d109a566486 67 //Reset buffercount for next buffer
Sissors 4:8d109a566486 68 write("#");
Sissors 2:8c44f28c122c 69 buffercount = 0;
Sissors 4:8d109a566486 70 count += BUFFER_SIZE;
Sissors 2:8c44f28c122c 71 }
Sissors 2:8c44f28c122c 72 } else {
Sissors 2:8c44f28c122c 73 //No new data, increase timeout
Sissors 2:8c44f28c122c 74 timeout++;
Sissors 4:8d109a566486 75
Sissors 4:8d109a566486 76 //We have received no new data for a while, assume we are done
Sissors 2:8c44f28c122c 77 if (timeout > TIMEOUT) {
Sissors 4:8d109a566486 78 //If there is data left in the buffer, program it
Sissors 2:8c44f28c122c 79 if (buffercount != 0) {
Sissors 4:8d109a566486 80 for (int i = buffercount; i<BUFFER_SIZE; i++) {
Sissors 2:8c44f28c122c 81 buffer[i] = 0xFF;
Sissors 4:8d109a566486 82 }
Sissors 4:8d109a566486 83 program_flash(count, buffer, BUFFER_SIZE);
Sissors 2:8c44f28c122c 84 }
Sissors 2:8c44f28c122c 85 break; //We should be done programming :D
Sissors 2:8c44f28c122c 86 }
Sissors 2:8c44f28c122c 87 }
Sissors 1:782a3ddc329e 88 }
Sissors 2:8c44f28c122c 89 write("Done programming!\r\n");
Sissors 4:8d109a566486 90 NVIC_SystemReset();
Sissors 4:8d109a566486 91
Sissors 4:8d109a566486 92 //Shouldn't arrive here
Sissors 4:8d109a566486 93 while(1);
Sissors 1:782a3ddc329e 94 }
Sissors 1:782a3ddc329e 95
Sissors 4:8d109a566486 96 __attribute__((section(".ARM.__at_0x10080"))) static void setupserial(void) {
Sissors 2:8c44f28c122c 97 //Setup USBTX/USBRX pins (PTB16/PTB17)
Sissors 2:8c44f28c122c 98 SIM->SCGC5 |= 1 << SIM_SCGC5_PORTB_SHIFT;
Sissors 2:8c44f28c122c 99 PORTB->PCR[16] = (PORTB->PCR[16] & 0x700) | (3 << 8);
Sissors 2:8c44f28c122c 100 PORTB->PCR[17] = (PORTB->PCR[17] & 0x700) | (3 << 8);
Sissors 2:8c44f28c122c 101
Sissors 2:8c44f28c122c 102 //Setup UART (ugly, copied resulting values from mbed serial setup)
Sissors 2:8c44f28c122c 103 SIM->SCGC4 |= SIM_SCGC4_UART0_MASK;
Sissors 4:8d109a566486 104
Sissors 3:8c39a7751758 105 UART0->BDH = 3;
Sissors 3:8c39a7751758 106 UART0->BDL = 13;
Sissors 2:8c44f28c122c 107 UART0->C4 = 8;
Sissors 2:8c44f28c122c 108 UART0->C2 = 12; //Enables UART
Sissors 2:8c44f28c122c 109
Sissors 2:8c44f28c122c 110 }
Sissors 1:782a3ddc329e 111
Sissors 4:8d109a566486 112 __attribute__((section(".ARM.__at_0x100A0"))) static void write(char *value)
Sissors 4:8d109a566486 113 {
Sissors 2:8c44f28c122c 114 int i = 0;
Sissors 4:8d109a566486 115 //Loop through string and send everything
Sissors 2:8c44f28c122c 116 while(*(value+i) != '\0') {
Sissors 2:8c44f28c122c 117 while(!(UART0->S1 & UART_S1_TDRE_MASK));
Sissors 2:8c44f28c122c 118 UART0->D = *(value+i);
Sissors 2:8c44f28c122c 119 i++;
Sissors 2:8c44f28c122c 120 }
Sissors 2:8c44f28c122c 121 }