Перейти к содержанию

Архитектура ARM Cortex-M3

Cortex-M3 — 32-битное ядро ARM с архитектурой ARMv7-M, на котором построены STM32F1, STM32F2, STM32L1, отечественные К1986ВЕ92 и многие другие микроконтроллеры. Ниже — регистры и режимы, карта памяти с bit-banding, исключения и NVIC с тактами по техническому руководству ARM, SysTick, MPU, отладка и рабочий пример для платы Blue Pill на CMSIS.

Обновлено Редакция GAW

Cortex-M3 — 32-битное процессорное ядро ARM для микроконтроллеров с архитектурой ARMv7-M. Ядро выполняет только команды Thumb-2, имеет конвейер из трёх ступеней, аппаратное деление и встроенный контроллер прерываний NVIC, а память и периферия лежат в одной фиксированной карте адресов на 4 ГБ. Первая редакция технического руководства ARM (TRM) на ядро вышла в декабре 2005 года, последняя ревизия ядра, r2p1, описана в 2010 году.

Производитель кристалла сам решает, сколько прерываний и бит приоритета реализовать, ставить ли блок защиты памяти MPU и какие блоки отладки. В STM32F1 стоит ревизия ядра r1p1, в STM32F2 и STM32L1 — r2p0: это видно по регистру CPUID, который читается как 0x411F C231 и 0x412F C230 (PM0056). Разница иногда важна: например, у r1p1 после сброса бит STKALIGN равен нулю, а в r2p0 и новее — единице. Примеры ниже — для STM32F103, но всё, что касается ядра, верно для любого Cortex-M3.

Коротко

  • Архитектура ARMv7-M: только команды Thumb-2 длиной 16 и 32 бита, конвейер из трёх ступеней, три шины AHB-Lite — I-Code и D-Code для области Code и System для остального пространства.
  • Скорость: 1,25 DMIPS/МГц по Dhrystone 2.1 (ARM, ST); умножение 32 × 32 — 1 такт, деление — 2–12 тактов.
  • Регистры: R0–R12, указатель стека в двух копиях — MSP и PSP, LR, PC, регистр состояния xPSR, маски PRIMASK, FAULTMASK и BASEPRI, регистр CONTROL.
  • Прерывания: до 240 внешних и NMI, от 8 до 256 уровней приоритета, у STM32F1 — 16; вход в обработчик — до 12 тактов, переход между обработчиками — 6 тактов.
  • Карта памяти: Code с 0x0000 0000, SRAM с 0x2000 0000, периферия с 0x4000 0000, регистры ядра с 0xE000 0000; bit-banding в первом мегабайте SRAM и периферии.
  • Опции: MPU на 8 областей, отладка по SWD или JTAG с блоками FPB, DWT и ITM, трассировка ETM.

Ядро: ARMv7-M, Thumb-2 и конвейер из трёх ступеней

Cortex-M3 выполняет только набор команд Thumb в варианте Thumb-2: 16-битные команды дают плотный код, 32-битные — доступ ко всем регистрам, большие константы и сложные операции. Состояния ARM, как у ARM7TDMI, у ядра нет, и переключаться между наборами не нужно. В набор входят аппаратное деление SDIV и UDIV, умножение с накоплением, насыщение SSAT и USAT, работа с битовыми полями BFI, BFC, UBFX и SBFX, перестановка битов RBIT, подсчёт ведущих нулей CLZ и условные блоки IT, в которых до четырёх команд выполняются по условию.

Конвейер состоит из трёх ступеней — выборки, декодирования и исполнения: пока одна команда исполняется, следующая декодируется, а третья выбирается из памяти. Команды по адресу перехода ядро начинает выбирать заранее, спекулятивно, поэтому перезаполнение конвейера после перехода стоит 1–3 такта — в таблицах TRM эта величина обозначена буквой P.

Структура процессора Cortex-M3: ядро, NVIC, MPU, матрица шин и отладкаЯдро ARMv7-M с конвейером из трёх ступеней — выборка, декодирование, исполнение — связано с контроллером прерываний NVIC и необязательным блоком защиты памяти MPU. Матрица шин выводит наружу три шины AHB-Lite — I-Code и D-Code для области Code, System для остального адресного пространства — и шину PPB для отладочных блоков. Внизу — пример STM32F103: к I-Code и D-Code подключён контроллер Flash-памяти, к System — матрица шин микроконтроллера с SRAM, периферией и DMA.Процессор Cortex-M3NVICдо 240 прерыванийи NMI, 8–256 уровнейЯдро ARMv7-M, команды Thumb-2ВыборкаДекодированиеИсполнениеMPU8 областей,есть не во всех МКSWJ-DPSWD, JTAGAHB-APдоступ к шинеFPBточки остановаDWTнаблюдениеITMтрассировкаETMопцияМатрица шин · невыровненный доступ, bit-banding, буфер записиI-Codeкоманды из CodeD-Codeданные из CodeSystem0x2000 0000–0xDFFF FFFFPPBшина APBFlash-память (интерфейс FLITF)0x0800 0000, копия с 0x0000 0000Матрица шин МК + DMASRAM, периферияTPIU, ETM,ROM-таблицаВнизу — что подключено к шинам в STM32F103 (RM0008, разд. 3.1)
Процессор Cortex-M3: ядро с конвейером, NVIC, необязательный MPU, матрица шин и блоки отладки. Внизу — что подключено к шинам в STM32F103.

Шины I-Code, D-Code и System

Снаружи у ядра три шины AHB-Lite и шина PPB:

  • I-Code — выборка команд из области Code, 0x0000 0000–0x1FFF FFFF. Выборка всегда идёт 32-битными словами; сколько команд в слове — одна 32-битная, две 16-битные или их части, — зависит от кода и выравнивания.
  • D-Code — данные из той же области: константы-литералы из Flash-памяти и обращения отладчика. Ядро обслуживается раньше отладчика.
  • System — команды и данные во всём остальном пространстве: 0x2000 0000–0xDFFF FFFF и 0xE010 0000–0xFFFF FFFF, то есть SRAM, периферия и внешняя память. Порядок на шине: сначала данные, затем выборка команд и векторов, затем отладчик.
  • PPB — шина APB для внешних отладочных блоков TPIU, ETM и ROM-таблицы, 0xE004 0000–0xE00F FFFF. NVIC, SysTick и регистры управления ядром лежат во внутренней части PPB, и к ним ядро обращается само.

Это гарвардская организация: команды и данные идут по разным шинам одновременно, но адресное пространство общее, и одна команда LDR читает и константу из Flash-памяти, и переменную в SRAM. ARM рекомендует держать программу в области Code: из SRAM код тоже выполняется, но там выборка команд и обращения к данным идут по одной шине System.

В STM32F103 к I-Code и D-Code подключён интерфейс Flash-памяти с буфером предвыборки на два блока по 64 бита, а шина System уходит в матрицу шин микроконтроллера, где ядро по очереди с контроллерами DMA обращается к SRAM и периферии (RM0008, разд. 3.1). Записи в D-Code и System проходят через буфер записи на одно слово: команда сохранения не ждёт медленную периферию, а команды DMB и DSB дожидаются, пока буфер опустеет.

Производительность и такты команд

ARM указывает для Cortex-M3 1,25 DMIPS/МГц по Dhrystone 2.1, ST — столько же для STM32F103 при чтении памяти без тактов ожидания; в сводной таблице ARM 2023 года — 1,24 DMIPS/МГц и 3,45 CoreMark/МГц. Такты основных команд по TRM для памяти без тактов ожидания:

КомандыТактовПримечание
MOV, ADD, SUB, AND, ORR, CMP, сдвиги1
MUL (32 × 32 → 32 бита)1
MLA, MLS2умножение с накоплением и с вычитанием
UMULL, SMULL (32 × 32 → 64 бита)3–5для коротких операндов быстрее
UMLAL, SMLAL4–7то же
SDIV, UDIV2–12зависит от операндов; прерывание отменяет деление, после возврата оно начинается заново
LDR, LDRH, LDRB2идущие подряд LDR и STR совмещаются и экономят такт
STR с непосредственным смещением1данные записываются одновременно со следующей командой
LDM, STM, PUSH, POP из N регистров1 + NPOP с загрузкой PC — ещё P тактов
B, BL, BX, BLX1 + PP = 1–3 такта на перезаполнение конвейера
условный переход, CBZ, CBNZ1 или 1 + P1 такт, если переход не выполняется
TBB, TBH2 + Pпереход по таблице
IT1после 16-битной команды может выполняться за 0 тактов

Это такты для идеальной памяти. У STM32F103 Flash-память на частотах от 48 до 72 МГц читается с двумя тактами ожидания (поле LATENCY регистра FLASH_ACR). Линейный код выручает буфер предвыборки — пока ядро берёт одно слово, следующее уже готово, — а после переходов, когда нужного слова в буфере нет, такты ожидания проявляются. Время конкретного участка программы надёжнее измерить счётчиком тактов DWT_CYCCNT — пример есть в разделе об отладке.

Регистры Cortex-M3

У ядра 13 регистров общего назначения R0–R12 и три регистра с особой ролью: R13 — указатель стека SP, R14 — регистр связи LR, R15 — счётчик команд PC. Все они 32-битные. Отдельно существуют специальные регистры xPSR, PRIMASK, FAULTMASK, BASEPRI и CONTROL: их читают и пишут командами MRS и MSR, а маски PRIMASK и FAULTMASK меняет ещё и команда CPS.

Регистры Cortex-M3: R0–R15, MSP и PSP, xPSR, PRIMASK, FAULTMASK, BASEPRI, CONTROLТринадцать регистров общего назначения R0–R12: младшие R0–R7 доступны всем командам, старшие R8–R12 — 32-битным командам и немногим 16-битным. R13 — указатель стека, у которого две физические копии: основной MSP и стек процесса PSP. R14 — регистр связи LR, R15 — счётчик команд PC. Специальные регистры: xPSR из трёх частей APSR, IPSR и EPSR, маски PRIMASK, FAULTMASK и BASEPRI и регистр CONTROL.Регистры общего назначенияСпециальные регистры · доступ командами MRS, MSR и CPSR0R1R2R3R4R5R6R7R8R9R10R11R12младшие:все командыстаршие:в основном32-битныеR13 · SPR14 · LRR15 · PCMSPPSPHandler — всегда MSP, Thread — MSP или PSPадрес возврата; в обработчике исключения — код EXC_RETURNадрес команды; после сброса — из вектора по адресу 0x0000 0004APSRIPSREPSRxPSR: флаги N, Z, C, V, Q, номерисключения, бит Thumb T и IT/ICIPRIMASK1 бит1 — запрещены все исключенияс настраиваемым приоритетомFAULTMASK1 бит1 — запрещено всё, кроме NMI;сбрасывается при выходе из обработчикаBASEPRI8 битпорог: не обслуживаются исключенияс приоритетом ≥ BASEPRI; 0 — без порогаCONTROL2 битаnPRIV — привилегии в режиме Thread,SPSEL — стек Thread: MSP или PSP
Регистры Cortex-M3. R13 существует в двух копиях — MSP и PSP; специальные регистры доступны только командами MRS, MSR и CPS.
  • R0–R7, младшие регистры, доступны всем командам, которые работают с регистрами общего назначения. R8–R12, старшие, — 32-битным командам и немногим 16-битным, например MOV, ADD и CMP: большинство 16-битных команд отводит на номер регистра три бита.
  • SP (R13) есть в двух копиях: основной указатель стека MSP и указатель стека процесса PSP. Какая из них работает как SP, определяют режим ядра и бит SPSEL регистра CONTROL. Два младших бита SP аппаратно нулевые — стек выровнен на слово.
  • LR (R14) получает адрес возврата при вызове подпрограммы командами BL и BLX, а при входе в исключение — код EXC_RETURN. После сброса LR = 0xFFFF FFFF.
  • PC (R15) — адрес команды. Бит 0 всегда равен 0, команды выровнены на полуслово. Команда, читающая PC, получает свой адрес плюс 4.

Регистр состояния xPSR

Регистры состояния APSR, IPSR и EPSR занимают непересекающиеся биты одного 32-битного слова, поэтому их называют вместе xPSR. Команда MRS r0, PSR читает все три сразу, MRS r0, APSR — только флаги.

ЧастьБитыЧто хранит
APSR31 — N, 30 — Z, 29 — C, 28 — V, 27 — Qфлаги результата: отрицательный, ноль, перенос, переполнение; Q — было насыщение в SSAT или USAT
IPSR8–0номер текущего исключения: 0 — режим Thread, 2 — NMI, 3 — HardFault, 11 — SVCall, 15 — SysTick, 16 и больше — прерывания IRQ0, IRQ1…
EPSR24 — T; 26–25 и 15–10 — ICI/ITбит Thumb и состояние блока IT или прерванной команды LDM/STM; программа читает EPSR как ноль

После сброса xPSR = 0x0100 0000: установлен только бит T. Он обязан оставаться единицей, ведь Cortex-M3 выполняет только Thumb. Сбросить его могут переход BX или BLX и команда POP {PC} на чётный адрес, восстановление xPSR из кадра стека и вектор прерывания с нулевым младшим битом — и тогда первая же команда вызывает отказ (подробнее — в разделе о типичных ошибках).

Поле ICI позволяет прервать длинную команду LDM, STM, PUSH или POP посередине: ядро запоминает в EPSR, какой регистр передавать следующим, и после обработчика продолжает с него. Если же LDM или STM стоит внутри блока IT, после прерывания ядро выполняет её заново. Поэтому TRM запрещает обращаться командами LDM и STM к регистрам, для которых повторное чтение или запись имеет побочный эффект, — например, к FIFO периферии.

Маски прерываний и регистр CONTROL

РегистрБитыДействиеПосле сброса
PRIMASK01 — запрещены все исключения с настраиваемым приоритетом: текущий приоритет поднимается до 0, работают только NMI и HardFault0
FAULTMASK01 — запрещено всё, кроме NMI: приоритет поднимается до −1, уровня HardFault; бит сбрасывается при выходе из любого обработчика, кроме NMI0
BASEPRI7–0не 0 — не обслуживаются исключения с приоритетом, численно большим или равным BASEPRI; 0 — порога нет0
CONTROL1 — SPSEL, 0 — nPRIVSPSEL: 0 — режим Thread работает на MSP, 1 — на PSP; nPRIV: 0 — Thread привилегированный, 1 — нет0

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

Режимы Thread и Handler, привилегии и два стека

РежимЧто выполняетсяПривилегииСтек
Threadосновная программа; в этот режим ядро выходит из сбросапривилегированный или нет — по биту CONTROL.nPRIVMSP или PSP — по биту CONTROL.SPSEL
Handlerобработчики исключенийвсегда привилегированныйвсегда MSP

После сброса ядро работает в режиме Thread, привилегированно и со стеком MSP — большинство программ без ОС так и остаются в этом состоянии. Непривилегированный код не может выполнять команду CPS, менять маски и регистр CONTROL, обращаться к NVIC, SysTick и регистрам блока управления SCB, а MPU может закрыть ему и области памяти. Вернуть себе привилегии он не может: единственный путь — исключение, обычно вызов SVC, обработчик которого выполняет запрос или меняет CONTROL.nPRIV.

ОС реального времени используют оба стека, и ARM рекомендует именно такую схему: задачи работают на PSP, у каждой свой стек, а ядро ОС и обработчики прерываний — на MSP. Переключение задач сводится к смене PSP, и его обычно выполняют в обработчике PendSV с самым низким приоритетом, когда остальные исключения уже обслужены.

Перевести режим Thread на PSP можно двумя способами: записать 1 в бит SPSEL регистра CONTROL — сразу после этого нужна команда ISB, чтобы следующие команды уже работали с новым стеком, — или вернуться из исключения с EXC_RETURN = 0xFFFF FFFD. Менять стек посреди функции на C опасно: её локальные переменные и сохранённые регистры остались в старом стеке. Поэтому ОС переключают стек в стартовом коде на ассемблере или при запуске первой задачи — возвратом из исключения.

Карта памяти Cortex-M3

Адресное пространство в 4 ГБ разделено на области с фиксированным назначением и свойствами, одинаковыми у всех кристаллов на Cortex-M3. От свойств области зависит, можно ли выполнять из неё код и может ли ядро переставлять и буферизовать обращения к ней.

Карта памяти Cortex-M3: 4 ГБ, области Code, SRAM, периферия, PPB и bit-bandingАдресное пространство 4 ГБ разделено на фиксированные области: Code с 0x0000 0000 (0,5 ГБ), SRAM с 0x2000 0000 (0,5 ГБ), периферия с 0x4000 0000 (0,5 ГБ), внешнее ОЗУ с 0x6000 0000 (1 ГБ), внешние устройства с 0xA000 0000 (1 ГБ), шина PPB с регистрами ядра с 0xE000 0000 (1 МБ) и область производителя с 0xE010 0000. В первом мегабайте областей SRAM и периферии работает bit-banding. Масштаб не соблюдён.начальный адрестип памяти и пример STM32F103Code0,5 ГБ · шины I-Code и D-Code0x0000 0000Normal, исполняемая: программа и константыSTM32F103: Flash с 0x0800 0000, системнаяпамять 0x1FFF F000, с адреса 0 — копиявыбранной выводами BOOT памятиSRAM0,5 ГБ · шина System0x2000 0000Normal, исполняемаяbit-band: 0x2000 0000–0x200F FFFF (1 МБ)алиас: 0x2200 0000–0x23FF FFFF (32 МБ)STM32F103C8: 20 КБ, до 0x2000 4FFFПериферия0,5 ГБ · шина System0x4000 0000Device, XN — выполнять код нельзяbit-band: 0x4000 0000–0x400F FFFF (1 МБ)алиас: 0x4200 0000–0x43FF FFFF (32 МБ)STM32F103: APB1 — 0x4000 0000, APB2 — 0x4001 0000Внешнее ОЗУ1 ГБ · шина System0x6000 0000Normal, исполняемая: внешняя памятьчерез контроллер вроде FSMCВнешние устройства1 ГБ · шина System0xA000 0000Device, XNPPB1 МБ · регистры ядра0xE000 0000Strongly-ordered, XNITM 0xE000 0000, DWT 0xE000 1000, FPB 0xE000 2000,SCS 0xE000 E000: SysTick, NVIC, SCB, MPU;TPIU 0xE004 0000, ETM 0xE004 1000Область производителя511 МБ · шина System0xE010 0000Device, XN0xFFFF FFFF
Карта памяти Cortex-M3 и размещение памяти STM32F103. Масштаб не соблюдён.
АдресаОбластьТип памятиВыполнение кодаШина
0x0000 0000–0x1FFF FFFFCode, 0,5 ГБNormalможноI-Code и D-Code
0x2000 0000–0x3FFF FFFFSRAM, 0,5 ГБNormalможноSystem
0x4000 0000–0x5FFF FFFFпериферия, 0,5 ГБDeviceнельзя (XN)System
0x6000 0000–0x9FFF FFFFвнешнее ОЗУ, 1 ГБNormalможноSystem
0xA000 0000–0xDFFF FFFFвнешние устройства, 1 ГБDeviceнельзяSystem
0xE000 0000–0xE00F FFFFPPB, 1 МБStrongly-orderedнельзявнутренняя и APB
0xE010 0000–0xFFFF FFFFобласть производителяDeviceнельзяSystem

Типы памяти различаются порядком обращений. С памятью Normal ядро может переставлять обращения и читать наперёд. Обращения к Device выполняются в порядке программы относительно других обращений к Device и Strongly-ordered, а запись можно буферизовать. Strongly-ordered упорядочена относительно всех обращений и не буферизуется. Атрибут XN (Execute Never) запрещает выборку команд: попытка выполнить код из периферии вызывает отказ MemManage даже без MPU.

Внутри PPB лежат регистры самого ядра:

АдресБлок
0xE000 0000ITM — программная трассировка
0xE000 1000DWT — точки наблюдения, счётчик тактов
0xE000 2000FPB — аппаратные точки останова
0xE000 E010SysTick
0xE000 E100NVIC: разрешение, ожидание и приоритеты прерываний
0xE000 ED00SCB: CPUID, ICSR, VTOR, AIRCR, SCR, CCR, приоритеты системных исключений, регистры отказов
0xE000 ED90MPU
0xE000 EDF0регистры отладки ядра DHCSR, DCRSR, DCRDR, DEMCR
0xE004 0000TPIU — вывод трассировки
0xE004 1000ETM — трассировка команд
0xE00F F000ROM-таблица отладочных блоков

В STM32F103 Flash-память начинается с 0x0800 0000, системная память со встроенным загрузчиком — с 0x1FFF F000, SRAM — с 0x2000 0000 (у STM32F103C8 её 20 КБ, до 0x2000 4FFF). Периферия шины APB1 начинается с 0x4000 0000, APB2 — с 0x4001 0000; на шине AHB, например, RCC (0x4002 1000) и интерфейс Flash-памяти (0x4002 2000). Адрес 0 — копия той памяти, которую выбрали выводы BOOT0 и BOOT1 (подробнее — в разделе о старте).

Bit-banding: атомарная запись одного бита

Первый мегабайт SRAM (0x2000 0000–0x200F FFFF) и первый мегабайт периферии (0x4000 0000–0x400F FFFF) продублированы областями-алиасами по 32 МБ с адресов 0x2200 0000 и 0x4200 0000. В алиасе каждому биту соответствует целое 32-битное слово:

адрес слова=база алиаса+смещение байта×32+номер бита×4,\text{адрес слова} = \text{база алиаса} + \text{смещение байта} \times 32 + \text{номер бита} \times 4,

где смещение байта отсчитывается от начала региона bit-band (0x2000 0000 или 0x4000 0000), а номер бита — от 0 до 7. Если взять смещение 32-битного регистра, номер бита можно считать от 0 до 31: при little-endian, как у STM32, результат тот же.

Пример — бит ODR13 регистра GPIOC_ODR, который управляет выводом PC13 на плате Blue Pill. Регистр лежит по адресу 0x4001 100C, смещение от начала региона — 0x1100C: 0x4200 0000 + 0x1100C × 32 + 13 × 4 = 0x4200 0000 + 0x22 0180 + 0x34 = 0x4222 01B4.

Bit-banding: каждому биту регистра GPIOC_ODR соответствует слово в области-алиасеШестнадцать младших битов регистра GPIOC_ODR по адресу 0x4001 100C и шестнадцать слов в области-алиасе с адреса 0x4222 0180, по одному слову на бит с шагом 4 байта. Бит 13, который управляет выводом PC13, отображается на слово 0x4222 01B4: запись в него единицы устанавливает бит, запись нуля — сбрасывает, чтение возвращает 0 или 1.Регион bit-band: регистр GPIOC_ODR, адрес 0x4001 100C, биты 15…01514131211109876543210Регион-алиас: по 32-битному слову на каждый бит, 0x4222 0180 + 4 × n…1BC…1B8…1B4…1B0…1AC…1A8…1A4…1A0…19C…198…194…190…18C…188…184…180адрес алиаса = 0x4200 0000 + (адрес слова − 0x4000 0000) × 32 + номер бита × 4PC13: 0x4200 0000 + 0x1100C × 32 + 13 × 4 = 0x4222 01B4запись 1 в 0x4222 01B4 — установить бит 13, запись 0 — сбросить; чтение возвращает 0 или 1
Bit-banding на примере GPIOC_ODR: каждому биту регистра соответствует 32-битное слово в области-алиасе, биту 13 (вывод PC13) — слово 0x4222 01B4.

Запись в слово-алиас меняет только один бит: его значение берётся из младшего бита записанного слова — 0x01 и 0xFF дают одно и то же, — а шина System сама выполняет чтение, изменение и запись, которые нельзя прервать. Чтение слова-алиаса возвращает 0x0000 0000 или 0x0000 0001. На C:

#include <stdint.h>

/* Alias word for bit `bit` of the word at `addr` (SRAM or peripheral bit-band region) */
#define BITBAND(addr, bit)                                                    \
    (*(volatile uint32_t *)(((uint32_t)(addr) & 0xF0000000UL) + 0x02000000UL + \
                            (((uint32_t)(addr) & 0x000FFFFFUL) << 5) +         \
                            ((uint32_t)(bit) << 2)))

#define PC13_OUT BITBAND(0x4001100CUL, 13)    /* GPIOC_ODR bit 13 -> 0x422201B4 */

void led_on(void)  { PC13_OUT = 0; }           /* Blue Pill LED lights when PC13 is low */
void led_off(void) { PC13_OUT = 1; }

Ограничения и оговорки:

  • Bit-banding действует только для обращений ядра: DMA и другие ведущие шины видят обычную память (RM0008, разд. 3.3.2).
  • Регионы — только первый мегабайт SRAM и периферии. У STM32F103 туда попадают вся SRAM и регистры периферии на шинах APB и AHB.
  • Эксклюзивные обращения LDREX и STREX к областям bit-band не поддерживаются (TRM, разд. 3.6).
  • В ревизии r2p1 bit-banding стал опцией производителя кристалла; в r1p1, как у STM32F1, он есть всегда. В ядрах M0, M0+ и M7 его нет.
  • Для выводов GPIO у STM32 удобнее регистр BSRR: запись в него тоже атомарна и меняет сразу несколько выводов. Bit-banding полезнее для флагов в SRAM и для битов управления периферией, у которой нет отдельных регистров установки и сброса.

Исключения и прерывания: NVIC

Исключение — любое событие, которое прерывает обычный ход программы: сброс, отказ, вызов SVC, сигнал системного таймера или прерывание от периферии. Все они обрабатываются по одной схеме — через таблицу векторов, приоритеты и аппаратное сохранение регистров, — а внешние прерывания отличаются только номером. Контроллер прерываний NVIC встроен в ядро: у Cortex-M3 он поддерживает до 240 внешних прерываний и от 8 до 256 уровней приоритета, у STM32F103x8/xB подключено 43 прерывания периферии.

Таблица исключений Cortex-M3

НомерIRQn в CMSISИсключениеПриоритетСмещение вектораКогда возникает
1—Reset−30x04включение питания, сброс
2−14NMI−20x08немаскируемое прерывание; в STM32F1 — от системы контроля тактирования CSS
3−13HardFault−10x0Cотказ при обработке исключения или отказ, обработчик которого выключен
4−12MemManageнастраивается0x10нарушение прав MPU, выполнение кода из области XN
5−11BusFaultнастраивается0x14ошибка шины при выборке команды или обращении к данным
6−10UsageFaultнастраивается0x18неопределённая команда, бит T = 0, неверный EXC_RETURN; по настройке — деление на ноль и невыровненный доступ
11−5SVCallнастраивается0x2Cкоманда SVC
12−4DebugMonitorнастраивается0x30отладочный монитор
14−2PendSVнастраивается0x38программный запрос, обычно переключение задач ОС
15−1SysTickнастраивается0x3Cсистемный таймер дошёл до нуля
16 + nnIRQnнастраивается0x40 + 4nпрерывание периферии n = 0, 1, 2…

Номера 7–10 и 13 зарезервированы. В CMSIS системные исключения имеют отрицательные номера IRQn, поэтому одна функция NVIC_SetPriority задаёт приоритет и прерыванию периферии, и, например, SysTick.

Приоритеты: 4 бита у STM32F1, группы и подприоритеты

Правила приоритетов в ARMv7-M:

  • Меньшее число — более высокий приоритет. У Reset, NMI и HardFault приоритеты фиксированы: −3, −2 и −1. Остальные настраиваются начиная с 0, и после сброса у всех 0 — наивысший из настраиваемых.
  • Поле приоритета — 8 бит, но реализуются только старшие: от 3 до 8 бит, то есть от 8 до 256 уровней. У STM32F1 — 4 бита [7:4], 16 уровней; младшие биты читаются как ноль и игнорируют запись. Сколько бит реализовано, можно узнать, записав 0xFF в любой байт приоритета и прочитав его.
  • При равных приоритетах первым обслуживается исключение с меньшим номером, а исключение с тем же приоритетом выполняющееся не вытесняет — оно ждёт.
  • Поле PRIGROUP (биты 10–8 регистра AIRCR, 0xE000 ED0C) делит байт на приоритет группы, который решает вытеснение, и подприоритет, который задаёт только очередь среди ожидающих. Запись в AIRCR действует, только если в битах 31–16 записан ключ 0x05FA.
Поле приоритета NVIC у STM32F1: 4 старших бита и деление на группу и подгруппуБайт приоритета в регистрах NVIC_IPR и SHPR: у STM32F1 реализованы только биты 7–4, биты 3–0 читаются как ноль. Поле PRIGROUP регистра AIRCR задаёт, сколько из четырёх битов определяют приоритет группы (вытеснение), а сколько — подприоритет: при PRIGROUP = 3 — 16 групп без подприоритетов, при 4 — 8 групп по 2, при 5 — 4 по 4, при 6 — 2 по 8, при 7 — одна группа и 16 подприоритетов. В библиотеке HAL этим значениям соответствуют константы NVIC_PRIORITYGROUP_4…0.Байт приоритета (NVIC_IPRx, SHPRx): у STM32F1 реализованы биты 7…4бит 7бит 6бит 5бит 40000биты 3…0 читаются как 0, запись игнорируетсяAIRCR.PRIGROUP■ группа (вытеснение) ■ подприоритет3 (0b011)гггг16 уровней вытеснения, подприоритетов нетHAL: NVIC_PRIORITYGROUP_44 (0b100)гггп8 групп × 2 подприоритетаHAL: NVIC_PRIORITYGROUP_35 (0b101)ггпп4 группы × 4 подприоритетаHAL: NVIC_PRIORITYGROUP_26 (0b110)гппп2 группы × 8 подприоритетовHAL: NVIC_PRIORITYGROUP_17 (0b111)ппппвытеснения нет, 16 подприоритетовHAL: NVIC_PRIORITYGROUP_0PRIGROUP 0…2 при четырёх битах работают как 3. Меньшее число — более высокий приоритет.
Байт приоритета NVIC: у STM32F1 реализованы биты 7…4. PRIGROUP делит их на приоритет группы, от которого зависит вытеснение, и подприоритет, который задаёт только очередь ожидающих прерываний.

Пример: при PRIGROUP = 5 четыре бита делятся на 2 бита группы и 2 бита подприоритета. Пусть USART1 получит группу 1 и подприоритет 0, а TIM2 — группу 1 и подприоритет 3. Если оба ждут обслуживания одновременно, первым пойдёт USART1, но уже начатый обработчик TIM2 он не прервёт — группа одна. Прерывание группы 0 вытеснит любой из них. На CMSIS:

#include "stm32f1xx.h"

void nvic_setup(void)
{
    NVIC_SetPriorityGrouping(5);                         /* PRIGROUP = 5: 2 bits group, 2 bits sub */
    NVIC_SetPriority(USART1_IRQn, NVIC_EncodePriority(5, 1, 0));   /* byte 0x40 */
    NVIC_SetPriority(TIM2_IRQn, NVIC_EncodePriority(5, 1, 3));     /* byte 0x70 */
    NVIC_EnableIRQ(USART1_IRQn);
    NVIC_EnableIRQ(TIM2_IRQn);
}

NVIC_SetPriority сама сдвигает значение на 8 − __NVIC_PRIO_BITS бит, у STM32F1 — на 4: уровень 7 превращается в байт 0x70. Библиотека HAL при инициализации (HAL_Init) выбирает NVIC_PRIORITYGROUP_4 — это PRIGROUP = 3: 16 уровней вытеснения без подприоритетов. Для большинства программ так и проще: каждый уровень вытесняет все более низкие.

Вход в прерывание: 8 слов в стеке и 12 тактов

Когда приходит исключение с достаточным приоритетом, ядро без единой команды программы:

  1. сохраняет в текущий стек — MSP или PSP — восемь слов: R0–R3, R12, LR, адрес возврата и xPSR;
  2. одновременно читает адрес обработчика из таблицы векторов;
  3. записывает в LR код EXC_RETURN, переходит в режим Handler со стеком MSP и заносит номер исключения в IPSR;
  4. начинает выполнять обработчик — его первая команда выбирается параллельно с сохранением регистров.

Регистры R0–R3, R12 и LR по соглашению о вызовах AAPCS может портить любая функция, а R4–R11 функция обязана сохранить сама, если их использует. Поэтому обработчик прерывания — обычная функция на C без специальных атрибутов: всё нужное прерванному коду ядро уже сохранило.

Кадр стека при входе в исключение и значения EXC_RETURNПри входе в исключение ядро само сохраняет в текущий стек восемь слов: R0, R1, R2, R3, R12, LR, адрес возврата и xPSR — по возрастанию адресов от нового значения SP. В LR записывается код EXC_RETURN: 0xFFFFFFF1 — возврат в режим Handler со стеком MSP, 0xFFFFFFF9 — в режим Thread со стеком MSP, 0xFFFFFFFD — в режим Thread со стеком PSP.Кадр стека: 8 слов, 32 байтаданные прерванной программыxPSRSP +0x1CPC — адрес возвратаSP +0x18LRSP +0x14R12SP +0x10R3SP +0x0CR2SP +0x08R1SP +0x04R0SP +0x00SP до входаSP после входаадресарастутвверхБит 9 сохранённого xPSR = 1, если ядро сдвинуло SP для выравниванияна 8 байт (CCR.STKALIGN). R4–R11 сохраняет сам обработчик, если их меняет.LR при входе = EXC_RETURN0xFFFF FFF1возврат в Handler,кадр в MSP0xFFFF FFF9возврат в Thread,кадр в MSP0xFFFF FFFDвозврат в Thread,кадр в PSPВозврат — BX LR или POP {…, PC}:ядро видит 0xFFFF FFFx и снимает кадр
Кадр стека, который ядро сохраняет при входе в исключение, и коды EXC_RETURN в регистре LR.

Задержка — не больше 12 тактов от запроса до первой команды обработчика при памяти без тактов ожидания, столько же длится возврат (TRM, разд. 3.9.2). Длинные команды её не увеличивают: деление SDIV и UDIV ядро бросает и после возврата начинает заново, а LDM и STM прерывает посередине. На 72 МГц 12 тактов — это 167 нс; у STM32F103 Flash-память на этой частоте работает с двумя тактами ожидания, поэтому реальная задержка обычно больше.

Стек при входе выравнивается по биту STKALIGN регистра CCR (0xE000 ED14). При STKALIGN = 1 ядро выравнивает кадр на 8 байт и отмечает сдвиг битом 9 сохранённого xPSR, при 0 — только на 4 байта. В ревизиях r2p0 и новее после сброса STKALIGN = 1, а у STM32F1 с ядром r1p1 — 0 (PM0056). Обработчик на C — функция AAPCS, а AAPCS разрешает коду рассчитывать на выравнивание стека по 8 байт; ARM не рекомендует выравнивание на 4 байта и советует включать STKALIGN (ARMv7-M ARM, разд. B1.5.7; TRM, разд. 3.3.3). На STM32F1 это одна строка в начале программы, до разрешения прерываний: SCB->CCR |= SCB_CCR_STKALIGN_Msk;.

Хвостовые и опоздавшие прерывания

Если к концу обработчика ждёт другое исключение с достаточным приоритетом, ядро не восстанавливает регистры, чтобы тут же сохранить их снова, а сразу переходит к следующему обработчику — за 6 тактов вместо 12 + 12. Это хвостовая цепочка (tail-chaining). Кадр в стеке остаётся тем же: вернуться всё равно нужно в ту же прерванную программу.

Если во время сохранения регистров приходит исключение с более высоким приоритетом, ядро сохранение не прерывает — кадр нужен такой же, — но выбирает вектор нового исключения и начинает с его обработчика. Это опоздавшее прерывание (late arrival); опоздавшим считается исключение, пришедшее до того, как первая команда исходного обработчика дошла до ступени исполнения. Исходное исключение остаётся в ожидании и выполняется следом по хвостовой цепочке.

Хвостовые и опоздавшие прерывания Cortex-M3: такты входа, перехода и возвратаВерхняя диаграмма: прерывание 1 вызывает сохранение восьми регистров за 12 тактов, во время обработчика 1 приходит прерывание 2; после обработчика 1 ядро не восстанавливает регистры, а за 6 тактов переходит к обработчику 2 и только после него за 12 тактов восстанавливает кадр. Нижняя диаграмма: во время сохранения кадра для прерывания A приходит более приоритетное прерывание B; сохранение продолжается, но выбирается вектор B, и сначала выполняется его обработчик, затем по хвостовой цепочке — обработчик A. Такты указаны для памяти без тактов ожидания.Хвостовое прерывание (tail-chaining)IRQ 1IRQ 2 ждётThreadсохранение12 тактовобработчик 1переход6 тактовобработчик 2восстановление12 тактовThreadкадр стека не снимается и не сохраняется зановоОпоздавшее прерывание (late arrival)IRQ AIRQ B (выше)Threadсохранение12 тактовобработчик Bпереход6 тактовобработчик Aвосстановление12 тактовThreadтот же кадр, вектор — уже BТакты — для памяти без тактов ожидания (Cortex-M3 TRM, разд. 3.9.2). Ширина отрезков не в масштабе.
Хвостовые и опоздавшие прерывания. Такты — для памяти без тактов ожидания по TRM Cortex-M3.

EXC_RETURN и возврат из обработчика

При входе в исключение ядро записывает в LR не адрес возврата — он уже в кадре стека, — а код EXC_RETURN вида 0xFFFF FFFx. Обработчик завершается обычной командой возврата: BX LR, POP {..., PC} или загрузкой PC командой LDR. Увидев в PC значение, у которого биты 31–4 — все единицы, ядро понимает, что это выход из исключения, снимает кадр из нужного стека и продолжает прерванную программу.

EXC_RETURNКуда возвращаемсяИз какого стека берётся кадр
0xFFFF FFF1в режим Handler, в прерванный обработчикMSP
0xFFFF FFF9в режим ThreadMSP
0xFFFF FFFDв режим ThreadPSP

Остальные значения зарезервированы, и попытка вернуться с ними вызывает UsageFault с признаком INVPC. Обработчик, который сам вызывает функции, сохраняет LR в стеке — компилятор делает это автоматически. Возврат с 0xFFFF FFFD — тот способ, которым ОС запускает задачу на её собственном стеке PSP.

Таблица векторов и VTOR

Таблица векторов — массив 32-битных слов: в слове 0 лежит начальное значение MSP, в слове 1 — адрес обработчика сброса, дальше — адреса обработчиков по номерам исключений: NMI по смещению 0x08, HardFault — 0x0C, IRQ0 — 0x40, IRQn — 0x40 + 4n. У STM32F103xB таблица из 16 системных слов и 43 прерываний занимает 59 слов. Младший бит каждого адреса обработчика должен быть равен 1 — признак кода Thumb.

После сброса таблица находится по адресу 0. Регистр VTOR (0xE000 ED08) переносит её в другое место — в SRAM или в начало приложения, стоящего за загрузчиком. Адрес таблицы выравнивается на степень двойки, не меньшую размера таблицы, и не меньше чем на 128 байт (ARMv7-M ARM, разд. B1.5.3). По PM0056 у STM32F1 в VTOR записываются биты 29–9 адреса (поле TBLOFF): таблица должна начинаться с адреса, кратного 0x200, а бит 29 указывает, лежит она в области Code или SRAM. В ревизии r2p1 поле расширено до битов 31–7.

#include "stm32f1xx.h"

/* Application linked to 0x08002000 behind an 8 KB bootloader */
void relocate_vector_table(void)
{
    SCB->VTOR = FLASH_BASE | 0x2000U;   /* 0x08002000 is a multiple of 0x200, as STM32F1 requires */
    __DSB();                            /* complete the write before the next exception */
}

В шаблоне system_stm32f1xx.c из пакета STM32CubeF1 строка SCB->VTOR = … выполняется, только если раскомментировать #define USER_VECT_TAB_ADDRESS и задать VECT_TAB_OFFSET, — об этом легко забыть.

Маскирование: PRIMASK, FAULTMASK и BASEPRI

  • __disable_irq() и __enable_irq() — команды CPSID i и CPSIE i — устанавливают и снимают PRIMASK: запрещены все прерывания и системные исключения с настраиваемым приоритетом. Для короткого критического участка надёжнее сохранить прежнее значение маски и восстановить его, а не включать прерывания безусловно: участок может оказаться вложенным в другой.
  • BASEPRI запрещает только исключения ниже порога, а более срочные продолжают работать. Значение пишется в формате байта приоритета, со сдвигом: порог 5 у STM32F1 — это 0x50. NVIC_SetPriority сдвигает число сама, а __set_BASEPRI() — нет.
  • BASEPRI сравнивается только с приоритетом группы: подприоритеты на маскирование не влияют. Вариант __set_BASEPRI_MAX() меняет порог, только если новое значение строже текущего, — это удобно во вложенных участках.
  • FAULTMASK — команда CPSID f — поднимает приоритет до уровня HardFault. Его используют обработчики отказов, в обычном коде он почти не нужен.
#include "stm32f1xx.h"

void update_shared_data(void)
{
    uint32_t primask = __get_PRIMASK();
    __disable_irq();
    /* ... short critical section ... */
    __set_PRIMASK(primask);                         /* restore, do not enable unconditionally */
}

void mask_low_priorities(void)
{
    __set_BASEPRI(5U << (8U - __NVIC_PRIO_BITS));   /* 0x50: levels 5..15 masked, 0..4 run */
    /* ... */
    __set_BASEPRI(0U);                              /* 0 removes the threshold */
}

Отказы: HardFault, MemManage, BusFault и UsageFault

После сброса обработчики MemManage, BusFault и UsageFault выключены, и любой отказ превращается в HardFault — это называется эскалацией. Разделить их можно битами регистра SHCSR (0xE000 ED24), а деление на ноль, которое по умолчанию просто даёт частное 0, — превратить в отказ битом DIV_0_TRP регистра CCR:

#include "stm32f1xx.h"

void enable_fault_handlers(void)
{
    SCB->SHCSR |= SCB_SHCSR_USGFAULTENA_Msk | SCB_SHCSR_BUSFAULTENA_Msk |
                  SCB_SHCSR_MEMFAULTENA_Msk;
    SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk;   /* trap division by zero instead of returning 0 */
}

Причину отказа показывают регистры SCB: CFSR (0xE000 ED28) объединяет MMFSR, BFSR и UFSR; в HFSR (0xE000 ED2C) бит FORCED означает эскалацию, а VECTTBL — ошибку чтения вектора; MMFAR и BFAR хранят адрес, на котором случился отказ. Адрес команды, вызвавшей отказ, лежит в кадре стека по смещению 0x18, а в каком стеке искать кадр, подсказывает бит 2 кода EXC_RETURN в LR: 0 — MSP, 1 — PSP. Если отказ происходит в обработчике HardFault или NMI, ядро останавливается в состоянии lockup до сброса, NMI или остановки отладчиком.

Системный таймер SysTick

SysTick — 24-битный счётчик, который считает вниз от значения перезагрузки до нуля. На переходе из 1 в 0 он поднимает флаг COUNTFLAG и, если разрешено, запрос исключения SysTick, а затем снова загружается. Таймер одинаков у всех Cortex-M3, поэтому ОС реального времени берут его для системного тика.

Регистр (CMSIS)АдресНазначение
SysTick->CTRL0xE000 E010бит 0 ENABLE — пуск, бит 1 TICKINT — запрос исключения, бит 2 CLKSOURCE — источник такта, бит 16 COUNTFLAG — счётчик дошёл до нуля
SysTick->LOAD0xE000 E014значение перезагрузки, 24 бита: 1…0xFF FFFF
SysTick->VAL0xE000 E018текущее значение; любая запись обнуляет счётчик и COUNTFLAG
SysTick->CALIB0xE000 E01Cкалибровка; у STM32F1 — 9000 отсчётов: 1 мс при HCLK = 72 МГц и такте HCLK/8

Бит CLKSOURCE выбирает источник: 1 — частота ядра, 0 — внешний опорный такт, у STM32F1 это HCLK/8 (PM0056). Период прерываний:

T=LOAD+1fSysTick.T = \frac{\text{LOAD} + 1}{f_\text{SysTick}}.

Для 1 мс при 72 МГц LOAD = 72 000 − 1 = 71 999. Наибольший период при 72 МГц — 2²⁴ / 72 МГц ≈ 0,233 с, а при такте HCLK/8 — ≈ 1,864 с.

Функция CMSIS SysTick_Config(ticks) записывает в LOAD значение ticks − 1, ставит SysTick самый низкий приоритет — у STM32F1 это 15, — обнуляет счётчик и запускает его от частоты ядра с разрешённым прерыванием. Если ticks − 1 не помещается в 24 бита, функция возвращает 1 и таймер не трогает. Пока отладчик держит ядро остановленным, SysTick не считает; не считает он и в режимах сна, в которых останавливается такт ядра.

MPU: восемь областей защиты памяти

MPU — необязательный блок. У Cortex-M3 он делит адресное пространство на 8 областей со своими правами доступа и атрибутами памяти; при нарушении прав возникает отказ MemManage. У STM32F103C8 MPU нет — в заголовке CMSIS для STM32F103xB __MPU_PRESENT = 0, — а есть ли MPU в конкретном STM32, сказано в его даташите (PM0056, разд. 4.2). Проверить программно можно по регистру MPU_TYPE (0xE000 ED90): 0x0000 0800 означает 8 областей, 0 — MPU нет.

Как устроены области:

  • Размер — 2SIZE+1 байт, от 32 байт до 4 ГБ; базовый адрес выровнен на размер области.
  • Подобласти: область от 256 байт делится на 8 равных частей, и каждую можно исключить битом поля SRD.
  • Перекрытие: действуют атрибуты области с большим номером — область 7 главнее области 0.
  • Фоновая область: бит PRIVDEFENA регистра MPU_CTRL открывает привилегированному коду стандартную карту памяти там, где нет ни одной области.
  • Права и атрибуты: поле AP задаёт чтение и запись для привилегированного и непривилегированного кода, бит XN запрещает выполнение, TEX, C, B и S — тип памяти.
  • Перенастройка: после записи в регистры MPU нужны команды DSB и ISB или возврат из исключения, чтобы следующие команды работали уже с новыми правами.

Типичные применения — запрет выполнения кода из SRAM, защита данных ОС от задач, работающих без привилегий, и защитная область без доступа ниже стека: тогда переполнение стека вызывает отказ MemManage, а не тихо портит переменные.

Отладка: SWD, JTAG, FPB, DWT и ITM

Отладочный порт SWJ-DP в STM32F1 объединяет JTAG на пяти выводах и SWD на двух. После сброса активен JTAG, а на SWD отладчик переключает порт специальной последовательностью на линиях TMS и TCK. Все пять выводов после сброса отданы отладке: PA13 — SWDIO/JTMS, PA14 — SWCLK/JTCK, PA15 — JTDI, PB3 — JTDO/TRACESWO, PB4 — NJTRST (RM0008, разд. 31.4).

БлокЧто делаетУ STM32F103
FPBаппаратные точки останова в области Code, подмена слов кода6 компараторов адресов команд и 2 — литералов
DWTточки наблюдения за данными, счётчик тактов CYCCNT, профилирование4 компаратора
ITMпрограммная трассировка: 32 канала, вывод через SWOесть, вывод TRACESWO — PB3
ETMтрассировка выполнения командтолько в корпусах с выводами трассировки

Программных точек останова — команд BKPT в ОЗУ — может быть сколько угодно, а во Flash-памяти, куда команду просто так не вписать, отладчик использует компараторы FPB, и их шесть. Счётчик тактов DWT удобен для замера времени:

#include "stm32f1xx.h"

uint32_t measure_cycles(void (*work)(void))
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;   /* enable DWT and ITM */
    DWT->CYCCNT = 0U;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;              /* start the cycle counter */

    uint32_t start = DWT->CYCCNT;
    work();
    return DWT->CYCCNT - start;                       /* unsigned difference survives a wrap */
}

Если программа сразу после старта переназначит PA13 и PA14 — поле SWJ_CFG регистра AFIO_MAPR = 100 — или переведёт их в режим порта, отладчик больше не подключится. Помогает подключение под сбросом: пока NRST удерживается, все выводы SWJ принадлежат отладке. Другой выход — загрузка с BOOT0 = 1, при которой пользовательская программа не запускается. Для отладки в режимах сна в STM32F1 есть регистр DBGMCU_CR (0xE004 2004): биты DBG_SLEEP, DBG_STOP и DBG_STANDBY оставляют ядру тактирование, чтобы отладчик не терял связь (RM0008, разд. 31.16).

Режимы пониженного потребления: WFI, WFE и SLEEPDEEP

  • WFI (Wait For Interrupt) — сон до прерывания.
  • WFE (Wait For Event) — сон до события. Если флаг события уже установлен, команда только сбрасывает его. Будят прерывание, внешний сигнал события и команда SEV другого ядра, а при установленном бите SEVONPEND — ещё и любое новое ожидающее прерывание, даже запрещённое.
  • SLEEPONEXIT (бит 1 регистра SCR, 0xE000 ED10) — после последнего обработчика ядро сразу засыпает, не возвращаясь в основной цикл. Удобно, когда вся работа делается в прерываниях.
  • SLEEPDEEP (бит 2 SCR) — глубокий сон вместо обычного; что именно при этом отключается, решает производитель кристалла.

Если перед WFI установить PRIMASK, прерывание разбудит ядро, но обработчик выполнится только после снятия маски — так можно восстановить тактирование до того, как заработают обработчики. У STM32F1 сон ядра превращается в один из трёх режимов (RM0008, разд. 5.3):

Режим STM32F1Как войтиЧто отключаетсяЧем разбудить
SleepWFI или WFE при SLEEPDEEP = 0такт ядра, периферия работаетлюбое прерывание (WFI) или событие (WFE)
StopSLEEPDEEP = 1, бит PDDS регистра PWR_CR = 0, затем WFI или WFEвсе такты области 1,8 В, генераторы HSI и HSE; бит LPDS переводит регулятор в экономичный режимлюбая линия EXTI
StandbySLEEPDEEP = 1, PDDS = 1, затем WFI или WFEобласть 1,8 В обесточена, содержимое SRAM и регистров теряетсявывод WKUP, будильник RTC, сброс по NRST или от IWDG

После выхода из Stop микроконтроллер работает от внутреннего генератора HSI на 8 МГц, и PLL нужно запускать заново.

Старт после сброса

После сброса ядро (ARMv7-M ARM, псевдокод TakeReset):

  1. читает слово по адресу 0x0000 0000 и загружает его в MSP, обнулив два младших бита;
  2. читает слово 0x0000 0004 — вектор сброса: его бит 0 становится битом T регистра EPSR, а адрес без бита 0 — значением PC;
  3. начинает выполнять код в режиме Thread, привилегированно, со стеком MSP; маски PRIMASK, FAULTMASK и BASEPRI равны нулю, LR = 0xFFFF FFFF.

Что окажется по адресу 0, у STM32F1 решают выводы BOOT0 и BOOT1, которые защёлкиваются на четвёртом такте SYSCLK после сброса (RM0008, разд. 3.4):

BOOT1BOOT0Откуда загрузкаЧто видно по адресу 0
любой0основная Flash-памятькопия Flash-памяти с 0x0800 0000
01системная памятьвстроенный загрузчик ST, который прошивает Flash-память через USART1
11встроенная SRAMSRAM доступна только с 0x2000 0000, таблицу векторов переносят через VTOR

Дальше работает стартовый код. В шаблоне ST для GCC (startup_stm32f103xb.s) обработчик Reset_Handler вызывает SystemInit, копирует начальные значения переменных секции .data из Flash-памяти в SRAM, обнуляет секцию .bss, вызывает конструкторы (__libc_init_array) и только потом main. Таблица векторов там — массив слов в секции .isr_vector, которую скрипт компоновщика ставит в начало Flash-памяти: первое слово — _estack, второе — Reset_Handler.

Две детали этого шаблона полезно знать:

  • В скрипте компоновщика ST для STM32F103xB вершина стека задана как _estack = 0x20004FFF. Ядро при сбросе отбросит два младших бита и загрузит в MSP 0x2000 4FFC — стек выровнен только на 4 байта, тогда как AAPCS требует 8. Надёжнее указать конец ОЗУ: 0x2000 5000.
  • В шаблоне system_stm32f1xx.c из STM32CubeF1 функция SystemInit тактирование не настраивает: после старта ядро работает от HSI на 8 МГц, а переменная SystemCoreClock равна 8 000 000, пока программа сама не запустит PLL и не вызовет SystemCoreClockUpdate().

Пример: SysTick 1 мс и светодиод PC13 на Blue Pill

Blue Pill — неофициальная плата с STM32F103C8T6, кварцем 8 МГц и светодиодом на выводе PC13. Светодиод включён на втекающий ток и горит, когда на PC13 низкий уровень. Так и должно быть: по даташиту выводы PC13–PC15 питаются через внутренний ключ, выдерживают не больше 3 мА, работают на частоте до 2 МГц и не должны служить источником тока, например для светодиода (DS5319).

Программа ниже обходится без HAL — только CMSIS и регистры по RM0008. Она разгоняет ядро до 72 МГц от кварца, настраивает SysTick на прерывание каждую миллисекунду и мигает светодиодом раз в секунду.

#include "stm32f1xx.h"   /* CMSIS device header; define STM32F103xB for STM32F103C8 */

static volatile uint32_t ms_ticks;

void SysTick_Handler(void)   /* name taken from the vector table in startup_stm32f103xb.s */
{
    ms_ticks++;
}

static void delay_ms(uint32_t ms)
{
    uint32_t start = ms_ticks;
    while ((ms_ticks - start) < ms) {
        /* busy wait; __WFI() here would sleep until the next tick */
    }
}

static void clock_72mhz(void)
{
    RCC->CR |= RCC_CR_HSEON;                         /* 8 MHz crystal on the Blue Pill */
    while (!(RCC->CR & RCC_CR_HSERDY)) {
    }

    /* SYSCLK above 48 MHz needs two flash wait states; keep the prefetch buffer on */
    FLASH->ACR = FLASH_ACR_PRFTBE | (2U << FLASH_ACR_LATENCY_Pos);

    /* PLL = HSE x 9 = 72 MHz; AHB = 72 MHz, APB1 = 36 MHz (its maximum), APB2 = 72 MHz */
    RCC->CFGR = RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9 | RCC_CFGR_PPRE1_DIV2;
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY)) {
    }

    RCC->CFGR |= RCC_CFGR_SW_PLL;                    /* switch SYSCLK to the PLL */
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
    }

    SystemCoreClockUpdate();                         /* SystemCoreClock = 72000000 */
}

int main(void)
{
    clock_72mhz();

    /* PC13: general-purpose push-pull output, 2 MHz (MODE13 = 10, CNF13 = 00) */
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
    GPIOC->CRH = (GPIOC->CRH & ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13)) | GPIO_CRH_MODE13_1;

    SysTick_Config(SystemCoreClock / 1000U);         /* LOAD = 71999: interrupt every 1 ms */

    for (;;) {
        GPIOC->BSRR = GPIO_BSRR_BR13;                /* PC13 = 0: LED on */
        delay_ms(500);
        GPIOC->BSRR = GPIO_BSRR_BS13;                /* PC13 = 1: LED off */
        delay_ms(500);
    }
}

Что здесь важно:

  • Порядок настройки тактирования. Такты ожидания Flash-памяти (FLASH_ACR.LATENCY = 2 для 48–72 МГц) записываются до переключения на 72 МГц, а множитель PLL — пока PLL выключен: при включённом PLL биты PLLMUL не пишутся. APB1 делится на 2, потому что эта шина не должна работать быстрее 36 МГц.
  • Ловушка в заголовке. В заголовке STM32CubeF1 FLASH_ACR_LATENCY_2 — это маска бита 2 со значением 0x4, а не «два такта ожидания». Поэтому число тактов записано явно: 2U << FLASH_ACR_LATENCY_Pos.
  • SystemCoreClockUpdate(). Без этого вызова SystemCoreClock останется равным 8 000 000, SysTick_Config получит 8000 вместо 72 000, и «миллисекунда» окажется в 9 раз короче — светодиод замигает в 9 раз быстрее.
  • GPIOC->BSRR. Запись в BSRR атомарна, и чтение-изменение-запись регистра ODR не нужно. Биты 31–16 сбрасывают выводы, биты 15–0 устанавливают.
  • Без кварца программа зависнет на ожидании HSERDY. В изделии такие циклы ограничивают тайм-аутом и при ошибке остаются на HSI.

Собрать пример можно в STM32CubeIDE, PlatformIO или компилятором arm-none-eabi-gcc с ключами -mcpu=cortex-m3 -mthumb и определением STM32F103xB. Заголовки CMSIS-Core, файлы stm32f1xx.h и system_stm32f1xx.c, стартовый файл и скрипт компоновщика берутся из пакета STM32CubeF1 или репозитория cmsis-device-f1. Прошивают плату через адаптер ST-LINK по SWD — утилиты перечислены в разделе «Программы для электронщика».

Cortex-M3 в сравнении с M0, M0+ и M4

ПараметрCortex-M0Cortex-M0+Cortex-M3Cortex-M4
АрхитектураARMv6-MARMv6-MARMv7-MARMv7E-M
Конвейер3 ступени2 ступени3 ступени3 ступени
Шиныодна AHB-Liteодна AHB-Lite, однотактный порт ввода-вывода — опциятри AHB-Liteтри AHB-Lite
Набор командподмножество Thumbподмножество ThumbThumb-2Thumb-2 и DSP: SIMD, насыщение, умножение с накоплением за такт
Умножение 32 × 321 или 32 такта — выбирает производитель1 или 32 такта1 такт1 такт
Делениепрограммноепрограммное2–12 тактов2–12 тактов
FPUнетнетнетодинарной точности, опция
Внешние прерываниядо 32до 32до 240до 240
Уровни приоритета4, без групп4, без групп8–256, группы через PRIGROUP8–256, группы через PRIGROUP
МаскиPRIMASKPRIMASKPRIMASK, FAULTMASK, BASEPRIPRIMASK, FAULTMASK, BASEPRI
Отказытолько HardFaultтолько HardFaultHardFault, MemManage, BusFault, UsageFault с регистрами причинкак у M3
Перенос таблицы векторовнетVTOR — опцияVTORVTOR
MPUнет8 областей, опция8 областей, опция8 областей, опция
Bit-bandingне в ядрене в ядреесть, в r2p1 — опцияопция
Точки останова и наблюдениядо 4 и до 2до 4 и до 2до 8 и до 4до 8 и до 4
DMIPS/МГц по таблице ARM 2023 года0,960,991,241,26
Примеры МКSTM32F0STM32G0, RP2040, SAM D21STM32F1, К1986ВЕ92QISTM32F4, STM32G4, nRF52840

Программы для M0 и M0+ работают на M3 без изменений: ARMv6-M — подмножество ARMv7-M (Armv6-M ARM, прил. D3). Обратно это не так: у M0 нет деления, блоков IT, регистров BASEPRI и FAULTMASK и отдельных обработчиков отказов. Худшая задержка прерывания у M0+ — 15 тактов (его TRM), у M3 и M4 — 12; у M4 с активным контекстом FPU без ленивого сохранения она вырастает до 29 тактов. Обзор всех ядер, от M0 до M85, — в разделе «ARM Cortex-M».

Типичные ошибки

  1. HardFault сразу после старта или при первом прерывании: чётный адрес в таблице векторов. Бит 0 вектора становится битом T; при нуле первая команда обработчика вызывает UsageFault с признаком INVSTATE, а после сброса, пока UsageFault выключен, — HardFault (ARMv7-M ARM, разд. B1.5.3). Компоновщик ставит этот бит сам, если символ — функция Thumb: на C это любая функция, в ассемблере — метка, объявленная как .type имя, %function, как Reset_Handler в стартовом файле ST. Ошибка появляется, когда адрес вписан в таблицу числом, когда метка в ассемблере не объявлена функцией и когда загрузчик прыгает в приложение по вычисленному адресу вместо слова из его таблицы векторов. Признаки — бит INVSTATE в CFSR и FORCED в HFSR.
  2. «Перевёрнутые» приоритеты и байт вместо уровня в NVIC_SetPriority. Меньшее число — более высокий приоритет. NVIC_SetPriority принимает уровень 0–15 и сама сдвигает его на 4 бита: вызов NVIC_SetPriority(IRQn, 0xF0) в надежде задать самый низкий приоритет запишет (0xF0 << 4) & 0xFF = 0 — самый высокий. Правильно — NVIC_SetPriority(IRQn, 15).
  3. Неверный PRIGROUP. HAL и CMSIS нумеруют группировку по-разному: NVIC_PRIORITYGROUP_4 из HAL — это PRIGROUP = 3, четыре бита вытеснения, а NVIC_SetPriorityGrouping(4) из CMSIS ставит PRIGROUP = 4 — три бита группы и бит подприоритета. Если сменить группировку после настройки приоритетов, меняется смысл всех записанных значений, и прерывания с разными приоритетами могут перестать вытеснять друг друга. Группировку задают один раз в начале программы, до NVIC_SetPriority, и то же значение передают в NVIC_EncodePriority. Запись в AIRCR без ключа 0x05FA в битах 31–16 ядро игнорирует.
  4. BASEPRI = 0 ничего не маскирует. Ноль отключает порог, а не запрещает всё; запретить все прерывания с настраиваемым приоритетом может только PRIMASK. И порог пишется со сдвигом: __set_BASEPRI(5) у STM32F1 запишет 0x05, а младшие четыре бита не реализованы — в регистре окажется 0, то есть маски нет. Нужно __set_BASEPRI(5 << 4).
  5. Переполнение стека. Стек растёт вниз, навстречу секциям .bss и куче, а аппаратного контроля границ стека у Cortex-M3 нет — он появился только в ARMv8-M. Переполнение тихо портит переменные и проявляется позже, например HardFault при возврате по испорченному адресу. В шаблоне ST _Min_Stack_Size = 0x400 — компоновщик лишь проверяет, что 1 КБ ОЗУ остался свободным, но границу не охраняет. Помогают заполнение стека меткой при старте и проверка, сколько её уцелело, защитная область MPU ниже стека, если MPU есть, и проверки переполнения стеков задач в ОС.
  6. Неинициализированный VTOR в приложении за загрузчиком. Приложение собрано для 0x0800 2000, но после перехода из загрузчика VTOR обычно по-прежнему равен 0, а по адресу 0 — начало Flash-памяти, то есть таблица загрузчика. Первое же прерывание уходит в обработчик загрузчика или в пустой Default_Handler. Решение — SCB->VTOR = 0x08002000 в начале приложения (в шаблоне ST — USER_VECT_TAB_ADDRESS и VECT_TAB_OFFSET = 0x2000). Загрузчик перед переходом должен выключить в NVIC свои прерывания и остановить SysTick, загрузить MSP из первого слова таблицы приложения и перейти по адресу из второго. У STM32F1 смещение таблицы кратно 0x200.
  7. Смена стека посреди функции на C. Запись в CONTROL.SPSEL или __set_MSP() внутри функции оставляет её локальные переменные и сохранённый адрес возврата в старом стеке, и функция может вернуться по случайному адресу. Стек переключают в стартовом коде или возвратом из исключения.

Частые вопросы

Сколько тактов занимает вход в прерывание у Cortex-M3?

Не больше 12 тактов от запроса до первой команды обработчика, если память работает без тактов ожидания; возврат тоже занимает 12 тактов, а переход от одного обработчика к другому по хвостовой цепочке — 6 тактов (TRM Cortex-M3, разд. 3.9.2). За эти 12 тактов ядро само сохраняет в стек восемь регистров и параллельно читает адрес обработчика. На STM32F103 при 72 МГц Flash-память работает с двумя тактами ожидания, поэтому реальная задержка больше.

Сколько уровней приоритета прерываний у STM32F103?

16. В каждом байте приоритета NVIC у STM32F1 реализованы только 4 старших бита, младшие читаются как ноль (PM0056). Меньшее число означает более высокий приоритет. Поле PRIGROUP делит эти 4 бита на приоритет группы, от которого зависит вытеснение, и подприоритет; HAL при инициализации выбирает NVIC_PRIORITYGROUP_4 — 16 уровней вытеснения без подприоритетов.

Как работает bit-banding и где он есть?

Первый мегабайт SRAM (0x2000 0000–0x200F FFFF) и периферии (0x4000 0000–0x400F FFFF) продублирован областями-алиасами по 32 МБ с адресов 0x2200 0000 и 0x4200 0000, где каждому биту соответствует 32-битное слово. Его адрес — база алиаса + смещение байта × 32 + номер бита × 4. Запись 0 или 1 в такое слово меняет один бит без прерываемого чтения-изменения-записи. Bit-banding есть у Cortex-M3 и M4 (в r2p1 и у M4 — как опция), в ядрах M0, M0+ и M7 его нет.

Почему программа на Cortex-M3 падает в HardFault сразу после старта?

Частая причина — чётный адрес в таблице векторов. Бит 0 вектора становится битом Thumb в EPSR, и если он равен 0, первая команда обработчика вызывает отказ INVSTATE, который сразу после сброса превращается в HardFault. Другие причины — неверное начальное значение стека в первом слове таблицы, обращение по несуществующему адресу и переполнение стека. Причину показывают регистры CFSR (0xE000 ED28) и HFSR (0xE000 ED2C), а адрес команды, вызвавшей отказ, лежит в кадре стека по смещению 0x18.

Чем MSP отличается от PSP?

Это две копии указателя стека R13. Основной стек MSP загружается после сброса из адреса 0x0000 0000, и на нём всегда работают обработчики исключений. Стек процесса PSP можно включить для режима Thread битом SPSEL регистра CONTROL или возвратом из исключения с EXC_RETURN = 0xFFFF FFFD. Программы без ОС обычно обходятся одним MSP, а ОС реального времени дают каждой задаче свой стек на PSP.

Можно ли выполнять программу из ОЗУ Cortex-M3?

Да: области SRAM (с 0x2000 0000) и внешнего ОЗУ исполняемые, нельзя выполнять код только из периферии, внешних устройств, PPB и области производителя. Но ARM рекомендует держать программу в области Code: из SRAM команды выбираются по той же шине System, что и данные, и работают медленнее. STM32F1 умеет и загружаться из SRAM — при BOOT1 = 1 и BOOT0 = 1, — но тогда таблицу векторов нужно перенести через VTOR.

Источники

  1. Arm. Cortex-M3 Processor Technical Reference Manual, revision r2p1 (ARM 100165, ранее DDI 0337I), 2016 — разд. 2.3 — шины I-Code, D-Code, System и PPB; 3.3 — такты команд; 3.5 — буфер записи; 3.7 — bit-banding; 3.9 — задержка прерывания 12 тактов и хвостовая цепочка 6 тактов; гл. 5–7 — MPU, NVIC, FPB; разд. 1.6 — отличия ревизий r0p0–r2p1
  2. Arm. ARMv7-M Architecture Reference Manual (DDI 0403E.e), 2021 — разд. B1.4–B1.5: регистры, маски и приоритеты, PRIGROUP, таблица векторов и её выравнивание, сброс (TakeReset), кадр стека (PushStack), EXC_RETURN, выравнивание стека; B3.2 — VTOR и AIRCR; C1 — DWT и ITM
  3. Arm. Cortex-M3 Devices Generic User Guide (DUI 0552A), 2010 — гл. 2 — режимы и привилегии, регистры ядра, карта памяти и типы памяти, исключения, отказы, режимы сна
  4. Arm. Armv6-M Architecture Reference Manual (DDI 0419E) — прил. D3 — отличия ARMv6-M (Cortex-M0, M0+) от ARMv7-M: приоритеты, маски, отказы, VTOR
  5. Arm. Cortex-M0 Devices Generic User Guide (DUI 0497A); Cortex-M0+ Technical Reference Manual (DDI 0484C); Cortex-M4 Technical Reference Manual (100166) — для сравнения ядер: конвейеры, умножитель, число прерываний и точек останова, задержка прерывания M0+ и M4
  6. Arm. Arm Cortex-M Processor Comparison Table, 2023 — DMIPS/МГц и CoreMark/МГц ядер Cortex-M
  7. STMicroelectronics. PM0056: STM32F10xxx/20xxx/21xxx/L1xxxx Cortex-M3 programming manual, Rev 7, 2024 — разд. 4.3 — 4 бита приоритета; табл. 45 — PRIGROUP; 4.4 — CPUID ревизий r1p1 и r2p0, VTOR с полем TBLOFF[29:9], AIRCR, SCR, CCR (STKALIGN = 0 у STM32F1); 4.5 — SysTick и STK_CALIB = 9000; 4.2 — MPU
  8. STMicroelectronics. RM0008: Reference manual STM32F101xx–STM32F107xx, Rev 21, 2021 — разд. 3.1 — шины и матрица шин; 3.3 — карта памяти, bit-banding, FLASH_ACR; 3.4 — загрузка по BOOT0 и BOOT1; 5.3 — режимы пониженного потребления; 7.3 — RCC; 9.2 — GPIO; 31 — отладка: SWJ-DP, AFIO_MAPR, FPB, DWT, DBGMCU_CR
  9. STMicroelectronics. DS5319: STM32F103x8, STM32F103xB datasheet, Rev 20, 2025 — 72 МГц и 1,25 DMIPS/МГц, 20 КБ SRAM, 43 канала прерываний, ограничения выводов PC13–PC15
  10. Arm. CMSIS-Core (Cortex-M) documentation — NVIC_SetPriority, NVIC_SetPriorityGrouping, NVIC_EncodePriority, SysTick_Config, функции доступа к специальным регистрам
  11. STMicroelectronics. STM32CubeF1: CMSIS device files и HAL (репозитории cmsis-device-f1 и stm32f1xx-hal-driver) — stm32f103xb.h (__NVIC_PRIO_BITS = 4, __MPU_PRESENT = 0), system_stm32f1xx.c, startup_stm32f103xb.s, STM32F103XB_FLASH.ld; константы NVIC_PRIORITYGROUP_x в stm32f1xx_hal_cortex.h
  12. Arm. ELF for the Arm Architecture (AAELF32); Procedure Call Standard for the Arm Architecture (AAPCS32) — бит 0 адреса функций Thumb (STT_FUNC); выравнивание стека на 8 байт на границе функций
  13. STM32-base. STM32F103C8T6 Blue Pill — кварц 8 МГц, светодиод на PC13, включённый на втекающий ток