- •Оглавление
- •Предисловие
- •Почему я написал книгу?
- •Для кого эта книга?
- •Как использовать эту книгу?
- •Как организована книга?
- •Об авторе
- •Ошибки и предложения
- •Поддержка книги
- •Как помочь автору
- •Отказ от авторского права
- •Благодарность за участие
- •Перевод
- •Благодарности
- •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
20. Организация памяти
Каждый раз, когда мы компилируем нашу микропрограмму, используя инструментарий GCC ARM, происходит ряд нетривиальных вещей. Компилятор переводит исходный код Си в ассемблерный код для ARM и подготавливает его для загрузки в используемый микроконтроллер STM32. Каждая микропроцессорная архитектура определяет модель выполнения, которая должна быть «согласована» с моделью выполнения языка программирования Си. Это означает, что во время начальной загрузки выполняется несколько операций, задачей которых является подготовка среды выполнения для нашего приложения: создание стека и кучи, инициализация памяти данных, инициализация таблицы векторов – это лишь некоторые из действий, выполняемых во время запуска. Более того, некоторые микроконтроллеры STM32 предоставляют дополнительные виды памяти или позволяют взаимодействовать с внешней с помощью контроллера FSMC, которая может быть завязана на выполнение особых задач в течение жизненного цикла микропрограммы.
Данная глава призвана пролить свет на те вопросы, которые являются общими для многих разработчиков STM32. Что происходит при сбросе микроконтроллера? Почему функция main()обязательна? И сколько требуется времени до начала выполнения после сброса микроконтроллера? Как хранить переменные во Flash-памяти вместо SRAM? Как использовать CCM-память STM32?
20.1. Модель организации памяти в STM32
В Главе 1 мы проанализировали, как микроконтроллеры STM32 организуют адресное пространство памяти объемом 4 ГБ. Рисунок 4 из этой главы четко показывает, как первые 0,5 ГБ памяти выделяются под область кода. В свою очередь, данная область подразделяется на несколько подобластей. Наиболее важная из них, начинающаяся с адреса 0x0800 0000, предназначена для отображения внутренней Flash-памяти. Вместо этого внутренняя память SRAM начинается с адреса 0x2000 0000 и состоит из нескольких подобластей, предназначенных для определенных задач, которые мы вскоре увидим.
На рисунке 1 показана типовая организации Flash-памяти и памяти SRAM (статическое ОЗУ) в микроконтроллере STM321. В Главе 7 мы узнали, что начальные байты Flash-
памяти выделены под указатель основного стека (Main Stack Pointer, MSP) и таблицу век-
торов2. MSP содержит адрес начала стека. Архитектура Cortex-M дает максимальную свободу в размещении стека в памяти SRAM, а также в других типах внутренней (например, RAM CCM, доступной в некоторых микроконтроллерах STM32) или внешней (подключенной к контроллеру FSMC) памятях. Это объясняет необходимость MSP.
1Важно отметить, что эта организация отражает только одну из возможных конфигураций памяти, и она изменяется в случае использования ОСРВ. Тем не менее, основные концепции остаются прежними, и здесь будет лучшим рассмотреть данную организацию памяти.
2Помните, что, как мы увидим далее, архитектура Cortex-M определяет адрес 0x0000 0000 как ячейку памяти, с которой начинается размещение MSP и таблицы векторов. Это означает, что начальный адрес
Flash-памяти (0x0800 0000) отражен (aliased) на 0x0000 0000.
Организация памяти |
513 |
Flash-память также может использоваться для хранения данных только для чтения (данные RO), также известных как неизменяемые данные (const data), поскольку переменные, объявленные как const, автоматически размещаются в этой памяти. Наконец, Flashпамять содержит ассемблерный код, сгенерированный из исходного кода Си.
Рисунок 1: Типовая организация Flash-памяти и памяти SRAM
Память SRAM также организована в виде нескольких подобластей. Область переменного размера, начинающаяся с конца SRAM и растущая вниз (то есть ее базовый адрес – наибольший адрес в SRAM), выделяется под стек (stack). Это происходит потому, что ядра Cortex-M используют модель стековой памяти, которая называется нисходящим сте-
ком с указателем на занятый элемент (full-descending stack). Базовый указатель стека,
также называемый указателем основного стека (MSP), вычисляется на этапе компиляции и сохраняется по адресу 0x0800 0000 во Flash-памяти. Как только мы вызываем функцию, в стек помещается новый кадр стека (stack frame). Это означает, что указатель на текущий кадр стека (SP) автоматически декрементируется при каждом вызове функции (например, инструкция push ассемблера ARM автоматически декрементирует его).
SRAM также используется для хранения изменяемых данных (variable data), и данная область обычно начинается в начале SRAM (0x2000 0000). В свою очередь, данная область делится между инициализированными и неинициализированными данными. Для понима-
ния разницы между ними, давайте рассмотрим следующий фрагмент кода:
Организация памяти |
514 |
...
uint8_t var1 = 0xEF;
uint8_t var2;
...
var1 и var2 – две глобальные переменные. var1 является инициализированной переменной (мы фиксируем ее начальное значение во время компиляции), в то время как значение var2 неинициализировано: среда выполнения может инициализировать ее как ноль. По той же причине у нас есть две секции .data: одна хранится во Flash-памяти, а другая – в оперативной памяти, как мы увидим далее.
Наконец, память SRAM может содержать еще одну растущую область: кучу (heap). В ней хранятся переменные, которые выделяются динамически во время выполнения микропрограммы (с помощью процедуры Си malloc() или аналогичной). Данная область, в свою очередь, может быть организована в несколько подобластей в соответствии с ис-
пользуемым распределителем памяти или аллокатором, англ. allocator (в следующей главе мы увидим, как FreeRTOS предоставляет несколько аллокаторов для управления динамическим выделением памяти). Куча растет вверх (то есть базовый адрес является наименьшим в своей области) и имеет фиксированный максимальный размер.
С точки зрения компилятора, эти секции традиционно называются по-разному внутри бинарного файла приложения. Например, секция, содержащая ассемблерный код, называется .text, .rodata – секция, содержащая неизменяемые переменные и строки, а секция с инициализированными данными – .data. Данные имена также являются общими для других компьютерных архитектур, таких как x86 и MIPS. Другие же специфичны для «мира микроконтроллеров». Например, секция .isr_vector предназначена для хранения таблицы векторов в микроконтроллерах на базе Cortex-M3.
Поскольку каждый микроконтроллер STM32 имеет свое собственное количество SRAM и Flash-памяти, и поскольку каждая программа имеет разное количество команд и переменных, то размеры и расположение этих секций в памяти различаются. Прежде чем мы увидим, как дать указание компилятору сгенерировать бинарный файл для конкретного микроконтроллера, мы должны понять все шаги и инструменты, задействованные во время генерации объектных файлов (object files).
20.1.1. Основы процессов компиляции и компоновки
Процесс, начинающийся с компиляции исходного кода Си и заканчивающийся генерацией конечного бинарного образа для загрузки на наш микроконтроллер, включает в себя несколько шагов и инструментов, предоставляемых инструментарием GCC. Рисунок 2 пытается обрисовать в общих чертах данный процесс. Все начинается с файлов с исходным кодом Си. Обычно они содержат следующие программные структуры.
•Глобальные переменные: их можно в свою очередь разделить на неинициализированные и инициализированные переменные; глобальная переменная также может быть определена как статическая (static), то есть ее видимость ограничена текущим файлом с исходным кодом.
•Локальные переменные: их можно разделить между простыми локальными (также называемыми автоматическими) переменными и статическими локальными
3 Однако, как мы увидим далее, это имя – всего лишь соглашение.
Организация памяти |
515 |
переменными (то есть теми переменными, время жизни которых длится все время выполнения программы).
•Неизменяемые (постоянные) данные: они могут быть в свою очередь разделены на константные типы данных (например, const int c = 5) и строковые константы
(например, "Hello World!").
•Процедуры (подпрограммы): они формируют программу и будут транслированы в ассемблерные инструкции.
•Внешние ресурсы: это глобальные переменные (объявленные как extern) и процедуры, определенные в других исходных файлах. Задачей компоновщика будет «связать» ссылки на их символьные имена, определенные в других файлах с исходным кодом, и объединить секции, поступающие из соответствующих бинарных файлов.
Рисунок 2: Процесс компиляции из файла с исходным кодом в конечный бинарный образ
Как только файл с исходным кодом скомпилирован, вышеприведенные программные структуры отображаются в определенных секциях бинарного файла. В таблице 1 приведены наиболее важные из них.
Таблица 1: Отображение структур программы и секций бинарных файлов
|
Секция |
Область памяти |
Структура языка |
бинарного файла |
при выполнении |
|
|
|
Глобальные неинициализированные переменные |
.common |
Данные (SRAM) |
Глобальные инициализированные переменные |
.data |
Данные (SRAM+Flash) |
Глобальные статические неинициализированные |
.bss |
Данные (SRAM) |
переменные |
|
|
Глобальные статические инициализированные |
.data |
Данные (SRAM+Flash) |
переменные |
|
|
Локальные переменные |
<не определена> |
Стек или куча (SRAM) |
Локальные статические неинициализированные |
.bss |
Данные (SRAM) |
переменные |
|
|
Локальные статические инициализированные |
.data |
переменные |
|
Неизменяемые (постоянные) типы данных |
.rodata |
Неизменяемые (постоянные) строки |
.rodata.1 |
Процедуры |
.text |
Данные (SRAM+Flash)
Код (Flash) Код (Flash) Код (Flash)
Организация памяти |
516 |
Для каждого файла с исходным кодом (.c), составляющего наше приложение, компилятор сгенерирует соответствующий объектный файл (.o), который содержит секции из таблицы 14. Объектный файл – это тип бинарного файла, который придерживается широко известному стандарту. Существует множество стандартов для бинарных файлов (PE, COFF, ELF и т. д.). GCC ARM использует ELF32 – достаточно популярный открытый стандарт благодаря его использованию в операционных системах на основе Linux и широко поддерживаемый прочими инструментами, такими как OpenOCD и STM32CubeProgrammer. Однако файлы, оканчивающиеся на .o5, представляют собой особый тип объектных файлов. Они также известны как перемещаемые файлы (relocatable files). Это название происходит от того факта, что все адреса в памяти, содержащиеся в этом типе файла, относительные и начинаются с адреса 0x0000 0000. Это также означает, что секция .text будет начинаться с этого адреса, а мы знаем, что он отличается от начального адреса Flash-памяти (0x0800 0000) в микроконтроллере STM326.
Начиная с последовательности перемещаемых файлов (плюс некоторые другие конфигурационные файлы, которые мы увидим через некоторое время), компоновщик будет собирать их содержимое в один общий объектный файл, который будет представлять собой нашу микропрограмму для загрузки в микроконтроллер. В этом процессе, называемом компоновкой (linking), компоновщик переместит (relocate) все относительные адреса в фактические адреса в памяти. Этот тип файла также известен как абсолютный файл (absolute file), потому что все адреса являются абсолютными и специфичными для используемого микроконтроллера STM327.
Как компоновщик знает, где разместить в памяти секции, содержащиеся в абсолютном файле? Именно благодаря скриптам компоновщика, англ. linker scripts, (файлы, оканчивающиеся на .ld) мы можем выстроить содержимое абсолютного файла в соответствии с фактической организацией памяти. Мы уже видели скрипт компоновщика в Главе 4, когда сконфигурировали файл mem.ld для указания правильного начального адреса (origin address) Flash-памяти. CubeMX также встраивает правильный скрипт компоновщика для нашего микроконтроллера в сгенерированный проект Си (он содержится во
4Важно подчеркнуть, что объектный файл содержит гораздо больше секций. Большинство из них связаны с отладкой и содержат соответствующую информацию, такую как исходный код, все символьные имена, содержащиеся в исходном файле (даже те, которые были оптимизированы компилятором) и так далее. Однако для целей данного обсуждения лучше их не учитывать.
5Имейте в виду, что с точки зрения компилятора расширение файла – это просто соглашение.
6Те из вас, кто хочет углубиться в данный вопрос, могут взглянуть на инструмент readelf, представленный в инструментарии GCC ARM.
7Здесь, опять же, более сложная ситуация. Прежде всего, компоновщик может собрать другие части из нескольких внешних статически связанных библиотек (заканчивающихся на .a). Эти библиотеки, также известные как архивные файлы, представляют собой не более чем объединение нескольких перемещаемых файлов. В процессе компоновки только те программные структуры, которые фактически используются в нашем приложении, будут объединены с конечной микропрограммой. Другой важный аспект, который следует отметить, заключается в том, что данный процесс практически одинаков для каждой микропроцессорной платформы (например, x86 и т. д.), и он также называется статической компоновкой (static linking). Более мощные архитектуры сталкиваются с продвинутым процессом компоновки, также известным как динамическая компоновка (dynamic linking), которая откладывает процесс компоновки до момента, когда программа будет загружена как процесс ОС. Это позволяет существенно уменьшить размер исполняемых файлов и обновлять зависимости библиотек без перекомпиляции всего приложения. В библиоте-
ках с динамической компоновкой, также называемых разделяемыми объектами, англ. shared objects (или раз-
деляемыми библиотеками (shared libraries), или DLL в Windows), и в современных операционных системах можно совместно использовать одну и ту же секцию .text этих библиотек между процессами, которые их используют, воспользовавшись mmap() или аналогичным системными вызовами. Это также позволяет сократить потребление SRAM процессами (подумайте о тысячах системных библиотек, которые должны быть «дублированы» в нескольких процессах, работающих на современном ПК).
Организация памяти |
517 |
вложенной папке SW4STM32). Однако довольно сложно изучить содержимое этих скриптов, если мы не освоим несколько концепций, оговоренных ранее. Поэтому лучше всего начать постепенно создавать голый фундамент приложения STM32.
20.2. Действительно минимальное приложение
STM32
Большинство приложений, рассмотренных до сих пор, кажутся действительно простыми. Вместо этого, как с точки зрения организации памяти, так и с точки зрения операций, выполняемых при начальной загрузке микроконтроллера, «под капотом» они уже выполняют множество операций. По этой причине мы собираемся создать действительно базовое приложение.
Первый шаг – создание пустого проекта с использованием Eclipse. Перейдите в меню File → New → C Project. Выберите тип проекта Empty project и выберите инструментарий Cross ARM GCC, как показано на рисунке 3. Завершите работу с мастером проекта.
|
|
|
Рисунок 3: Выбираемые параметры проекта |
|
Создайте новый файл с именем main.c и поместите в него следующий код8. |
||
|
Имя файла: src/main-ex1.c |
||
|
|
|
|
1 |
|
typedef unsigned long uint32_t; |
|
2 |
|
|
|
3 |
|
/* Начальные адреса памятей и периферии (общие для всех микроконтроллеров STM32) */ |
|
4 |
|
#define FLASH_BASE |
0x08000000 |
5 |
|
#define SRAM_BASE |
0x20000000 |
6 |
|
#define PERIPH_BASE |
0x40000000 |
7 |
|
|
|
8 Этот код предназначен для Nucleo-F401RE. Обратитесь к примерам книги для других плат Nucleo.
Организация памяти |
518 |
8/* Определение конечного адреса SRAM как указателя начала стека
9* (специфично для каждого микроконтроллера STM32) */
10 |
#define SRAM_SIZE |
96*1024 |
// STM32F401RE имеет 96 КБ SRAM |
11 |
#define SRAM_END |
(SRAM_BASE + SRAM_SIZE) |
|
12 |
|
|
|
13/* Адреса системы RCC, применимые к GPIOA
14* (специфично для каждого микроконтроллера STM32) */
15 |
#define RCC_BASE |
(PERIPH_BASE + 0x23800) |
|
16 |
#define RCC_APB1ENR |
((uint32_t*)(RCC_BASE + |
0x30)) |
17 |
|
|
|
18/* Адреса периферийного устройства GPIOA
19* (специфично для каждого микроконтроллера STM32) */
20 |
#define GPIOA_BASE |
(PERIPH_BASE + 0x20000) |
21 |
#define GPIOA_MODER |
((uint32_t*)(GPIOA_BASE + 0x00)) |
22 |
#define GPIOA_ODR |
((uint32_t*)(GPIOA_BASE + 0x14)) |
23 |
|
|
24 |
/* Пользовательские функции */ |
25int main(void);
26void delay(uint32_t count);
28/* Минимальная таблица векторов */
29uint32_t *vector_table[] __attribute__((section(".isr_vector"))) = {
30(uint32_t *)SRAM_END, // указатель начала стека
31(uint32_t *)main // main в качестве Reset_Handler
32};
33
34int main() {
35/* Разрешение подачи тактирования на периферийное устройство GPIOA */
36*RCC_APB1ENR = 0x1;
37/* Конфигурирование PA5 в качестве выхода с подтяжкой к питанию */
38*GPIOA_MODER |= 0x400; // Установка MODER[11:10] = 0x1
39
40while(1) {
41*GPIOA_ODR = 0x20;
42delay(200000);
43*GPIOA_ODR = 0x0;
44delay(200000);
45}
46}
47
48void delay(uint32_t count) {
49while(count--);
50}
Первые 22 строки содержат только макросы, которые определяют наиболее распространенные периферийные адреса STM32. Некоторые из них являются общими, а некоторые специфичными для каждого микроконтроллера. В строке 29 мы определяем таблицу векторов. Будучи «минимальной», она просто содержит две записи: адрес MSP в SRAM (помните, что это первая запись в таблице векторов, и она должна быть размещена по
Организация памяти |
519 |
адресу 0x0800 0000) и указатель на обработчик исключения сброса Reset. Что именно мы делаем?
В Главе 7 мы упоминали, что при сбросе микроконтроллера контроллер NVIC генерирует исключение сброса Reset после нескольких тактовых циклов. Это означает, что его обработчик является реальной точкой входа нашего приложения, и оттуда начинается выполнение микропрограммы. Здесь мы собираемся определить функцию main() в качестве обработчика исключения сброса Reset. Ключевое слово GCC
__attribute__((section(".isr_vector"))) указывает компилятору поместить массив
vector_table в секцию с именем .isr_vector, которая, в свою очередь, будет содержаться в объектном файле main.o. Наконец, процедура main() не содержит ничего, кроме классического приложения мигающего светодиода.
Прежде чем мы сможем скомпилировать микропрограмму, нам нужно задать несколько параметров проекта. Зайдите в Project settings → C/C++ Build → Settings. В параметрах Target Processor выберите ядро Cortex-M, соответствующее вашему микроконтроллеру. Затем перейдите в раздел Cross ARM C Linker → General, в котором поставьте флажок в пункте Do not use standard start files9, а с пункта Remove unused sections
снимите его. Удалите неиспользуемые секции, как показано на рисунке 4.
Рисунок 4: Выбранные параметры проекта
9 Если этот параметр не будет отмечен, то будут использоваться процедуры инициализации из libc. Они обычно «менее оптимизированы», поскольку им приходится иметь дело с некоторыми продвинутыми возможностями из libc, связанными с языком программирования C++. Таким образом, обычно процедуры запуска от ST являются специфичными для этой платформы, что позволяет сэкономить много Flashпамяти и сократить время начальной загрузки.
Организация памяти |
520 |
Если вы попытаетесь скомпилировать приложение, то увидите следующее предупреждение в консоли Eclipse:
warning: cannot find entry symbol _start; defaulting to 0000000000008000
Что это означает? GCC (или, вернее, LD) говорит нам, что он не знает, что является процедурой входа нашего приложения (имя точки входа _start() является соглашением в GCC), и он не знает, по какому абсолютному адресу в памяти начать размещение кода. Итак, как мы можем решить это? Нам необходим скрипт компоновщика.
Создайте новый файл с именем ldscript.ld и поместите в него следующий код.
Имя файла: src/ldscript.ld
1 /* организация памяти для STM32F401RE */
2
3MEMORY
4{
5FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
6SRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K
7}
8
9 ENTRY(main)
10
11/* секции на выходе */
12SECTIONS
13{
14/* код программы во FLASH */
15.text : ALIGN(4)
16{
17 |
*(.vector_table) |
/* |
Таблица векторов |
*/ |
18 |
*(.text) |
/* |
Код программы */ |
|
19KEEP(*(.vector_table))
20} >FLASH
21
22/* Инициализированные глобальные и статические переменные
23(которых у нас нет в данном примере) в SRAM */
24.data :
25{
26*(.data)
27} >SRAM
28}
Давайте рассмотрим содержимое данного файла. Строки [3:7] содержат определение областей Flash-памяти и памяти SRAM. Каждая область может иметь несколько атрибутов (w = доступна для записи, r = читаемая, x = исполняемая). Мы также указываем их начальный адрес и размер (в приведенном выше примере они относятся к микроконтроллеру STM32F401RE). Строка 9 определяет функцию main() как точку входа