Микроконтроллер и ядро ARM Cortex-M3
Как уже говорилось выше, микроконтроллер (англ. microcontroller или MCU — MicroController Unit), в отличие от компьютерного процессора, включает в себя не только цепочки для выполнения математических операций (АЛУ), но и оперативную и постоянную память, различные контроллеры (например NVIC), модули преобразователей (ADC, DAC) и аппаратные реализации различного рода интерфейсов передачи данных (SPI, USART, I2C, CAN и т.д.).
Как все вещества состоят из атомов, так и микроконтроллер (по большей части) состоит из транзисторов, соединенных определенным образом между собой. Память, периферия — это всё транзисторные цепочки. Подав напряжение на определенный компонент, называемый регистром (англ. register), можно включить или отключить другую внутреннюю цепочку, тем самым, скажем, настроив порт ввода-вывода на вход в режиме Hi-Z или увеличив множитель в системе тактирования. Возможно, сейчас это звучит не очень понятно, но мы вернемся к этим понятиям позже.
Сердцем любого МК является ядро (англ. core). Некоторые компании занимаются разработкой и продвижением собственных архитектур (так, Atmel/Microchip продвигает AVR, а Texas Instruments — MSP430), другие же выпускают микроконтроллеры с лицензируемой архитектурой ARM, разрабатываемой британской компанией ARM Limited. На данный момент ARM представляется наиболее эффективной, предлагая превосходную производительность с относительно низкой ценой. Тем не менее, простые 8- и 16-битные микроконтроллеры до сих пор находят применение там, где нужна минимальная цена за кристалл.
Все вендоры, производящие МК или процессоры на базе решений ARM, выплачивают роялти ARM Limited. Вероятно, в ближайшее время часть производителей будет переходить на ядро с открытой архитектурой RISC-V.
В начале мы заявили, что приводить примеры кода будем для микроконтроллера от компании ST Microelectronics — stm32f103с8, построенного на ядре ARM Cortex-M3. Как должно быть понятно из названия, данный микроконтроллер является 32-битным, т.е. элементарная ячейка данных представляет собой 32 бита, или 4 байта.
Устоявшейся классификации не существует, однако все МК можно разделить по трем классам параметров: набору инструкций, разрядности (размер обрабатываемых данных — 2n бит) и назначению.
Классификация по набору инструкций
- CISC (англ. Complex Instruction Set Computing) — больше присуща процессорам (так, x86 имеет данную архитектуру), из микроконтроллеров ее унаследовало ядро 8051 (которое разработала Intel);
- RISC (англ. Reduced Instruction Set Computing) — используется в большинстве микроконтроллеров.
Нашей целью не является разбор архитектур и описание их преимуществ и недостатков, заметим лишь, что в RISC, исходя из названия, набор команд сокращенный. Все инструкции фиксированной длины и выполняются за один цикл. Такой подход позволяет упростить реализацию в железе, но повышает сложность компилятора. ARM-ядро (от англ. Advanced RISC Machine — усовершенствованная RISC-машина) имеет RISC-архитектуру, но не в чистом виде, так как не все инструкции ARM выполняются за один цикл.
Классификация МК по разрядности шины
- 8-битные (Atmel ATtiny/ATmega/ATXmega, STM8 и др.);
- 16-битные (Texas Instruments MSP430, Microchip PIC24 и др.);
- 32-битные (STM32, NXP LPC2xxx и др.)
Классификация по назначению
- универсальные — данный вид МК появился раньше всех, они содержат разнообразную периферию;
- специализированные — по мере развития цифровой техники стало ясно, что для решения конкретных задач нужны «заточенные» под определенную задачу микроконтроллеры, например, MP3-декодер или различного рода DSP (от англ. digital signal processor).
Так как ARM ядро унифицировано, т.е. у разных производителей оно построено одинаково, то и программный код должен исполняться одинаково для МК любого производителя. Для упрощения разработки ARM Limited предоставляет библиотеку CMSIS (от англ. Cortex Microcontroller Software Interface Standard), позволяющую писать почти кросс-вендорный код. Слово «почти» здесь написано по той причине, что помимо ядра в микроконтроллере присутствуют и периферийные блоки, которые уже являются вендор-зависимыми, т.е. разрабатываются самими производителями микроконтроллеров. Так, в дополнение к CMSIS для семейства МК stm32f10x от ST Microelectronics предоставляется заголовочный файл stm32f10x.h
, который по сути является драйвером МК — в нем описаны все адреса регистров для работы с периферией, макроопределения и т.д. Ниже приведена диаграмма устройства данной линейки.

Периферийные блоки могут сильно отличаться от микроконтроллера к микроконтроллеру, в зависимости от задач, которые они должны решать. Как правило, в любом МК можно встретить:
- порты ввода-вывода общего назначения (англ. general-purpose input/output) — служат для управления внешними по отношению к МК цепями и устройствами;
- таймеры (англ. timers), базовая функция которых считать, однако на их основе можно формировать задержки, генерировать широтно-импульсную модуляцию, работать с энкодерами и т.д.;
- аналого-цифровой преобразователь, АЦП (англ. analog-to-digital converter, ADC) — преобразует аналоговый сигнал, например напряжение от 0 до 3,3 В в число в диапазоне от 0 до 4095 (12-битный АЦП);
- цифро-аналоговый преобразователь (англ. digital-to-analog converter, DAC) — противоположный АЦП, позволяет формировать аналоговый сигнал;
- аппаратные решения для разнообразных интерфейсов — USART, SPI, I^2^C и т.д.
Отличаться может и само ядро. В stm32f103c8 располагается ARM Cortex-M3 (ARMv7-M), пожалуй, наиболее распространенная архитектура на сегодня. Сами Cortex’ы бывают трех семейств:
- Cortex-A — ядра общего назначения, такие устанавливаются, например, в смартфоны;
- Cortex-M — для встраиваемых систем;
- Cortex-R — для приложений реального времени.
Встраиваемой системой называют особый вид компьютерной системы, которая решает одну специализированную задачу, чаще всего в режиме реального времени, т.е. время обработки сигналов имеет критическое значение.
Cortex-M также подразделяется на несколько типов, которые отличаются по производительности:

- Cortex-M0, Cortex-M0+ (более энергоэффективное) и Cortex-M1 (оптимизировано для применения в ПЛИС) с архитектурой ARMv6-M;
- Cortex-M3 с архитектурой ARMv7-M;
- Cortex-M4 (добавлены SIMD-инструкции, опционально FPU) и Cortex-M7 (FPU с поддержкой чисел одинарной и двойной точности) с архитектурой ARMv7E-M;
- Cortex-M23 и Cortex-M33 с архитектурой ARMv8-M.
К ключевым особенностям Cortex M3 относятся: 32-битное ядро / шина данных; гарвардская архитектура (англ. Harvard architecture) — раздельная шина данных и инструкций; трехступенчатый конвейер (англ. pipeline): этап выборки (англ. fetch), дешифровки (англ. decode), и исполнения (англ. execute); блок векторов прерываний (англ. nested vectored interrupt controller), позволяющий обрабатывать исключительные события; поддержка интерфейса отладки JTAG или SWD (англ. serial wire debug).

Любое устройство воспринимается микроконтроллером как модуль памяти, хотя физически таковым может и не являться. Память программы, оперативная память, регистры устройства ввода-вывода — все они находятся в едином адресном пространстве. Структура адресного пространства Cortex-M3 закреплена стандартом и не изменяется от производителя к производителю. Так как ядро 32-битное, то размер адресуемого пространства численно равен 232 = 4 гигабайтам.
Не стоит путать адресное пространство с реальным объёмом памяти. 4 гигабайта — это то количество элементарных ячеек памяти (байтов), которое способна адресовать шина. В реальности у микроконтроллера на борту может быть всего 16 килобайт flash-памяти и 6 килобайт ОЗУ.
Разрядность, не гарантирует размер адресного пространства, иначе микроконтроллер STM8 мог бы адресовать всего 512 байт. По этой причине адресное пространство расширяют. Так, например, у STM8 до 24 бит, а у MSP430 до 20.
Первый гигабайт памяти распределен между областью кода и статического ОЗУ. Следующие полгигабайта памяти отведены для встроенных устройств ввода-вывода. Следующие два гигабайта отведены для внешнего статического ОЗУ и внешних устройств ввода-вывода. Последние полгигабайта зарезервированы для системных ресурсов процессора Cortex. Диаграмма карты памяти (англ. memory map) приведена ниже.

За более подробным описанием карты памяти следует обратиться к документации.
В дальнейшем мы раскроем подробнее некоторые понятия, такие как «конвейер», а сейчас перейдем к понятию «прерывание» (англ. interrupt) и модулю NVIC в целом.
Мало в каком устройстве нет кнопок, состояние которых необходимо отслеживать. Обычно подобные элементы управления подключаются к одному из входов МК таким образом, что при нажатии на нем изменяется напряжение. Например, кнопка отжата — на входе 0 вольт, кнопка нажата — на входе 3,3 вольта. Если отслеживание уровня напряжения происходит простым считыванием входного регистра, то существует вероятность пропустить сам факт нажатия. Допустим, время выполнения некоторого участка кода занимает 1 секунду, а время кнопки в нажатом состоянии всего 200 миллисекунд. При таком раскладе можно нажать кнопку раза четыре и не получить ответной реакции. Для обработки подобных асинхронных, по отношению к самой программе, событий (англ. event), необходимо использовать механизм прерываний. Он позволяет реагировать на их появление моментально.
В действительности имеется небольшая задержка (англ. latency), откуда она берётся, вы поймёте чуть позже.
Для пояснения данного понятия прибегнем к аналогии из жизни. Представьте, что вы попросили своего друга объяснить, что же такое прерывание. И спустя каких-то пять секунд после этого звонит ваша возлюбленная, что заставляет вас прервать беседу и со словами: «Прости, мне надо ответить…» — взять трубку. Закончив разговор, вы возвращаетесь к понятию прерывания и ждете, когда ваш друг даст определение. Всё, что ему нужно сказать, — «Собственно, это оно и было». Другими словами, программа останавливается (при этом ее текущее состояние сохраняется на стек), и начинает работать другой участок кода, называемый обработчиком (англ. handler) прерывания. По завершении выполнения обработчика программа возвращается на то место, где была прервана, и продолжает свою работу.

Каждое прерывание вызывается событием, но не каждое событие вызывает прерывание.
Это два пересекающихся множества.

В зависимости от источника, прерывания можно разделить на три типа.
- Асинхронные (или внешние) — это такие события, которые исходят от внешних источников, таких как периферийные устройства, а значит, могут произойти в произвольный момент времени. Они создают запрос на прерывание (англ. Interrupt ReQuest, IRQ).
- Синхронные (или внутренние) — это события непосредственно в ядре, вызванные нарушением условий при исполнении кода: делением на ноль, переполнением стека, обращением к недопустимым адресам памяти и т.д.
В 1996 году один из кораблей американского флота, USS Yorktown (CG-48), был модернизирован по программе Smart Ship. 27 компьютеров на базе Intel Pentium, работавших под управлением Windows NT 4.0, следили за состоянием систем и управляли кораблём. Во время манёвров 21 сентября 1997 года оператор ввёл в одно из полей базы данных число
0
, которое участвовало в делении. Из-за отсутствия проверки была произведена операция деления на ноль, из-за чего вся бортовая сеть вышла из строя, повредив при этом двигатели. Корабль 4 часа стоял в море, пока его не отбуксировали в порт. В Cortex-M3/M4 деление на ноль вызывает исключительную ситуацию и программа падает в обработчикUsageFault()
. В Cortex-M0/M0+ аппаратной инструкции деления нет, вместо неё вызываются процедуры из стандартной библиотеки. Другими словами, это неопределённое поведение (англ. undefined behavior). Так, в компиляторе GCC справедливоx / 0 == 0
, а в компиляторе от IAR —x / 0 = x
.
- Программные (частный случай внутреннего прерывания) — прерывание может быть вызвано непосредственно в коде исполняемой программы.
Все имена существующих векторов прерываний описаны в файле startup_<mcu>.s
.
#include "stm32f10x.h"
// ...
.word PendSV_Handler
.word SysTick_Handler
.word WWDG_IRQHandler /* Window WatchDog */
.word RTC_IRQHandler /* RTC through the EXTI line */
.word FLASH_IRQHandler /* FLASH */
.word RCC_CRS_IRQHandler /* RCC and CRS */
.word EXTI0_1_IRQHandler /* EXTI Line 0 and 1 */
// ...
Код, который помещается в обработчик, должен выполняться настолько быстро, насколько это возможно, чтобы передать управление основной программе.
Возможны разнообразные прерывания по самым разным причинам. Поэтому каждому прерыванию ставят в соответствие число — так называемый номер прерывания (англ. position). Чтобы связать адрес обработчика с номером, используется таблица векторов прерываний (англ. vector table).
Таблица прерываний в микроконтроллерах с ядром ARM является векторной. Каждый элемент в ней — это 32-битный адрес, указывающий на определенный обработчик: вектор с адресом 0x08
указывает на NMI-прерывание, а 0x0C
соответствует HardFault
.
Первые 15 элементов строго закреплены стандартом ядра, т.е. одинаковы для всех микроконтроллеров на данной архитектуре. Все последующие прерывания называются вендор-зависимыми (англ. vendor specific), т.е. зависят от производителя (прерывания от блоков RTC, USB, UART и т.д.). Таблицу со стандартной частью можно найти в Cortex-M3 Devices Generic User Guide.
Номер исключения | IRQ | Приоритет | Смещение адреса |
---|---|---|---|
1 | — | -3 | 0x00000004 |
2 | -14 | -2 | 0x00000008 |
3 | -13 | -1 | 0x0000000C |
4 | -12 | Настраиваемый | 0x00000010 |
5 | -11 | Настраиваемый | 0x00000014 |
6 | -10 | Настраиваемый | 0x00000018 |
7-10 | — | — | — |
11 | -5 | Настраиваемый | 0x0000002C |
13 | — | — | — |
14 | -2 | Настраиваемый | 0x00000038 |
15 | -1 | Настраиваемый | 0x0000003C |
16 | 0 | Настраиваемый | 0x00000040 |
В таблице можно заметить такую колонку, как «приоритет» (англ. priority). Это контринтуитивно, но чем меньше число, описывающее его, тем более важным является прерывание. Первые три прерывания (Reset
, NMI
и HardFault
) описываются отрицательным числом. Приоритеты всех остальных прерываний можно настраивать (по умолчанию они имеют нулевой приоритет).
Но зачем нужны приоритеты? Буква N в названии модуля NVIC происходит от слова nested, т.е. «вложенный». Если во время работы обработчика некоторого прерывания произойдет другое, приоритет которого больше, чем того, что обрабатывается сейчас, то произойдет то же самое, что и с основной программой, — обработчик будет остановлен, управление перехватит более приоритетное прерывание.
Между началом выполнения кода в обработчике и моментом вызова проходит 12 тактов. Если во время прерывания происходит другое (менее приоритетное), то оно встаёт в очередь, при этом между завершением текущего и запуском следующего прерывания тратится всего 6 тактов.
Системные прерывания по умолчанию имеют наивысший уровень, а все остальные — более низкий и одинаковый. Т. е. одно внешнее прерывание не может вытеснить другое внешнее прерывание. Если во время обработки сообщения по UART произойдет прерывание от АЦП, то оно будет ждать завершения прерывания от UART. (Выполняться они будут от меньшего номера к большему). Программист самостоятельно должен менять приоритеты, чтобы добиться желаемой реакции.
Для понимания работы некоторых вещей, описанных далее, понадобятся три системных (т.е. в любом ARM) прерывания: SysTick_Handler
, SVC_Handler
и PendSV_Handler
, поэтому приведем их краткое описание.
SysTick_Handler
. В состав любого микроконтроллера с Cortex-M входит системный 24-битный таймер SysTick. Таймеры предназначены в основном для подсчета (хотя их использование этим не ограничивается), например тактов. Сам микроконтроллер понятия не имеет о такой сущности, как время, — всё, что ему понятно, это тактовый сигнал. Однако если частота известна и стабильна, то, отсчитав некоторое число тактов, можно отмерить время. Например, пусть тактирующая частота, составляет 16 МГц (f), тогда за одну миллисекунду должно пройти f / 1000 тактов, т.е. 16000. При достижении данного числа таймер произведет прерывание.SVC_Handler
. Данный обработчик прерывания выполняется после вызова инструкцииsvc
(сокр. от Super Visor Call), которая запрашивает привилегированный режим работы у ядра. Используется для запуска планировщика задач (точнее, позволяет планировщику запустить первую задачу в списке при старте системы), о котором мы еще поговорим.PendSV_Handler
. Данное прерывание (сокр. от Pendable SerVice) используется операционной системой для переключения задач.
Последнее, о чём следует упомянуть в данном разделе, это специализированный тип памяти, называемый регистром (англ. register). Регистры бывают двух типов:
- регистры ядра;
- регистры периферии.
В Cortex-M3 насчитывается 21 регистр ядра. Первые 13 называют регистрами общего назначения и разбивают на две группы: нижние R0
—R7
и верхние R8
—R12
. К нижним возможно применять как 16-битные инструкции Thumb, так и 32-битные Thumb-2, а к верхним применимы только 16-битные, и то не все. Впрочем, такие тонкости вряд ли нужны разработчикам на Си, так как обращаться к этим регистрам можно только через язык ассемблера.

Регистры общего назначения — это ячейки памяти, расположенные непосредственно в ядре и предназначенные для выполнения инструкций. Именно в них подгружаются значения переменных и затем совершаются такие операции, как сложение или вычитание. Отсюда вытекают некоторые особенности: поскольку в Cortex-M3 нет инструкций для работы с числами с плавающей запятой, данные операции раскладываются на ряд элементарных доступных инструкций. Следовательно, обычное сложение таких чисел займет больше одного цикла. Другая особенность — желательно, чтобы количество используемых переменных в области видимости (в функции) не превышало количество регистров общего назначения. В противном случае «лишние» переменные будут храниться в оперативной памяти, обращение к которой — довольно медленная операция.
Регистр R13
отводится под указатель стека (англ. stack pointer, SP). На самом деле их два, но в любой момент времени доступен только один из них. Первый называется системным (англ. main stack pointer, MSP), а второй пользовательским (англ. process stack pointer, PSP). Подробное описание архитектуры не входит в наши задачи, но в грубом приближении такое разделение необходимо для разделения программы привилегированного уровня выполнения (прерывания или операционной системы) и пользовательского приложения (т.е. нашей основной программы).
При написании обычной прошивки стек MSP всегда используется для обработки исключительных ситуаций (прерываний), а PSP — только для исполнения обычной программы. В случае ОС компания ARM рекомендует использовать MSP для ядра системы и прерываний, а стек PSP — для выполнения задач.
Регистр связи (англ. link register) R14
используется для запоминания адреса возврата при вызове подпрограммы (функции), что позволяет вернуться к выполнению прерванного кода.

Регистр R15
, счетчик команд (англ. program counter), отводится для хранения адреса текущей команды.
Все последующие регистры именуются специальными (англ. special registers). Первый из них называется PSR (от англ. program status register) и состоит из трех частей:
APSR
— регистр, хранящий состояния приложения при помощи флагов:N
(англ. negative flag) — отрицательный результат операции;Z
(англ. zero flag) — нулевой результат операции;C
(англ. carry flag) — флаг переноса/займа;V
(англ. overflow flag) — флаг переполнения;Q
(англ. saturation flag) — флаг насыщения.
IPSR
— регистр, хранящий номер обрабатываемого прерывания;EPSR
— регистр состояния выполнения.
В регистре PRIMASK
(англ. priority mask) используется только один бит (из 32), который по умолчанию установлен в 0
, запрещая все прерывания с настраиваемым приоритетом (т.е. все прерывания, кроме системных). Если записать туда 1
, прерывания разрешаются.
Следующий регистр, FAULTMASK
, управляет маскируемыми (переключаемыми) прерываниями, глобально разрешая или запрещая их, кроме NMI
(англ. non-maskable interrupt). По умолчанию нулевой бит сброшен в ноль, т.е. такие прерывания запрещены.
Регистр BASEPRI
использует первые 8 бит и применяется для запрета прерываний, приоритет которых меньше или равен записанному в него значению. Чем меньше значение, тем выше уровень приоритета. Всего получается 128 уровней.
В stm32 используются только первые 4 бита, т.е. уровней прерываний всего 16.
Последний регистр, CONTROL
, отвечает за режим работы процессора и используемого стека.

Режимов работы у ядра может быть два: привилегированный (англ. privileged) и непривилегированный (англ. unprivileged). Нулевой бит регистра, nPRIV
, задает режим, а первый, SPSEL
, — используемый стек. В привилегированном режиме доступны все области памяти и инструкции. При попытке обращения к запрещенным областям памяти или вызова некоторых инструкций в непривилегированном режиме последует исключение, и выполнение программы прекратится, т. е. выполнение перейдет к одному из обработчиков исключительных ситуаций: Hard Fault, MemManage Fault, Usage Fault или Bus Fault.
Причины и соответствующие им обработчики описаны в документе Cortex-M3 Devices Generic User Guide.
void HardFault_Handler(void) {
while(1) {}
}
Данный обзор нам пригодится для описания работы операционной системы реального времени. Второй тип регистров — регистры периферии. С их помощью происходит управление внутренними цепями микроконтроллера через триггеры (англ. trigger), что позволяет подключать или отключать необходимую функциональность.
По умолчанию тактирование всей периферии отключено для экономии энергии. Следовательно, если разработчик желает использовать тот или иной модуль, ему придется вручную включать его. Если в некоторых целях используется порт ввода-вывода A
, то первое, что необходимо сделать, — подать на него питание и тактирующий сигнал. У выбранного нами в начале МК за данную функциональность отвечает один из регистров блока сброса и тактирования (англ. reset and clock control):

Используя драйвер stm32f10x.h
, тактирование можно включить следующим образом:
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
Данной строчкой кода мы обращаемся к структуре RCC
, поля которой не что иное, как регистры. Каждый регистр, например APB2ENR
, имеет свой адрес и задан макросом в драйвере.
#define RCC ((RCC_TypeDef *) RCC_BASE)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
#define PERIPH_BASE ((uint32_t)0x40000000)
// (0x40000000 + 0x20000 + 0x1000) = 0x40021000
Т.е. RCC
ссылается на байт по адресу 0x40021000
в пространстве памяти, который приводится к типу структуры RCC_TypeDef
, которая, в свою очередь, инкапсулирует регистры данного модуля. Сверим данный адрес с документацией на МК:

Маска RCC_APB2ENR_IOPAEN
заменяется на число 4, т.е. 22, или 1 на третьей позиции в регистре APB2ENR
. Данная ячейка в памяти является триггером и управляет тактированием порта A
. Строчку кода выше можно с тем же успехом записать по-другому:
// APB2ENR address offset is 0x18
*((uint32_t *)0x40021018) = 0x00000004;
Библиотека CMSIS позволяет писать более читаемый код.
Последующие уровни абстракции (англ. hardware abstraction layer, HAL) и вовсе избавляют от необходимости работать с регистрами напрямую. Подобные библиотеки сводят приведенную выше строчку к вызову функции с соответствующими аргументами.
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
А настройка периферии превращается в простое заполнение структуры с последующей передачей ее в качестве аргумента в функцию инициализации, что удобно, но уменьшает производительность.
Вышеописанное на данном этапе может быть не до конца понятным, но всё прояснится в дальнейшем. Возможно, по окончании прочтения книги имеет смысл вернуться к этой главе и перечитать ее.