Библиотеки МК

Стоимость разработки ПО — ключевая составляющая в производстве любого современного продукта. Любое устройство можно разбить на маленькие компоненты, выполняющие задачи, которые часто повторяются. Например, кнопка может быть использована как в микроволновке, так и в телефоне. Стандартизируя интерфейсы микроконтроллеров от разных производителей, можно значительно упростить работу программиста и ускорить ее. Компания ARM предлагает библиотеку CMSIS — это независимый от производителя уровень абстракции над железом для серии ядер Cortex-M, который предоставляет простой интерфейс к ядру его периферии и операционной системе реального времени.

Абстракция, наряду с разделением труда, одно из величайших «изобретений» человечества.

CMSIS позволяет разработчику на языке Си обращаться к ячейкам памяти (регистрам) не напрямую, по их адресу, а по синонимам.

Это удобно, так как вам не нужно залезать в документацию, чтобы посмотреть адрес и положение нужного вам бита. Однако названия не всегда очевидны, и вам нужно действительно хорошо знать, как работает МК и какие регистры вам нужны. С повышением уровня абстракции разработка упрощается, но зачастую за счет понижения производительности.

Компания ST, решая проблему ускорения разработки, выпустила стандартную библиотеку периферии (StdPeriph, SPL). Многим она нравится, так как вместо работы с регистрами напрямую разработчику предлагается заполнять структуры и вызывать функции, которые в свою очередь производят настройку тех или иных блоков.

Стандартная библиотека, однако, получилась не такой, какой виделась — с расширением линейки МК приходилось добавлять всё новые возможности. В конечном итоге одна и та же библиотека, предоставляющая схожий интерфейс, выглядит по-разному. В данный момент ее разработка прекращена, но ей продолжают пользоваться.

Решая проблему унификации, ST разработала еще две библиотеки: низкоуровневую (Low Layer), которая по сути является оберткой для CMSIS и позволяет выполнять все необходимые операции вызовом макросов или инлайновых функций; и библиотеку аппаратной абстракции (HAL), использующую низкоуровневую библиотеку внутри.

Какую библиотеку использовать — решение программиста (иногда — заказчика). Одни позволяют получить высокую производительность и компактный бинарник (требуется меньше памяти), другие за счет меньшей производительности позволяют ускорить разработку, улучшить переносимость и поддерживаемость кода.

Библиотека CMSIS

Библиотека CMSIS включает в себя следующие компоненты:

Рассмотрим только CMSIS-CORE.

Библиотека состоит из стандартной (предоставляется ARM) и вендор-зависимой (предоставляется в нашем случае ST) частей.

Стандартная часть

Заголовочный файл core_<processor_unit>.h предоставляет интерфейс к ядру. Для stm32f103c8 это core_cm3.h, так как он работает на Cortex-M3. Для Cortex-M0+ это будет файл core_cm0plus.h.

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

Вам не нужно работать с регистрами ядра напрямую.

Другие файлы нам не столь интересны, но справедливости ради упомянем их. Например файл core_cmInstr.h содержит обертки инструкций, а core_cmFunc.h — обертки некоторых важных системных функций.

Если вы не разрабатываете приложение на самом низком уровне, то заглядывать в эти файлы незачем. Тем не менее, подробное описание работы ядра можно найти в документе ARM — Cortex-M3 Devices Generic User Guide, и мы им даже воспользуемся при настройке системного таймера.

Вендор-зависимая часть

Вторая часть библиотеки пишется непосредственным производителем микроконтроллера. Это происходит потому, что микроконтроллер — это не только его ядро, а еще и периферия. Реализация периферии не стандартизована, и каждый производитель делает ее так, как считает нужным. Адреса и даже поведение внутренних модулей (ADC, SPI, USART и т.д.) могут отличаться.

В ассемблеровском файле startup_<device>.s (в нашем случае это startup_stm32f10x_md.s) реализуется функция обработчика сброса Reset_Handler. Он задает поведение МК при запуске, т.е. выполняет некоторые задачи до входа в функцию main(), в частности, вызывает функцию SystemInit() из файла system_<device>.c} (system_stm32f10x.c). Также в нем задается таблица векторов прерываний (англ. interrupt vector table) с их названиями:

Заголовочный файл system_<device>.h (system_stm32f10x.h) предоставляет интерфейс двум функциям и глобальной переменной и отвечает за систему тактирования.

Меняя это число, вы не меняете тактовую частоту! Переменную SystemCoreClock стоит использовать только как индикатор. Более того, никто не гарантирует, что число, записанное в этой переменной, будет отображать реальную частоту: во-первых, оно может не обновиться после изменения регистров; во-вторых, оно никак не учитывает погрешность хода генератора; и в-третьих, стандартная частота (определенная как макрос HSE_VALUE в библиотеке) внешнего кварцевого генератора — 8 МГц, но никто не мешает разработчику поставить, скажем, кварц на 12 МГц.

И последний файл, самый важный для программиста, это драйвер микроконтроллера <device>.h} (stm32f10x.h). Вся карта памяти микроконтроллера (о ней еще поговорим) записана там в виде макросов. Например, адрес начала регистров периферии, флеш и оперативной памяти:

Регистры модулей, таких как порты ввода-вывода, обернуты в структуры.

Вместо того, чтобы обращаться к ячейке по нужному адресу, это можно сделать через структуру.

Так как элементы в структуре расположены линейно, друг за другом, а длина регистра фиксирована (uint32_t, 4 байта), то регистр CRL хранится по адресу GPIOA_BASE, а следующий за ним CRH через четыре байта, по адресу GPIOA_BASE + 4. Ниже приведен пример настройки одной из ножек порта на выход. Вам этот код пока что ничего не скажет, но суть сейчас в другом — вам нужно увидеть пример использования библиотеки.

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

Мы рассмотрели, как эти операции работают, в разделе «Микроконтроллер под микроскопом». Т.е. код выше можно переписать так (и он будет более читаем):

Для всех стандартных типов (определенных в <stdint.h>) вводятся сокращенные синонимы, например:

Слово __IO не входит в стандарт языка, а переопределено в core_cm3.h:

Последнее, о чём нужно упомянуть, это перечисление IRQn_Type.

Оно устанавливает номер исключительной ситуации в соответствие с его названием. Когда вы вызываете функции NVIC_Enable() или NVIC_Disable(), в качестве параметра нужно использовать одно из имен в этом перечислении.

Стандартная библиотека периферии

Библиотека CMSIS абстрагирует программиста от карты памяти микроконтроллера. Код получается эффективным, так как программист просит компилятор сделать только нужные вещи (записать какое-нибудь значение в нужное место). Проблема такого подхода в том, что нужно заглядывать в документацию, чтобы определить, в какой регистр и что нужно записать. У одного и того же производителя регистры на разных МК могут отличаться, как названием, так и количеством. Это неудобно.

Абстракция — мощный инструмент, которую легко реализовать. Вместо обращения к регистрам можно просто вызывать функции. И в CMSIS такая абстракция уже присутствует (совсем чуть-чуть).

Если модуль несложный (те же порты ввода-вывода), то больших усилий для его инициализации прикладывать не нужно. Но вот если нужно настроить какой-нибудь таймер в нестандартный режим работы, то рутина по выставлению нужных битов в памяти принимает устрашающий характер. Стандартная библиотека периферии помогает заглядывать в документацию реже1. Всё, что должен сделать программист (в общем случае) — это заполнить структуру с читаемыми параметрами и выполнить функцию.

Стандартный проект будет включать в себя библиотеку CMSIS (она используется внутри StdPeriph), пользовательские файлы и файлы самой библиотеки.

Архив с библиотекой и примерами ее использования можно найти на странице целевого МК в разделе Design Resources. Каждому модулю периферии соответствует два файла: заголовочный (stm32f10x_ppp.h) и исходного кода (stm32f10x_ppp.c). Здесь ppp — название периферии. К примеру, для работы с аналого-цифровым преобразователем нужны файлы stm32f10x_adc.h и stm32f10x_adc.c. Файлы misc.h и misc.c реализуют работу с контроллером прерываний NVIC и системным таймером SysTick (эти функции есть в CMSIS).

Чтобы подключить стандартную библиотеку, нужно в файле stm32f10x.h определить макрос USE_STDPERIPH_DRIVER2.

Заголовочный файл stm32f10x_conf.h не является частью библиотеки, он пользовательский. С его помощью можно подключать или отключать части библиотеки.

Оставшиеся два файла (stm32f10x_it.h и stm32f10x_it.c) выделены для реализации обработчиков прерываний, именно туда следует помещать данные функции.

В стандартной библиотеке периферии есть соглашение о наименовании функций и обозначений.

С полным списком вы можете ознакомиться в файле поддержки библиотеки.

Перепишем инициализацию порта ввода-вывода из предыдущего раздела. Во-первых, нужно включить тактирование модуля (подать питание) — делается это через функцию, объявленную в stm32f10x_rcc.h:

Возможные варианты первого аргумента можно найти в комментарии к функции или в заголовочном файле. Так как мы работаем с портом A, нам нужен RCC_APB2Periph_GPIOA. Перечисление FunctionalState определено в stm32f10x.h:

Далее нужно обратиться к структуре порта из stm32f10x_gpio.h:

Параметры структуры можно найти заголовочном файле.

Главное — запомнить порядок инициализации: включаем тактирование периферии, объявляем структуру, заполняем структуру, вызываем функцию инициализации. Другие периферийные устройства обычно настраиваются по подобной схеме.

Низкоуровневая библиотека

У стандартной библиотеки периферии есть два недостатка (личное мнение): в ходе разработки она слишком разрослась и не предоставляет унифицированного интерфейса (поэтому был придумал HAL); и не все операции являются атомарными (хотя в случае инициализации вряд ли это можно назвать проблемой).

По сути низкоуровневая библиотека (low layer) — это реинкарнация стандартной (разработка которой прекращена). Однако она не такая гибкая, как ее предшественник: предусмотрены функции только для основных возможностей периферии3, если вам нужно работать с USB, то сделать это через LL не получится. Кроме функций, дублирующих возможности StdPeriph (объявление, заполнение и передача в функцию инициализации структуры), низкоуровневая библиотека предоставляет inline-функции прямого доступа (атомарного) к регистрам.

Такой подход (атомарных операций) лучше: во-первых, их можно вызывать без опасения, что они будут прерваны исключительной ситуацией; во-вторых, не нужно тратить дополнительную память на хранение структур; и в-третьих, снижаются накладные расходы, ведь вызывать функцию (а значит, и сохранять стек) не приходится — inline-функция вставляется в место вызова, как макрос. Для того чтобы подключить библиотеку, нужно объявить макрос USE_FULL_LL_DRIVER (в настройках проекта).

Ниже приведена типичная структура проекта.

Низкоуровневая библиотека, как и стандартная, для своей работы использует CMSIS, имеет схожий принцип именования файлов (stm32yyxx_ll_ppp.h, stm32yyxx_ll_ppp.c) и разбита на три подуровня.

Низкоуровневая библиотека, как и стандартная, для своей работы использует CMSIS, имеет схожий принцип именования файлов (stm32yyxx_ll_ppp.h, stm32yyxx_ll_ppp.c) и разбита на три подуровня.

Функциональность включения/отключения периферийных блоков вынесена из _rcc в stm32f1xx_ll_bus.h. В файле stm32f1xx_ll_system.h расположены некоторые функции для работы с флеш-памятью и отладчиком. В файле stm32f1xx_ll_utils.h присутствуют функции для настройки PLL, задержки и считывания идентификатор МК (уникальный номер).

Перепишем всё тот же пример инициализации ножки микроконтроллера на выход.

Для перевода старого проекта на низкоуровневую библиотеку ST разработала утилиту SPL2LL-Converter. Детальное описание библиотеки можно найти в документе UM1850.

Слой аппаратной абстракции HAL

Последняя библиотека — слой аппаратной абстракции (англ. Hardware Abstraction Layer, HAL). Ее основные задачи — сократить время разработки и позволить писать портируемый на любое семейство STM32 (F0, F1 и т.д.) код. С одной стороны, она похожа на стандартную библиотеку: для инициализации периферийного блока используется структура. Перепишем инициализацию порта ввода-вывода с использованием библиотеки HAL:

Код не сильно отличается от StdPeriph или LL, а именование файлов осуществляется схожим образом: stm32f1xx_hal_ppp.c / stm32f1xx_hal_ppp.h, где ppp — название периферии. Учитывая опыт создания стандартной библиотеки, в HAL введено разделение на общие (т.е. применимые для всех семейств МК) и специфические функции. Вторые, если они есть, помещаются в отдельный файл stm32f1xx_hal_ppp_ex.c / stm32f1xx_hal_ppp_ex.h (суффикс ex происходит от слова extend, расширенный).

Все модули подключаются через конфигурационный файл stm32f1xx_hal_conf.h:

Функции для работы с системными ресурсами (SysTick и NVIC) переопределены в файлах stm32f1xx_hal_cortex.c / stm32f1xx_hal_cortex.h. Инициализация периферийных устройств осуществляется через файл stm32f1xx_hal_msp.c.

Первое, что должно быть сделано в функции main() — вызов HAL_Init(), которая настраивает доступ к флеш-памяти и системный таймер. По умолчанию он используется для реализации функции задержки, по этой причине при использовании операционной системы реального времени в качестве источника системного тика должен быть выбран другой таймер.

 

Не надо думать, что на этом все особенности библиотеки заканчиваются. Если речь не идет об общих или системных ресурсах (GPIO, SysTick, NVIC, PWR, RCC, FLASH), вводится еще одна сущность — дескриптор (англ. handle). Он используется для полного описания обьекта4 в системе. Если мы говорим о модуле коммуникации (USART, SPI, I2C и т.д.), мы должны помнить о его основной задаче — обмене данными, которые обычно хранятся в буфере. Проблема в том, что нужно завести как минимум два массива (на прием и на отправку) плюс еще две переменных (или макроса) с размером буферов. У блока может быть несколько режимов работы (через USART реализуется IrDA и SMARTCARD, например), кроме того, сам блок имеет внутреннее состояние (он сейчас что-то отправляет или, наоборот, ожидает команд). В устройстве при этом может находится несколько таких блоков — два, три, кто знает? В итоге получается, что для обслуживания одного «экземпляра» (англ. instance) USART требуется создать кучу переменных, в именовании которых можно легко запутаться. Логичным решением является обертка всех этих переменных в структуру.

Библиотека HAL реализована таком образом, что все функции в ней являются реентрантными (англ. reentrant), т. е. их можно без опаски выполнять «одновременно», что актуально для операционной системы реального времени. Реализуется это посредством механизма блокировки (файл stm32f1xx_hal_def.h).

Взгляните на фрагмент из модуля SPI:

Блокировка предотвращает возможность одновременного доступа к ресурсу (в данном случае к периферийному блоку SPI). Сама функция, как можно заметить, возвращает код ошибки: если линия занята, то вернется HAL_BUSY, а если операция завершена успешно — OK.

Кроме всего прочего, библиотека предусматривает максимальное время работы (англ. time-out) с периферийным блоком. Если блок используется дольше определенного времени, функция завершает свою работу и возвращает код ошибки HAL_TIMEOUT.

Также HAL реализует механизм пользовательских обратных функций (англ. user-callback).

Все они в файле библиотеки объявляются с «модификатором» __weak5 (слабый) и могут быть переопределены разработчиком (менять их внутри библиотеки не нужно!)

Если необходимо произвести действие по событию завершения отправки сообщения, то функцию HAL_UART_TxCpltCallback() нужно поместить в файл обработчиков прерываний stm32fxx_it.c. Библиотека достаточно сильно абстрагирует от железа. Обязательно загляните в файлы исходного кода и ужаснитесь, какой ценой. Детальное описание библиотеки можно найти в документе UM1850.

Отношение к библиотеке HAL у многих разработчиков отрицательное: она очень громоздкая и запутанная. Еще и использует goto, что многими считается плохой практикой. Вот комментарий одного из пользователей на stackoverflow:

My advice: forget the hal. Use bare registers instead

Мой совет: забудь про HAL. Работай с голыми регистрами.

Если, однако, вы работаете с каким-нибудь stm32f7, у вас высокая тактовая частота и мегабайты флеш-памяти, HAL можно использовать без раздумий — нужно только проникнуться ее «философией». Подключить библиотеку к проекту можно, определив макрос USE_HAL_DRIVER в настройках среды разработки.


Назад | Оглавление


1 Никто не защищен от ошибок. В библиотеке могут быть ошибки, и когда что-то не работает, возможно, причина в этом.
2 Менять содержимое библиотеки — плохая практика. В интегрированных средах разработки обычно можно определять константы и вводить маркеры для компилятора. Это делается в настройках.
3 Модули FLASH, NVIC (есть в CMSIS), DFSDM, CRYP, HASH, SDMMC(SDIO) низкоуровневой библиотекой не поддерживаются.
4 Си не является объектно-ориентированным языком программирования.
5 Атрибут __attribute__((weak)) позволяет сообщить компоновщику, что данная функция «слабая», т.е. имеет «меньший приоритет». Если находится такая же функция без данного атрибута, то она считается «сильной» и перезаписывает «слабую».