Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Кармин Новиелло - Освоение STM32.pdf
Скачиваний:
2743
Добавлен:
23.09.2021
Размер:
47.68 Mб
Скачать

Процесс начальной загрузки

586

22.3. Разработка пользовательского загрузчика

Прочитайте внимательно

Загрузчик, описанный в данном параграфе, корректно работает тогда и только тогда, когда версия микропрограммы интерфейса ST-LINK – 2.27.15 или выше. В старых выпусках (релизах) существует баг в VCP, мешающий интерфейсу USART работать должным образом. Убедитесь, что ваша Nucleo обновлена.

Во многих случаях интегрированные загрузчики работают хорошо. Многие реальные проекты могут извлечь выгоду из их применения. Кроме того, бесплатные инструменты, предоставляемые ST, могут уменьшить усилия, необходимые для разработки пользовательских приложений, которые загружают микропрограмму на микроконтроллер. Однако для некоторых приложений могут потребоваться дополнительные функции, не реализованные в стандартных загрузчиках. Например, мы можем захотеть зашифровать распространяемую микропрограмму, чтобы только встроенный загрузчик мог ее дешифровать, используя предварительный общий ключ (pre-shared key, PSK), жестко закодированный в коде загрузчика.

Сейчас мы собираемся разработать собственный пользовательский загрузчик (custom bootloader), который позволит нам загрузить новую микропрограмму на целевой микроконтроллер. По сути, он обеспечит лишь часть функций, реализованных встроенными загрузчиками, но даст нам возможность рассмотреть основные этапы, необходимые для разработки пользовательского загрузчика. Он будет обеспечивать следующие функциональные возможности:

Загрузка новой микропрограммы, используя интерфейс UART (в нашем случае интерфейс UART2, предоставляемый всеми платами Nucleo).

Получение информации о типе микроконтроллера.

Стирание определенного количества секторов/страниц Flash-памяти.

Запись последовательности байтов, начиная с заданного адреса.

Шифрование/дешифрование микропрограммы, используя алгоритм AES-12814.

Код, который мы будем здесь анализировать, основан на организации Flash-памяти микроконтроллеров STM32F401RE, которая показана в таблице 2, извлеченной из соответствующего справочного руководства. Как видите, 512 КБ Flash-памяти разделены на восемь секторов. Первый, сектор 0, выделенный синим цветом в таблице 2, будет использоваться для хранения встроенного загрузчика. Если вы работаете с другим микроконтроллером STM32, обратитесь к примерам книги, чтобы увидеть, как был сконфигурирован загрузчик для вашего микроконтроллера.

14 Насколько я знаю, ST по запросу предоставляет специальный загрузчик, который реализует шифрование микропрограммы, так же, как и другие производители интегральных схем. Однако я почти уверен, что вам нужно будет собрать и подписать множество лицензионных соглашений, и, вероятно, вам нужно будет доказать, что вы будете использовать микроконтроллеры STM32 в своих проектах. Как мы увидим дальше, не так сложно создать собственный пользовательский загрузчик с такими возможностями.

Процесс начальной загрузки

587

Таблица 2: Организация Flash-памяти в микроконтроллере STM32F401RE

После сброса микроконтроллера начинается выполнение загрузчик15. Это означает, что загрузчик скомпилирован так, что он отображается, начиная с адреса 0x0800 0000, как это происходит для всех стандартных приложений STM32, рассматриваемых в книге.

В примере определена действительно минимальная таблица векторов, которая позволяет микроконтроллеру правильно начать выполнение. Таким образом, загрузчик производит выборку вывода PC13, который почти во всех платах Nucleo соответствует синей кнопке на плате. Если кнопка нажата, то плата начинает принимать команды через интерфейс UART2. В противном случае загрузчик немедленно перемещает (relocates) регистр VTOR и передает управление обработчику исключения Reset основной микропрограммы.

Также предоставляется сопутствующий скрипт, написанный на Python. Он называется flasher.py, и вы можете найти его в примерах книги. Мы опишем, как использовать его в следующем параграфе.

Прежде чем мы углубимся в детали реализации команд, используемых для обмена сообщениями с загрузчиком, начнем с анализа процедур, выполняемых в процессе начальной загрузки, и со способа передачи управления основной микропрограмме.

 

Имя файла: src/main-bootloader.c

 

 

 

 

 

 

7

 

/* Глобальные макросы */

 

 

8

 

#define ACK

0x79

 

9

 

#define NACK

0x1F

 

10

 

#define CMD_ERASE

0x43

 

11

 

#define CMD_GETID

0x02

 

12

 

#define CMD_WRITE

0x2b

 

13

 

 

 

 

14

 

#define APP_START_ADDRESS

0x08004000 /* В STM32F401RE это соответствует начальному

15

 

 

 

адресу Сектора 1 */

16

 

 

 

 

17

 

#define SRAM_SIZE

96*1024

// STM32F401RE имеет 96 КБ ОЗУ

18

 

#define SRAM_END

(SRAM_BASE + SRAM_SIZE)

19

 

 

 

 

20#define ENABLE_BOOTLOADER_PROTECTION 0

15 Очевидно, что выводы микроконтроллера должны быть сконфигурированы так, чтобы Flash-память была источником начальной загрузки по умолчанию.

 

Процесс начальной загрузки

588

21

/* Переменные ----------------------------------------------------------------

*/

22

 

 

23/* AES_KEY не может быть определен как const, поскольку функция aes_enc_dec()

24временно изменяет его содержимое */

25uint8_t AES_KEY[] = { 0x4D, 0x61, 0x73, 0x74, 0x65, 0x72, 0x69, 0x6E, 0x67,

26

0x20, 0x20, 0x53, 0x54, 0x4D, 0x33, 0x32 };

27

 

28extern CRC_HandleTypeDef hcrc;

29extern UART_HandleTypeDef huart2;

Макрос APP_START_ADDRESS в строке 14 определяет начальный адрес основной микропрограммы. В соответствии с организацией памяти микроконтроллера STM32F401RE второй сектор начинается с этого адреса, и основная микропрограмма приложения будет храниться там. Это означает, что MSP будет помещен в 0x0800 4000, а адрес обработчика исключения сброса Reset – в 0x0800 4004 Flash-памяти. Массив AES_KEY, определенный в строке 25, содержит шестнадцать байт, образующих ключ AES-128, используемый для шифрования/дешифрования загруженной микропрограммы. Мы проанализируем его использование позже.

Имя файла: src/main-bootloader.c

44/* Минимальная таблица векторов */

45uint32_t *vector_table[] __attribute__((section(".isr_vector"))) = {

46(uint32_t *) SRAM_END, // указатель на начало стека

47(uint32_t *) _start, // _start является Reset_Handler

480, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (uint32_t *) SysTick_Handler };

Таблица векторов определена в строке 45. Она просто содержит указатель MSP, который совпадает с концом памяти SRAM, указатель на обработчик исключения сброса Reset (в данном случае _start, который ничего не делает, кроме как инициализирует секции

.data и .bss и передает управление функции main()) и указатель на SysTick_Handler. Он необходим, поскольку мы будем использовать стандартные процедуры HAL для взаимодействия с периферийными устройствами, а HAL построен на унифицированном временном отсчете, обычно генерируемым с использованием таймера SysTick. HAL необходимо включить этот таймер и перехватить событие переполнения, чтобы увеличить глобальный счетчик тиков.

Имя файла: src/main-bootloader.c

93int main(void) {

94uint32_t ulTicks = 0;

95uint8_t ucUartBuffer[20];

97/* HAL_Init() устанавливает таймер SysTick так, чтобы он переполнялся каждую 1 мс */

98HAL_Init();

99MX_GPIO_Init();

101#if ENABLE_BOOTLOADER_PROTECTION

102/* Гарантия того, что первый сектор Flash-памяти защищен от записи, предотвращая

103тем самым перезапись загрузчика */

104CHECK_AND_SET_FLASH_PROTECTION();

105#endif

Процесс начальной загрузки

589

106

107/* Если USER_BUTTON нажата */

108if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) {

109/* Включение периферийных устройств CRC и UART2 */

110MX_CRC_Init();

111MX_USART2_UART_Init();

113ulTicks = HAL_GetTick();

115while (1) {

116/* Каждые 500 мс светодиод LD2 мигает, чтобы мы могли видеть работу загрузчика. */

117if (HAL_GetTick() - ulTicks > 500) {

118HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);

119ulTicks = HAL_GetTick();

120}

121/* Проверяем новые команды, поступающие на UART2 */

123HAL_UART_Receive(&huart2, ucUartBuffer, 20, 10);

124switch (ucUartBuffer[0]) {

125case CMD_GETID:

126cmdGetID(ucUartBuffer);

127break;

128case CMD_ERASE:

129cmdErase(ucUartBuffer);

130break;

131case CMD_WRITE:

132cmdWrite(ucUartBuffer);

133break;

134};

135}

136} else {

137/* USER_BUTTON не нажата. Сначала мы проверяем, содержат ли первые 4 байта, начиная с

138APP_START_ADDRESS, MSP (конец SRAM). Если нет, то светодиод LD2 быстро мигает. */

139if (*((uint32_t*) APP_START_ADDRESS) != SRAM_END) {

140while (1) {

141HAL_Delay(30);

142HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);

143}

144} else {

145/* Похоже, во втором секторе существует оформленная должным образом программа:

146готовим микроконтроллер для запуска основной микропрограммы */

147MX_GPIO_Deinit(); // Перевод GPIO в состояние по умолчанию

148SysTick->CTRL = 0x0; // Отключение таймера SysTick и связанных с ним прерываний

149HAL_DeInit();

150

151RCC->CIR = 0x00000000; // Запрет всех прерываний, связанных с тактированием

152__set_MSP(*((volatile uint32_t*) APP_START_ADDRESS)); // Установка MSP

153

154__DMB(); // ARM говорит использовать инструкцию DMB перед перемещением VTOR */

155SCB->VTOR = APP_START_ADDRESS; // Перемещаем таблицу векторов в сектор 1

Процесс начальной загрузки

590

156 __DSB(); // ARM говорит использовать инструкцию DSB сразу после перемещения VTOR */

157

158/* Теперь мы готовы перейти к основной микропрограмме */

159uint32_t JumpAddress = *((volatile uint32_t*) (APP_START_ADDRESS + 4));

160void (*reset_handler)(void) = (void*)JumpAddress;

161reset_handler(); // Запускаем выполнение из Reset_Handler основной микропрограммы

163for (;;)

164; // Сюда никогда не приходим

165}

166}

167}

Теперь объясним задачи, выполняемые процедурой main(). Как только она вызывается обработчиком исключения сброса Reset (процедурой _start()), она сначала инициализирует CubeHAL, сводя к минимуму количество операций, выполняемых на данном этапе: это помогает сократить время начальной загрузки. Процедура HAL_Init() также конфигурирует таймер SysTick таким образом, чтобы он истекал каждые 1 мс. Вывод PC13 отобран, и если пользователь продолжает нажимать пользовательскую кнопку USER, то процедура переходит в бесконечный цикл, принимая три команды по UART2. Мы проанализируем их позже. Обратите внимание, что мы оставляем источник тактового сигнала по умолчанию как есть (то есть HSI-генератор).

Если, напротив, пользовательскую кнопку USER оставить не нажатой, тогда процедура main() проверяет, содержит ли первая ячейка в памяти второго сектора MSP (мы просто проверяем, содержит ли она значение SRAM_END). Если нет, то микропрограмма начинает очень быстро мигать светодиодом LD2, сигнализируя об отсутствии основного приложения для запуска.

Если эта ячейка памяти содержит указатель MSP (строка 144), мы можем запустить последовательность начальной загрузки. Таким образом, GPIO переводятся в состояние по умолчанию, HAL деинициализируется, таймер SysTick останавливается и его исключение запрещается. Все прерывания, связанные с тактированием, запрещаются в строке 151, и для MSP задается адрес, указанный в первых 4 байтах сектора 1 (поскольку таблица векторов размещена там, как мы увидим позже). Базовым адресом VTOR является APP_START_ADDRESS (то есть 0x0800 4000 для загрузчика STM32F401RE). Адрес исключения сброса Reset основной микропрограммы берется из ячейки памяти 0x0800 4004 и определяется указатель на эту функцию. Наконец, в строке 161 вызывается исключение сброса Reset, и выполнение загрузчика завершается.

Прежде чем мы проанализируем три команды, реализованные в загрузчике, лучше всего взглянуть на другое приложение, предоставляемое примерами данной главы. Оно называется main-app1.c, и это не более чем просто приложение, мигающее светодиодом LD2 и печатающее сообщение по UART2. Единственное, на что следует обратить внимание – это сопутствующий скрипт компоновщика с именем ldscript-app.ld, который определяет область памяти FLASH следующим образом:

Имя файла: src/ldscript-app.ld

14MEMORY {

15FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 512K - 16K

16RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K

Процесс начальной загрузки

591

Как видите, компоновщик переместит код приложения, начиная с адреса 0x0800 4000. Кроме того, размер этой области памяти установлен в 496 КБ: поскольку первый сектор имеет размер 16 КБ, тогда 512 – 16 равно 496. Это определение области Flash-памяти также позволяет нам загружать и отлаживать микропрограмму с помощью OpenOCD (или STM32CubeProgrammer) без перезаписи загрузчика.

В соответствии с рассмотренным из предыдущего параграфа, значение VTOR, установленное загрузчиком, будет перезаписано процедурой начального запуска основного приложения. Однако код продолжит работать без сбоев, поскольку в скрипте компоновщика для main-app1.c символьное имя

__vectors_start совпадает с макросом APP_START_ADDRESS (то есть 0x0800 4000).

Это важный аспект, который необходимо учитывать при программировании загрузчика.

Теперь самое время проанализировать три команды, поддерживаемые данным загруз-

чиком: CMD_GETID, CMD_ERASE и CMD_WRITE.

Команда Get ID

Команда CMD_GETID используется для получения идентификатора (ID) микроконтроллера16 и имеет структуру, показанную на рисунке 4. Загрузчик, таким образом, ожидает получить байт 0x02, за которым следует CRC-32 этого байта. Загрузчик отвечает на запрос, отправляя байт ACK (который определен в строке 8 файла mainbootloader.c и равен 0x79), за которым следуют два байта, содержащие идентификатор микроконтроллера.

Рисунок 4: Структура команды CMD_GETID

Имя файла: src/main-bootloader.c

223void cmdGetID(uint8_t *pucData) {

224uint16_t usDevID;

225uint32_t ulCrc = 0;

226uint32_t ulCmd = pucData[0];

228memcpy(&ulCrc, pucData + 1, sizeof(uint32_t));

230/* Проверка правильности предоставленного CRC */

231if (ulCrc == HAL_CRC_Calculate(&hcrc, &ulCmd, 1)) {

232usDevID = (uint16_t) (DBGMCU->IDCODE & 0xFFF); //Получение ID МК из интерфейса ОТЛАДКИ

234/* Отправка байта ACK */

235pucData[0] = ACK;

236HAL_UART_Transmit(&huart2, pucData, 1, HAL_MAX_DELAY);

16 Идентификатор микроконтроллера отличается от идентификатора ЦПУ. Первый идентифицирует семейство STM32 и тип микросхемы (например, 0x433 идентифицирует микроконтроллер STM32F401RE). Последний является уникальным идентификатором, который идентифицирует именно этот микроконтроллер, и невозможно (или, по крайней мере, действительно сложно) создать два микроконтроллера STM32 с одинаковым идентификатором ЦПУ.

Процесс начальной загрузки

592

238/* Отправка ID микроконтроллера */

239HAL_UART_Transmit(&huart2, (uint8_t *) &usDevID, 2, HAL_MAX_DELAY);

240} else {

241/* CRC неверный: отправка байта NACK */

242pucData[0] = NACK;

243HAL_UART_Transmit(&huart2, pucData, 1, HAL_MAX_DELAY);

244}

245}

Приведенный выше код показывает, как реализована команда. Как видите, CRC извлекается из сообщения, поступающего по UART, и сравнивается с сообщением, вычисленным периферийным устройством CRC. Если два значения совпадают, то идентификатор микроконтроллера берется из интерфейса ОТЛАДКИ и передается по UART вместе с байтом ACK. Если CRC не совпадает, то отправляется байт NACK (который равен 0x1F).

Команда Erase

Команда CMD_ERASE используется для стирания заданного сектора Flash-памяти, и она имеет структуру, показанную на рисунке 5. Команда состоит из ID 0x43, идентифицирующего тип команды, за которым следует количество секторов для стирания (или значение 0xFF для стирания всех секторов, кроме первого, в котором находится загрузчик) и CRC-32. Загрузчик отвечает, отправляя ACK после завершения процедуры стирания.

Рисунок 5: Структура команды CMD_ERASE

Имя файла: src/main-bootloader.c

180void cmdErase(uint8_t *pucData) {

181FLASH_EraseInitTypeDef eraseInfo;

182uint32_t ulBadBlocks = 0, ulCrc = 0;

183uint32_t pulCmd[] = { pucData[0], pucData[1] };

185memcpy(&ulCrc, pucData + 2, sizeof(uint32_t));

187/* Проверка правильности предоставленного CRC */

188if (ulCrc == HAL_CRC_Calculate(&hcrc, pulCmd, 2) &&

189(pucData[1] > 0 && (pucData[1] < FLASH_SECTOR_TOTAL - 1 || pucData[1] == 0xFF))) {

190/* Если data[1] содержит 0xFF, то стираются все сектора;

191* в противном случае стирается указанное число секторов */

192eraseInfo.Banks = FLASH_BANK_1;

193eraseInfo.Sector = FLASH_SECTOR_1;

194eraseInfo.NbSectors = pucData[1] == 0xFF ? FLASH_SECTOR_TOTAL - 1 : pucData[1];

195eraseInfo.TypeErase = FLASH_TYPEERASE_SECTORS;

196eraseInfo.VoltageRange = FLASH_VOLTAGE_RANGE_3;

198HAL_FLASH_Unlock(); // Разблокировка Flash-памяти

199HAL_FLASHEx_Erase(&eraseInfo, &ulBadBlocks); // Стирание заданных секторов */

200HAL_FLASH_Lock(); // Снова блокировка Flash-памяти

Процесс начальной загрузки

593

202/* Отправка байта ACK */

203pucData[0] = ACK;

204HAL_UART_Transmit(&huart2, (uint8_t *) pucData, 1, HAL_MAX_DELAY);

205} else {

206/* CRC неверный: отправка байта NACK */

207pucData[0] = NACK;

208HAL_UART_Transmit(&huart2, pucData, 1, HAL_MAX_DELAY);

209}

210}

Приведенный выше код показывает, как реализована команда. Как видите, CRC извлекается из сообщения, поступающего по UART, и сравнивается с сообщением, вычисленным периферийным устройством CRC. Обратите внимание, что, поскольку периферийное устройство CRC имеет 32-разрядный регистр данных и CRC-32 вычисляется по всему регистру, мы преобразуем первые два байта в два 32-разрядных значения.

Если CRC совпадает, то экземпляр структуры FLASH_EraseInitTypeDef заполняется так, чтобы сектора Flash-памяти стирались, начиная со второго (строка 193), до числа указанных секторов (строка 194). Flash-память разблокируется (строка 198), и процедура стирания выполняется путем вызова процедуры HAL_FLASHEx_Erase().

Команда Write

Команда CMD_WRITE используется для сохранения шестнадцати байт (то есть четырех слов), начиная с заданной ячейки памяти, и имеет структуру, представленную на рисунке 6. Команда состоит из двух отдельных частей. Первая состоит из ID команды 0x2b, за которым следует начальный адрес размещения байт данных и CRC-32 команды. Если CRC совпадает и указанный адрес равен или превышает APP_START_ADDRESS, загрузчик отвечает байтом ACK. Загрузчик таким образом ожидает получения другой последовательности из шестнадцати байт и контрольной суммы CRC-32 этих байт.

Рисунок 6: Структура команды CMD_WRITE

Имя файла: src/main-bootloader.c

267void cmdWrite(uint8_t *pucData) {

268uint32_t ulSaddr = 0, ulCrc = 0;

270memcpy(&ulSaddr, pucData + 1, sizeof(uint32_t));

271memcpy(&ulCrc, pucData + 5, sizeof(uint32_t));

273uint32_t pulData[5];

274for(int i = 0; i < 5; i++)

275pulData[i] = pucData[i];

Процесс начальной загрузки

594

276

277/* Проверка правильности предоставленного CRC */

278if (ulCrc == HAL_CRC_Calculate(&hcrc, pulData, 5) && ulSaddr >= APP_START_ADDRESS) {

279/* Отправка байта ACK */

280pucData[0] = ACK;

281HAL_UART_Transmit(&huart2, (uint8_t *) pucData, 1, HAL_MAX_DELAY);

282

283/* Теперь получение заданного количества байт плюс CRC32 */

284if (HAL_UART_Receive(&huart2, pucData, 16 + 4, 200) == HAL_TIMEOUT)

285return;

286

287 memcpy(&ulCrc, pucData + 16, sizeof(uint32_t)); 288

289/* Проверка правильности предоставленного CRC */

290if (ulCrc == HAL_CRC_Calculate(&hcrc, (uint32_t*) pucData, 4)) {

291HAL_FLASH_Unlock(); // Разблокировка Flash-памяти

292

293/* Расшифровка отправленных байт с помощью алгоритма AES-128 ECB */

294aes_enc_dec((uint8_t*) pucData, AES_KEY, 1);

295for (uint8_t i = 0; i < 16; i++) {

296/* Сохранение каждого байта во Flash-памяти, начиная с заданного адреса */

297HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, ulSaddr, pucData[i]);

298ulSaddr += 1;

299}

300HAL_FLASH_Lock(); // Снова блокировка Flash-памяти

301

302/* Отправка байта ACK */

303pucData[0] = ACK;

304HAL_UART_Transmit(&huart2, (uint8_t *) pucData, 1, HAL_MAX_DELAY);

305} else {

306goto sendnack;

307}

308} else {

309goto sendnack;

310}

311

312sendnack:

313pucData[0] = NACK;

314HAL_UART_Transmit(&huart2, (uint8_t *) pucData, 1, HAL_MAX_DELAY);

315}

Приведенный выше код показывает, как реализована команда. Как видите, CRC первой части сообщения проверяется по переданному значению (строки [273:278]). Если она совпадает, то отправляется байт ACK и обрабатываются следующие байты. Если CRC-32 этих других байтов совпадает (строка 290), то отправленные байты данных дешифруются с использованием алгоритма AES-12817 и предварительного общего ключа (PSK). Байты данных сохраняются во Flash-памяти, начиная с заданной ячейки памяти.

17 Функция aes_enc_dec() взята из библиотеки Эрика Питерса (Eric Peeters), сотрудника TI. Ее можно загрузить с веб-сайта TI (http://www.ti.com/tool/AES-128), и лицензия библиотеки позволяет свободно

Процесс начальной загрузки

595

Существует еще один момент для анализа: функция CHECK_AND_SET_FLASH_PROTECTION() вызывается функцией main(), если макрос ENABLE_BOOTLOADER_PROTECTION установлен в 1.

Имя файла: src/main-bootloader.c

317void CHECK_AND_SET_FLASH_PROTECTION(void) {

318FLASH_OBProgramInitTypeDef obConfig;

319

320/* Получение текущего байта конфигурации */

321HAL_FLASHEx_OBGetConfig(&obConfig);

322

323/* Если первый сектор не защищен */

324if ((obConfig.WRPSector & OB_WRP_SECTOR_0) == OB_WRP_SECTOR_0) {

325HAL_FLASH_Unlock(); // Разблокировка Flash-памяти

326HAL_FLASH_OB_Unlock(); // Разблокировка байтов конфигурации

327obConfig.OptionType = OPTIONBYTE_WRP;

328obConfig.WRPState = OB_WRPSTATE_ENABLE; // Разрешение изменения параметров WRP

329obConfig.WRPSector = OB_WRP_SECTOR_0; // Включение защиты от записи в первом секторе

330HAL_FLASHEx_OBProgram(&obConfig); // Программирование байтов конфигурации

331HAL_FLASH_OB_Launch();// Гарантирует, что новая конфигурация сохранена во Flash-памяти

332HAL_FLASH_OB_Lock(); // Блокировка байтов конфигурации

333HAL_FLASH_Lock(); // Блокировка Flash-памяти

334}

335}

Данная функция просто извлекает текущую конфигурацию байтов конфигурации и проверяет, защищен ли первый сектор от записи (строка 324). Если нет, то включается защита от записи, таким образом загрузчик не может быть перезаписан.

Если вы хотите поэкспериментировать с данной функцией, то для отключения защиты от записи вы можете использовать STM32CubeProgrammer.

Некоторые соображения по поводу пользовательского

загрузчика

Пользовательский загрузчик, представленный здесь, далек от завершенного. В нем отсутствуют некоторые необходимые функции и, что наиболее важно, он недостаточно надежен в охвате некоторых ошибок. Более того, одиночный загрузчик для платформ STM32F0/L0 составляет около 13 КБ при компиляции с опцией GCC -Os, которая создает бинарный образ с оптимизацией размера. Это определенно слишком много для загрузчика. К сожалению, HAL имеет отнюдь не малые накладные расходы на окончательный размер бинарного образа. Хорошо спроектированный загрузчик программируется, сводя к минимуму занимаемое им пространство. Этот аспект выходит за рамки данной книги, которая просто показывает основные принципы, лежащие в основе процесса начальной загрузки.

использовать ее. ST предоставляет полноценную криптографическую библиотеку для платформы STM32,

которая также совместима с фреймворком Cube (http://www.st.com/content/st_com/en/products/embedded- software/mcus-embedded-software/stm32-embedded-software/stm32cube-expansion-software/x-cube-cryp- tolib.html?icmp=tt3888_gl_pron_jul2016). Данная библиотека также может использовать преимущества тех микроконтроллеров STM32, которые предоставляют специальный аппаратный модуль криптографии. Однако лицензия данной библиотеки запрещает автору книги поставлять библиотеку с примерами из этой книги.

Процесс начальной загрузки

596

22.3.1.Перемещение таблицы векторов в микроконтроллерах

STM32F0

До сих пор мы видели, что в микроконтроллерах на базе Cortex-M0 невозможно переместить таблицу векторов, как это происходит в микроконтроллерах Cortex-M0+/3/4/7. Это означает, что мы не можем использовать код, показанный ранее (в строках [154:161]), чтобы передать управление основной микропрограмме, потому что ядра Cortex-M0 всегда ожидают найти таблицу векторов по адресу 0x0000 0000, который совпадает с таблицей векторов загрузчика в нашем сценарии.

Однако мы можем обойти это ограничение немного хитрее. Идея, которую мы собираемся проанализировать, основана на том факте, что программное физическое перераспределение памяти позволяет отражать (alias) память SRAM на адрес 0x0000 0000, тогда как исходная Flash-память всегда доступна по адресу 0x0800 0000. Мы можем переместить таблицу векторов основной микропрограммы перед передачей управления его обработчику исключения сброса Reset, просто скопировав «целевую» таблицу векторов внутри SRAM и затем выполнив физическое перераспределение памяти. Адреса, содержащиеся в целевой таблице векторов, по-прежнему доступны в их исходных ячейках, что позволяет правильно выполнять обработчики исключений и ISR.

Рисунок 7 пытается продемонстрировать данную процедуру. С левой стороны у нас основное приложение (загрузчик не показан). Для простоты предположим, что его таблица векторов размещена по адресу 0x0800 2C00. Это означает, что, начиная с адреса 0x0800 2C04, у нас адрес обработчиков исключений и ISR в памяти ядра Cortex-M0. Ясно, что эти адреса указывают на другие ячейки памяти выше адреса 0x0800 2C00 (на рисунке 7 они представлены серыми стрелками).

Рисунок 7: Как можно перемещать таблицу векторов в микроконтроллерах STM32F0

Загрузчик работает следующим образом. Он копирует таблицу векторов в память SRAM, начиная с размещения ее содержимого с начального адреса 0x2000 0000. Это означает,

Процесс начальной загрузки

597

что с ячейки памяти 0x2000 0004 у нас адреса обработчиков исключений и ISR во Flashпамяти. Ясно, что эти адреса все еще указывают на те же исходные ячейки Flash-памяти, как показано черными стрелками на рисунке 7. В конце процедуры копирования память перераспределяется, так что адрес 0x0000 0000 теперь совпадает с адресом 0x2000 0000. Затем управление передается обработчику исключения сброса Reset основной микропрограммы и происходит ее выполнение. Таким образом, мы обошли ограничение микроконтроллеров на базе Cortex-M0, которые не позволяют перемещать таблицу векторов в памяти.

Следующий код показывает наш загрузчик, реализованный для микроконтроллера STM32F030. Показана только часть, касающаяся перемещения таблицы векторов.

Имя файла: src/main-bootloader.c

146} else {

147/* Похоже, во втором секторе существует оформленная должным образом программа:

148готовим микроконтроллер для запуска основной микропрограммы */

149MX_GPIO_Deinit(); // Перевод GPIO в состояние по умолчанию

150SysTick->CTRL = 0x0; // Отключение таймера SysTick и связанных с ним прерываний

151HAL_DeInit();

152

153 RCC->CIR = 0x00000000; // Запрет всех прерываний, связанных с тактированием

154

155uint32_t *pulSRAMBase = (uint32_t*)SRAM_BASE;

156uint32_t *pulFlashBase = (uint32_t*)APP_START_ADDRESS;

157uint16_t i = 0;

158

159do {

160if(pulFlashBase[i] == 0xAABBCCDD)

161break;

162pulSRAMBase[i] = pulFlashBase[i];

163} while(++i);

164

165 __set_MSP(*((volatile uint32_t*) APP_START_ADDRESS)); // Установка MSP 166

167 SYSCFG->CFGR1 |= 0x3; /* __HAL_RCC_SYSCFG_CLK_ENABLE() 168 уже вызван из HAL_MspInit() */

169

170/* Теперь мы готовы перейти к основной микропрограмме */

171uint32_t JumpAddress = *((volatile uint32_t*) (APP_START_ADDRESS + 4));

172void (*reset_handler)(void) = (void*)JumpAddress;

173reset_handler(); // Запускаем выполнение из Reset_Handler основной микропрограммы

175for (;;)

176; // Сюда никогда не приходим

177}

178}

Интересующий нас код начинается со строки 155. Определяются два указателя: один начинается с начала памяти SRAM (pulSRAMBase), а другой – с начала основной микропрограммы (pulFlashBase, который равен 0x0800 2C00, как и в предыдущем примере). Цикл в строках [159:163] делает копию таблицы векторов в SRAM, пока текущая ячейка