Загрузчики очень полезны: они позволяют программировать файл приложения без отладчика. Это делает его идеальным для обновления системы в полевых условиях.
Как правило, имеются предложения по применению и примеры от поставщиков кремния. Но, как правило, они предназначены для определенного микроконтроллера, и его сложно сменить на другую систему без особых знаний о его реализации. Что мне нужно для проекта на базе FRDM-KL25Z , так это загрузчик, который должен быть небольшим и портативным. Поскольку я использую Processor Expert для обеспечения переносимости своих приложений между различными семействами микроконтроллеров: почему бы не создать загрузчик с компонентами Processor Expert? С доступными драйверами Processor Expert все становится намного проще по сравнению с «традиционным» подходом. С размером менее 10 Кбайт?
Загрузчик — это программа, которая может загрузить другую программу (прикладную программу). Обычно программа загрузчика не изменяется и хранится в микроконтроллере. Таким образом, загрузчик может загружать снова и снова другую программу.
Архитектурно может быть «мини» или «микро» загрузчик, который может загрузить «настоящий» загрузчик. Например, загрузчик OpenSDA на платах Freedom имеет такую возможность.
Загрузчик код и Bootloader векторы запрограммированы в новую часть (например , с помощью отладчика или автономный флэш — программиста (например , с USBDM ). Тогда Загрузчик может быть использован для загрузки или изменения кода приложения и приложения векторов. При этом, Загрузчик остается прежним, а приложение можно обновлять.
Последовательность загрузчика
Типичный загрузчик делает что-то вроде этого
- Загрузчик решает при запуске, должен ли он перейти в режим загрузчика или запустить приложение. Обычно это решается с помощью кнопки или перемычки, установленной (или удаленной). Если он запустит приложение, загрузчик вызывает приложение, и мы закончили .
- В противном случае загрузчик перепрограммирует приложение новым файлом. Для этого часто используются файлы S19 (S-Record), так как их легко анализировать, и каждая цепочка инструментов может их создавать.
- Загрузчик должен использовать канал связи для чтения этого файла. Это может быть RS-232, USB или файловая система SD-карты (например, FatFS).
- Используя этот файл, загрузчик программирует флэш-память. Особое внимание следует уделить таблице векторов приложений. Поскольку в загрузчике заканчивается сброс, он использует собственную (векторную) векторную таблицу и должен переместить векторную таблицу при запуске приложения.
Можно было бы использовать кнопку сброса на плате FRDM-KL25Z в качестве пользовательской кнопки (см. Этот пост). Для простоты я использую специальную кнопку загрузчика на PTB8.
Поэтому для написания загрузчика требуются следующие части:
- Канал связи : File I / O или любые другие средства для чтения файла приложения.
- File Reader : читатель, который читает файл приложения.
- Программирование Flash : для программирования Приложения.
- Перенаправление вектора : для переключения между загрузчиком и векторной таблицей приложения.
- Пользовательский интерфейс : отображение статуса и информации для пользователя, а также переключение между режимом приложения и загрузчика при запуске системы.
Processor Expert поставляется с установленными компонентами Flash Programming и Communication (USB, SCI, I2C,…). У меня уже есть пользовательский интерфейс Shell , плюс создан компонент для чтения файлов S19 . Объединение этого с другими моими компонентами должно позволить мне сделать загрузчик .
Флэш-память загрузчика
Чтобы убедиться, что загрузчик связан только с его пространством, я уменьшил для него флэш-память. С настройками ниже я ограничиваю флэш-память от 0 × 0000 (векторная таблица) до 0x3FFF. Это означает, что область памяти моего приложения начинается с 0 × 4000.
Поэтому я изменил доступную флэш-память для загрузчика в свойствах процессора и сократил доступный размер флэш-памяти на KL25Z128 с 0x1FBF0 (~ 128 КБ) на вкладке «Параметры сборки» до 0x3FB0:
При этом загрузчик занимает пространство от адреса 0 × 0000 (таблица векторов) до 0x3FFF.
Флэш-защита
Мой загрузчик находится на первой нижней флеш-странице. Чтобы приложение не разрушалось и не перезаписывалось, я защищаю флэш-блоки загрузчика. В свойствах компонента ЦП есть настройка, в которой я могу защитить 4 КБ области:
Терминальная программа
Для моего загрузчика мне нужен способ отправить файл с помощью программы терминала. Поскольку в моем последовательном соединении есть только Tx и Rx, но нет линий RTS / CTC для управления потоком, полезно, если в терминальной программе реализовано программное управление потоком (XON / XOFF) или значение задержки для отправки файла.
After some searching the internet, I have found an open source terminal program which exactly can do this: https://sites.google.com/site/terminalbpp/
It supports sending a file with a delay (shown above with 1 ms delay), and supports XON and XOFF. I used it successfully with my bootloader.
Using a zero delay did not work in all cases. Not yet sure why. What worked was sending a file with a 1 ms delay setting.
Bootloader Shell
The bootloader features a shell with following commands:
-------------------------------------------------------------- FRDM Shell Bootloader -------------------------------------------------------------- CLS1 ; Group of CLS1 commands help|status ; Print help or status information BL ; Group of Bootloader commands help|status ; Print help or status information erase ; Erase application flash blocks restart ; Restart application with jump to reset vector load s19 ; Load S19 file
The ‘BL status’ command shows the application flash range, and the content of the application vector table (more about this later):
App Flash : 0x00004000..0x0001FFFF @0x00004000: 0xFFFFFFFF 0xFFFFFFFF
With ‘BL restart’ it starts the user application (if any), and with ‘BL erase’ the application flash can be erased:
CMD> Erasing application flash blocks...done!
Bootloading an Application
With ‘BL load s19′ a new application file can be loaded. It will first erase the application flash blocks, and then waits for the S19. To send the file, I use the ‘Send File’ button:
It writes then the address of each S19 line programmed to the shell console:
CMD> Erasing application flash blocks...done! Waiting for the S19 file... S0 address 0x00000000 S1 address 0x00008000 S1 address 0x00008010 ... S1 address 0x00009420 S1 address 0x00009430 S1 address 0x00009440 S9 address 0x00009025 done! CMD>
Bootloader Details
If I enter ‘BL Load S19′, it executes the function BL_LoadS19() in Bootloader.c:
static uint8_t BL_LoadS19(CLS1_ConstStdIOType *io) { unsigned char buf[16]; uint8_t res = ERR_OK; /* first, erase flash */ if (BL_EraseAppFlash(io)!=ERR_OK) { return ERR_FAILED; } /* load S19 file */ CLS1_SendStr((unsigned char*)"Waiting for the S19 file...", io->stdOut); parserInfo.GetCharIterator = GetChar; parserInfo.voidP = (void*)io; parserInfo.S19Flash = BL_onS19Flash; parserInfo.status = S19_FILE_STATUS_NOT_STARTED; parserInfo.currType = 0; parserInfo.currAddress = 0; parserInfo.codeSize = 0; parserInfo.codeBuf = codeBuf; parserInfo.codeBufSize = sizeof(codeBuf); while (AS1_GetCharsInRxBuf()>0) { /* clear any pending characters in rx buffer */ AS1_ClearRxBuf(); WAIT1_Waitms(100); } do { if (S19_ParseLine(&parserInfo)!=ERR_OK) { CLS1_SendStr((unsigned char*)"ERROR!\r\nFailed at address 0x", io->stdErr); buf[0] = '\0'; UTIL1_strcatNum32Hex(buf, sizeof(buf), parserInfo.currAddress); CLS1_SendStr(buf, io->stdErr); CLS1_SendStr((unsigned char*)"\r\n", io->stdErr); res = ERR_FAILED; break; } else { CLS1_SendStr((unsigned char*)"\r\nS", io->stdOut); buf[0] = parserInfo.currType; buf[1] = '\0'; CLS1_SendStr(buf, io->stdOut); CLS1_SendStr((unsigned char*)" address 0x", io->stdOut); buf[0] = '\0'; UTIL1_strcatNum32Hex(buf, sizeof(buf), parserInfo.currAddress); CLS1_SendStr(buf, io->stdOut); } if (parserInfo.currType=='7' || parserInfo.currType=='8' || parserInfo.currType=='9') { /* end of records */ break; } } while (1); if (res==ERR_OK) { CLS1_SendStr((unsigned char*)"\r\ndone!\r\n", io->stdOut); } else { while (AS1_GetCharsInRxBuf()>0) {/* clear buffer */ AS1_ClearRxBuf(); WAIT1_Waitms(100); } CLS1_SendStr((unsigned char*)"\r\nfailed!\r\n", io->stdOut); /* erase flash again to be sure we do not have illegal application image */ if (BL_EraseAppFlash(io)!=ERR_OK) { res = ERR_FAILED; } } return res; }
It first fills a callback structure of type S19_ParserStruct:
typedef struct S19_ParserStruct { uint8_t (*GetCharIterator)(uint8_t*, void*); /* character stream iterator */ void *voidP; /* void pointer passed to iterator function */ uint8_t (*S19Flash)(struct S19_ParserStruct*); /* called for each S19 line to be flashed */ /* the following fields will be used by the iterator */ S19_FileStatus status; /* current status of the parser */ uint8_t currType; /* current S19 record, e.g. 1 for S1 */ uint32_t currAddress; /* current code address of S19 record */ uint16_t codeSize; /* size of code in bytes in code buffer */ uint8_t *codeBuf; /* code bufffer */ uint16_t codeBufSize; /* total size of code buffer, in bytes */ } S19_ParserStruct;
That structure contains a callbacks to read from the input stream:
static uint8_t GetChar(uint8_t *data, void *q) { CLS1_ConstStdIOType *io; io = (CLS1_ConstStdIOType*)q; if (!io->keyPressed()) { #if USE_XON_XOFF SendXONOFF(io, XON); #endif while(!io->keyPressed()) { /* wait until there is something in the input buffer */ } #if USE_XON_XOFF SendXONOFF(io, XOFF); #endif } io->stdIn(data); /* read character */ if (*data=='\0') { /* end of input? */ return ERR_RXEMPTY; } return ERR_OK; }
Parsing of the S19 file is done in S19_ParesLine() which is implemented in a Processor Expert component which I already used for another bootloader project:
This parser is calling my callback BL_onS19Flash()
for every S19 line:
static uint8_t BL_onS19Flash(S19_ParserStruct *info) { uint8_t res = ERR_OK; switch (info->currType) { case '1': case '2': case '3': if (!BL_ValidAppAddress(info->currAddress)) { info->status = S19_FILE_INVALID_ADDRESS; res = ERR_FAILED; } else { /* Write buffered data to Flash */ if (BL_Flash_Prog(info->currAddress, info->codeBuf, info->codeSize) != ERR_OK) { info->status = S19_FILE_FLASH_FAILED; res = ERR_FAILED; } } break; case '7': case '8': case '9': /* S7, S8 or S9 mark the end of the block/s-record file */ break; case '0': case '4': case '5': case '6': default: break; } /* switch */ return res; }
Of interest are the S1, S2 and S3 records as they contain the code. With BL_ValidAppAddress()
it checks if the address is within the application FLASH memory range:
/*! * \brief Determines if the address is a valid address for the application (outside the bootloader) * \param addr Address to check * \return TRUE if an application memory address, FALSE otherwise */ static bool BL_ValidAppAddress(dword addr) { return ((addr>=MIN_APP_FLASH_ADDRESS) && (addr<=MAX_APP_FLASH_ADDRESS)); /* must be in application space */ }
If things are ok, it flashes the memory block:
/*! * \brief Performs flash programming * \param flash_addr Destination address for programming. * \param data_addr Pointer to data. * \param nofDataBytes Number of data bytes. * \return ERR_OK if everything was ok, ERR_FAILED otherwise. */ static byte BL_Flash_Prog(dword flash_addr, uint8_t *data_addr, uint16_t nofDataBytes) { /* only flash into application space. Everything else will be ignored */ if(BL_ValidAppAddress(flash_addr)) { if (IFsh1_SetBlockFlash((IFsh1_TDataAddress)data_addr, flash_addr, nofDataBytes) != ERR_OK) { return ERR_FAILED; /* flash programming failed */ } } return ERR_OK; }
The Flash Programming itself is performed by the IntFLASH Processor Expert components:
This component is used for erasing too:
/*! * \brief Erases all unprotected pages of flash * \return ERR_OK if everything is ok; ERR_FAILED otherwise */ static byte BL_EraseApplicationFlash(void) { dword addr; /* erase application flash pages */ for(addr=MIN_APP_FLASH_ADDRESS;addr<=MAX_APP_FLASH_ADDRESS;addr+=FLASH_PAGE_SIZE) { if(IFsh1_EraseSector(addr) != ERR_OK) { /* Error Erasing Flash */ return ERR_FAILED; } } return ERR_OK; }
Bootloader or Not, that’s the Question
One important piece is still missing: the bootloader needs to decide at startup if it shall run the Bootloader or the application. For this we need to have a decision criteria, which is typically a jumper or a push button to be pressed at power up to enter bootloader mode.
In this bootloader this is performed by BL_CheckForUserApp()
:
/*! * \brief This method is called during startup! It decides if we enter bootloader mode or if we run the application. */ void BL_CheckForUserApp(void) { uint32_t startup; /* assuming 32bit function pointers */ startup = ((uint32_t*)APP_FLASH_VECTOR_START)[1]; /* this is the reset vector (__startup function) */ if (startup!=-1 && !BL_CheckBootloaderMode()) { /* we do have a valid application vector? -1/0xfffffff would mean flash erased */ ((void(*)(void))startup)(); /* Jump to application startup code */ } }
The function checks if the ‘startup’ function in the vector table (index 1) is valid or not. If the application flash has been erased, it will read -1 (or 0xffffffff). So if we have an application present and the user does not want to run the bootloader, we jump to the application startup.
Below is the code to decide if the user is pressing the button to enter the startup code:
static bool BL_CheckBootloaderMode(void) { /* let's check if the user presses the BTLD switch. Need to configure the pin first */ /* PTB8 as input */ /* clock all port pins */ SIM_SCGC5 |= SIM_SCGC5_PORTA_MASK | SIM_SCGC5_PORTB_MASK | SIM_SCGC5_PORTC_MASK | SIM_SCGC5_PORTD_MASK | SIM_SCGC5_PORTE_MASK; /* Configure pin as input */ (void)BitIoLdd3_Init(NULL); /* initialize the port pin */ if (!BL_SW_GetVal()) { /* button pressed (has pull-up!) */ WAIT1_Waitms(50); /* wait to debounce */ if (!BL_SW_GetVal()) { /* still pressed */ return TRUE; /* go into bootloader mode */ } } /* BTLD switch not pressed, and we have a valid user application vector */ return FALSE; /* do not enter bootloader mode */ }
I’m using BitIOLdd3_Init()
to initialize my port pin, which is part of the BitIO component for the push button:
When creating a BitIO component for Kinetis, Processor Expert automatically creates a BitIO_LDD component for it. As I do not have control over the name of that BitIO_LDD, I need to use in my bootloader whatever Processor Expert has assigned as name.
I’m using PTB8 of the Freedom board, and have it connected to a break-out board (pull-up to 3.3V if button is not pressed, GND if button is pressed):
You might wonder why I have to initialize it, as this is usually done automatically by PE_low_level_init()
in main()
? The reasons is: I need to do this *before* main()
gets called, very early in the startup()
code. And that’s the reason as well why I need to set the SIM_SCGC5
register on Kinetis to clock the peripheral.
Inside the CPU component properties, there is a Build option setting where I can add my own code to be inserted as part of the system startup:
To make sure it has the proper declaration, I add the header file too:
These code snippets get added to the __init_hardware()
function which is called from the bootloader startup code:
This completes the Bootloader itself. Next topic: what to do in the application to be loaded…
Application Memory Map
As shown above: the bootloader is sitting in a part of the memory which is not available by the application. So I need to make sure that application does not overlap with the FLASH area of the bootloader. My bootloader starts at address 0×0000 and ends at 0x3FFF:
While the application can be above 0×4000. These numbers are used in Bootloader.c:
/* application flash area */ #define MIN_APP_FLASH_ADDRESS 0x4000 /* start of application flash area */ #define MAX_APP_FLASH_ADDRESS 0x1FFFF /* end of application flash */ #define APP_FLASH_VECTOR_START 0x4000 /* application vector table in flash */ #define APP_FLASH_VECTOR_SIZE 0xc0 /* size of vector table */
The application just needs to stay outside the FLASH used by the bootloader:
To make this happen, I need to change the addresses for m_interrupts
and m_text
in the CPU build options:
That’s it
As for the ARM Cortex-M4/0+ do not need to copy the vector table in the bootloader to a different location, I can debug the application easily without the bootloader.
S-Record (S19) Application File Generation
The bootloader loads S19 or S-Records. This post explains how to create S19 files for Kinetis and GNU gcc.
Code Size
The bootloader is compiled with gcc for the FRDM-KL25Z board. Without optimization (-O0), it needs 13 KByte of FLASH. But optimized for size, it needs only 8 KByte :
text data bss dec hex filename 8024 24 2396 10444 28cc FRDM_Bootloader.elf
Summary
With this, I have a simple serial bootloader for only 8 KByte of code. The bootloader project and sources are are available on GitHub here.
And I have several ideas for extensions:
- Using a memory stick to load the appliation file (USB MSD Host).
- Using a SD-Card interface with FatFS.
- Using a USB MSD device to load the file.
- Performing vector auto-relocation: the bootloader should detect the vector table at address 0×00 of the application file and automatically relocate it to another location in FLASH. That way I can debug the Application without change of the vector table.
- Making sure it runs on other boards and microcontroller families.
- Creating a special component for the bootloader.
While the bootloader only needs 8 KByte for now, I keep the reserved range at 16 KByte, just to have room for future extensions.
Happy Bootloading