Ещё раз хочу написать про простой старт с STM32, только на этот раз без использования чьих–то шаблонов или примеров - с объяснением каждого шага. В статьях будет сквозная нумерация шагов.

1. Устанавливаем IAR

Сборка проекта в IAR

1. Препроцессор

  1. удаляет комментарии

2. Компилятор

3. Линкер

3. Создаём новый проект в IAR

После запуска IAR появляется окно информационного центра, который нам не нужен. Нажимаем меню Project –> Create New Project. Выбираем toolchain: ARM (вряд ли у вас будет что–то ещё в том списке), Project templates: C –> main.

В левом окне («Workspace») правой кнопкой мыши вызываем меню и создаём новую группу (Add –>

Щёлкаем правой кнопкой по CMSIS

В группу Startup

С CMSIS закончили.

В группу StdPeriphLib

В группу User

5. Настраиваем проект

  1. General options –> Target –>
Выбираем ST –> STM32F100 –> ST STM32F100xB. Это наш контроллер. 2. General options –> Library Configuration –> CMSIS: ставим галочку Use CMSIS. Так мы будем использовать встроенную в компилятор библиотеку CMSIS. С версии 6.30 IAR стал поставляться со встроенной CMSIS, и это вроде как лучше - но внесло некоторую неразбериху со старыми проектами. 3. C/C++ compiler –>
$PROJ_DIR$\

* Debugger –> Setup –> Driver: выбираем ST–Link, поскольку именно такой программатор встроен в плату Discovery. Теперь настраиваем сам программатор: * Debugger –> ST–LINK –> Interface: выбираем SWD (программатор на плате подключен к контроллеру по SWD, а не по JTAG). * Debugger –>
#include "stm32f10x_conf.h"

void main()
{
while(1)
{
}
}

<1000000; i++);


for(i=0; i<1000000; i++);

#include "stm32f10x_conf.h"

void main()
{





int i;
while(1)
{

for(i=0; i<1000000; i++);

<1000000; i++); } }

архив с проектом GPIO. К счастью, этот проект можно сохранить и дальше использовать его как шаблон, чтобы не проводить всю настройку заново. Весь цикл: 1. Порты ввода–вывода (/index.php/stm32-from_zero_to_rtos-2_timers/ "STM32 - с нуля до RTOS. 2: Таймер и прерывания") (/index.php/stm32-from_zero_to_rtos-3_timer_outputs/ "STM32 - с нуля до RTOS. 3: Выходы таймера") [Ещё раз хочу написать про простой старт с STM32, только на этот раз без использования чьих–то шаблонов или примеров - с объяснением каждого шага. В статьях будет сквозная нумерация шагов.

0. Добываем плату STM32VLDiscovery

Покупаем в магазине, стоит 600 рублей. Нужно будет установить драйвера на плату - я думаю, это затруднений не вызовет.

1. Устанавливаем IAR

Работать будем в IAR - хорошая IDE с прекрасным компилятором. Ей недостаёт удобства написания кода - но для наших целей её вполне достаточно. Я использую IAR версии 6.50.3, взять - сами знаете где.

2. Скачиваем библиотеку периферии

Я не сторонник работы с регистрами на этапе обучения. Поэтому предлагаю скачать библиотеку периферии от ST для получения удобных функций доступа ко всем нужным настройкам.

Создаём папку «STM32_Projects», складываем туда папку Libraries из скачанного архива (stsw-stm32078.zip/an3268/stm32vldiscovery_package), в ней лежит CMSIS (библиотека от ARM для всех микроконтроллеров Cortex, описание и адреса всех ресурсов) и STM32F10x_StdPeriph_Driver - библиотека периферии от ST со всеми функциями.

Также создаём там папку «1. GPIO», в которой и будет лежать наш первый проект.

Дерево папок - на рисунке. Сделайте именно так, потому что потом будут очень важны относительные пути в этом дереве.

Ну и чтобы понимать, о чём идёт речь - скачайте 1100–страничный документ на эти контроллеры.

Сборка проекта в IAR

Необходимо чётко понимать суть процесса сборки проекта. Для удобства разобьём её на стадии.

1. Препроцессор

По всем.c–файлам проекта (и по main.c, и по всем файлам в workspace) проходит препроцессор. Он делает следующее:

  1. удаляет комментарии
  2. раскрывает директивы #include, подставляя вместо них содержимое указанного файла. Этот процесс проходит рекурсивно, начиная с.c–файла и заходя в каждый встреченный #include .h, и если в.h–файле тоже встретились директивы #include - препроцессор зайдёт и в них. Получается такое дерево инклудов. Обратите внимание: он не обрабатывает ситуацию двойного включения инклудов, т.е. один и тот же.h-файл может включиться несколько раз, если он указан в #include в нескольких местах проекта. Такую ситуацию нужно обрабатывать дефайнами.
  3. совершает макроподстановки - раскрывает макросы
  4. собирает директивы компилятора.

Препроцессор генерирует файлы.i, которые довольно удобны при поиске ошибок сборки - хотя бы потому что в них полностью раскрыты все макросы. Сохранение этих файлов можно включить в настройках проекта.

На этом этапе сборщик имеет все.c–файлы в проекте, готовые для компиляции - в виде файлов.i. Никаких связей между файлами пока нет.

2. Компилятор

После прохода препроцессора компилятор оптимизирует и компилирует каждый.i–файл, создавая бинарный код. Именно здесь требуется указание типа процессора, доступной памяти, языка программирования, уровня оптимизации и подобных вещей.

Что делает компилятор, встречая в каком-нибудь.c–файле вызов функции, которая не описана в этом файле? Он ищет её в заголовках. Если заголовки говорят, что функция лежит в другом.c–файле - он просто оставляет на этом месте указатель на этот другой файл.

На этом этапе сборщик имеет все.c–файлы проекта, собранные в.o–файлы. Они называются скомпилированными модулями. Теперь между файлами есть связи в виде указателей в местах вызова «чужих» функций - но это до сих пор несколько разных файлов.

3. Линкер

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

Линкер также может выполнить какие-нибудь финальные действия с бинарником, например посчитать его контрольную сумму.

Первый проект - работа с портами ввода–вывода

3. Создаём новый проект в IAR

После запуска IAR появляется окно информационного центра, который нам не нужен. Нажимаем меню Project –> Create New Project. Выбираем toolchain: ARM (вряд ли у вас будет что–то ещё в том списке), Project templates: C –> main.

Теперь у вас есть новый пустой проект на C и файл main.c.

4. Подключаем к проекту библиотеки

В левом окне («Workspace») правой кнопкой мыши вызываем меню и создаём новую группу (Add –> Add Group), назовём её CMSIS. Точно так же создадим группы StdPeriphLib, Startup и User. Теперь добавляем файлы в группы (я подчеркну все файлы, чтобы было удобнее следить).

Щёлкаем правой кнопкой по CMSIS , Add, Add files - заходим в Libraries/CMSIS/CM3, из папки DeviceSupport/ST/STM32F10x (поддержка кристалла) берём system_stm32f10x.c (это описание периферии конкретного кристалла и настройка тактирования). В папке CoreSupport (поддержка ядра) лежит ещё и core_cm3.c (это описание ядра Cortex M3), но мы не станем его брать - потому что он уже есть в компиляторе. Я напишу про это дальше.

В группу Startup складываем файл startup_stm32f10x_md_vl.s из папки Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/iar. Это - действия, которые нужно выполнить при старте. Практически полностью это настройка обработчиков различных прерываний (сами обработчики будут немного дальше). Там ещё есть файлы для других кристаллов, но нас интересует именно md_vl - это значит medium density (средний объём памяти, есть ещё кристаллы с маленьким и большим объёмом), value line (оценочная линия - кристалл STM32F100 предназначен лишь для оценки возможностей, и перехода на следующие семейства).

С CMSIS закончили.

В группу StdPeriphLib добавляем из папки Libraries/STM32F10x_StdPeriph_Driver/src файлы stm32f10x_rcc.c и stm32f10x_gpio.c . Первый - это функции работы с системой тактирования, а второй - работа с ножками ввода–вывода.

В группу User перетаскиваем наш main.c . Это необязательно, но так красивее.

Теперь дерево проекта GPIO выглядит так:

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

Осталось только положить в папку с проектом ещё один файл, подключающий заголовки ко всем файлам библиотеки периферии. Можно написать его самостоятельно, но проще взять готовый. Заходим в stsw-stm32078.zip/an3268/stm32vldiscovery_package/Project/Examples/GPIOToggle - там забираем файл stm32f10x_conf.h (конфигурация проекта) и кладём его в папку «1. GPIO». Это - единственный готовый файл, который мы берём.

stm32f10x_conf.h - просто свалка инклудов нужных модулей и функций assert. Эта функция будет вызываться при ошибках работы с функциями библиотеки периферии: например, засунуть в функцию GPIO_WriteBit вместо GPIOC какую–нибудь фигню - короче, ST замечательно перестраховалась. В этой функции можно просто запустить бесконечный цикл - while(1); Нам всё равно потребуется лезть в stm32f10x_conf.h - чтобы закомментировать строки включения файлов ненужной периферии, оставив только stm32f10x_rcc.h, stm32f10x_gpio.h и misc.h - поэтому можно было написать его и самостоятельно.

5. Настраиваем проект

Нажимаем правой кнопкой мыши в окне Workspace по названию проекта:

  1. General options –> Target –> Processor variant: выбираем «Device», нажимаем кнопку правее
Выбираем ST –> STM32F100 –> ST STM32F100xB. Это наш контроллер. 2. General options –> Library Configuration –> CMSIS: ставим галочку Use CMSIS. Так мы будем использовать встроенную в компилятор библиотеку CMSIS. С версии 6.30 IAR стал поставляться со встроенной CMSIS, и это вроде как лучше - но внесло некоторую неразбериху со старыми проектами. 3. C/C++ compiler –> Preprocessor. Здесь пишем пути до папок библиотеки:
$PROJ_DIR$\
$PROJ_DIR$\..\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x
$PROJ_DIR$\..\Libraries\STM32F10x_StdPeriph_Driver\inc
Макрос $PROJ_DIR$ означает текущую папку (папку проекта), а.. - переход на один уровень выше. Мы прописали пути до папки с описанием кристалла, а также до заголовочных файлов библиотеки периферии, поскольку все.c файлы в проекте подключают свои заголовки, и компилятор должен знать где их искать. Ещё здесь нужно написать USE\_STDPERIPH\_DRIVER в Defined symbols. Это подключит нужные файлы конфигурации (например, упомянутый stm32f10x_conf.h) к проекту. Таким образом, вкладка Preprocessor будет выглядеть так: * Debugger –> Setup –> Driver: выбираем ST–Link, поскольку именно такой программатор встроен в плату Discovery. Теперь настраиваем сам программатор: * Debugger –> ST–LINK –> Interface: выбираем SWD (программатор на плате подключен к контроллеру по SWD, а не по JTAG). * Debugger –> Download: ставим галочку Use flash loader(s), «Заливать прошивку во flash–память». Логично, без неё ничего не зальётся. ## 6. Пишем код А для начала напишу, что этот код будет делать. Он будет демонстрировать простую вещь, мигание светодиодом (PC8 на плате Discovery) с паузой в бесконечном цикле. Подключаем заголовочный файл конфигурации проекта, stm32f10x\_conf.h. В нём находим строчку #include «stm32f10x\_exti.h» - это 35 строка, и закомментируем её двумя слешами. Дело в том, что в нашем проекте не понадобится модуль EXTI. В файле main.c уже есть функция int main, а в ней единственное действие - return 0. Удаляем эту строчку (мы не собираемся возвращать никаких значений), меняем тип функции на void (по той же причине), и пишем бесконечный цикл:
#include "stm32f10x_conf.h"

void main()
{
while(1)
{
}
}

### Запускаем модуль GPIO Порты ввода–вывода в STM32 называются GPIO - General Purpose Input/Output. Поэтому мы и подключали библиотеку stm32f10x_gpio.c. Однако это не всё что нам нужно, немного теории: Вся периферия на кристалле по умолчанию отключена, и от питания и от тактовой частоты. Чтобы её включить, нужно подать сигнал тактирования. Этим заведует модуль RCC, а для работы с ним существует файл stm32f10x_rcc.c. Модуль GPIO висит на шине APB2. Есть ещё AHB (аналог шины процессор–северный мост) и APB1 (так же как и APB2 - аналог шины северный мост–южный мост). Поэтому первое, что нам нужно сделать - включить тактирование модуля GPIOC. Это модуль, отвечающий за PORTC; есть ещё GPIOA, GPIOB и так далее. Делается это так: RCC\_APB2PeriphClockCmd(RCC\_APB2Periph_GPIOC, ENABLE); Всё просто - вызываем функцию подачи тактового сигнала с шины APB2 на модуль GPIOC, и тем самым включаем этот модуль. Конечно, это делаем в самом начале функции void main. Здесь - только основы, нужные для понимания. У меня также есть гораздо более [подробная статья про модуль GPIO](/index.php/stm32-%e2%86%92-%d0%bf%d0%be%d1%80%d1%82%d1%8b-gpio/ "STM32 → Порты GPIO"). ### Настраиваем модуль GPIOC Осталось совсем немного, нужно сконфигурировать модуль GPIOC. Устанавливаем ножку на выход (ещё бывает вход и альтернативные функции), настраиваем резкость фронтов (в целях ЭМ совместимости), выходной драйвер (пуш–пулл или открытый исток). Это делаем сразу после инициализации порта. GPIO\_InitTypeDef GPIO\_InitStructure; GPIO\_InitStructure.GPIO\_Speed = GPIO\_Speed\_2MHz; GPIO\_InitStructure.GPIO\_Mode = GPIO\_Mode\_Out_PP; GPIO\_InitStructure.GPIO\_Pin = GPIO\_Pin\_8; GPIO\_Init(GPIOC, &GPIO\_InitStructure); Ну всё, после этого ножка PC8 будет работать как пуш–пулл выход с относительно плавными фронтами (максимальная частота переключения 2МГц. Острые фронты - это 50МГц). Плавность фронтов глазом не заметим, а на осциллографе это видно. ### Включаем светодиод Вызываем функцию GPIO\_WriteBit(GPIOC, GPIO\_Pin\_8, Bit\_SET); Светодиод включится. ### Включаем–выключаем его в цикле В цикле while(1) пишем код включения, паузы, выключения и снова паузы:

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET); for(i=0; i<1000000; i++);

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET);
for(i=0; i<1000000; i++);

Таким образом, полностью весь файл main.c выглядит так:

#include "stm32f10x_conf.h"

void main()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_Init(GPIOC, &GPIO_InitStructure);

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET);

int i;
while(1)
{
GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET);
for(i=0; i<1000000; i++);

GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET); for(i=0; i<1000000; i++); } }

## 7. Запускаем! Подключаем плату STM32VLDiscovery к компьютеру через microUSB, нажимаем в IAR кнопку Download and Debug. Программа заливается в микроконтроллер (можно заметить окошко с прогрессбаром, которое быстро закрывается - настолько мал размер программы), и начинается отладка. IAR останавливается на первой инструкции кода (это довольно удобно при отладке), нужно запустить его кнопкой Go. Всё должно работать - синий светодиод PC8 на плате STM32VLDiscovery должен Как всегда, вы можете скачать архив с проектом GPIO. К счастью, этот проект можно сохранить и дальше использовать его как шаблон, чтобы не проводить всю настройку заново. Весь цикл: 1. Порты ввода–вывода (/index.php/stm32-from_zero_to_rtos-2_timers/ "STM32 - с нуля до RTOS. 2: Таймер и прерывания") (/index.php/stm32-from_zero_to_rtos-3_timer_outputs/ "STM32 - с нуля до RTOS. 3: Выходы таймера")

](/index.php/stm32-from_zero_to_rtos-4_exti_nvic/ “STM32 - с нуля до RTOS. 4: Внешние прерывания и NVIC”) 5. Ставим FreeRTOS

Когда только начинаешь программировать микроконтроллеры или давно не занимался программированием, то разбираться в чужом коде довольно не легко. Вопросы "Что это такое?" и "Откуда это взялось?" возникают чуть ли не на каждом сочетании букв и цифр. И чем быстрее приходит понимание логики "что? зачем? и откуда?", тем легче проходит изучение чужого кода, в том числе и примеров. Правда иногда для этого приходиться не один день "попрыгать по коду" и "полистать мануалов".

У всех микроконтроллеров STM32F4xx довольно много периферии. За каждым периферийным устройством микроконтроллеров закреплена определённая, конкретная и неперемещаемая область памяти. Каждая область памяти состоит из регистров памяти, причём эти регистры могут быть 8-разрядными, 16-разрядными, 32-разрядными или ещё как, зависит от микроконтроллера. В микроконтроллере STM32F4 эти регистры 32-разрядные и каждый регистр имеет своё назначение и свой конкретный адрес. Ничто не мешает в своих программах обращаться к ним напрямую, указывая адрес. По какому адресу размещен тот или иной регистр и к какому периферийному устройству он относиться указывается в карте памяти. Для STM32F4 такая карта памяти есть в документе DM00031020.pdf, который можно найти на сайте st.com. Документ называется

RM0090
Reference manual
STM32F405xx/07xx, STM32F415xx/17xx, STM32F42xxx and STM32F43xxx advanced ARM-based 32-bit MCUs

В разделе 2.3 Memory map на странице 64 начинается таблица с адресами областей регистров и их принадлежностью к периферийному устройству. В той же таблице есть ссылка на раздел с более подробным распределением памяти для каждой периферии.

Слева в таблице указан диапазон адресов, в середине название периферии и в последнем столбце - где находиться более подробное описание распределения памяти.

Так для портов ввода-вывода общего назначения GPIO в таблице распределения памяти можно найти что для них выделены адреса начиная с 0х4002 0000. Порт ввода-вывода общего назначения GPIOA занимает диапазон адресов от 0х4002 000 до 0х4002 03FF. Порт GPIOB занимает диапазон адресов 0х4002 400 - 0х4002 07FF. И так далее.

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

Здесь также находиться таблица, но уже с картой памяти для диапазона адресов GPIO. Согласно этой карте памяти первые 4 байта принадлежат регистру MODER, следующие 4 байта принадлежат регистру OTYPER и так далее. Адреса регистров считаются от начала диапазона, принадлежащему конкретному порту GPIO. То есть каждый регистр GPIO имеет конкретный адрес, который можно использовать при разработке программ для микроконтроллера.

Но использование адресов регистров для человека неудобно и чревато большим количеством ошибок. Поэтому производители микроконтроллеров создают стандартные библиотеки, которые облегчают работу с микроконтроллерами. В этих библиотеках физическим адресам ставиться в соответствие их буквенное обозначение. Для STM32F4xx эти соответствия заданы в файле stm32f4xx.h . Файл stm32f4xx.h принадлежит библиотеке CMSIS и лежит в папке Libraries\CMSIS\ST\STM32F4xx\Include\.

Посмотрим как определяется в библиотеках порт GPIOA. Аналогично определяется и всё остальное. Достаточно понять принцип. Файл stm32f4xx.h довольно большой и поэтому лучше использовать поиск или возможности, которые предоставляет ваш toolchain.

Для порта GPIOA находим строку в которой упоминается GPIOA_BASE

GPIOA_BASE определяется через AHB1PERIPH_BASE

AHB1PERIPH_BASE в свою очередь определяется через PERIPH_BASE

А в свою очередь PERIPH_BASE определяется как 0х4000 0000. Если посмотреть карту рапределения памяти периферийных устройств (в разделе 2.3 Memory map на странице 64), то увидим этот адрес в самом низу таблицы. С этого адреса начинаются регистры всей периферии микроконтроллера STM32F4. То есть PERIPH_BASE - это начальный адрес всей периферии микроконтроллеров STM32F4xx вообще, и микроконтроллера STM32F407VG в частности..

AHB1PERIPH_BASE определяется как сумма (PERIPH_BASE + 0x00020000). (см.картинки обратно). Это будет адрес 0х4002 0000. В карте памяти с этого адреса начинаются порты ввода-вывода общего назначения GPIO.

GPIOA_BASE определяется как (AHB1PERIPH_BASE + 0x0000), то есть это начальный адрес группы регистров порта GPIOA.

Ну а сам порт GPIOA определяется как структура из регистров, размещение которых в памяти начинается с адреса GPIOA_BASE (см строку #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE).

Структура каждого порта GPIO определяется как тип GPIO_TypeDef.

Таким образом, стандартные библиотеки, в данном случае файл stm32f4xx.h , просто очеловечивают машинную адресацию. Если вы увидите запись GPIOA->ODR = 1234, то это означает, что по адресу 0х40020014 будет записано число 1234. GPIOA имеет начальный адрес 0х40020000 и регистр ODR имеет адрес 0х14 от начала диапазона, поэтому GPIOA->ODR имеет адрес 0х40020014.

Или например, вам не нравиться запись GPIOA->ODR, то можно определить #define GPIOA_ODR ((uint32_t *) 0x40020014) и получить тот же самый результат, записав GPIOA_ODR = 1234;. Только вот насколько это целесообразно? Если действительно хочется ввести свои обозначения, то лучше просто переназначить стандартные. Как это делается, можно посмотреть в файле stm32f4_discovery.h Например, вот так там определяется один из светодиодов:

#define LED4_PIN GPIO_Pin_12
#define LED4_GPIO_PORT GPIOD
#define LED4_GPIO_CLK RCC_AHB1Periph_GPIOD

Более детальное описание периферии портов находиться в stm32f4xx_gpio.h

Я указывал, что к системе подключается стандартная библиотека. На самом деле, подключается CMSIS - система обобщенного структурного представления МК, а также SPL - стандартная библиотека периферии. Рассмотрим каждую из них:

CMSIS
Представляет собой набор заголовочных файлов и небольшого набора кода для унификации и структурировании работы с ядром и периферией МК. По сути, без этих файлов невозможно нормально работать с МК. Получить библиотеку можно на странице к МК.
Эта библиотека если верить описанию создавалась для унификации интерфейсов пр работе с любым МК семейства Cortex. Однако, на деле выходит, что это справедливо только для одного производителя, т.е. перейдя на МК другой фирмы вы вынуждены изучать его периферию почти с нуля.
Хотя те файлы которые касаются процессорного ядра МК у всех производителей идентичны (хотя бы потому, что модель процессорного ядра у них одна - предоставленная в виде ip-блоков компанией ARM).
Поэтому работа с такими частями ядра как регистры, инструкции, прерывания и сопроцессорные блоки стандартна для всех.
Что касается периферии то у STM32 и STM8 (внезапно) она почти похожа, также частично это справедливо и для других МК выпущенных компанией ST. В практической части, я покажу насколько просто использовать CMSIS. Однако трудности в его использовании связаны с нежеланием людей читать документацию и разбираться в устройстве МК.

SPL
Standard Peripheral Library - стандартная библиотека периферии. Как следует из названия, назначение этой библиотеки - создание абстракции для периферии МК. Библиотека состоит из заголовочных файлов где объявлены человеко-понятные константы для конфигурирования и работы с периферией МК, а также файлы исходного кода собираемые собственно в саму библиотеку для операций с периферией.
SPL является абстракцией над CMSIS представляя пользователю общий интерфейс для всех МК не только одного производителя, но и вообще всех МК с процессорным ядром Cortex-Mxx.
Считается, что она более удобна новичкам, т.к. позволяет не думать как работает периферия, однако качество кода, универсальность подхода и скованность интерфейсов накладывают на разработчика определенные ограничения.
Также функционал библиотеки не всегда позволяет точно реализовать настройку некоторых компонентов таких как USART (универсальный синхронный-асинхронный последовательный порт) в определённых условиях. В практической части, я также опишу работу с этой частью библиотеки.

Итак, на ноги мы уже встали, в смысле на выводы микроконтроллера на плате STM32VL Discovery у нас подключено все что надо, говорить мы научились, на языке программирования Си, пора бы и в первый класс проект создать.

Написание программы

Закончив с созданием и настройкой проекта, можно приступить к написанию реальной программы. Как повелось у всех программистов, первой программой написанной для работы на компьютере, является программа, выводящая на экран надпись «HelloWorld», так и у всех микроконтроллерщиков первая программа для микроконтроллера производит мигание светодиода. Мы не будем исключением из этой традиции и напишем программу, которая будет управлять светодиодом LD3 на плате STM32VL Discovery.

После создания пустого проекта в IAR, он создает минимальный код программы:

Теперь наша программа будет всегда «крутиться» в цикле while .

Для того, чтобы мы могли управлять светодиодом, нам необходимо разрешить тактирование порта к которому он подключен и настроить соответствующий вывод порта микроконтроллера на выход. Как мы уже рассматривали ранее в первой части, за разрешение тактирования порта С отвечает битIOPCEN регистра RCC_APB2ENR . Согласно документу «RM0041 Reference manual .pdf » для разрешения тактирования шины порта С необходимо в регистре RCC_APB2ENR установить бит IOPCEN в единицу. Чтобы при установке данного бита, мы не сбросили другие, установленные в данном регистре, нам необходимо к текущему состоянию регистра применить операцию логического сложения (логического «ИЛИ») и после этого записать полученное значение в содержимое регистра. В соответствии со структурой библиотеки ST, обращение к значению регистра для его чтения и записи производится через указатель на структуру RCC -> APB 2 ENR . Таким образом, вспоминая материал со второй части, можно записать следующий код, выполняющий установку бита IOPCEN в регистре RCC_APB2ENR :

Как можно убедиться, из файла «stm32f10x.h», значение бита IOPCEN определено как 0x00000010, что соответствует четвертому биту (IOPCEN ) регистра APB2ENR и совпадает со значением указанным в даташите.

Теперь аналогичным образом настроим вывод 9 порта С . Для этого нам необходимо настроить данный вывод порта на выход в режиме push-pull. За настройку режима порта на вход/выход отвечает регистр GPIOC_CRH , мы его уже рассматривали в , его описание также находится в разделе «7.2.2 Port configuration register high» даташита. Для настройки вывода в режим выхода с максимальным быстродействием 2МГц, необходимо в регистре GPIOC_CRH установить MODE9 в единицу и сбросить бит MODE9 в нуль. За настройку режима работы вывода в качестве основной функции с выходом push-pull отвечают биты CNF 9 иCNF 9 , для настройки требуемого нам режима работы, оба эти бита должны быть сброшены в ноль.

Теперь вывод порта, к которому подключен светодиод, настроен на выход, для управления светодиодом нам необходимо изменить состояние вывода порта, установив на выходе логическую единицу. Для изменения состояния вывода порта существует два способа, первый это запись непосредственно в регистр состояния порта измененного содержимого регистра порта, так же как мы производили настройку порта. Данный способ не рекомендуется использовать в виду возможности возникновения ситуации, при которой в регистр порта может записаться не верное значение. Данная ситуация может возникнуть если во время изменения состояния регистра, с момента времени когда уже было произведено чтение состояния регистра и до момента когда произведется запись измененного состояния в регистр, какое либо периферийное устройство или прерывание произведет изменение состояния данного порта. По завершению операции по изменению состояния регистра произойдет запись значения в регистр без учета произошедших изменений. Хотя вероятность возникновения данной ситуации является очень низкой, все же стоит пользоваться другим способом, при котором описанная ситуация исключена. Для этого в микроконтроллере существует два регистра GPIOx_BSRR и GPIOx_BRR . При записи логической единицы в требуемый бит регистра GPIOx_BRR произойдет сброс соответствующего вывода порта в логический ноль. Регистр GPIOx_BSRR может производить как установку, так и сброс состояния выводов порта, для установки вывода порта в логическую единицу необходимо произвести установку битов BSn , соответствующих номеру необходимого бита, данные биты располагаются в младших регистрах байта. Для сброса состояния вывода порта в логический нуль, необходимо произвести запись битов BRn соответствующих выводов, эти биты располагаются в старших разрядах регистра порта.

Светодиод LD3 подключен к выводу 9 порта С . Для включения данного светодиода, нам необходимо подать на соответствующем выводе порта логическую единицу, чтобы «зажечь» светодиод.

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

//Не забываем подключить заголовочный файл с описанием регистров микроконтроллера

#include "stm32f10x.h"

void Delay (void );

void Delay (void )
{
unsigned long i;
for (i=0; i<2000000; i++);
}

//Наша главная функция

void main(void )
{


RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

//очистим разряды MODE9 (сбросить биты MODE9_1 и MODE9_0 в нуль)
GPIOC->CRH &= ~GPIO_CRH_MODE9;

//Выставим бит MODE9_1, для настройки вывода на выход с быстродействием 2MHz
GPIOC->CRH |= GPIO_CRH_MODE9_1;

//очистим разряды CNF (настроить как выход общего назначения, симметричный (push-pull))
GPIOC->CRH &= ~GPIO_CRH_CNF9;

while (1)
{

//Установка вывода 9 порта С в логическую единицу («зажгли» светодиод)
GPIOC->BSRR = GPIO_BSRR_BS9;


Delay();


GPIOC->BSRR = GPIO_BSRR_BR9;


Delay();

}
}

Скачать архив с исходным кодом программы, написанной с использованием непосредственного управления регистрами микроконтроллера можно по ссылке .

Наша первая работоспособная программа написана, при её написании, для работы и настройки периферии, мы пользовались данными из официального даташита «RM0041 Reference manual .pdf », данный источник информации о регистрах микроконтроллера является самым точным, но для того чтобы им пользоваться приходится перечитывать много информации, что усложняет написание программ. Для облегчения процесса настройки периферии микроконтроллера, существуют различные генераторы кода, официальной утилитой от компании ST представлена программа Microxplorer , но она пока еще малофункциональна и по этой причине сторонними разработчиками была создана альтернативная программа «STM32 Генератор программного кода » . Данная программа позволяет легко получить код настройки периферии, используя удобный, наглядный графический интерфейс (см. рис. 2).


Рис. 2 Скриншот программы STM32 генератор кода

Как видно из рисунка 2, сгенерированный программой код настройки вывода светодиода совпадает с кодом написанным нами ранее.

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

Видео режима отладки программы мигания светодиодом

Видео работы программы мигания светодиодом на плате STM32VL Discovery

Библиотечные функции работы с периферией

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

Теперь напишем нашу программу с использованием библиотеки ST. В программе требуется произвести настройку портов ввода/вывода, для использования библиотечных функций настройки портов, необходимо произвести подключение заголовочного файла «stm32f10x_gpio.h » (см. табл. 1). Подключение данного файла можно произвести расскоментированием соответствующей строки в подключенном заголовочном конфигурационном файле «stm32f10x_conf.h ». В конце файла «stm32f10x_gpio.h » имеется список объявлений функций для работы с портами. Подробное описание всех имеющихся функций можно прочитать в файле «stm32f10x_stdperiph_lib_um.chm », краткое описание наиболее часто применяемых приведено в таблице 2.

Таблица 2.Описание основных функций настройки портов

Функция

Описание функции, передаваемых и возвращаемых параметров

GPIO_DeInit (
GPIO_TypeDef* GPIOx)

Производит установку значений регистров настройки порта GPIOx на значения по умолчанию

GPIO_Init (
GPIO_TypeDef* GPIOx,

Производит установку регистров настройки порта GPIOx в соответствии с указанными параметрами в структуре GPIO_InitStruct

GPIO_StructInit (
GPIO_InitTypeDef* GPIO_InitStruct)

Заполняет все поля структуры GPIO_InitStruct, значениями по умолчания

uint8_t GPIO_ReadInputDataBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin);

Чтение входного значения вывода GPIO_Pin порта GPIOx

uint16_t GPIO_ReadInputData (
GPIO_TypeDef* GPIOx)

Чтение входных значений всех выводов порта GPIOx

GPIO_SetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Установка выходного значения вывода GPIO_Pin порта GPIOx в логическую единицу

GPIO_ResetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Сброс выходного значения вывода GPIO_Pin порта GPIOx в логический ноль

GPIO_WriteBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin,
BitAction BitVal)

Запись значения BitVal в вывод GPIO_Pin порта GPIOx

GPIO_Write(
GPIO_TypeDef* GPIOx,
uint16_t PortVal)

Запись значения PortVal в порт GPIOx

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

Список переменных, включенных в структуры для функций работы с портами, описаны в том же файле несколько выше описания функций. Так, например, структура «GPIO_InitTypeDef » имеет следующую структуру:

typedef struct
{

uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */

GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIOSpeed_TypeDef */

GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIOMode_TypeDef */

}GPIO_InitTypeDef;

Первое поле данной структуры содержит переменную «GPIO _ Pin » типа unsigned short , в данную переменную необходимо записывать флаги номеров соответствующих выводов, для которых предполагается произвести необходимую настройку. Можно произвести настройку сразу несколько выводов, задав в качестве параметра несколько констант через оператор побитовое ИЛИ (см. ). Побитовое ИЛИ «соберёт» все единички из перечисленных констант, а сами константы являются маской, как раз предназначенной для такого использования. Макроопределения констант указаны в этом же файле ниже.

Второе поле структуры «GPIO_InitTypeDef » задает максимально возможную скорость работы выхода порта. Список возможных значений данного поля перечислен выше:

Описание возможных значений:

  • GPIO_Mode_AIN - аналоговый вход (англ. Analog INput);
  • GPIO_Mode_IN_FLOATING - вход без подтяжки, болтающийся (англ. Input float) в воздухе
  • GPIO_Mode_IPD - вход с подтяжкой к земле (англ. Input Pull-down)
  • GPIO_Mode_IPU - вход с подтяжкой к питанию (англ. Input Pull-up)
  • GPIO_Mode_Out_OD - выход с открытым стоком (англ. Output Open Drain)
  • GPIO_Mode_Out_PP - выход двумя состояниями (англ. Output Push-Pull - туда-сюда)
  • GPIO_Mode_AF_OD - выход с открытым стоком для альтернативных функций (англ. Alternate Function). Используется в случаях, когда выводом должна управлять периферия, прикрепленная к данному выводу порта (например, вывод Tx USART1 и т.п.)
  • GPIO_Mode_AF_PP - то же самое, но с двумя состояниями

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

Для работы со структурами, их также как и переменные, необходимо объявить и присвоить им уникальное имя, после чего можно обращаться к полям объявленной структуры, по присвоенному ей имени.

//Объявляем структуру

/*
Прежде чем начать заполнение полей структуры, рекомендуется проинициализировать содержимое структуры данными по умолчанию, это делается в целях предотвращения записи неверных данных, если по какой либо причине не все поля структуры были заполнены.

Для передачи значений структуры в функцию необходимо перед именем структуры поставить символ &. Данный символ говорит компилятору, что необходимо передавать функции не сами значения, содержащиеся в структуре, а адрес в памяти, по которому располагаются данные значения. Это делается для того, чтобы уменьшить количество необходимых действий процессора по копированию содержимого структуры, а также позволяет экономить оперативную память. Таким образом, вместо передачи в функцию множества содержащихся в структуре байт, будет передан только один, содержащий адрес структуры.
*/

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */

GPIO_Init_struct.GPIO_Pin=GPIO_Pin_9;

/* Подобным образом заполним поле GPIO_Speed */

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

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

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

//Не забываем подключить заголовочный файл с описание регистров микроконтроллера

#include "stm32f10x.h"
#include "stm32f10x_conf.h"

//объявляем функцию программной задержки

void Delay (void );

//сама функция программной задержки

void Delay (void )
{
unsigned long i;
for (i=0; i<2000000; i++);
}

//Наша главная функция

void main(void )
{

//Разрешаем тактирование шины порта С
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

//Объявляем структуру для настройки порта
GPIO_InitTypeDef GPIO_Init_struct;

//Заполняем структуру начальными значениями
GPIO_StructInit(&GPIO_Init_struct);

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */
GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

// Подобным образом заполним поля GPIO_Speed и GPIO_Mode
GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

//Передаем заполненную структуру, для выполнения действий по настройке регистров
GPIO_Init(GPIOC, &GPIO_Init_struct);

//Наш основной бесконечный цикл
while (1)
{
//Установка вывода 9 порта С в логическую единицу ("зажгли" светодиод)
GPIO_SetBits(GPIOC, GPIO_Pin_9);

//Добавляем программную задержку, чтобы светодиод светился некоторое время
Delay();

//Сброс состояния вывода 9 порта С в логический ноль
GPIO_ResetBits(GPIOC, GPIO_Pin_9);

//Добавляем снова программную задержку
Delay();
}
}

ссылке .

Из приведенного выше примера видно, что использование библиотечных функций работы с периферией позволяет приблизить написание программ для микроконтроллера к объектно-ориентированному программированию, а также снижает необходимость в частом обращению к даташиту для чтения описания регистров микроконтроллера, но использование библиотечных функций требует более высоких знаний языка программирования. В виду этого, для людей не особо близко знакомых с программированием, более простым вариантом написания программ будет являться способ написания программ без использования библиотечных функций, с прямым обращением к регистрам микроконтроллера. Для тех же, кто хорошо знает язык программирования, но плохо разбирается в микроконтроллерах, в частности STM32, использование библиотечных функций существенно упрощает процесс написания программ.

Данное обстоятельство, а также тот факт, что компания ST позаботилась о высокой степени совместимости, как в аппаратном, так и в программном плане, различных своих микроконтроллеров, способствует более простому их изучению, в виду того, что не требуется углубляться на особенности строения различных контроллеров серии STM32 и позволяет в качестве микроконтроллера для изучения выбрать любой из имеющихся в линейке STM32 микроконтроллер.

Обработчик прерывания

Микроконтроллеры имеют одну замечательную способность – останавливать выполнение основной программы по какому-то определенному событию, и переходить к выполнению специальной подпрограммы – обработчику прерывания . В качестве источников прерывания могут выступать как внешние события – прерывания по приему/передаче данных через какой либо интерфейс передачи данных, или изменение состояния вывода, так и внутренние – переполнение таймера и т.п.. Список возможных источников прерывания для микроконтроллеров серии STM32 приведен в даташите «RM0041 Reference manual » в разделе «8 Interrupts and events ».

Поскольку обработчик прерывания также является функцией, то и записываться она будет как обычная функция, но чтобы компилятор знал, что данная функция является обработчиком определенного прерывания, в качестве имени функции следует выбрать заранее определенные имена, на которые указаны перенаправления векторов прерывания. Список имен этих функций с кратким описанием находится в ассемблерном файле «startup_stm32f10x_md_vl.s ». На один обработчик прерывания может приходиться несколько источников вызывающих прерывания, например функция обработчик прерывания «USART1_IRQHandler » может быть вызвана в случае окончания приема и окончания передачи байта и т.д..

Для начала работы с прерываниями следует настроить и проинициализировать контроллер прерываний NVIC. В архитектуре Cortex M3 каждому прерыванию можно выставить свою группу приоритета для случаев, когда возникает несколько прерываний одновременно. Затем следует произвести настройку источника прерывания.

В поле NVIC_IRQChannel указывается, какое именно прерывание мы хотим настроить. Константа USART1_IRQn обозначает канал, отвечающий за прерывания, связанные с USART1. Она определена в файле «stm32f10x.h », там же определены другие подобные константы.

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

В функцию NVIC_Init , также как и при настройке портов передается указатель на структуру для применения внесенных настроек и записи их в соответствующие регистры микроконтроллера.

Теперь в настройках модуля необходимо установить параметры, по которым данный модуль будет генерировать прерывание. Для начала следует произвести включение прерывания, это делается вызовом функции name _ITConfig() , которая находится заголовочном файле периферийного устройства.

//Разрешаем прерывания по окончанию передачи байта по USART1
USART_ITConfig(USART1, USART_IT_TXE, ENABLE);

//Разрешаем прерывания по окончанию приема байта по USART1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

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

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

Для выполнения различных, небольших, повторяющихся с точным периодом действий, в микроконтроллерах с ядром Cortex-M3 имеется специально предназначенный для этого системный таймер. В функции данного таймера входит только вызов прерывания через строго заданные интервалы времени. Как правило, в вызываемом этим таймером прерывании, размещают код для измерения продолжительности различных процессов. Объявление функции настройки таймера размещено в файле «core _ cm 3. h ». В качестве передаваемого функции аргумента указывается число тактов системной шины между интервалами вызова обработчика прерывания системного таймера.

SysTick_Config(clk);

Теперь разобравшись с прерываниями, перепишем нашу программу, используя в качестве времязадающего элемента системный таймер. Поскольку таймер «SysTick » является системным и им могут пользоваться различные функциональные блоки нашей программы, то разумным будет вынести функцию обработки прерывания от системного таймера в отдельный файл, из этой функции вызывать функции для каждого функционального блока по отдельности.

Пример файла «main.с» программы мигания светодиода с использованием прерывания:

//Подключаем заголовочный файл с описанием регистров микроконтроллера

#include "stm32f10x.h"
#include "stm32f10x_conf.h"
#include "main.h"

unsigned int LED_timer;

//Функция, вызываемая из функции-обработчика прерываний системного таймера

void SysTick_Timer_main(void )
{
//Если переменная LED_timer еще не дошла до 0,
if (LED_timer)
{
//Проверяем ее значение, если оно больше 1500 включим светодиод
if (LED_timer>1500) GPIOC->BSRR= GPIO_BSRR_BS9;

//иначе если меньше или равно 1500 то выключим
else GPIOC->BSRR= GPIO_BSRR_BR9;

//Произведем декремент переменной LED_timer
LED_timer--;
}

//Ели же значение переменной дошло до нуля, зададим новое значение 2000
else LED_timer=2000;
}

//Наша главная функция

void main(void )
{

//Разрешаем тактирование шины порта С
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

//Объявляем структуру для настройки порта
GPIO_InitTypeDef GPIO_Init_struct;

//Заполняем структуру начальными значениями
GPIO_StructInit(&GPIO_Init_struct);

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */
GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

// Подобным образом заполним поля GPIO_Speed и GPIO_Mode
GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

//Передаем заполненную структуру, для выполнения действий по настройке регистров
GPIO_Init(GPIOC, &GPIO_Init_struct);

//выбираем приоритетную группу для прерываний
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

//Настраиваем работу системного таймера с интервалом 1мс
SysTick_Config(24000000/1000);

//Наш основной бесконечный цикл
while (1)
{
//В этот раз тут пусто, все управление светодиодом происходит в прерываниях
}
}

Часть исходного кода в файле «stm32f10x_it.c»:


#include "main.h"

/**
* @brief This function handles SysTick Handler.
* @param None
* @retval None
*/

void SysTick_Handler(void )
{
SysTick_Timer_main();
}

Пример рабочего проекта программы мигания светодиода с использованием прерывания можно скачать по ссылке .

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

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

Взаимодействие пользовательского кода с регистрами ядра и периферии микроконтроллеров STM32 может быть осуществлено двумя способами: с помощью стандартных библиотек или с помощью наборов сниппетов (программных подсказок). Выбор между ними зависит от объема собственной памяти контроллера, требуемого быстродействия, срока выполнения разработки. В статье анализируются особенности структуры, достоинства и недостатки наборов сниппетов для микроконтроллеров семейств STM32F1 и STM32L0 производства компании STMicroelectronics.

Одно из преимуществ использования микроконтроллеров STMicroelectronics – широкий спектр средств разработки: документации, отладочных плат, программного обеспечения.

Программное обеспечение для STM32 включает в себя собственное ПО производства компании STMicroelectronics, источники Open Source, коммерческое ПО.

ПО от STMicroelectronics обладает важными достоинствами. Во-первых, оно доступно для бесплатного скачивания. Во-вторых, программные библиотеки представлены в виде исходных кодов – пользователь сам может модифицировать код, учитывая незначительные ограничения, описанные в лицензионном соглашении.

Библиотеки STMicroelectronics соответствуют ANSI-C и могут быть разделены по уровню абстракции (рисунок 1):

  • CMSIS (Core Peripheral Access Layer) – уровень регистров ядра и периферии, ARM библиотека;
  • Hardware Abstraction Layer – низкоуровневые библиотеки: стандартные библиотеки периферии (standard peripheral library), наборы сниппетов (snippets);
  • Middleware – библиотеки среднего уровня: операционные системы реального времени (RTOS), файловые системы, USB, TCP/IP, Bluetooth, Display, ZigBee, Touch Sensing и другие;
  • Application Field – библиотеки прикладного уровня: аудио, управление двигателями, автомобильные и промышленные решения.

На рисунке 1 видно, что для взаимодействия с уровнем CMSIS компания STMicroelectronics предлагает использовать два основных инструмента – стандартные библиотеки и сниппеты.

Стандартная библиотека – это набор драйверов. Каждый драйвер предоставляет пользователю функции и определения для работы с конкретным периферийным блоком (SPI, USART, ADC и так далее). Напрямую пользователь с регистрами уровня CMSIS не взаимодействует.

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

Каждый из способов имеет достоинства и недостатки. Выбор между ними делается с учетом доступного объема FLASH и ОЗУ, требуемого быстродействия, срока выполнения разработки, опытности программистов и других обстоятельств.

Уровень CMSIS

Микроконтроллер – это сложная цифро-аналоговая микросхема, состоящая из процессорного ядра, памяти, периферийных блоков, цифровых шин и так далее. Взаимодействие с каждым блоком происходит с помощью регистров.

С точки зрения программистов, микроконтроллер представляет собой пространство памяти. В нем размещены не только ОЗУ, FLASH и EEPROM, но и программные регистры. Каждому аппаратному регистру соответствует ячейка памяти. Таким образом, чтобы записать данные в регистр или вычитать его значение, программисту необходимо обратиться к соответствующей ячейке адресного пространства.

Человек имеет некоторые особенности восприятия. Например, символьные названия воспринимаются им гораздо лучше, чем адреса ячеек памяти. Это особенно заметно, когда используется большое число ячеек. В микроконтроллерах ARM число регистров, а значит, и используемых ячеек, превышает тысячу. Чтобы упростить работу, необходимо произвести определение символьных указателей. Это определение выполнено на уровне CMSIS.

Например, чтобы установить состояние выводов порта А, нужно записать данные в регистр GPIOA_ODR. Это можно сделать двумя способами – воспользоваться указателем с адресом ячейки 0xEBFF FCFF со смещением 0x14 или применить указатель с символьным названием GPIOA и готовую структуру, определяющую смещение. Очевидно, что второй вариант гораздо проще для восприятия.

CMSIS выполняет и другие функции. Он реализован в виде следующей группы файлов:

  • startup_stm32l0xx.s содержит ассемблерный стартовый код Cortex-M0+ и таблицу векторов прерываний. После выполнения стартовой инициализации происходит передача управления сначала функции SystemInit() (ниже будут приведены пояснения), а затем – основной функции int main(void);
  • stm32l0xx.h содержит определения, необходимые для выполнения основных операций с битами и определение типа используемого микропроцессора;
  • system_stm32l0xx.c/.h. После начальной инициализации выполняется функция SystemInit(). Она производит первичную настройку системной периферии, таймингов блока RCC;
  • stm32l0yyxx.h – файлы реализации конкретных микроконтроллеров (например, stm32l051xx.h). Именно в них определяются символьные указатели, структуры данных, битовые константы и смещения.

Взаимодействие со CMSIS. Стандартные библиотеки и сниппеты

Число регистров для микроконтроллеров STM32 в большинстве моделей превышает тысячу. Если использовать прямое обращение к регистрам, пользовательский код станет нечитаемым и абсолютно непригодным для поддержки и модернизации. Эта проблема может быть решена при использовании стандартной библиотеки периферии (standard peripheral library).

Стандартная библиотека периферии – это набор низкоуровневых драйверов. Каждый драйвер предоставляет пользователю набор функций для работы с периферийным блоком. Таким образом пользователь использует функции, а не обращается напрямую к регистрам. При этом уровень CMSIS оказывается скрытым от программиста (рисунок 2а).

Рис. 2. Взаимодействие с CMSIS с помощью стандартной библиотеки (а) и сниппетов (б)

Например, взаимодействие с портами ввода/вывода в STM32L0 реализовано с помощью драйвера, выполненного в виде двух файлов: stm32l0xx_hal_gpio.h и stm32l0xx_hal_gpio.c. В stm32l0xx_hal_gpio.h даны основные определения типов и функций, а в stm32l0xx_hal_gpio.c представлена их реализация.

Такой подход имеет вполне очевидные достоинства (таблица 1):

  • Быстрота создания кода. Программисту не требуется изучать перечень регистров. Он сразу начинает работать на более высоком уровне. Например, для прямого взаимодействия с портом ввода/вывода в STM32L0 необходимо знать и уметь работать с одиннадцатью регистрами управления/состояния, большинство из которых имеют до 32 настраиваемых битов. При использовании библиотечного драйвера достаточно освоить восемь функций.
  • Простота и наглядность кода. Пользовательский код не забит названиями регистров, может быть прозрачным и легко читаемым, что важно при работе команды разработчиков.
  • Высокий уровень абстракции. При использовании стандартной библиотеки код оказывается достаточно платформо-независимым. Например, если сменить микроконтроллер STM32L0 на микроконтроллер STM32F0 , часть кода, работающего с портами ввода/вывода, вообще не придется менять.

Таблица 1. Сравнение способов реализации пользовательского кода

Параметр сравнения При использовании стандартной
библиотеки периферии
При использовании наборов сниппетов
Размер кода средний минимальный
Затраты ОЗУ средние минимальные
Быстродействие среднее максимальное
Читаемость кода отличная низкая
Уровень независимости от платформы средний низкий
Скорость создания программ высокая низкая

Наличие дополнительной оболочки в виде драйверов имеет и очевидные недостатки (таблица 1):

  • Увеличение объема кода программы. Реализованные в библиотечном коде функции требуют дополнительного места в памяти.
  • Повышенные затраты ОЗУ за счет увеличения числа локальных переменных и использования громоздких структур данных.
  • Снижение быстродействия за счет увеличения накладных расходов при вызове библиотечных функций.

Именно наличие этих недостатков приводило к тому, что пользователь зачастую был вынужден оптимизировать код – самостоятельно реализовывать функции взаимодействия с CMSIS, оптимизировать библиотечные функции, убирая все лишнее, копировать реализации библиотечных функций непосредственно в свой код, использовать __INLINE-директивы для увеличения скорости выполнения. В результате, тратилось дополнительное время на доработку кода.

Компания STMicroelectronics, идя навстречу разработчикам, выпустила сборники сниппетов STM32SnippetsF0 и STM32SnippetsL0 .

Сниппеты входят в пользовательский код (рисунок 2б).

Использование сниппетов предоставляет очевидные преимущества:

  • повышение эффективности и быстродействия кода;
  • уменьшение объема программы;
  • снижение объемов используемой ОЗУ и нагрузки на стек.

Впрочем, стоит отметить и недостатки:

  • уменьшение простоты и наглядности кода за счет «загрязнения» его названиями регистров и самостоятельной реализацией низкоуровневых функций;
  • исчезновение платформо-независимости.

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

Стандартные библиотеки драйверов и сниппетов STM32F0 и STM32L0 (таблица 2) доступны для свободного скачивания на сайте www.st.com.

Таблица 2. Низкоуровневые библиотеки для STM32F10 и STM32L0

Более тесное знакомство со сниппетами, как и с любым ПО, следует начинать с рассмотрения особенностей лицензионного соглашения.

Лицензионное соглашение

Любой ответственный программист перед использованием сторонних программных продуктов внимательно изучает лицензионное соглашение. Несмотря на то, что сборники сниппетов производства ST Microelectronics не требуют лицензирования и доступны для свободного скачивания, это не значит, что на их использование не накладываются ограничения.

Лицензионное соглашение входит в комплект всех свободно скачиваемых продуктов производства компании STMicroelectronics. После загрузки STM32SnippetsF0 и STM32SnippetsL0 в корневом каталоге легко обнаружить документ MCD-ST Liberty SW License Agreement V2.pdf, который знакомит пользователя с правилами использования данного ПО.

В папке Project содержатся подкаталоги с примерами для конкретных периферийных блоков, готовые проекты для ARM Keil и EWARM, а также файлы main.c.

Запуск и особенности использования наборов сниппетов STM32SnippetsF0 и STM32SnippetsL0

Особенностью данных наборов снипетов является их платформозависимость. Они предназначены для работы с конкретными платами. STM32SnippetsL0 использует платформу STM32L053 Discovery board, а STM32SnippetsF0 – плату STM32F072 Discovery board.

При использовании плат собственной разработки код и проекты должны быть изменены, об этом будет более подробно рассказано в последнем разделе.

Для запуска примера необходимо выполнить ряд шагов:

  • запустить готовый проект из директории с требуемым примером. Для простоты можно воспользоваться готовыми проектами для сред ARM Keil или EWARM, расположенными в папке MDK-ARM\ и EWARM\ соответственно;
  • включить питание отладочной платы STM32L053 Discovery/STM32F072 Discovery;
  • подключить питание отладочной платы к ПК с помощью USB-кабеля. Благодаря встроенному отладчику ST-Link/V2 дополнительного программатора не потребуется;
  • открыть, настроить и запустить проект;
    • Для ARM Keil:
      • открыть проект;
      • скомпилировать проект – Project → Rebuild all target files;
      • загрузить его в контроллер – Debug → Start/Stop Debug Session;
      • запустить программу в окне Debug → Run (F5).
    • Для EWARM:
      • открыть проект;
      • скомпилировать проект – Project → Rebuild all;
      • загрузить его в контроллер – Project → Debug;
      • запустить программу в окне Debug → Go(F5).
  • провести тестирование в соответствии с алгоритмом, описанном в main.c.

Для анализа программного кода рассмотрим конкретный пример из STM32SnippetsL0: Projects\LPUART\01_WakeUpFromLPM\.

Запуск примера для LPUART

Отличительной особенностью новых микроконтроллеров семейства STM32L0 на ядре Cortex-M0+ является возможность динамического изменения потребления за счет большого числа нововведений. Одним из таких новшеств стало появление Low Power-периферии: 16-битного таймера LPTIM и приемопередатчика LPUART. Эти блоки обладают способностью тактирования, не зависящего от тактирования основной периферийной шины APB. При необходимости снижения потребляемой мощности рабочая частота шины APB (PCLK) может быть уменьшена, а сам контроллер переведен в режим пониженного потребления. При этом Low Power-периферия продолжает работу с максимальной производительностью.

Рассмотрим пример из директории Projects\LPUART\01_WakeUpFromLPM\, в котором рассматривается возможность независимой работы LPUART в режиме пониженного потребления.

При открытии проекта в среде ARM Keil отображаются всего три файла: startup_stm32l053xx.s, system_stm32l0xx.c и main.c (рисунок 4). В случае применения стандартной библиотеки в проект было бы необходимо добавить файлы драйверов.

Функционирование и анализ структуры файла Main.c

Программа из выбранного примера выполняется в несколько этапов.

После старта запускается функция SystemInit(), реализованная в system_stm32l0xx.c. Она проводит настройку параметров блока тактирования RCC (тайминги и рабочие частоты). Далее осуществляется передача управления в основную функцию int main(void). В ней инициализируется пользовательская периферия – порты вводы/вывода, LPUART – после чего контроллер переводится в режим пониженного потребления STOP. В нем обычная периферия и ядро остановлены, работает только LPUART. Он ждет начала передачи данных от внешнего устройства. При приходе стартового бита LPUART пробуждает систему и принимает сообщение. Прием сопровождается мерцанием светодиода отладочной платы. После этого контроллер вновь переводится в состояние STOP и ждет следующей передачи данных, если не было обнаружено ошибок.

Передача данных происходит при помощи виртуального COM-порта и дополнительного ПО.

Рассмотрим main.c из нашего проекта. Этот файл представляет собой стандартный С-файл. Главной его особенностью является самодокументация – наличие подробных комментариев, пояснений и рекомендаций. Пояснительная часть содержит несколько разделов:

  • заголовок с указанием названия файла, версии, даты, автора, краткого пояснения назначения;
  • описание последовательности настройки системной периферии (RCC specific features): FLASH, ОЗУ, системы питания и тактирования, периферийных шин и так далее;
  • перечень используемых ресурсов микроконтроллера (MCU Resources);
  • краткое пояснение по использованию данного примера (How to use this example);
  • краткое пояснение по тестированию примера и алгоритм его проведения (How to test this example).

Функция int main(void) имеет компактную форму и снабжена комментариями, которые в листинге 1, для большей наглядности, переведены на русский.

Листинг 1. Пример реализация функции main

int main(void)
{
/* К началу выполнения этой части когда уже произведена конфигурация системных блоков в функции SystemInit(), реализованной в system_stm32l0xx.c. */
/* конфигурация периферийных блоков*/
Configure_GPIO_LED();
Configure_GPIO_LPUART();
Configure_LPUART();
Configure_LPM_Stop();
/* проверка наличия ошибок при приеме */
while (!error) /* бесконечный цикл */
{
/* ожидание готовности LPUART и переход в режим STOP */
if((LPUART1->ISR & USART_ISR_REACK) == USART_ISR_REACK)
{
__WFI();
}
}
/* при возникновении ошибки */
SysTick_Config(2000); /* установка периода прерываний системного таймера 1 мс */
while(1);
}

В файле main.c объявлены и определены функции конфигурации периферии и две функции обработки прерываний. Рассмотрим их особенности.

В приведенном примере используются четыре функции конфигурации (листинг 2). Все они не имеют аргументов и не возвращают значений. Их главное предназначение – быстро и с наименьшими затратами занимаемого кода произвести инициализацию периферии. Это реализуется за счет двух особенностей: применения прямого обращения к регистрам и использования директивы __INLINE (листинг 3).

Листинг 2. Объявление функций конфигурации периферии

void Configure_GPIO_LED(void);
void Configure_GPIO_LPUART(void);
void Configure_LPUART(void);
void Configure_LPM_Stop(void);

Листинг 3. Пример реализации __INLINE-функции с прямым доступом к регистрам LPUART

INLINE void Configure_LPUART(void)
{
/* (1) Enable power interface clock */
/* (2) Disable back up protection register to allow the access to the RTC clock domain */
/* (3) LSE on */
/* (4) Wait LSE ready */
/* (5) Enable back up protection register to allow the access to the RTC clock domain */
/* (6) LSE mapped on LPUART */
/* (7) Enable the peripheral clock LPUART */
/* Configure LPUART */
/* (8) oversampling by 16, 9600 baud */
/* (9) 8 data bit, 1 start bit, 1 stop bit, no parity, reception mode, stop mode */
/* (10) Set priority for LPUART1_IRQn */
/* (11) Enable LPUART1_IRQn */
RCC->APB1ENR |= (RCC_APB1ENR_PWREN); /* (1) */
PWR->CR |= PWR_CR_DBP; /* (2) */
RCC->CSR |= RCC_CSR_LSEON; /* (3) */
while ((RCC->CSR & (RCC_CSR_LSERDY)) != (RCC_CSR_LSERDY)) /*(4)*/
{
/* add time out here for a robust application */
}
PWR->CR &=~ PWR_CR_DBP; /* (5) */
RCC->CCIPR |= RCC_CCIPR_LPUART1SEL; /* (6) */
RCC->APB1ENR |= RCC_APB1ENR_LPUART1EN; /*(7) */
LPUART1->BRR = 0x369; /* (8) */
LPUART1->CR1 = USART_CR1_UESM | USART_CR1_RXNEIE | USART_CR1_RE | USART_CR1_UE; /* (9) */
NVIC_SetPriority(LPUART1_IRQn, 0); /* (10) */
NVIC_EnableIRQ(LPUART1_IRQn); /* (11) */
}

Обработчики прерываний от системного таймера и от LPUART также используют прямое обращение к регистрам.

Таим образом, общение с CMSIS производится без стандартной библиотеки. Код оказывается компактным и высокоэффективным. Однако его читаемость значительно ухудшатся из-за обилия обращений к регистрам.

Использование сниппетов в собственных разработках

Предложенные наборы сниппетов имеют ограничения: необходимо использовать отладочную плату STM32L053 Discovery board для STM32SnippetsL0 , а плату STM32F072 Discovery board – для STM32SnippetsF0 .

Для применения сниппетов в своих разработках потребуется произвести ряд изменений. Во-первых, необходимо переконфигурировать проект под нужный процессор. Для этого в нем нужно сменить стартовый файл startup_stm32l053xx.s на файл другого контроллера и определить нужную константу: STM32L051xx, STM32L052xx, STM32L053xx, STM32L062xx, STM32L063xx, STM32L061xx, STM32F030, STM32F031, STM32F051 и другие. После этого при компиляции stm32l0xx.h, будет автоматически подключен нужный файл с определением периферии контроллера stm32l0yyxx.h (stm32l051xx.h/stm32l052xx.h/stm32l053xx.h/stm32l061xx.h/stm32l062xx.h/stm32l063). Во-вторых, нужно выбрать соответствующий программатор в настройках свойств проекта. Во-третьих – изменить код функций из примеров, если они не отвечают требованиям пользовательского приложения.

Заключение

Наборы сниппетов и стандартные библиотеки периферии производства компании ST Microelectronics не являются взаимоисключающими. Они дополняют друг друга, добавляя гибкость при создании приложений.

Стандартная библиотека дает возможность быстрого создания ясного кода с высоким уровнем абстракции.

Сниппеты позволяют повысить эффективность кода – увеличить производительность и сократить объем занимаемой памяти FLASH и ОЗУ.

Литература

  1. Data brief. STM32SnippetsF0. STM32F0xx Snippets firmware package. Rev. 1. – ST Microelectronics, 2014.
  2. Data brief. STM32SnippetsL0. STM32F0xx Snippets firmware package. Rev. 1. – ST Microelectronics, 2014.
  3. MCD-ST Liberty SW License Agreement V2.pdfElectromechanical Relays. Technical Information. – ST Microelectronics, 2011.
  4. Data brief. 32L0538DISCOVERY Discovery kit for STM32L053 microcontrollers. Rev. 1. – ST Microelectronics, 2014.
  5. http://www.st.com/.
О компании ST Microelectronics