Архитектура ARM Cortex-M3
Cortex-M3 — 32-битное ядро ARM с архитектурой ARMv7-M, на котором построены STM32F1, STM32F2, STM32L1, отечественные К1986ВЕ92 и многие другие микроконтроллеры. Ниже — регистры и режимы, карта памяти с bit-banding, исключения и NVIC с тактами по техническому руководству ARM, SysTick, MPU, отладка и рабочий пример для платы Blue Pill на CMSIS.
Обновлено Редакция GAW
Содержание
- Ядро: ARMv7-M, Thumb-2 и конвейер из трёх ступеней
- Регистры Cortex-M3
- Режимы Thread и Handler, привилегии и два стека
- Карта памяти Cortex-M3
- Исключения и прерывания: NVIC
- Системный таймер SysTick
- MPU: восемь областей защиты памяти
- Отладка: SWD, JTAG, FPB, DWT и ITM
- Режимы пониженного потребления: WFI, WFE и SLEEPDEEP
- Старт после сброса
- Пример: SysTick 1 мс и светодиод PC13 на Blue Pill
- Cortex-M3 в сравнении с M0, M0+ и M4
- Типичные ошибки
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.
Шины 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, MLS | 2 | умножение с накоплением и с вычитанием |
| UMULL, SMULL (32 × 32 → 64 бита) | 3–5 | для коротких операндов быстрее |
| UMLAL, SMLAL | 4–7 | то же |
| SDIV, UDIV | 2–12 | зависит от операндов; прерывание отменяет деление, после возврата оно начинается заново |
| LDR, LDRH, LDRB | 2 | идущие подряд LDR и STR совмещаются и экономят такт |
| STR с непосредственным смещением | 1 | данные записываются одновременно со следующей командой |
| LDM, STM, PUSH, POP из N регистров | 1 + N | POP с загрузкой PC — ещё P тактов |
| B, BL, BX, BLX | 1 + P | P = 1–3 такта на перезаполнение конвейера |
| условный переход, CBZ, CBNZ | 1 или 1 + P | 1 такт, если переход не выполняется |
| TBB, TBH | 2 + P | переход по таблице |
| IT | 1 | после 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.
- 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 — только флаги.
| Часть | Биты | Что хранит |
|---|---|---|
| APSR | 31 — N, 30 — Z, 29 — C, 28 — V, 27 — Q | флаги результата: отрицательный, ноль, перенос, переполнение; Q — было насыщение в SSAT или USAT |
| IPSR | 8–0 | номер текущего исключения: 0 — режим Thread, 2 — NMI, 3 — HardFault, 11 — SVCall, 15 — SysTick, 16 и больше — прерывания IRQ0, IRQ1… |
| EPSR | 24 — 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
| Регистр | Биты | Действие | После сброса |
|---|---|---|---|
| PRIMASK | 0 | 1 — запрещены все исключения с настраиваемым приоритетом: текущий приоритет поднимается до 0, работают только NMI и HardFault | 0 |
| FAULTMASK | 0 | 1 — запрещено всё, кроме NMI: приоритет поднимается до −1, уровня HardFault; бит сбрасывается при выходе из любого обработчика, кроме NMI | 0 |
| BASEPRI | 7–0 | не 0 — не обслуживаются исключения с приоритетом, численно большим или равным BASEPRI; 0 — порога нет | 0 |
| CONTROL | 1 — SPSEL, 0 — nPRIV | SPSEL: 0 — режим Thread работает на MSP, 1 — на PSP; nPRIV: 0 — Thread привилегированный, 1 — нет | 0 |
Непривилегированный код читает маски как ноль, а его запись в них игнорируется. Как маски применяют на практике — в разделе о NVIC.
Режимы Thread и Handler, привилегии и два стека
| Режим | Что выполняется | Привилегии | Стек |
|---|---|---|---|
| Thread | основная программа; в этот режим ядро выходит из сброса | привилегированный или нет — по биту CONTROL.nPRIV | MSP или 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. От свойств области зависит, можно ли выполнять из неё код и может ли ядро переставлять и буферизовать обращения к ней.
| Адреса | Область | Тип памяти | Выполнение кода | Шина |
|---|---|---|---|---|
| 0x0000 0000–0x1FFF FFFF | Code, 0,5 ГБ | Normal | можно | I-Code и D-Code |
| 0x2000 0000–0x3FFF FFFF | SRAM, 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 FFFF | PPB, 1 МБ | Strongly-ordered | нельзя | внутренняя и APB |
| 0xE010 0000–0xFFFF FFFF | область производителя | Device | нельзя | System |
Типы памяти различаются порядком обращений. С памятью Normal ядро может переставлять обращения и читать наперёд. Обращения к Device выполняются в порядке программы относительно других обращений к Device и Strongly-ordered, а запись можно буферизовать. Strongly-ordered упорядочена относительно всех обращений и не буферизуется. Атрибут XN (Execute Never) запрещает выборку команд: попытка выполнить код из периферии вызывает отказ MemManage даже без MPU.
Внутри PPB лежат регистры самого ядра:
| Адрес | Блок |
|---|---|
| 0xE000 0000 | ITM — программная трассировка |
| 0xE000 1000 | DWT — точки наблюдения, счётчик тактов |
| 0xE000 2000 | FPB — аппаратные точки останова |
| 0xE000 E010 | SysTick |
| 0xE000 E100 | NVIC: разрешение, ожидание и приоритеты прерываний |
| 0xE000 ED00 | SCB: CPUID, ICSR, VTOR, AIRCR, SCR, CCR, приоритеты системных исключений, регистры отказов |
| 0xE000 ED90 | MPU |
| 0xE000 EDF0 | регистры отладки ядра DHCSR, DCRSR, DCRDR, DEMCR |
| 0xE004 0000 | TPIU — вывод трассировки |
| 0xE004 1000 | ETM — трассировка команд |
| 0xE00F F000 | ROM-таблица отладочных блоков |
В 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-битное слово:
где смещение байта отсчитывается от начала региона 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.
Запись в слово-алиас меняет только один бит: его значение берётся из младшего бита записанного слова — 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 | −3 | 0x04 | включение питания, сброс |
| 2 | −14 | NMI | −2 | 0x08 | немаскируемое прерывание; в STM32F1 — от системы контроля тактирования CSS |
| 3 | −13 | HardFault | −1 | 0x0C | отказ при обработке исключения или отказ, обработчик которого выключен |
| 4 | −12 | MemManage | настраивается | 0x10 | нарушение прав MPU, выполнение кода из области XN |
| 5 | −11 | BusFault | настраивается | 0x14 | ошибка шины при выборке команды или обращении к данным |
| 6 | −10 | UsageFault | настраивается | 0x18 | неопределённая команда, бит T = 0, неверный EXC_RETURN; по настройке — деление на ноль и невыровненный доступ |
| 11 | −5 | SVCall | настраивается | 0x2C | команда SVC |
| 12 | −4 | DebugMonitor | настраивается | 0x30 | отладочный монитор |
| 14 | −2 | PendSV | настраивается | 0x38 | программный запрос, обычно переключение задач ОС |
| 15 | −1 | SysTick | настраивается | 0x3C | системный таймер дошёл до нуля |
| 16 + n | n | IRQn | настраивается | 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.
Пример: при 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 тактов
Когда приходит исключение с достаточным приоритетом, ядро без единой команды программы:
- сохраняет в текущий стек — MSP или PSP — восемь слов: R0–R3, R12, LR, адрес возврата и xPSR;
- одновременно читает адрес обработчика из таблицы векторов;
- записывает в LR код EXC_RETURN, переходит в режим Handler со стеком MSP и заносит номер исключения в IPSR;
- начинает выполнять обработчик — его первая команда выбирается параллельно с сохранением регистров.
Регистры R0–R3, R12 и LR по соглашению о вызовах AAPCS может портить любая функция, а R4–R11 функция обязана сохранить сама, если их использует. Поэтому обработчик прерывания — обычная функция на C без специальных атрибутов: всё нужное прерванному коду ядро уже сохранило.
Задержка — не больше 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); опоздавшим считается исключение, пришедшее до того, как первая команда исходного обработчика дошла до ступени исполнения. Исходное исключение остаётся в ожидании и выполняется следом по хвостовой цепочке.
EXC_RETURN и возврат из обработчика
При входе в исключение ядро записывает в LR не адрес возврата — он уже в кадре стека, — а код EXC_RETURN вида 0xFFFF FFFx. Обработчик завершается обычной командой возврата: BX LR, POP {..., PC} или загрузкой PC командой LDR. Увидев в PC значение, у которого биты 31–4 — все единицы, ядро понимает, что это выход из исключения, снимает кадр из нужного стека и продолжает прерванную программу.
| EXC_RETURN | Куда возвращаемся | Из какого стека берётся кадр |
|---|---|---|
| 0xFFFF FFF1 | в режим Handler, в прерванный обработчик | MSP |
| 0xFFFF FFF9 | в режим Thread | MSP |
| 0xFFFF FFFD | в режим Thread | PSP |
Остальные значения зарезервированы, и попытка вернуться с ними вызывает 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->CTRL | 0xE000 E010 | бит 0 ENABLE — пуск, бит 1 TICKINT — запрос исключения, бит 2 CLKSOURCE — источник такта, бит 16 COUNTFLAG — счётчик дошёл до нуля |
| SysTick->LOAD | 0xE000 E014 | значение перезагрузки, 24 бита: 1…0xFF FFFF |
| SysTick->VAL | 0xE000 E018 | текущее значение; любая запись обнуляет счётчик и COUNTFLAG |
| SysTick->CALIB | 0xE000 E01C | калибровка; у STM32F1 — 9000 отсчётов: 1 мс при HCLK = 72 МГц и такте HCLK/8 |
Бит CLKSOURCE выбирает источник: 1 — частота ядра, 0 — внешний опорный такт, у STM32F1 это HCLK/8 (PM0056). Период прерываний:
Для 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 | Как войти | Что отключается | Чем разбудить |
|---|---|---|---|
| Sleep | WFI или WFE при SLEEPDEEP = 0 | такт ядра, периферия работает | любое прерывание (WFI) или событие (WFE) |
| Stop | SLEEPDEEP = 1, бит PDDS регистра PWR_CR = 0, затем WFI или WFE | все такты области 1,8 В, генераторы HSI и HSE; бит LPDS переводит регулятор в экономичный режим | любая линия EXTI |
| Standby | SLEEPDEEP = 1, PDDS = 1, затем WFI или WFE | область 1,8 В обесточена, содержимое SRAM и регистров теряется | вывод WKUP, будильник RTC, сброс по NRST или от IWDG |
После выхода из Stop микроконтроллер работает от внутреннего генератора HSI на 8 МГц, и PLL нужно запускать заново.
Старт после сброса
После сброса ядро (ARMv7-M ARM, псевдокод TakeReset):
- читает слово по адресу 0x0000 0000 и загружает его в MSP, обнулив два младших бита;
- читает слово 0x0000 0004 — вектор сброса: его бит 0 становится битом T регистра EPSR, а адрес без бита 0 — значением PC;
- начинает выполнять код в режиме Thread, привилегированно, со стеком MSP; маски PRIMASK, FAULTMASK и BASEPRI равны нулю, LR = 0xFFFF FFFF.
Что окажется по адресу 0, у STM32F1 решают выводы BOOT0 и BOOT1, которые защёлкиваются на четвёртом такте SYSCLK после сброса (RM0008, разд. 3.4):
| BOOT1 | BOOT0 | Откуда загрузка | Что видно по адресу 0 |
|---|---|---|---|
| любой | 0 | основная Flash-память | копия Flash-памяти с 0x0800 0000 |
| 0 | 1 | системная память | встроенный загрузчик ST, который прошивает Flash-память через USART1 |
| 1 | 1 | встроенная SRAM | SRAM доступна только с 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-M0 | Cortex-M0+ | Cortex-M3 | Cortex-M4 |
|---|---|---|---|---|
| Архитектура | ARMv6-M | ARMv6-M | ARMv7-M | ARMv7E-M |
| Конвейер | 3 ступени | 2 ступени | 3 ступени | 3 ступени |
| Шины | одна AHB-Lite | одна AHB-Lite, однотактный порт ввода-вывода — опция | три AHB-Lite | три AHB-Lite |
| Набор команд | подмножество Thumb | подмножество Thumb | Thumb-2 | Thumb-2 и DSP: SIMD, насыщение, умножение с накоплением за такт |
| Умножение 32 × 32 | 1 или 32 такта — выбирает производитель | 1 или 32 такта | 1 такт | 1 такт |
| Деление | программное | программное | 2–12 тактов | 2–12 тактов |
| FPU | нет | нет | нет | одинарной точности, опция |
| Внешние прерывания | до 32 | до 32 | до 240 | до 240 |
| Уровни приоритета | 4, без групп | 4, без групп | 8–256, группы через PRIGROUP | 8–256, группы через PRIGROUP |
| Маски | PRIMASK | PRIMASK | PRIMASK, FAULTMASK, BASEPRI | PRIMASK, FAULTMASK, BASEPRI |
| Отказы | только HardFault | только HardFault | HardFault, MemManage, BusFault, UsageFault с регистрами причин | как у M3 |
| Перенос таблицы векторов | нет | VTOR — опция | VTOR | VTOR |
| MPU | нет | 8 областей, опция | 8 областей, опция | 8 областей, опция |
| Bit-banding | не в ядре | не в ядре | есть, в r2p1 — опция | опция |
| Точки останова и наблюдения | до 4 и до 2 | до 4 и до 2 | до 8 и до 4 | до 8 и до 4 |
| DMIPS/МГц по таблице ARM 2023 года | 0,96 | 0,99 | 1,24 | 1,26 |
| Примеры МК | STM32F0 | STM32G0, RP2040, SAM D21 | STM32F1, К1986ВЕ92QI | STM32F4, 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».
Типичные ошибки
- HardFault сразу после старта или при первом прерывании: чётный адрес в таблице векторов. Бит 0 вектора становится битом T; при нуле первая команда обработчика вызывает UsageFault с признаком INVSTATE, а после сброса, пока UsageFault выключен, — HardFault (ARMv7-M ARM, разд. B1.5.3). Компоновщик ставит этот бит сам, если символ — функция Thumb: на C это любая функция, в ассемблере — метка, объявленная как
.type имя, %function, как Reset_Handler в стартовом файле ST. Ошибка появляется, когда адрес вписан в таблицу числом, когда метка в ассемблере не объявлена функцией и когда загрузчик прыгает в приложение по вычисленному адресу вместо слова из его таблицы векторов. Признаки — бит INVSTATE в CFSR и FORCED в HFSR. - «Перевёрнутые» приоритеты и байт вместо уровня в NVIC_SetPriority. Меньшее число — более высокий приоритет. NVIC_SetPriority принимает уровень 0–15 и сама сдвигает его на 4 бита: вызов
NVIC_SetPriority(IRQn, 0xF0)в надежде задать самый низкий приоритет запишет(0xF0 << 4) & 0xFF= 0 — самый высокий. Правильно —NVIC_SetPriority(IRQn, 15). - Неверный PRIGROUP. HAL и CMSIS нумеруют группировку по-разному: NVIC_PRIORITYGROUP_4 из HAL — это PRIGROUP = 3, четыре бита вытеснения, а
NVIC_SetPriorityGrouping(4)из CMSIS ставит PRIGROUP = 4 — три бита группы и бит подприоритета. Если сменить группировку после настройки приоритетов, меняется смысл всех записанных значений, и прерывания с разными приоритетами могут перестать вытеснять друг друга. Группировку задают один раз в начале программы, до NVIC_SetPriority, и то же значение передают в NVIC_EncodePriority. Запись в AIRCR без ключа 0x05FA в битах 31–16 ядро игнорирует. - BASEPRI = 0 ничего не маскирует. Ноль отключает порог, а не запрещает всё; запретить все прерывания с настраиваемым приоритетом может только PRIMASK. И порог пишется со сдвигом:
__set_BASEPRI(5)у STM32F1 запишет 0x05, а младшие четыре бита не реализованы — в регистре окажется 0, то есть маски нет. Нужно__set_BASEPRI(5 << 4). - Переполнение стека. Стек растёт вниз, навстречу секциям .bss и куче, а аппаратного контроля границ стека у Cortex-M3 нет — он появился только в ARMv8-M. Переполнение тихо портит переменные и проявляется позже, например HardFault при возврате по испорченному адресу. В шаблоне ST
_Min_Stack_Size = 0x400— компоновщик лишь проверяет, что 1 КБ ОЗУ остался свободным, но границу не охраняет. Помогают заполнение стека меткой при старте и проверка, сколько её уцелело, защитная область MPU ниже стека, если MPU есть, и проверки переполнения стеков задач в ОС. - Неинициализированный 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. - Смена стека посреди функции на 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.
Источники
- 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
- 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
- Arm. Cortex-M3 Devices Generic User Guide (DUI 0552A), 2010 — гл. 2 — режимы и привилегии, регистры ядра, карта памяти и типы памяти, исключения, отказы, режимы сна
- Arm. Armv6-M Architecture Reference Manual (DDI 0419E) — прил. D3 — отличия ARMv6-M (Cortex-M0, M0+) от ARMv7-M: приоритеты, маски, отказы, VTOR
- Arm. Cortex-M0 Devices Generic User Guide (DUI 0497A); Cortex-M0+ Technical Reference Manual (DDI 0484C); Cortex-M4 Technical Reference Manual (100166) — для сравнения ядер: конвейеры, умножитель, число прерываний и точек останова, задержка прерывания M0+ и M4
- Arm. Arm Cortex-M Processor Comparison Table, 2023 — DMIPS/МГц и CoreMark/МГц ядер Cortex-M
- 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
- 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
- STMicroelectronics. DS5319: STM32F103x8, STM32F103xB datasheet, Rev 20, 2025 — 72 МГц и 1,25 DMIPS/МГц, 20 КБ SRAM, 43 канала прерываний, ограничения выводов PC13–PC15
- Arm. CMSIS-Core (Cortex-M) documentation — NVIC_SetPriority, NVIC_SetPriorityGrouping, NVIC_EncodePriority, SysTick_Config, функции доступа к специальным регистрам
- 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
- Arm. ELF for the Arm Architecture (AAELF32); Procedure Call Standard for the Arm Architecture (AAPCS32) — бит 0 адреса функций Thumb (STT_FUNC); выравнивание стека на 8 байт на границе функций
- STM32-base. STM32F103C8T6 Blue Pill — кварц 8 МГц, светодиод на PC13, включённый на втекающий ток
Смотрите также
- ARM Cortex-MЯдра M0–M85: архитектуры, FPU, DSP, MPU, TrustZone, выбор
- МикроконтроллерыAVR, ARM Cortex-M, ESP32, PIC, MSP430: архитектура и выбор
- ПрограммыСАПР плат, симуляторы схем, среды и утилиты прошивки МК
- CANCAN_H и CAN_L, терминаторы, кадр, арбитраж, ошибки, тайминг, MCP2515
- SPISCLK, MOSI, MISO, CS, режимы 0–3, daisy chain, частота, SPI в AVR
- Система команд AVRВсе инструкции с флагами и тактами, адресация, примеры кода