- •Оглавление
- •Предисловие
- •Почему я написал книгу?
- •Для кого эта книга?
- •Как использовать эту книгу?
- •Как организована книга?
- •Об авторе
- •Ошибки и предложения
- •Поддержка книги
- •Как помочь автору
- •Отказ от авторского права
- •Благодарность за участие
- •Перевод
- •Благодарности
- •I Введение
- •1. Введение в ассортимент микроконтроллеров STM32
- •1.1. Введение в процессоры на базе ARM
- •1.1.1. Cortex и процессоры на базе Cortex-M
- •1.1.1.10. Внедренные функции Cortex-M в ассортименте STM32
- •1.2. Введение в микроконтроллеры STM32
- •1.2.1. Преимущества ассортимента STM32….
- •1.2.2. ….И его недостатки
- •1.3. Краткий обзор подсемейств STM32
- •1.3.1. Серия F0
- •1.3.2. Серия F1
- •1.3.3. Серия F2
- •1.3.4. Серия F3
- •1.3.5. Серия F4
- •1.3.6. Серия F7
- •1.3.7. Серия H7
- •1.3.8. Серия L0
- •1.3.9. Серия L1
- •1.3.10. Серия L4
- •1.3.11. Серия L4+
- •1.3.12. Серия STM32WB
- •1.3.13. Как правильно выбрать для себя микроконтроллер?
- •1.4. Отладочная плата Nucleo
- •2. Установка инструментария
- •2.1. Почему выбирают Eclipse/GCC в качестве инструментария для STM32
- •2.1.1. Два слова о Eclipse…
- •2.2. Windows – Установка инструментария
- •2.2.1. Windows – Установка Eclipse
- •2.2.2. Windows – Установка плагинов Eclipse
- •2.2.3. Windows – Установка GCC ARM Embedded
- •2.2.4. Windows – Установка инструментов сборки
- •2.2.5. Windows – Установка OpenOCD
- •2.2.6. Windows – Установка инструментов ST и драйверов
- •2.3. Linux – Установка инструментария
- •2.3.2. Linux – Установка Java
- •2.3.3. Linux – Установка Eclipse
- •2.3.4. Linux – Установка плагинов Eclipse
- •2.3.5. Linux – Установка GCC ARM Embedded
- •2.3.6. Linux – Установка драйверов Nucleo
- •2.3.7. Linux – Установка OpenOCD
- •2.3.8. Linux – Установка инструментов ST
- •2.4. Mac – Установка инструментария
- •2.4.1. Mac – Установка Eclipse
- •2.4.2. Mac – Установка плагинов Eclipse
- •2.4.3. Mac – Установка GCC ARM Embedded
- •2.4.4. Mac – Установка драйверов Nucleo
- •2.4.5. Mac – Установка OpenOCD
- •2.4.6. Mac – Установка инструментов ST
- •3. Hello, Nucleo!
- •3.1. Прикоснитесь к Eclipse IDE
- •3.2. Создание проекта
- •3.3. Подключение Nucleo к ПК
- •3.5. Изучение сгенерированного кода
- •4. Инструмент STM32CubeMX
- •4.1. Введение в инструмент CubeMX
- •4.1.1. Представление Pinout
- •4.1.2. Представление Clock Configuration
- •4.1.3. Представление Configuration
- •4.1.4. Представление Power Consumption Calculator
- •4.2. Генерация проекта
- •4.2.1. Генерация проекта Си при помощи CubeMX
- •4.2.2. Создание проекта Eclipse
- •4.2.3. Ручное импортирование сгенерированных файлов в проект Eclipse
- •4.3. Изучение сгенерированного кода приложения
- •4.3.1. Добавим что-нибудь полезное в микропрограмму
- •4.4. Загрузка исходного кода примеров книги
- •5. Введение в отладку
- •5.1. Начало работы с OpenOCD
- •5.1.1. Запуск OpenOCD
- •5.1.2. Подключение к OpenOCD Telnet Console
- •5.1.3. Настройка Eclipse
- •5.1.4. Отладка в Eclipse
- •5.2. Полухостинг ARM
- •5.2.1. Включение полухостинга в новом проекте
- •5.2.2. Включение полуохостинга в существующем проекте
- •5.2.3. Недостатки полухостинга
- •5.2.4. Как работает полухостинг
- •II Погружение в HAL
- •6. Управление GPIO
- •6.2. Конфигурация GPIO
- •6.2.1. Режимы работы GPIO
- •6.2.2. Режим альтернативной функции GPIO
- •6.2.3. Понятие скорости GPIO
- •6.3. Управление GPIO
- •6.4. Деинициализация GPIO
- •7. Обработка прерываний
- •7.1. Контроллер NVIC
- •7.1.1. Таблица векторов в STM32
- •7.2. Разрешение прерываний
- •7.2.1. Линии запроса внешних прерываний и контроллер NVIC
- •7.2.2. Разрешение прерываний в CubeMX
- •7.3. Жизненный цикл прерываний
- •7.4. Уровни приоритета прерываний
- •7.4.1. Cortex-M0/0+
- •7.4.2. Cortex-M3/4/7
- •7.4.3. Установка уровня прерываний в CubeMX
- •7.5. Реентерабельность прерываний
- •8. Универсальные асинхронные последовательные средства связи
- •8.1. Введение в UART и USART
- •8.2. Инициализация UART
- •8.3. UART-связь в режиме опроса
- •8.3.1. Установка консоли последовательного порта в Windows
- •8.3.2. Установка консоли последовательного порта в Linux и MacOS X
- •8.4. UART-связь в режиме прерываний
- •8.5. Обработка ошибок
- •8.6. Перенаправление ввода-вывода
- •9. Управление DMA
- •9.1. Введение в DMA
- •9.1.1. Необходимость DMA и роль внутренних шин
- •9.1.2. Контроллер DMA
- •9.2. Модуль HAL_DMA
- •9.2.1. DMA_HandleTypeDef в HAL для F0/F1/F3/L0/L1/L4
- •9.2.2. DMA_HandleTypeDef в HAL для F2/F4/F7
- •9.2.3. DMA_HandleTypeDef в HAL для L0/L4
- •9.2.4. Как выполнять передачи в режиме опроса
- •9.2.5. Как выполнять передачи в режиме прерываний
- •9.2.8. Разнообразные функции модулей HAL_DMA и HAL_DMA_Ex
- •9.3. Использование CubeMX для конфигурации запросов к DMA
- •10. Схема тактирования
- •10.1. Распределение тактового сигнала
- •10.1.1. Обзор схемы тактирования STM32
- •10.1.1.1. Многочастотный внутренний RC-генератор в семействах STM32L
- •10.1.3.1. Подача тактового сигнала от высокочастотного генератора
- •10.1.3.2. Подача тактового сигнала от 32кГц генератора
- •10.2. Обзор модуля HAL_RCC
- •10.2.1. Вычисление тактовой частоты во время выполнения
- •10.2.2. Разрешение Выхода синхронизации
- •10.2.3. Разрешение Системы защиты тактирования
- •10.3. Калибровка HSI-генератора
- •11. Таймеры
- •11.1. Введение в таймеры
- •11.1.1. Категории таймеров в микроконтроллере STM32
- •11.1.2. Доступность таймеров в ассортименте STM32
- •11.2. Базовые таймеры
- •11.2.1. Использование таймеров в режиме прерываний
- •11.2.2. Использование таймеров в режиме опроса
- •11.2.3. Использование таймеров в режиме DMA
- •11.2.4. Остановка таймера
- •11.3. Таймеры общего назначения
- •11.3.1.1. Режим внешнего тактирования 2
- •11.3.1.2. Режим внешнего тактирования 1
- •11.3.2. Режимы синхронизации ведущего/ведомого таймеров
- •11.3.2.1. Разрешение прерываний, относящихся к триггерной цепи
- •11.3.2.2. Использование CubeMX для конфигурации синхронизации ведущего/ведомого устройств
- •11.3.3. Программная генерация связанных с таймером событий
- •11.3.4. Режимы отсчета
- •11.3.5. Режим захвата входного сигнала
- •11.3.5.1. Использование CubeMX для конфигурации режима захвата входного сигнала
- •11.3.6. Режим сравнения выходного сигнала
- •11.3.6.1. Использование CubeMX для конфигурации режима сравнения выходного сигнала
- •11.3.7. Генерация широтно-импульсного сигнала
- •11.3.7.1. Генерация синусоидального сигнала при помощи ШИМ
- •11.3.7.2. Использование CubeMX для конфигурации режима ШИМ
- •11.3.8. Одноимпульсный режим
- •11.3.8.1. Использование CubeMX для конфигурации одноимпульсного режима
- •11.3.9. Режим энкодера
- •11.3.9.1. Использование CubeMX для конфигурации режима энкодера
- •11.3.10.1. Режим датчика Холла
- •11.3.10.2. Комбинированный режим трехфазной ШИМ и другие функции управления двигателем
- •11.3.10.3. Вход сброса таймера и блокировка регистров таймера
- •11.3.10.4. Предварительная загрузка регистра автоперезагрузки
- •11.3.11. Отладка и таймеры
- •11.4. Системный таймер SysTick
- •12. Аналого-цифровое преобразование
- •12.1. Введение в АЦП последовательного приближения
- •12.2. Модуль HAL_ADC
- •12.2.1. Режимы преобразования
- •12.2.1.1. Режим однократного преобразования одного канала
- •12.2.1.2. Режим сканирования с однократным преобразованием
- •12.2.1.3. Режим непрерывного преобразования одного канала
- •12.2.1.4. Режим сканирования с непрерывным преобразованием
- •12.2.1.5. Режим преобразования инжектированных каналов
- •12.2.1.6. Парный режим
- •12.2.2. Выбор канала
- •12.2.3. Разрядность АЦП и скорость преобразования
- •12.2.4. Аналого-цифровые преобразования в режиме опроса
- •12.2.6. Аналого-цифровые преобразования в режиме DMA
- •12.2.6.1. Многократное преобразование одного канала в режиме DMA
- •12.2.6.3. Непрерывные преобразования в режиме DMA
- •12.2.7. Обработка ошибок
- •12.2.8. Преобразования, управляемые таймером
- •12.2.9. Преобразования, управляемые внешними событиями
- •12.2.10. Калибровка АЦП
- •12.3. Использование CubeMX для конфигурации АЦП
- •13.1. Введение в периферийное устройство ЦАП
- •13.2. Модуль HAL_DAC
- •13.2.1. Управление ЦАП вручную
- •13.2.2. Управление ЦАП в режиме DMA с использованием таймера
- •13.2.3. Генерация треугольного сигнала
- •13.2.4. Генерация шумового сигнала
- •14.1. Введение в спецификацию I²C
- •14.1.1. Протокол I²C
- •14.1.1.1. START- и STOP-условия
- •14.1.1.2. Формат байта
- •14.1.1.3. Кадр адреса
- •14.1.1.4. Биты «Подтверждено» (ACK) и «Не подтверждено» (NACK)
- •14.1.1.5. Кадры данных
- •14.1.1.6. Комбинированные транзакции
- •14.1.1.7. Удержание синхросигнала
- •14.1.2. Наличие периферийных устройств I²C в микроконтроллерах STM32
- •14.2. Модуль HAL_I2C
- •14.2.1.1. Операции I/O MEM
- •14.2.1.2. Комбинированные транзакции
- •14.3. Использование CubeMX для конфигурации периферийного устройства I²C
- •15.1. Введение в спецификацию SPI
- •15.1.1. Полярность и фаза тактового сигнала
- •15.1.2. Управление сигналом Slave Select
- •15.1.3. Режим TI периферийного устройства SPI
- •15.1.4. Наличие периферийных устройств SPI в микроконтроллерах STM32
- •15.2. Модуль HAL_SPI
- •15.2.1. Обмен сообщениями с использованием периферийного устройства SPI
- •15.2.2. Максимальная частота передачи, достижимая при использовании CubeHAL
- •15.3. Использование CubeMX для конфигурации периферийного устройства SPI
- •16. Циклический контроль избыточности
- •16.1. Введение в расчет CRC
- •16.1.1. Расчет CRC в микроконтроллерах STM32F1/F2/F4/L1
- •16.2. Модуль HAL_CRC
- •17. Независимый и оконный сторожевые таймеры
- •17.1. Независимый сторожевой таймер
- •17.1.1. Использование CubeHAL для программирования таймера IWDG
- •17.2. Системный оконный сторожевой таймер
- •17.2.1. Использование CubeHAL для программирования таймера WWDG
- •17.3. Отслеживание системного сброса, вызванного сторожевым таймером
- •17.4. Заморозка сторожевых таймеров во время сеанса отладки
- •17.5. Выбор сторожевого таймера, подходящего для вашего приложения
- •18. Часы реального времени
- •18.1. Введение в периферийное устройство RTC
- •18.2. Модуль HAL_RTC
- •18.2.1. Установка и получение текущей даты/времени
- •18.2.1.1. Правильный способ чтения значений даты/времени
- •18.2.2. Конфигурирование будильников
- •18.2.3. Блок периодического пробуждения
- •18.2.5. Калибровка RTC
- •18.2.5.1. Грубая калибровка RTC
- •18.2.5.2. Тонкая калибровка RTC
- •18.2.5.3. Обнаружение опорного тактового сигнала
- •18.3. Использование резервной SRAM
- •III Дополнительные темы
- •19. Управление питанием
- •19.1. Управление питанием в микроконтроллерах на базе Cortex-M
- •19.2. Как микроконтроллеры Cortex-M управляют рабочим и спящим режимами
- •19.2.1. Переход в/выход из спящих режимов
- •19.2.1.1. «Спящий режим по выходу»
- •19.3. Управление питанием в микроконтроллерах STM32F
- •19.3.1. Источники питания
- •19.3.2. Режимы питания
- •19.3.2.1. Рабочий режим
- •19.3.2.2. Спящий режим
- •19.3.2.3. Режим останова
- •19.3.2.4. Режим ожидания
- •19.3.2.5. Пример работы в режимах пониженного энергопотребления
- •19.4. Управление питанием в микроконтроллерах STM32L
- •19.4.1. Источники питания
- •19.4.2. Режимы питания
- •19.4.2.1. Рабочие режимы
- •19.4.2.2. Спящие режимы
- •19.4.2.2.1. Режим пакетного сбора данных
- •19.4.2.3. Режимы останова
- •19.4.2.4. Режимы ожидания
- •19.4.2.5. Режим выключенного состояния
- •19.4.3. Переходы между режимами питания
- •19.4.4. Периферийные устройства с пониженным энергопотреблением
- •19.4.4.1. LPUART
- •19.4.4.2. LPTIM
- •19.5. Инспекторы источников питания
- •19.6. Отладка в режимах пониженного энергопотребления
- •19.7. Использование калькулятора энергопотребления CubeMX
- •20. Организация памяти
- •20.1. Модель организации памяти в STM32
- •20.1.1. Основы процессов компиляции и компоновки
- •20.2.1. Исследование бинарного ELF-файла
- •20.2.2. Инициализация секций .data и .bss
- •20.2.2.1. Пара слов о секции COMMON
- •20.2.3. Секция .rodata
- •20.2.4. Области Стека и Кучи
- •20.2.5. Проверка размера Кучи и Стека на этапе компиляции
- •20.2.6. Различия с файлами скриптов инструментария
- •20.3. Как использовать CCM-память
- •20.3.1. Перемещение таблицы векторов в CCM-память
- •20.4.1. Программирование MPU с использованием CubeHAL
- •21. Управление Flash-памятью
- •21.1. Введение во Flash-память STM32
- •21.2. Модуль HAL_FLASH
- •21.2.1. Разблокировка Flash-памяти
- •21.2.2. Стирание Flash-памяти
- •21.2.3. Программирование Flash-памяти
- •21.3. Байты конфигурации
- •21.3.1. Защита от чтения Flash-памяти
- •21.4. Дополнительные памяти OTP и EEPROM
- •21.5. Задержка чтения Flash-памяти и ускоритель ART™ Accelerator
- •21.5.1. Роль TCM-памятей в микроконтроллерах STM32F7
- •22. Процесс начальной загрузки
- •22.1.1. Программное физическое перераспределение памяти
- •22.1.2. Перемещение таблицы векторов
- •22.1.3. Запуск микропрограммы из SRAM с помощью инструментария GNU MCU Eclipse
- •22.2. Встроенный загрузчик
- •22.2.1. Запуск загрузчика из встроенного программного обеспечения
- •22.2.2. Последовательность начальной загрузки в инструментарии GNU MCU Eclipse
- •22.3. Разработка пользовательского загрузчика
- •22.3.2. Как использовать инструмент flasher.py
- •23. Запуск FreeRTOS
- •23.1. Введение в концепции, лежащие в основе ОСРВ
- •23.2.1. Структура файлов с исходным кодом FreeRTOS
- •23.2.1.2. Как импортировать FreeRTOS с использованием CubeMX и CubeMXImporter
- •23.3. Управление потоками
- •23.3.1. Состояния потоков
- •23.3.2. Приоритеты потоков и алгоритмы планирования
- •23.3.3. Добровольное освобождение от управления
- •23.3.4. Холостой поток idle
- •23.4. Выделение памяти и управление ею
- •23.4.1. Модель динамического выделения памяти
- •23.4.1.1. heap_1.c
- •23.4.1.2. heap_2.c
- •23.4.1.3. heap_3.c
- •23.4.1.4. heap_4.c
- •23.4.1.5. heap_5.c
- •23.4.2. Модель статического выделения памяти
- •23.4.3. Пулы памяти
- •23.4.4. Обнаружение переполнения стека
- •23.5. Примитивы синхронизации
- •23.5.1. Очереди сообщений
- •23.5.2. Cемафоры
- •23.5.3. Сигналы потоков
- •23.6. Управление ресурсами и взаимное исключение
- •23.6.1. Мьютексы
- •23.6.2. Критические секции
- •23.6.3. Обработка прерываний совместно с ОСРВ
- •23.7. Программные таймеры
- •23.7.1. Как FreeRTOS управляет таймерами
- •23.8. Пример из практики: Управление энергосбережением с ОСРВ
- •23.8.1. Перехват холостого потока idle
- •23.8.2. Бестиковый режим во FreeRTOS
- •23.9. Возможности отладки
- •23.9.1. Макрос configASSERT()
- •23.9.2. Статистика среды выполнения и информация о состоянии потоков
- •23.10. Альтернативы FreeRTOS
- •23.10.1. ChibiOS
- •23.10.2. ОС Contiki
- •23.10.3. OpenRTOS
- •24. Продвинутые методы отладки
- •24.1. Введение в исключения отказов Cortex-M
- •24.1.1.1. Как инструментарий GNU MCU Eclipse обрабатывает исключения отказов
- •24.1.1.2. Как интерпретировать содержимое регистра LR при переходе в исключение
- •24.1.2. Исключения отказов и их анализ
- •24.2.1. Представление Expressions
- •24.2.1.1. Мониторы памяти
- •24.2.2. Точки наблюдения
- •24.2.3. Режим Instruction Stepping Mode
- •24.2.4. Keil Packs и представление Peripheral Registers
- •24.2.5. Представление Core Registers
- •24.3. Средства отладки от CubeHAL
- •24.4. Внешние отладчики
- •24.4.1. Использование SEGGER J-Link для отладчика ST-LINK
- •24.4.2. Использование интерфейса ITM и трассировка SWV
- •24.5. STM Studio
- •24.6. Одновременная отладка двух плат Nucleo
- •25. Файловая система FAT
- •25.1. Введение в библиотеку FatFs
- •25.1.1. Использование CubeMX для включения в ваши проекты библиотеки FatFs
- •25.1.2. Наиболее важные структуры и функции FatFs
- •25.1.2.1. Монтирование файловой системы
- •25.1.2.2. Открытие файлов
- •25.1.2.3. Чтение и запись файла
- •25.1.2.4. Создание и открытие каталога
- •25.1.3. Как сконфигурировать библиотеку FatFs
- •26. Разработка IoT-приложений
- •26.2. Ethernet контроллер W5500
- •26.2.1. Как использовать шилд W5500 и модуль ioLibrary_Driver
- •26.2.1.1. Конфигурирование интерфейса SPI
- •26.2.1.2. Настройка буферов сокетов и сетевого интерфейса
- •26.2.2. API-интерфейсы сокетов
- •26.2.2.1. Управление сокетами в режиме TCP
- •26.2.2.2. Управление сокетами в режиме UDP
- •26.2.3. Перенаправление ввода-вывода на сокет TCP/IP
- •26.2.4. Настройка HTTP-сервера
- •26.2.4.1. Веб-осциллограф
- •27. Начало работы над новым проектом
- •27.1. Проектирование оборудования
- •27.1.1. Послойная разводка печатной платы
- •27.1.2. Корпус микроконтроллера
- •27.1.3. Развязка выводов питания
- •27.1.4. Тактирование
- •27.1.5. Фильтрация вывода сброса RESET
- •27.1.6. Отладочный порт
- •27.1.7. Режим начальной загрузки
- •27.1.8. Обратите внимание на совместимость с выводами…
- •27.1.9. …и на выбор подходящей периферии
- •27.1.10. Роль CubeMX на этапе проектирования платы
- •27.1.11. Стратегии разводки платы
- •27.2. Разработка программного обеспечения
- •27.2.1. Генерация бинарного образа для производства
- •Приложение
- •Принудительный сброс микроконтроллера из микропрограммы
- •B. Руководство по поиску и устранению неисправностей
- •Проблемы с установкой GNU MCU Eclipse
- •Проблемы, связанные с Eclipse
- •Eclipse не может найти компилятор
- •Eclipse постоянно прерывается при выполнении каждой инструкции во время сеанса отладки
- •Пошаговая отладка очень медленная
- •Микропрограмма работает только в режиме отладки
- •Проблемы, связанные с STM32
- •Микроконтроллер не загружается корректно
- •Невозможно загрузить микропрограмму или отладить микроконтроллер
- •C. Схема выводов Nucleo
- •Nucleo-F446RE
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F411RE
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F410RB
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F401RE
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F334R8
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F303RE
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F302R8
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F103RB
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F091RC
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F072RB
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F070RB
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-F030R8
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-L476RG
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-L152RE
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-L073R8
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •Nucleo-L053R8
- •Разъемы, совместимые с Arduino
- •Morpho-разъемы
- •D. Корпусы STM32
- •LFBGA
- •LQFP
- •TFBGA
- •TSSOP
- •UFQFPN
- •UFBGA
- •VFQFP
- •WLCSP
- •E. Изменения книги
- •Выпуск 0.1 – Октябрь 2015
- •Выпуск 0.2 – 28 октября 2015
- •Выпуск 0.2.1 – 31 октября 2015
- •Выпуск 0.2.2 – 1 ноября 2015
- •Выпуск 0.3 – 12 ноября 2015
- •Выпуск 0.4 – 4 декабря 2015
- •Выпуск 0.5 – 19 декабря 2015
- •Выпуск 0.6 – 18 января 2016
- •Выпуск 0.6.1 – 20 января 2016
- •Выпуск 0.6.2 – 30 января 2016
- •Выпуск 0.7 – 8 февраля 2016
- •Выпуск 0.8 – 18 февраля 2016
- •Выпуск 0.8.1 – 23 февраля 2016
- •Выпуск 0.9 – 27 марта 2016
- •Выпуск 0.9.1 – 28 марта 2016
- •Выпуск 0.10 – 26 апреля 2016
- •Выпуск 0.11 – 27 мая 2016
- •Выпуск 0.11.1 – 3 июня 2016
- •Выпуск 0.11.2 – 24 июня 2016
- •Выпуск 0.12 – 4 июля 2016
- •Выпуск 0.13 – 18 июля 2016
- •Выпуск 0.14 – 12 августа 2016
- •Выпуск 0.15 – 13 сентября 2016
- •Выпуск 0.16 – 3 октября 2016
- •Выпуск 0.17 – 24 октября 2016
- •Выпуск 0.18 – 15 ноября 2016
- •Выпуск 0.19 – 29 ноября 2016
- •Выпуск 0.20 – 28 декабря 2016
- •Выпуск 0.21 – 29 января 2017
- •Выпуск 0.22 – 2 мая 2017
- •Выпуск 0.23 – 20 июля 2017
- •Выпуск 0.24 – 11 декабря 2017
- •Выпуск 0.25 – 3 января 2018
- •Выпуск 0.26 – 7 мая 2018
Процесс начальной загрузки |
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, пока текущая ячейка