- •Оглавление
- •Предисловие
- •Почему я написал книгу?
- •Для кого эта книга?
- •Как использовать эту книгу?
- •Как организована книга?
- •Об авторе
- •Ошибки и предложения
- •Поддержка книги
- •Как помочь автору
- •Отказ от авторского права
- •Благодарность за участие
- •Перевод
- •Благодарности
- •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
Продвинутые методы отладки |
668 |
24.1. Введение в исключения отказов Cortex-M
В начале этого долгого путешествия мы увидели, что микроконтроллеры на базе ядер Cortex-M реализуют ряд системных исключений. Некоторые из них связаны с отказами, то есть эти исключения срабатывают, когда что-то происходит не так во время выполнения обычного потока. Реализуя надлежащим образом обработчики для этих исключений отказов, мы можем избавиться от источника отказа. Это очень полезно во время отладки, поскольку все это помогает нам изолировать проблему от остальной части приложения. Однако правильная обработка отказов может быть полезна даже в «рабочей версии» микропрограммы: после обнаружения отказа мы можем перевести устройство в безопасное состояние, прежде чем будем пытаться перезагрузить плату.
Ядра Cortex-M3/4/7 предлагают программистам четыре исключения, связанных с отказами (см. таблицу 1 в Главе 7):
•Отказ системы управления памятью: Memory Management Fault
•Отказ шины: Bus Fault
•Отказ программы: Usage Fault
•Тяжелый отказ: Hard Fault
Первые три исключения срабатывают, когда имеют место узкоспециализированные отказы, и они доступны только в ядрах Cortex-M3/4/7. Последнее исключение, Hard Fault, является единственным доступным даже в ядрах Cortex-M0/0+. Оно также называется общим исключением отказа (generic fault exception), поскольку оно не может быть запрещено и действует как сборщик для условий узкоспециализированных отказов, когда соответствующие им исключения отказов запрещены.
Когда возникает исключение отказа, мы можем попытаться определить его причину, проанализировав содержимое некоторых «системных регистров». Более того, простой анализ трассировки стека может привести нас к корню отказа, по крайней мере, в большинстве причин отказа.
Какие обстоятельства могут вызвать системный отказ? Ответ на этот очевидный вопрос не тривиален. Наиболее частым источником отказов является баг в микропрограмме, особенно на этапе разработки. Доступ к неправильной ячейке памяти (довольно часто из-за поврежденного указателя) является наиболее частым источником условий отказа. Недопустимая или плохо реализованная таблица векторов является еще одним распространенным источником отказов. Переполнение стека – еще одно довольно частое условие отказа, особенно в недорогих микроконтроллерах STM32 при работе с ОСРВ.
Иногда происхождение отказа не связано с программным обеспечением, а может быть вызвано внешними факторами, такими как:
•Плохие конструкция и разводка печатной платы (бывают чаще, чем вы думаете).
•Нестабильный или плохой источник питания (довольно часто встречается в плохих конструкциях).
•Электрические шумы.
•Электромагнитные помехи (EMI) или электростатический разряд (ESD).
•Экстремальные условия эксплуатации (например, температура, влажность и пр.).
•Повреждение некоторых компонентов (например, устройства Flash/EEPROM, кварцевые генераторы, электролитические конденсаторы).
Продвинутые методы отладки |
669 |
•Радиоактивное излучение.
Диагностировать вышеупомянутые неприятные условия отказа действительно трудно. Это те условия, которые ни один разработчик аппаратного обеспечения никогда не захочет повстречать, и они выходят за рамки данной книги. Здесь мы сосредоточимся только на программных отказах и способах их выявления. Однако, прежде чем приступить к анализу причин, вызывающих эти четыре исключения отказов, необходимо проанализировать, как генерируется исключение с точки зрения программного обеспечения. Это важно для выявления или, по крайней мере, для попытки выявления кода, который приводит к исключению отказа.
24.1.1.Последовательность перехода в исключения Cortex-M и Соглашение ARM о вызовах
Для высокоуровневых программистов2 вызов процедуры кажется очевидным. Мы просто записываем имя функции, которую будем вызывать, передавая ей определенное количество параметров. Однако, с точки зрения процессора, то, что происходит «под капотом», должно быть конкретизировано до мельчайших деталей, и оно должно соответствовать как архитектуре процессора, так и семантике языка программирования. По этой причине принято говорить о соглашении о вызовах (calling convention) при описании процесса размещения новой процедуры в стеке.
Стандарт архитектуры ARM о вызове процедур (ARM Architecture Procedure Call Standard, AAPCS) точно определяет соглашение о вызовах для архитектур на базе ARM. В Главе 1 мы увидели, что микроконтроллеры на базе Cortex-M предоставляют несколько регистров ядра, которые для вашего удобства вновь показаны на рисунке 1. Не все эти регистры ядра доступны во всех ядрах Cortex-M: например, регистры S0-S31 модуля FPU доступны только в ядрах Cortex-M4F и Cortex-M7, когда FPU включен и используется.
Рисунок 1: Регистры ядра процессора Cortex-M
2 Как программисты Си, мы все «высокоуровневые программисты», верите вы этому или нет.
Продвинутые методы отладки |
670 |
Некоторые регистры ядра играют особую роль, потому что они используются для выполнения действий процессора. R13 – это указатель стека (Stack Pointer, SP), то есть указатель на адрес SRAM (так что он похож на 0x2000 XXXX в STM32), а именно на адрес самой последней записи, помещенной в стек. Эта запись представляет собой область локальной памяти данной функции, и в автодекрементном стеке по записи (full-descendent stack) SP совпадает с самым младшим адресом стека. R14 – это регистр (обратной) связи (Link Register, LR), то есть адрес инструкции во FLASH3 (так что он похож на 0x0800X XXXX в STM32), следующей за инструкцией, вызвавшей данную функцию в стеке. R15 – это счетчик команд (Program Counter, PC), то есть регистр, который содержит адрес во Flashпамяти текущей ассемблерной инструкции.
Регистры R0-R3 играют еще одну важную роль в Соглашении ARM о вызовах. Они используются для хранения первых четырех параметров, передаваемых вызываемой функцией. Если вызываемая функция использует менее четырех параметров, то в первые четыре регистра общего назначения помещается содержимое этих параметров. Очевидно, здесь мы предполагаем, что аргументы выровнены по словам (выровнены на границе четырех байт). Если, напротив, наша функция принимает более четырех параметров или их общий размер превышает шестнадцать байт, то нам нужно выделить достаточно места в стеке вызываемой функции для хранения других параметров, прежде чем передать ей управление. Такое использование регистров R0-R3 позволяет ускорить процесс вызова и уменьшить количество используемой SRAM. Наконец, регистры R0-R1 также используются для хранения возвращаемого значения функции. Таким образом, хорошим тоном было бы ограничить количество параметров максимум четырьмя, где это возможно. Если это невозможно, то вы должны попытаться поместить наиболее часто используемые параметры в R0-R3 (то есть определить их как первые четыре параметра функции), чтобы минимизировать доступ к стеку вызываемой функции.
Поскольку некоторые из регистров общего назначения играют определенные роли, как вызываемая функция, мы не можем свободно изменять их содержимое, но мы должны придерживаться следующих соглашений:
•Вызываемая функция может свободно изменять регистры R0, R1, R2 и R3.
−Это подразумевает, что вызывающая функция должна сохранить свой контекст (если он используется для хранения важных данных для вызывающей функции) перед передачей управления вызываемой функции.
•Вызываемая функция не может предполагать что-либо в отношении содержимого R0, R1, R2 и R3, если только они не играют роль параметров.
•Вызываемая функция может свободно изменять регистр LR, но его значение при переходе в функцию будет необходимо при выходе из нее (поэтому это значение необходимо хранить в кадре стека вызываемой функции).
•Вызываемая функция может изменять все оставшиеся регистры, если их значения восстанавливаются после выхода из функции. Они включают в себя SP и регистры R4-R11. Это означает, что после вызова функции мы должны предполагать, что (только) регистры R0-R3, R12 и LR были перезаписаны.
•Функция не должна делать какие-либо предположения относительно содержи-
мого регистра текущего состояния программы (Current Program Status Register, CPSR).
3 Это не совсем так, потому что ЦПУ может выполнять код, размещенный в SRAM, а также в других внешних памятях. Но в данном контексте это можно считать правдой.
Продвинутые методы отладки |
671 |
•Если FPU включен и используется, вызываемая функция может свободно изменять регистры S0-S15, которые должны быть сохранены (вместе с регистром FPSCR)
вызывающей функцией перед вызовом вызываемой. И наоборот, вызываемой функ-
ции необходимо сохранить содержимое регистров S16-S31 перед изменением их содержимого.
•R12 – это специальный «помеченный регистр», используемый компоновщиками для динамического связывания. Он не очень полезен во встроенных микроконтроллерах, таких как Cortex-M, но это регистр, который должен быть сохранен
вызывающей функцией в соответствии с AAPCS4.
Итак, подведем итоги. С точки зрения вызывающей функции, прежде чем вызывать другую процедуру, нам нужно сохранить содержимое следующих регистров: R0-R3, R12, R14, CPSR (плюс S0-S15 и FPSCR, если FPU включен). Эти регистры выделены красным на
рисунке 1.
Как высокоуровневым программистам, нам не нужно заботиться об этих правилах. Обеспечение соблюдения правил AAPCS является задачей компилятора. В Главе 7 мы увидели, что отличительной особенностью ядер Cortex-M является возможность использовать обычные функции Си в качестве обработчиков исключений. Это означает, что обработчики исключений «складываются» в основной стек как обычная процедура Си. Но это подразумевает, что для того, чтобы разрешить использование функции Си в качестве обработчика исключений, механизм исключений должен соответствовать требованиям соглашения о вызовах AAPCS, и поэтому ему необходимо автоматически сохранять эти «красные» регистры на рисунке 1 при переходе в исключение, и восстанавливать их при выходе из исключения под контролем процессора. Поэтому при возврате в прерванную программу все регистры будут иметь те же значения, что и при запуске последовательности перехода в прерывание.
Кроме того, поскольку исключение соответствует прерыванию основного потока программы, и поскольку оно может сработать в любое время, нам необходимо сохранять содержимое PC, в противном случае у нас не будет способа вернуться к основному потоку при выходе из исключения. При обычном вызове функции значение PC сохраняется в регистре LR с помощью инструкций ветвления. При срабатывании же исключения значение адреса возврата (PC) не сохраняется в LR (механизм исключения помещает специальный код EXC_RETURN в LR при переходе в исключение, который используется при возврате из исключения – мы проанализируем его в чуть позже), поэтому значение адреса возврата также необходимо сохранять с помощью последовательности исключения
(exception sequence).
Таким образом, во время последовательности обработки исключения на микроконтроллерах на базе Cortex-M, в общем, необходимо сохранять восемь регистров:
•R0-R3
•R12
•SP
•LR
•CPSR
4 Важно подчеркнуть, что такое же Соглашение ARM о вызовах применимо и к микропроцессорам на базе Cortex-A, которые имеют все возможности для обработки динамического связывания для высокоуровневых операционных систем, таких как Linux и Windows.
Продвинутые методы отладки |
672 |
Кроме того, должны быть сохранены S0-S15 и регистр FPSCR, если используется FPU.
Рисунок 2: Как регистры ядра загружаются в стек ЦПУ при переходе в исключение
А где же процессор хранит эти регистры? Очевидно, что они хранятся в стеке5, в самом начале кадра стека обработчика исключения. Данная процедура называется загрузкой в стек (stacking), и этот процесс четко показан на рисунке 2. Обратите внимание, что на рисунке 2 цвет регистров ядра светлее, чем тот, который использовался на рисунке 1. Это потому, что важно подчеркнуть, что процессоры хранят в этих ячейках содержимое регистров ядра до перехода в последовательность исключения. Когда срабатывает исключение, содержимое регистров ядра обновляется данными контекста исключения (например, PC будет указывать на первую инструкцию обработчика исключения, или SP будет указывать на вершину MSP сразу после загруженных в стек регистров ядра).
Содержимое сохраненных регистров ядра может быть действительно полезным для оценки того, что именно породило исключение отказа. Например, если исключение отказа срабатывает из-за доступа к неверной ячейке памяти (возможно, из-за поврежденного указателя), проверяя эти регистры, мы можем попытаться понять место, где осуществляется недопустимый доступ к памяти. Поэтому возникает вопрос: как высокоуровневые программисты, можем ли мы получить доступ к этим значениям? Конечно же! Нам нужно всего лишь немного программирования на ассемблере.
Предположим, мы хотим получить доступ к содержимому загруженного в стек регистра, когда вызывается EXTI15_10_IRQHandler() (это ISR, которая вызывается, когда вывод PC13, связанный с синей кнопкой Nucleo, сконфигурирован в режиме прерываний на
5 Здесь ситуация немного сложнее. В зависимости от того, используется ОСРВ или нет, может быть «несколько» стеков одновременно: основной стек (Main Stack) или стек, специфичный для отдельного потока, называемый стеком процесса (Process Stack). Эта тема выходит за рамки данной книги. Более подробную информацию о них можно найти в превосходной книге Джозефа Ю (http://amzn.to/1P5sZwq) об архи-
тектурах Cortex-M.
Продвинутые методы отладки |
673 |
большинстве микроконтроллеров STM32). Мы можем определить ISR следующим образом:
1void EXTI15_10_IRQHandler(void) {
2asm volatile(
3 |
" tst lr,#4 |
\n" |
||
4 |
" ite |
eq |
\n" |
|
5 |
" mrseq r0,msp |
\n" |
||
6 |
" |
mrsne r0,psp |
\n" |
|
7 |
" |
mov |
r1,lr |
\n" |
8" ldr r2,=EXTI15_10_IRQHandler_C \n"
9" bx r2"
10);
11}
12
13EXTI15_10_IRQHandler (uint32_t *core_registers, uint32_t lr) {
14/* core_registers указывает на регистры R0-R3, R13, SP и CPSR,
15 |
в то время как аргумент lr содержит содержимое регистра LR |
16 |
непосредственно перед переходом в исключение */ |
17....
18}
Приведенный выше ассемблерный код может показаться сложным для понимания, но это не искусство черной магии. Инструкция tst выполняет побитовое сравнение между содержимым регистра LR (текущего регистра, а не того, который сохранен в стеке) и константой 4. Если они совпадают (то есть четвертый бит регистра LR установлен в 1), тогда стек PSP использовался во время перехода в исключение. В противном случае MSP был текущим используемым стеком. Причина, по которой эта проверка выполняется, скоро станет ясна. Примите это как есть.
Команда в строке 7 делает простую вещь (это хитрая часть): содержимое текущего регистра LR помещается в регистр R1, и вызывается функция EXTI15_10_IRQHandler_C() (обратите внимание на окончание _C). Эта другая функция принимает два параметра: core_registers и lr. Согласно спецификации AAPCS, core_registers будет совпадать с регистром R06, а lr – с содержимым R1. Когда происходит переход в обработчик исключения, R0 совпадает с начальным адресом в текущем стеке (MSP или PSP), где были сохранены регистры ядра.
Рисунок 3 ясно разъясняет это. Как видите, core_registers соответствует регистру R0, который содержит базовый адрес загруженных в стек регистров. lr соответствует регистру R1, содержимое которого заполнено регистром LR с помощью ассемблерной инструкции в строке 7. Таким образом, мы можем получить доступ к загруженным в стек регистрам из процедуры EXTI15_10_IRQHandler_C() и выполнить анализ их содержимого, как мы увидим позже.
6 Обратите внимание, что параметр core_registers является указателем, поэтому регистр R0 будет содержать адрес ячейки памяти (32-разрядное целое число), с которой были сохранены регистры ядра.