Продолжаем обсуждать bootloader и сегодня, как я и обещал, создадим свой собственный загрузчик и попробуем его в деле. Все теоретические аспекты мы рассмотрели в предыдущей статье (ссылка), так что сейчас только практика и ничего кроме практики 👍
Итак, для начала разберемся с постановкой задачи. В качестве электронной части будет выступать отладочная плата MiniSTM32, соответственно, проект будем создавать для семейства микроконтроллеров STM32F10x. Наш bootloader будет работать с SD-картой, то есть при обнаружении файла прошивки на подключенной к плате карте, загрузчик должен будем выполнить перепрошивку контроллера. В качестве среды разработки я в этот раз буду использовать IAR. И, конечно же, в конце статьи я обязательно выложу полные проекты. Со вступлением на этом закончиваем, переходим к делу.
Определим нужные адреса, а также параметры Flash-памяти. В моем контроллере памяти 512 кБ - 256 страниц по 2 кБ. Кроме того, мы выделим отдельную страницу для хранения специального ключа-флага. Зачем он нужен? А для того, чтобы сохранить информацию при перезапуске контроллера. Перед переходом по адресу основной программы нам нужно сбросить всю периферию в начальное состояние, и это проще всего сделать просто перезапустив микроконтроллер.
Таким образом, если bootloader обнаружил прошивку и запрограммировал контроллер, он запишет в специальное место во Flash произвольное, заранее выбранное, значение и перезапустит контроллер. При входе в функцию main() после ресета контроллер считает значение "ключа" из Flash-памяти, и если там записано нужное значение, то значит программа была прошита и можно выполнять переход, а если этого значения там нет, то значит перепрошивки не было, и bootloader продолжит сканировать SD-карту. Проще говоря, при включении микроконтроллер будет считывать значение из памяти, и если оно совпадет с ключом, то произойдет переход на адрес основной программы, а если нет, то bootloader продолжит просматривать карту на предмет файла прошивки.
В нашем примере в качестве ключа будет выступать значение 0xAAAA5555, и храниться ключ будет в 19-ой странице Flash-памяти:
// Bootloader key configuration #define BOOTLOADER_KEY_START_ADDRESS (uint32_t)0x08009800 #define BOOTLOADER_KEY_PAGE_NUMBER 19 #define BOOTLOADER_KEY_VALUE 0xAAAA5555 // Flash configuration #define MAIN_PROGRAM_START_ADDRESS (uint32_t)0x0800A000 #define MAIN_PROGRAM_PAGE_NUMBER 20 #define NUM_OF_PAGES 256 #define FLASH_PAGE_SIZE 2048
Основная же программа, как видите, будет прошиваться начиная с 20-ой страницы Flash. Теперь определим нужные нам функции для сохранения и чтения ключа:
/***************************************************************************************/ // Function SetKey() // Description Sets bootloader key // Parameters None // RetVal None /***************************************************************************************/ void SetKey() { FLASH_Unlock(); FLASH_ProgramWord(BOOTLOADER_KEY_START_ADDRESS, BOOTLOADER_KEY_VALUE); FLASH_Lock(); } // End of SetKey() /***************************************************************************************/ // Function ResetKey() // Description Resets bootloader key // Parameters None // RetVal None /***************************************************************************************/ void ResetKey() { FLASH_Unlock(); FLASH_ErasePage(BOOTLOADER_KEY_START_ADDRESS); FLASH_Lock(); } // End of ResetKey() /***************************************************************************************/ // Function ReadKey() // Description Reads bootloader key value // Parameters None // RetVal None /***************************************************************************************/ uint32_t ReadKey() { return (*(__IO uint32_t*) BOOTLOADER_KEY_START_ADDRESS); } // End of ReadKey() /***************************************************************************************/
Готово! Без лишних остановок двигаемся дальше. И на очереди функция main()
. В самом начале проверяем ключ, и если он совпал совершаем переход:
if (ReadKey() == BOOTLOADER_KEY_VALUE) { ResetKey(); __disable_irq(); NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS); jumpAddress = *(__IO uint32_t*) (MAIN_PROGRAM_START_ADDRESS + 4); Jump_To_Application = (pFunction) jumpAddress; __set_MSP(*(__IO uint32_t*) MAIN_PROGRAM_START_ADDRESS); Jump_To_Application(); }
Переход осуществляется следующим образом. Первым делом сбрасываем ключ и выключаем прерывания. Затем переносим таблицу векторов прерываний на адреса, соответствующие основной программе. А непосредственно переход заключается в последующих четырех строках. Важным моментом является то, что мы берем адрес (MAIN_PROGRAM_START_ADDRESS + 4)
, это связано с тем, что по адресу MAIN_PROGRAM_START_ADDRESS
сначала записывается таблица векторов прерываний.
Идем дальше и тут начинается самое интересное - если ключ не совпал, то мы начинаем проверять SD-карту на наличие файла прошивки. И для начала инициализируем интерфейс SDIO:
NVIC_InitTypeDef NVIC_InitStructure; SD_Init(); // SDIO Interrupt ENABLE NVIC_InitStructure.NVIC_IRQChannel = SDIO_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // DMA2 Channel4 Interrupt ENABLE NVIC_InitStructure.NVIC_IRQChannel = DMA2_Channel4_5_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_Init(&NVIC_InitStructure); SD_GetCardInfo(&SDCard); SD_SelectDeselect((uint32_t)(SDCard.RCA << 16));
Работу с картами памяти мы уже много раз обсуждали, поэтому на этом не будем подробно останавливаться. А дальше мы проверяем, есть ли на карте файл прошивки (зададим для него имя - program.bin):
FATFS SDfs; FIL program; if(f_mount(&SDfs, "0", 1) == FR_OK) { uint8_t path[12] = "program.bin"; path[11] = '\0'; result = f_open(&program, (char*)path, FA_READ); if (result == FR_OK) { // Program........
И если нужный нам файл найден, то следует произвести программирование. Реализуем это, и для начала разблокируем Flash-память:
FLASH_Unlock();
Теперь нам необходимо произвести очистку всех страниц памяти, соответствующих основной программе. В нашем случае это страницы с 20-ой до последней, 256-ой:
for(uint8_t i = 0; i < (NUM_OF_PAGES - MAIN_PROGRAM_PAGE_NUMBER); i++) { FLASH_ErasePage(MAIN_PROGRAM_START_ADDRESS + i * FLASH_PAGE_SIZE); }
Само программирование мы будем осуществлять порциями по 512 байт, то есть считываем из файла 512 байт, записываем их во Flash, затем считываем следующие 512 байт и так далее... При этом будем сохранять в переменной количество уже записанных байт и сравнивать это значение с общим числом байт. Если осталось записать больше 512 байт, то считываем их из файла и записываем. Как только нам останется записать меньше 512 байт, мы считываем оставшееся количество и записываем во Flash эти последние байты. В программе это выглядит следующим образом:
programBytesToRead = program.fsize; programBytesCounter = 0; currentAddress = MAIN_PROGRAM_START_ADDRESS; while ((programBytesToRead - programBytesCounter) >= 512) { f_read(&program, readBuffer, 512, &readBytes); programBytesCounter += 512; for (uint32_t i = 0; i < 512; i += 4) { FLASH_ProgramWord(currentAddress, *(uint32_t*)&readBuffer[i]); currentAddress += 4; } } if (programBytesToRead != programBytesCounter) { f_read(&program, readBuffer, (programBytesToRead - programBytesCounter), &readBytes); for (uint32_t i = 0; i < (programBytesToRead - programBytesCounter); i += 4) { FLASH_ProgramWord(currentAddress, *(uint32_t*)&readBuffer[i]); currentAddress += 4; } programBytesCounter = programBytesToRead; }
И после окончания этих операций не забываем заблокировать Flash и закрыть файл:
FLASH_Lock(); f_close(&program);
Кроме того, удаляем файл прошивки с SD-карты, ведь иначе при каждом перезапуске bootloader будет находить один и тот же файл и раз за разом записывать его во Flash:
f_unlink((char*)path);
Отключаем прерывания, сохраняем ключ и выполняем перезапуск контроллера:
NVIC_DisableIRQ(DMA2_Channel4_5_IRQn); NVIC_DisableIRQ(SDIO_IRQn); ResetKey(); SetKey(); NVIC_SystemReset();
На этом с программированием памяти вроде бы все. А что делать, если файл не обнаружен? В этом случае мы анализируем байты, записанные по адресу основной программы и если они сигнализируют нам о том, что основная программа уже находится во Flash (то есть была туда помещена ранее), то мы делаем вывод, что она не нуждается в обновлении (ведь файл прошивки отсутствует), записываем ключ и выполняем перезапуск. А контроллер при включении, увидев ключ, просто перейдет на основную программу.
Если же основная программа отсутствует в памяти, то мы также выполняем перезапуск, но не записываем при этом ключ. Тогда контроллер, перезапустившись, начнет заново сканировать карту-памяти на наличие файла прошивки:
if (((*(uint32_t*)MAIN_PROGRAM_START_ADDRESS) & 0x2FFF0000 ) == 0x20000000) { ResetKey(); SetKey(); NVIC_SystemReset(); } else { NVIC_SystemReset(); }
Собственно, на этом мы и заканчиваем работу над нашим собственным загрузчиком, но не заканчиваем статью. Давайте создадим тестовый проект, моргающий светодиодом, для того, чтобы протестировать работу нашего бутлодера.
Итак, создаем абсолютно новый проект, и код там будет достаточно простой - инициализируем таймер и вывод, на котором находится светодиод на плате. И в прерывании по таймеру моргаем диодом. Отличие от обычных проектов тут будет в том, что в начале функции main()
мы вызываем функцию для переноса таблицы векторов прерываний в нужное нам место:
int main() { __set_PRIMASK(1); NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0800A000); __set_PRIMASK(0); __enable_irq(); initAll(); TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); TIM_Cmd(TIM4, ENABLE); NVIC_EnableIRQ(TIM4_IRQn); while(1) { } return 0; } void TIM4_IRQHandler() { TIM_ClearITPendingBit(TIM4, TIM_IT_Update); if (currentState == 0) { currentState = 1; GPIO_SetBits(GPIOC, GPIO_Pin_6); } else { currentState = 0; GPIO_ResetBits(GPIOC, GPIO_Pin_6); } }
Этот тестовый проект я также выложу в конце статьи.
На самом деле и это еще не все, наш проект мы должны еще правильным образом настроить. И для этого нам надо зайти в свойства проекта во вкладку Linker. Там, нажав на кнопку Edit, мы должны прописать адреса, соответствующие нашей основной программе, то есть в данном случае адрес 0x0800A000 (пример для IAR):
Также во вкладке Memory Regions в поле ROM Start надо указать тот же адрес - 0x0800A000.
Возможно вы обратили внимание на то, что у меня используется не стандартный файл линкера, а другой (на скриншоте выставлена галочка Override default и указан путь к моему файлу). Сейчас расскажу, зачем это сделано.
Если мы будем использовать стандартный файл, входящий в состав IAR, и поменяем в нем адреса для проекта с bootloader'ом, то затем, создав новый проект для такого же контроллера, в котором bootloader не используется или используются другие адреса, мы получим ошибки, ведь файл линкера один и тот же и он уже изменен нами ранее совсем для другого проекта. Поэтому для всех проектов я копирую стандартный файл линкера к себе в папку с проектом и уже в него спокойно записываю новые адреса, поскольку этот файл будет использоваться только для этого проекта.
И вот на этом мы на сегодня заканчиваем, надеюсь статья получилась полезной и понятной. Как и обещал, выкладываю два полных проекта:
До скорых встреч на нашем сайте!