МИФИ · МПС · STM32F103C8T6 · Keil µVision 5 и make
Си для STM32F103
Ты уже знаешь, как устроен чип: база + смещение, биты, EXTI, NVIC, таблица векторов. Си ничего из этого не отменяет. Он только берёт на себя рутину: складывает адреса, пишет LDR/ORR/STR, сохраняет регистры и заполняет таблицу векторов. Этот конспект про то, что именно он делает за тебя и где за ним надо следить.
00 · главная мысль
Что меняется, а что нет
Вот твой обработчик PA3 из сданной ЛР2, тот же обработчик на Си и то, во что компилятор Keil превратил Си. Наведи на строку Си — подсветятся её инструкции.
A3BUT_HANDLER
LDR R0, =EXTI_PR
LDR R12, =(1 << 3)
STR R12, [R0]
LDR R0, =GPIOA_ODR
LDR R1, [R0]
EOR R12, R1, #(1 << 7)
STR R12, [R0]
BX LRvoid EXTI3_IRQHandler(void) { EXTI->PR = EXTI_PR_PR3; GPIOA->ODR ^= GPIO_ODR_ODR7; }
MOV r0,#0xfc08 MOV r1,#0x80c MOVT r0,#0xffff MOVT r1,#0x4001 MOVS r2,#8 STR r2,[r1,r0] LDR r0,[r1,#0] EOR r0,r0,#0x80 STR r0,[r1,#0] BX lr
Те же регистры, те же числа: 8 — бит 3 в EXTI_PR, 0x80 — бит 7 в ODR, и тот же BX lr на выходе. Компилятор даже схитрил: в r1 лежит адрес GPIOA_ODR = 0x4001080C, а в r0 — число 0xFFFFFC08, то есть −0x3F8. Их сумма — 0x40010414, это EXTI_PR. Одна загрузка адреса на оба регистра вместо двух.
Что остаётся за тобой, как и в ассемблере: какие регистры, какие биты, в каком порядке. Что берёт на себя компилятор: сложение базы и смещения, выбор регистров R0–R3/R12, сохранение остальных, BX LR и строку в таблице векторов.
Дизассемблер получен из armclang -O1 -mcpu=cortex-m3 (Keil MDK 5.40, ArmClang V6.22) и fromelf -c для этого файла. Имена EXTI_PR_PR3 и GPIO_ODR_ODR7 — .
01 · проект
Из чего собирается программа
зачемКто теперь пишет таблицу векторов и Reset_Handler
В ЛР2 весь чип описывал один твой файл: EQU с адресами, таблица DCD, Reset_Handler, код. В проекте на Си эти роли разложены по готовым файлам из пакета Keil, а твой — только main.c (и, если нужно, .s с обработчиком).
| Файл | Откуда | Что делает | В ЛР2 это было |
|---|---|---|---|
| startup_stm32f10x_md.s | пакет Keil, компонент Device → Startup | таблица векторов на все 59 строк с именами, стек 0x400 байт, Reset_Handler | твои DCD и Reset_Handler |
| system_stm32f10x.c | там же | SystemInit() — настраивает тактирование до main (72 МГц, глава 07) | не было — работали от HSI 8 МГц |
| stm32f10x.h | пакет Keil | все регистры как структуры, имена битов, номера IRQn | твои EQU |
| core_cm3.h | пакет CMSIS 6.1.0 | регистры ядра (NVIC, SCB, SysTick) и функции NVIC_… | твои NVIC_ISER0 EQU … |
| main.c | ты | настройка и логика | остальной lab2.s |
какПуть от сброса до main
обнуляет переменные
__main — служебная функция библиотеки Си. Она копирует начальные значения глобальных переменных из флеша в ОЗУ и обнуляет остальные. В ассемблере ты такого не делал, потому что не было переменных в ОЗУ. Сборка через make делает то же самое в своём стартовом файле tools/c/startup_md.c.
Какой стартовый файл брать, решает размер флеша: у C8 64 КБ — это medium density, файл _md. По той же причине Keil сам передаёт компилятору макрос STM32F10X_MD, по которому stm32f10x.h выбирает нужный набор прерываний.
Стек и таблица — , ; вызов SystemInit и __main — . Макрос для C8 — .
02 · регистры
Регистр — поле структуры
зачемБаза + смещение без ручного сложения
В stm32f10x.h каждый блок периферии описан как структура: список регистров в том порядке, в каком они лежат в памяти. Поле структуры занимает 4 байта, поэтому смещение каждого следующего поля на 4 больше. Это ровно таблица регистров блока из RM0008.
слова
- struct
- структура: несколько полей под одним именем, лежат в памяти подряд
- EXTI_TypeDef
- тип «блок EXTI»: поля IMR, EMR, RTSR, FTSR, SWIER, PR
- EXTI
- указатель на такую структуру, стоящую по адресу 0x40010400
- ->
- «поле структуры, на которую указывает указатель»:
EXTI->RTSR= регистр по адресу база + смещение поля - __IO
- то же, что
volatile(глава 04): каждое обращение к полю — настоящее чтение или запись в регистр
какТри строки заголовка — и адрес готов
typedef struct {
__IO uint32_t IMR; /* +0x00 */
__IO uint32_t EMR; /* +0x04 */
__IO uint32_t RTSR; /* +0x08 */
__IO uint32_t FTSR; /* +0x0C */
__IO uint32_t SWIER; /* +0x10 */
__IO uint32_t PR; /* +0x14 */
} EXTI_TypeDef;
#define PERIPH_BASE ((uint32_t)0x40000000)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) /* 0x40010000 — начало APB2 */
#define EXTI_BASE (APB2PERIPH_BASE + 0x0400) /* 0x40010400 */
#define EXTI ((EXTI_TypeDef *) EXTI_BASE)
Комментарии со смещениями дописаны для наглядности, в заголовке их нет — смещение получается из порядка полей. Дальше EXTI->RTSR компилятор превращает в адрес 0x40010400 + 0x08 = 0x40010408, тот же, что у тебя в EXTI_RTSR EQU 0x40010408. Цепочка PERIPH_BASE → APB2PERIPH_BASE → EXTI_BASE — это карта памяти из RM0008 Table 3, записанная макросами.
попробуйПоле → адрес
EXTICR[4], а массив в Си нумеруется с нуля. Поэтому AFIO->EXTICR[0] — это EXTICR1 (линии 0–3), AFIO->EXTICR[1] — EXTICR2 (линии 4–7) и так далее. Правило из ЛР2 «регистр = линия // 4 + 1» в Си превращается в «индекс = линия // 4». Та же история, что с нумерацией строк таблицы векторов с нуля.Структуры — , , ; базы — , ; указатель EXTI — .
03 · биты
Биты: четыре операции на все случаи
Всё, что ты делал тройками LDR → операция → STR, в Си — одна строка с составным оператором. Компилятор сам развернёт её в те же три команды.
| Нужно | Си | Ассемблер ЛР2 | Где у тебя было |
|---|---|---|---|
| поставить биты | REG |= маска; | LDR / ORR / STR | IMR, RTSR, EXTICR2, RCC_APB2ENR |
| сбросить биты | REG &= ~маска; | LDR / BIC / STR | EXTICR1 (биты 15:12), MAPR |
| переключить | REG ^= маска; | LDR / EOR / STR | ODR в обработчиках |
| записать целиком | REG = значение; | STR | EXTI_PR, ISER (запись 0 ничего не делает) |
| проверить бит | if (REG & маска) { … } | LDR / TST / IT NE | IDR в ЛР1 |
маскиЧислом или готовым именем
Маску можно писать как раньше — 1 << 3, 0xF << 12, — а можно готовым именем из заголовка. Имена устроены по схеме БЛОК_РЕГИСТР_ПОЛЕ, в точности как в RM0008:
| Имя | Значение | Что это |
|---|---|---|
| RCC_APB2ENR_AFIOEN | 0x00000001 | бит 0 RCC_APB2ENR — тактирование AFIO |
| RCC_APB2ENR_IOPAEN | 0x00000004 | бит 2 — порт A |
| EXTI_IMR_MR3 | 0x00000008 | бит 3 IMR — линия 3 |
| EXTI_PR_PR3 | 0x00000008 | бит 3 PR |
| AFIO_EXTICR2_EXTI5_PB | 0x0010 | поле линии 5 в EXTICR2 = 0001 (порт B) |
| GPIO_ODR_ODR7 | 0x0080 | бит 7 ODR |
Что выбрать — дело вкуса. Имена читаются лучше, числа ближе к тому, что ты считал в ЛР1 и ЛР2. На защите важно одно: суметь сказать, какой это бит и откуда он.
PR сбрасывается записью единицы. Если написать EXTI->PR |= EXTI_PR_PR3;, процессор сначала прочитает PR — и получит единицы у всех линий, которые сейчас ждут обработки, — а потом запишет их обратно. Флаги чужих линий сбросятся, и их прерывания потеряются. Поэтому только EXTI->PR = EXTI_PR_PR3; — ровно так же, как ты делал STR маски без чтения.Имена битов — , , .
04 · типы
Типы и volatile
слова
- uint32_t
- беззнаковое 32-битное число — слово, как
DCD. Регистры периферии все такие - uint16_t, uint8_t
- 16 и 8 бит.
uint8_t— один байт, как у регистров приоритета IPR (STRB) - volatile
- «значение может измениться само»: компилятору запрещено выбрасывать или объединять обращения
какПочему без volatile пропадает задержка
Компилятор оптимизирует: если результат вычисления нигде не нужен, вычисление выбрасывается. Пустой цикл-задержка с точки зрения языка ничего не делает — и исчезает. Вот что armclang сделал из двух функций:
void delay_plain(void) {
for (uint32_t i = 0; i < 1000000; i++) { }
}
/* armclang -O1: */
BX lr ; цикла нет вообщеvoid delay_volatile(void) {
for (volatile uint32_t i = 0; i < 1000000; i++) { }
}
/* armclang -O1, тело цикла: */
LDR r1,[sp,#0]
ADDS r1,#1
STR r1,[sp,#0] ; i живёт в стеке
LDR r1,[sp,#0]
CMP r1,r0Поэтому в методичке и написано volatile uint32_t cnt. Та же причина, по которой все поля регистров объявлены как __IO — это просто другое имя volatile. Без него компилятор мог бы, например, прочитать GPIOA->IDR один раз и дальше считать, что значение не меняется.
Второе правило volatile: переменная, которую меняет обработчик прерывания, а читает main (флаг, счётчик тиков), должна быть volatile. Иначе main прочитает её один раз и будет крутиться со старым значением.
SUBS/BNE. Точное время зависит от компилятора и уровня оптимизации. Для «нескольких секунд» из эксперимента 2 подбирай число на плате, а не по формуле.__IO = volatile — . Дизассемблер — armclang -O1 для этих двух функций.
05 · прерывания
Обработчики прерываний
какОбработчик — функция с именем из таблицы
В стартовом файле строка таблицы векторов для EXTI3 выглядит так: DCD EXTI3_IRQHandler. А ниже эта метка объявлена «слабой» ([WEAK]) и указывает на заглушку Default_Handler — бесконечный цикл B .. Слабое имя значит «если где-то есть функция с таким же именем — возьми её».
Поэтому весь механизм — одна строка в main.c:
void EXTI3_IRQHandler(void) { /* имя — точно как в startup_stm32f10x_md.s */
EXTI->PR = EXTI_PR_PR3; /* сброс флага — первым делом */
GPIOA->ODR ^= GPIO_ODR_ODR7;
}
Компоновщик видит сильную функцию EXTI3_IRQHandler и кладёт её адрес в строку 25 вместо заглушки. Считать строки больше не нужно. Для твоих кнопок имена такие:
| Кнопка | Имя функции | IRQn в заголовке | Строка таблицы |
|---|---|---|---|
| PA3 | EXTI3_IRQHandler | EXTI3_IRQn = 9 | 25 |
| PB5 | EXTI9_5_IRQHandler | EXTI9_5_IRQn = 23 | 39 |
| PA10 | EXTI15_10_IRQHandler | EXTI15_10_IRQn = 40 | 56 |
EXTI3_IRQhandler (маленькая h) или EXTI3_Handler, ошибки при сборке не будет: это просто ещё одна функция, которую никто не вызывает. А прерывание уйдёт в Default_Handler, и программа зависнет в B .. Если в отладчике программа стоит в Default_Handler — проверь имя по стартовому файлу.регистрыКто теперь сохраняет R4–R11
В ЛР2 ты выяснил правило: ядро само сохраняет R0–R3, R12, LR, PC, xPSR, а R4–R11 — забота обработчика. Соглашение о вызовах Си (AAPCS) устроено так же: функция может портить R0–R3 и R12, а R4–R11 обязана вернуть как были. Поэтому любая функция на Си годится в обработчик без переделок: если ей понадобятся R4–R11, компилятор сам поставит PUSH/POP. И если она вызывает другую функцию, сам сохранит LR с EXC_RETURN.
Строка таблицы — ; слабое имя — ; номера — , (блок для STM32F10X_MD).
06 · ядро
NVIC и приоритеты по-сишному
Регистры ядра описаны в core_cm3.h так же структурами: NVIC->ISER[0], NVIC->IPR[9], SCB->AIRCR. Плюс CMSIS даёт короткие функции. Можно писать и так, и так — вот что за ними стоит:
| Функция CMSIS | Прямая запись | Что сделал armclang |
|---|---|---|
| NVIC_EnableIRQ(EXTI3_IRQn); | NVIC->ISER[0] = 1u << 9; | MOV r1,#0x200 → STR в 0xE000E100 |
| NVIC_SetPriority(EXTI9_5_IRQn, 2); | NVIC->IPR[23] = 2 << 4; | MOVS r3,#0x20 → STRB в 0xE000E417 |
| NVIC_SetPriorityGrouping(5); | SCB->AIRCR = 0x05FA0000 | (5 << 8); | 0x05FA0500 → STR в 0xE000ED0C |
Сдвиг на 4 делает функция. NVIC_SetPriority(…, 2) пишет в байт 0x20, а не 0x02: у STM32 из восьми бит приоритета работают только старшие четыре (биты 7:4), и функция сама сдвигает число на 8 − __NVIC_PRIO_BITS = 4. Если пишешь NVIC->IPR[...] напрямую — сдвигай сам. Иначе 2 попадёт в неработающие младшие биты и превратится в приоритет 0.
Ключ 0x05FA подставляет функция. NVIC_SetPriorityGrouping читает AIRCR, очищает старый ключ и PRIGROUP, вставляет 0x5FA в биты 31:16 и новое значение в биты 10:8. Без ключа запись в AIRCR игнорируется.
NVIC->IPR[]. В старых примерах из интернета и в CMSIS 5 он назывался NVIC->IP[] — такой код с твоим пакетом не соберётся. Функции NVIC_SetPriority и NVIC_EnableIRQ одинаковы в обеих версиях.Структура NVIC — ; NVIC_SetPriority — ; NVIC_SetPriorityGrouping — ; NVIC_EnableIRQ — ; 4 бита приоритета — .
07 · тактирование
Частота: SystemInit и 72 МГц
В ЛР1 и ЛР2 чип работал от внутреннего генератора HSI, 8 МГц. В проекте на Си стартовый код до main вызывает SystemInit(), а она по умолчанию включает кварц HSE 8 МГц и умножитель PLL ×9: 72 МГц. Перед этим она ставит задержку флеша 2 такта — без неё флеш не успевает на такой частоте. Шина APB1 делится на 2 (36 МГц — её максимум), APB2 работает на 72.
Что это значит для тебя:
- любой цикл-задержка идёт в 9 раз быстрее, чем на 8 МГц;
- текущую частоту программа знает из переменной
SystemCoreClock— ею удобно считать задержки и настройку SysTick; - на плате должен стоять кварц 8 МГц (на Blue Pill он есть). Если HSE не запустится,
SystemInitтихо оставит HSI 8 МГц.
system_stm32f10x.c в папку проекта (RTE/Device/STM32F103C8/), её можно править. Закомментируй строку #define SYSCLK_FREQ_72MHz — тогда SystemInit оставит HSI 8 МГц. Но если преподаватель задаёт частоту вариантом (как в ЛР по таймерам), лучше не трогать и считать от SystemCoreClock.Выбор 72 МГц — ; задержка флеша — ; HSE × 9 — .
08 · вместе
Си + ассемблер в одном проекте
Так задумал преподаватель эксперимент 1: main на Си, обработчик на ассемблере. Обработчик — отдельный .s файл в проекте рядом с main.c:
THUMB
AREA |.text|, CODE, READONLY
EXPORT EXTI3_IRQHandler ; имя — как в стартовом файле
IMPORT presses ; переменная из main.c
EXTI3_IRQHandler
LDR R0, =0x40010414 ; EXTI_PR
MOV R1, #(1 << 3)
STR R1, [R0]
... ; переключить светодиод, как в ЛР2
LDR R0, =presses ; presses++
LDR R1, [R0]
ADD R1, R1, #1
STR R1, [R0]
BX LR
END
- Таблицы векторов в
.sнет.EXPORT EXTI3_IRQHandlerделает метку видимой, и компоновщик подставляет её вместо слабой заглушки — так же, как функцию на Си. - Общая переменная. В
main.c:volatile uint32_t presses;. В.s:IMPORT pressesи работа через её адресLDR R0, =presses. - Регистры — только R0–R3 и R12, правило то же, что в ЛР2 и в AAPCS.
- Своего
Reset_Handler,STACK_TOPиAREA RESETбыть не должно — это теперь делает стартовый файл. Если оставить, будет ошибка «multiple definition».
Проверено сборкой через make (tools/rules-c.mk): по адресу 0x64 в прошивке — адрес EXTI3_IRQHandler из .s, переменная presses — в ОЗУ по 0x20000000.
09 · инструменты
Сборка и отладка
keilНовый проект в µVision
- Project → New µVision Project, устройство STM32F103C8.
- Откроется Manage Run-Time Environment: отметь CMSIS → CORE и Device → Startup. Keil добавит
startup_stm32f10x_md.sиsystem_stm32f10x.c. - Add New Item → C File →
main.c, в первой строке#include "stm32f10x.h". Для обработчика на ассемблере — Asm File. - Options for Target → Debug → ST-Link, как в ЛР2. Build (F7), Download (F8).
linuxЧерез make
В папке проекта файл Makefile из одной строки include ../tools/rules-c.mk. Дальше make, make disasm, make flash — как для ассемблера. Собираются все .c и .s из папки; заголовки и system_stm32f10x.c берутся из пакетов Keil, так что main.c один и тот же.
отладкаЧто смотреть
- Peripherals → System Viewer → EXTI / NVIC — те же регистры, что в ЛР2, с именами битов.
- Watch — переменные Си по имени. Нужно, чтобы переменная была
volatileили глобальной: иначе оптимизатор держит её в регистре, и Watch показывает «not in scope». - Уровень оптимизации (Options → C/C++ → Optimization). На
-O0каждая строка Си — свои инструкции, шаги в отладчике идут по порядку. На-O1и выше код компактнее, но строки перемешиваются. Для отладки удобнее-O0.
10 · сводка
Ловушки
| Симптом | Причина | Глава |
|---|---|---|
| задержка не работает, светодиод «горит» сразу | счётчик цикла без volatile — цикл выброшен | 04 |
| задержка в 9 раз короче расчёта | SystemInit включил 72 МГц | 07 |
после нажатия программа висит в Default_Handler | опечатка в имени обработчика | 05 |
| настроил «EXTICR2», а линия 5 не на порту B | писал в EXTICR[2], а EXTICR2 — это EXTICR[1] | 02 |
| иногда теряются прерывания другой кнопки | EXTI->PR |= … вместо = | 03 |
| приоритет не действует | NVIC->IPR[n] = 2 без сдвига на 4 | 06 |
| ошибка «IP is not a member» | пример под CMSIS 5, в CMSIS 6 — IPR | 06 |
| main «не видит» изменения из обработчика | общая переменная без volatile | 04 |
| «multiple definition of Reset_Handler» | в .s остались AREA RESET / Reset_Handler из ЛР2 | 08 |
| обработчик вызывается дважды | сброс PR в конце обработчика — то же, что в ЛР2 | 05 |
11 · сам
Упражнение: эксперимент 1 на Си
Лучший способ перейти на Си — переписать то, что уже работает. Возьми свой lab2.s и переведи настройку в main.c блок за блоком, по справочнику ниже. Обработчики — либо на Си, либо оставь ассемблерными, как хочет преподаватель.
Проверь себя по списку:
Если всё заработает — эксперимент 2 это те же обработчики плюс задержка и две строки приоритетов из главы 06. Присылай код, разберу так же, как ЛР2.
Справочник
Ассемблер ЛР2 → Си
| Что | Ассемблер (твой lab2.s) | Си |
|---|---|---|
| тактирование | LDR/ORR #0xC, #0x1/STR RCC_APB2ENR | RCC->APB2ENR |= 0xD; |
| SWJ_CFG | BIC #(7<<24) / ORR #(2<<24) | AFIO->MAPR = (AFIO->MAPR & ~(7u<<24)) | (2u<<24); |
| порт A для линии 3 | BIC #(0xF<<12) в 0x40010008 | AFIO->EXTICR[0] &= ~(0xFu << 12); |
| порт B для линии 5 | ORR #(1<<4) в 0x4001000C | AFIO->EXTICR[1] |= 1u << 4; |
| фронт и маска | ORR биты 3, 5, 10 в RTSR и IMR | EXTI->RTSR |= 0x428; EXTI->IMR |= 0x428; |
| NVIC | ORR #(1<<9), #(1<<23) в ISER0; #(1<<8) в ISER1 | NVIC->ISER[0] = (1u<<9) | (1u<<23); NVIC->ISER[1] = 1u << 8; |
| таблица векторов | 57 слов DCD, строки 25/39/56 | нет — имена функций EXTI3_IRQHandler, EXTI9_5_…, EXTI15_10_… |
| сброс флага | STR #(1<<3) в EXTI_PR | EXTI->PR = 1u << 3; |
| переключить светодиод | LDR / EOR #(1<<7) / STR GPIOA_ODR | GPIOA->ODR ^= 1u << 7; |
| выход из обработчика | BX LR | } — компилятор ставит BX LR |
| главный цикл | Main_Loop … B Main_Loop | while (1) { } |
1u — беззнаковая единица. Сдвиг обычной 1 на 31 бит — неопределённое поведение в Си; с 1u такого вопроса нет, поэтому для масок регистров удобно писать 1u << n всегда.