- •Оглавление
- •Предисловие
- •Почему я написал книгу?
- •Для кого эта книга?
- •Как использовать эту книгу?
- •Как организована книга?
- •Об авторе
- •Ошибки и предложения
- •Поддержка книги
- •Как помочь автору
- •Отказ от авторского права
- •Благодарность за участие
- •Перевод
- •Благодарности
- •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
Организация памяти |
523 |
20.2.2. Инициализация секций .data и .bss
Давайте произведем небольшую модификацию предыдущего примера.
36 volatile uint32_t dataVar = 0x3f;
37
38int main() {
39/* Разрешение подачи тактирования на периферийные устройства GPIOA и GPIOC */
40*RCC_APB1ENR = 0x1 | 0x4;
41*GPIOA_MODER |= 0x400; // Установка MODER[11:10] = 0x1
42
43while(dataVar == 0x3f) { // Условие цикла всегда истинно
44*GPIOA_ODR = 0x20;
45delay(200000);
46*GPIOA_ODR = 0x0;
47delay(200000);
48}
49}
На этот раз мы используем глобальную инициализированную переменную dataVar, чтобы начать цикл мигания. Переменная была объявлена как volatile просто для того, чтобы компилятор не оптимизировал ее (однако при компиляции этого примера отключите все оптимизации [-ON] в настройках проекта). Взглянув на код, можно прийти к выводу, что он делает то же самое, что и в предыдущем примере. Однако, если вы попытаетесь загрузить его в Nucleo, то увидите, что светодиод LD2 не мигает. Почему так?
Чтобы понять, что происходит, мы должны вспомнить некоторые моменты языка программирования Си. Рассмотрим следующий фрагмент кода:
...
uint32_t globalVar = 0x3f;
void foo() {
volatile uint32_t localVar = 0x4f;
while(localVar--);
}
Здесь у нас две переменные: одна определена в глобальной области видимости, другая
– в локальной. Переменная localVar инициализируется значением 0x4f. Что конкретно при этом происходит? Инициализация выполняется при вызове процедуры foo(), как показано в следующем ассемблерном коде:
1 |
void foo() { |
|
|
|
|
2 |
0: |
b480 |
push |
{r7} |
;Сохранить текущий указатель стекового кадра |
3 |
2: |
b083 |
sub |
sp, #12 |
;Выделить 12 Байт в стеке |
4 |
4: |
af00 |
add |
r7, sp, #0 |
;Сохранить новый указатель стекового кадра |
5 |
|
volatile uint32_t localVar = 0x4f; |
|
||
6 |
6: |
234f |
movs |
r3, #79 |
;Поместить 0x4f в r3 |
|
Организация памяти |
|
524 |
|
7 |
8: |
607b |
str |
r3, [r7, #4] ;Записать r3 (то есть 0x4f) в 4-й Байт |
8 |
|
|
|
|
9 |
|
while(localVar--); |
|
|
10 |
a: |
bf00 |
nop |
|
11 |
c: |
687b |
ldr |
r3, [r7, #4] |
12 |
e: |
1e5a |
subs |
r2, r3, #1 |
13 |
10: |
607a |
str |
r2, [r7, #4] |
14 |
12: |
2b00 |
cmp |
r3, #0 |
15 |
14: |
d1fa |
bne.n |
c <foo+0xc> |
16 |
} |
|
|
|
Строки [2:4] являются прологом функции. Каждая процедура отвечает за выделение своего собственного стекового кадра, сохраняя некоторые внутренние регистры ЦПУ. Это также называется соглашением о вызовах (calling convention), и способ его выполнения определен соответствующим стандартом (в случае процессоров на базе ARM оно опре-
деляется Стандартом вызова процедур архитектуры ARM (ARM Architecture Procedure Call Standard, AAPCS)). Мы не будем вдаваться в подробности данного вопроса, поскольку проанализируем соглашение о вызовах ARM лучше в Главе 24.
Интересующие нас команды в строках [5:6]. В них мы сохраняем значение 0x4f (которое составляет 79 в десятичной системе счисления) в регистре общего назначения R3, а затем перемещаем его содержимое во второе слово в стеке, которое соответствует переменной
localVar13.
Оставшаяся часть ассемблерного кода содержит while(localVar--) и эпилог функции (здесь не показан), который отвечает за восстановление состояния перед возвратом в вызывающую функцию.
Таким образом, соглашение о вызовах определяет, что локальные переменные автоматически инициализируются при вызове функции. А что насчет глобальных переменных? Поскольку они не участвуют в вызывающем процессе, они должны быть инициализированы каким-то определенным кодом при сбросе микроконтроллера (вспомните, что SRAM является энергозависимым, а его содержимое после сброса не определено). Это означает, что мы должны предоставить специальную функцию инициализации.
Следующая процедура может использоваться для простого копирования содержимого области Flash-памяти, содержащей значения инициализации, в область SRAM, выделенную для глобальных инициализированных переменных.
void __initialize_data (unsigned int* flash_begin, unsigned int* data_begin,
|
unsigned int* data_end) { |
unsigned |
int *p = data_begin; |
while (p |
< data_end) |
*p++ = |
*flash_begin++; |
}
13 Важно уточнить, что вышеприведенный ассемблерный код генерируется с отключенной оптимизацией.
Организация памяти |
525 |
Рисунок 3: Процесс копирования инициализированных данных из Flash-памяти в память SRAM
Прежде чем мы сможем использовать данную процедуру, нам нужно определить несколько других вещей. Прежде всего, нам нужно проинструктировать LD о необходимости хранения значений инициализации для каждой переменной, содержащейся в секции .data, в определенной области Flash-памяти, которая будет соответствовать адресу памяти LMA. Во-вторых, нам необходим способ передачи в функцию __initialize_data() начала и конца секции .data в SRAM (которые мы будем называть _sdata и _edata соответственно) и начального расположения (которое мы будем называть _sidata), где значения инициализации хранятся во Flash-памяти (важно подчеркнуть, что когда мы инициализируем переменную с заданным значением, нам нужно сохранить это значение где-то во Flash-памяти и использовать его для инициализации ячейки SRAM, соответствующей этой переменной). Рисунок 3 схематично отображает этот процесс.
Еще раз: все эти операции могут быть выполнены при помощи скрипта компоновщика, который мы можем модифицировать следующим образом:
25/* Используется при запуске для инициализации данных */
26_sidata = LOADADDR(.data);
27
28.data : ALIGN(4)
29{
30. = ALIGN(4);
31 |
_sdata = .; |
/* создание глобального символьного имени в начале данных */ |
32 |
|
|
33*(.data)
34*(.data*)
36. = ALIGN(4);
37 |
_edata = .; |
/* определение глобального символьного имени в конце данных */ |
38 |
} >SRAM AT>FLASH |
|
Организация памяти |
526 |
Команда в строке 26 определяет переменную _sidata, которая будет содержать адрес LMA секции .data (то есть начальный адрес Flash-памяти, содержащий значения инициализации). Команды в строках [30:31] используют специальный оператор: оператор “.”. Он называется счетчиком адресов (location counter) и является счетчиком, который отслеживает ячейку памяти, достигнутую во время генерации каждой секции. Счетчик адресов независимо отсчитывает ячейку памяти каждой секции памяти (SRAM, Flash-память и т. д.). Например, в приведенном выше коде он начинается с 0x2000 0000, поскольку секция .data является первой, загруженной в SRAM. Когда выполняются две команды *(.data) и *(.data*), счетчик адресов увеличивается на размер всех секций .data, содержащихся в файле. Командой . = ALIGN(4); мы просто заставляем счетчик адресов выравниваться по словам. Итак, подведем итог: _sdata будет содержать 0x2000 0000, а _edata будет равна размеру секции .data (в нашем примере секция .data содержит только одну переменную – dataVar – и, следовательно, ее размер равен 0x2000 0004). Наконец, директива >SRAM AT>FLASH сообщает компоновщику, что адрес VMA секции .data привязан к адресному пространству SRAM (т. е. 0x2000 0000), но адрес LMA (то есть, где хранятся значения инициализации) отображается внутри адресного пространства Flashпамяти.
Благодаря этой новой конфигурации организации памяти мы можем теперь перестроить файл main.c следующим образом:
Имя файла: src/main-ex2.c
22void _start (void);
23int main(void);
24void delay(uint32_t count);
26/* Минимальная таблица векторов */
27uint32_t *vector_table[] __attribute__((section(".isr_vector"))) = {
28(uint32_t *)SRAM_END, // указатель начала стека
29(uint32_t *)_start // main в качестве Reset_Handler
30};
31
32// Начальный адрес значений инициализации секции .data,
33// определенный в скрипте компоновщика.
34extern uint32_t _sidata;
35// Начальный адрес секции .data; определен в скрипте компоновщика
36extern uint32_t _sdata;
37// Конечный адрес секции .data; определен в скрипте компоновщика
38extern uint32_t _edata;
39
40
41 volatile uint32_t dataVar = 0x3f;
42
43inline void
44__initialize_data (uint32_t* flash_begin, uint32_t* data_begin,
45 |
uint32_t* data_end) { |
46uint32_t *p = data_begin;
47while (p < data_end)
48*p++ = *flash_begin++;
49}
Организация памяти |
527 |
50
51void __attribute__ ((noreturn,weak))
52_start (void) {
53__initialize_data(&_sidata, &_sdata, &_edata);
54main();
55
56for(;;);
57}
58
59 int main() {
60
61/* Разрешение подачи тактирования на периферийные устройства GPIOA и GPIOC */
62*RCC_APB1ENR = 0x1 | 0x4;
63*GPIOA_MODER |= 0x400; // Установка MODER[11:10] = 0x1
64
65while(dataVar == 0x3f) {
66*GPIOA_ODR = 0x20;
67delay(200000);
68*GPIOA_ODR = 0x0;
69delay(200000);
70}
71}
Точкой входа теперь является процедура _start(), которая используется в качестве обработчика исключения сброса Reset. Когда микроконтроллер сбрасывается, он вызывается автоматически и, в свою очередь, вызывает функцию __initialize_data() с передачей параметров _sidata, _sdata и _edata, вычисленных компоновщиком на этапе компоновки. Затем _start() вызывает процедуру main(), которая работает теперь как положено.
Используя инструмент objdump, мы можем проверить, как организованы секции в ELFфайле.
# ~/STM32Toolchain/gcc-arm/bin/arm-none-eabi-objdump -h nucleo-f401RE.elf
nucleo-f401RE.elf: |
file format elf32-littlearm |
|
||||
Sections: |
|
|
|
|
|
|
Idx Name |
Size |
VMA |
LMA |
File off |
Algn |
|
0 |
.text |
000000c0 |
08000000 |
08000000 |
00008000 |
2**2 |
|
|
CONTENTS, ALLOC, LOAD, READONLY, CODE |
|
|||
1 |
.data |
00000004 |
20000000 |
080000c0 |
00010000 |
2**2 |
|
|
CONTENTS, ALLOC, LOAD, DATA |
|
|
||
2 |
.comment |
00000070 |
00000000 00000000 00010004 2**0 |
|
||
|
|
CONTENTS, READONLY |
|
|
|
|
3 |
.ARM.attributes 00000033 00000000 00000000 |
00010074 2**0 |
||||
|
|
CONTENTS, READONLY |
|
|
|
Как видите, инструмент подтверждает, что секция .data имеет размер, равный 4 Байт, адрес VMA, равный 0x2000 0000, и адрес LMA, равный 0x0800 00c0, что соответствует концу секции .text.
То же самое относится и к секции .bss, которая зарезервирована для неинициализированных переменных. В соответствии со стандартом ANSI C содержимое этой секции
Организация памяти |
528 |
должно быть инициализировано нулем. Однако секция .bss не имеет соответствующей области Flash-памяти, содержащей все нули, поэтому она опять же зависит от кода запуска, инициализирующего эту область. Следующий фрагмент скрипта компоновщика показывает определение секции .bss14:
25/* Секция неинициализированных данных */
26.bss : ALIGN(4)
27{
28/* Это используется кодом запуска (startup) для инициализации секции .bss */
29 |
_sbss = .; |
/* определение глобального символьного имени начала .bss */ |
30*(.bss .bss*)
31*(COMMON)
32
33 . = ALIGN(4);
34 _ebss = .; /* определение глобального символьного имени конца .bss */ 35 } >SRAM AT>SRAM
в то время как следующая процедура, всегда вызываемая из _start(), используется для обнуления области .bss в SRAM:
void __initialize_bss (unsigned int* bss_begin, unsigned int* bss_end) { unsigned int *p = bss_begin;
while (p < bss_end) *p++ = 0;
}
Изменение процедуры main() следующим образом позволяет нам убедиться в правильной работе скрипта:
Имя файла: src/main-ex3.c
76volatile uint32_t dataVar = 0x3f;
77volatile uint32_t bssVar;
78
79 int main() {
80
81/* Разрешение подачи тактирования на периферийные устройства GPIOA и GPIOC */
82*RCC_APB1ENR = 0x1 | 0x4;
83*GPIOA_MODER |= 0x400; // Установка MODER[11:10] = 0x1
84
85while(bssVar == 0) {
86*GPIOA_ODR = 0x20;
87delay(200000);
88*GPIOA_ODR = 0x0;
89delay(200000);
90}
91}
14 Обратите внимание, что порядок секций в скрипте компоновщика отражает их порядок расположения в памяти. Если у нас есть две секции с именами A и B, и обе загружаются в SRAM, при этом если секция A определена до B, то она будет помещена в SRAM до B.