Устройство ядра Cortex-M4F: тактирование, память и запуск программы … ↩
mod. Суббота, 17 октября 2026 22:14
. См. 93
BeeFC · Урок S3 из 33 · Уровень «Старт»
Что происходит в микроконтроллере от подачи питания до функции main, как получить частоту ядра 168 МГц от кварцевого резонатора 8 МГц, как устроена память STM32F405 и как найти причину аварийной остановки программы.
В этом уроке BeeFC получает собственный сценарий компоновщика, настройку тактирования и обработчик аварий. Расчёт коэффициентов умножителя частоты проверяется модульным тестом на компьютере.
Содержание
1. Цели урока
Продолжительность урока — три часа: один час теории и два часа практики. После урока слушатель:
- описывает последовательность запуска программы от сброса до функции
main; - рассчитывает коэффициенты умножителя частоты для заданного кварцевого резонатора;
- объясняет назначение каждой области памяти STM32F405 и описывает её в сценарии компоновщика;
- находит адрес инструкции, вызвавшей аварийную остановку, по сохранённым регистрам.
2. Термины и сокращения
| Термин | Определение |
|---|---|
| внешний высокочастотный генератор (HSE) | генератор с кварцевым резонатором на плате, на полётных контроллерах обычно 8 МГц |
| внутренний генератор (HSI) | генератор внутри микроконтроллера частотой 16 МГц, работает сразу после сброса |
| умножитель частоты (PLL) | узел, который получает высокую частоту из частоты генератора |
| генератор, управляемый напряжением (ГУН) | часть умножителя частоты, которая формирует частоту, заданную коэффициентом N; допустимый диапазон 100–432 МГц |
| сопроцессор плавающей точки (FPU) | блок ядра, выполняющий операции с числами типа float |
| память, связанная с ядром (CCM) | область ОЗУ объёмом 64 КБ, доступная только ядру |
| оперативное запоминающее устройство (ОЗУ) | память для переменных и стека |
| таблица векторов | массив адресов обработчиков в начале флеш-памяти |
| сценарий компоновщика | файл, который сообщает компоновщику, где расположена каждая область памяти |
| блок трассировки (DWT) | отладочный блок ядра со счётчиком тактов CYCCNT |
3. Запуск программы
После сброса ядро Cortex-M4 читает два слова из начала флеш-памяти. Первое слово — начальное значение указателя стека, второе — адрес обработчика сброса Reset_Handler. Дальше выполняется код запуска из файла startup_stm32f405xx.s.
- Установить указатель стека на конец ОЗУ.
- Вызвать функцию
SystemInit. - Скопировать начальные значения инициализированных переменных из флеш-памяти в ОЗУ.
- Обнулить неинициализированные переменные.
- Вызвать конструкторы стандартной библиотеки функцией
__libc_init_array. - Вызвать функцию
main.
SystemInit вызывается до копирования начальных значений и до обнуления переменных. Если в ней записать значение в глобальную переменную, оно будет затёрто на третьем или четвёртом шаге. Поэтому в SystemInit BeeFC выполняется только одно действие — включение сопроцессора плавающей точки.Файл hal/stm32f4/system_beefc.c
void SystemInit(void)
{
/* Разрешить сопроцессор плавающей точки: полный доступ к CP10 и CP11.
* Без этого первая же инструкция с float вызовет аварию UsageFault. */
SCB->CPACR |= (3u << (10u * 2u)) | (3u << (11u * 2u));
__DSB();
__ISB();
}
↑ К оглавлению4. Тактирование
Дерево тактовых сигналов
Частота ядра STM32F405 после сброса — 16 МГц от внутреннего генератора. Максимальная частота — 168 МГц. Для её получения сигнал кварцевого резонатора проходит через умножитель частоты, а затем распределяется по шинам.
| Шина | Делитель | Частота | Таймеры шины | Примеры периферии |
|---|---|---|---|---|
| AHB | 1 | 168 МГц | — | порты ввода-вывода, контроллер прямого доступа к памяти |
| APB1 | 4 | 42 МГц | 84 МГц | TIM2–TIM5, USART2, USART3, SPI2, SPI3 |
| APB2 | 2 | 84 МГц | 168 МГц | TIM1, TIM8–TIM11, USART1, SPI1, АЦП |
Если делитель шины больше единицы, таймеры этой шины тактируются удвоенной частотой шины. Это правило понадобится в уроке S8 при расчёте частоты ШИМ.

Расчёт умножителя частоты
Умножитель частоты описывается четырьмя коэффициентами: M, N, P и Q.
\( f_{VCO} = \frac{f_{HSE}}{M} \cdot N, \quad f_{SYS} = \frac{f_{VCO}}{P}, \quad f_{USB} = \frac{f_{VCO}}{Q} \)
Справочное руководство накладывает ограничения: частота на входе генератора \( f_{HSE}/M \) лежит в диапазоне 1–2 МГц, частота генератора \( f_{VCO} \) — в диапазоне 100–432 МГц, коэффициент \( P \in \{2, 4, 6, 8\} \), а частота для USB должна быть ровно 48 МГц. Для кварца 8 МГц получается M = 4, N = 168, P = 2, Q = 7: генератор работает на частоте 336 МГц, ядро — на 168 МГц, USB — на 48 МГц.
Расчёт выполняется функцией ядра BeeFC, а не вручную: на учебных платах встречаются кварцы 8, 16 и 25 МГц.
Файл core/clock_calc.c
bool pll_calc(uint32_t hse_hz, uint32_t target_sysclk_hz, pll_cfg_t *out)
{
static const uint32_t p_values[] = { 2, 4, 6, 8 };
/* Перебор M от большего входа VCO к меньшему: вход 2 МГц даёт
* меньший джиттер, поэтому он проверяется первым. */
for (uint32_t m = 2; m <= 63; m++) {
uint32_t vco_in = hse_hz / m;
if (hse_hz % m != 0 || vco_in > 2000000u) continue;
if (vco_in < 1000000u) break;
for (uint32_t i = 0; i < 4; i++) {
uint32_t p = p_values[i];
uint32_t vco_out = target_sysclk_hz * p;
if (vco_out < 100000000u || vco_out > 432000000u) continue;
if (vco_out % vco_in != 0) continue;
uint32_t n = vco_out / vco_in;
if (n < 50 || n > 432) continue;
if (vco_out % 48000000u != 0) continue; /* USB: ровно 48 МГц */
uint32_t q = vco_out / 48000000u;
if (q < 2 || q > 15) continue;
out->m = m; out->n = n; out->p = p; out->q = q;
out->sysclk_hz = vco_out / p;
out->usb_hz = vco_out / q;
return true;
}
}
return false;
}
uint32_t flash_wait_states(uint32_t sysclk_hz)
{
/* Шаг 30 МГц для диапазона питания 2,7–3,6 В. */
return (sysclk_hz - 1u) / 30000000u;
}
Настройка тактирования
Функция clock_init выполняет шесть шагов в строгом порядке. Порядок важен: каждый шаг готовит условия для следующего.
Файл hal/stm32f4/clock_stm32.c
bool clock_init(uint32_t hse_hz)
{
pll_cfg_t pll;
if (!pll_calc(hse_hz, 168000000u, &pll)) return false;
RCC->CR |= RCC_CR_HSEON; /* 1. запустить кварц */
for (uint32_t t = 0; !(RCC->CR & RCC_CR_HSERDY); t++)
if (t == 1000000u) return false; /* кварц не запустился */
RCC->APB1ENR |= RCC_APB1ENR_PWREN; /* 2. стабилизатор: режим Scale 1 */
PWR->CR |= PWR_CR_VOS;
FLASH->ACR = FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_PRFTEN
| flash_wait_states(pll.sysclk_hz); /* 3. такты ожидания flash */
RCC->CFGR = RCC_CFGR_HPRE_DIV1 /* 4. AHB 168 МГц */
| RCC_CFGR_PPRE1_DIV4 /* APB1 42 МГц */
| RCC_CFGR_PPRE2_DIV2; /* APB2 84 МГц */
RCC->PLLCFGR = (pll.m << RCC_PLLCFGR_PLLM_Pos) /* 5. PLL */
| (pll.n << RCC_PLLCFGR_PLLN_Pos)
| (((pll.p / 2u) - 1u) << RCC_PLLCFGR_PLLP_Pos)
| (pll.q << RCC_PLLCFGR_PLLQ_Pos)
| RCC_PLLCFGR_PLLSRC_HSE;
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY)) { }
RCC->CFGR |= RCC_CFGR_SW_PLL; /* 6. переключить ядро на PLL */
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) { }
SystemCoreClock = pll.sysclk_hz;
return true;
}
void dwt_init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; /* включить блок трассировки */
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; /* счётчик тактов ядра */
}
↑ К оглавлению5. Память
| Область | Начальный адрес | Объём | Особенности |
|---|---|---|---|
| флеш-память | 0x08000000 |
1 МБ | код, константы, начальные значения переменных |
| SRAM1 и SRAM2 | 0x20000000 |
128 КБ | переменные, стек, буферы прямого доступа к памяти |
| CCM | 0x10000000 |
64 КБ | доступна только ядру, недоступна для прямого доступа к памяти |
| постоянная память загрузчика | 0x1FFF0000 |
30 КБ | загрузчик производителя |
Всего у STM32F405 192 КБ ОЗУ, но только 128 КБ доступны контроллеру прямого доступа к памяти. Буфер, в который приём по последовательному порту выполняется без участия процессора, нельзя размещать в CCM: передача не произойдёт, ошибки при этом не возникнет. Зато CCM хорошо подходит для данных фильтров и регуляторов, к которым ядро обращается на каждом проходе цикла.

Сценарий компоновщика
Файл ld/beefc_f405.ld
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K /* SRAM1 112 КБ и SRAM2 16 КБ */
CCM (rw) : ORIGIN = 0x10000000, LENGTH = 64K /* быстрая память, недоступна для DMA */
}
_estack = ORIGIN(RAM) + LENGTH(RAM); /* стек растёт вниз от конца RAM */
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH /* таблица векторов — в начале flash */
.text : { *(.text*) *(.rodata*) KEEP(*(.init)) KEEP(*(.fini)) } > FLASH
.ARM.exidx : { *(.ARM.exidx*) } > FLASH
_sidata = LOADADDR(.data); /* откуда копировать начальные значения */
.data : { _sdata = .; *(.data*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
.bss : { _sbss = .; __bss_start__ = .; *(.bss*) *(COMMON) . = ALIGN(4);
_ebss = .; __bss_end__ = .; } > RAM
PROVIDE(end = _ebss); /* начало кучи, если она понадобится */
.ccm (NOLOAD) : { *(.ccm*) } > CCM /* «горячие» данные: фильтры, ПИД */
}
Сценарий определяет символы _sidata, _sdata, _edata, _sbss и _ebss, которыми пользуется код запуска на третьем и четвёртом шагах. Переменная размещается в CCM атрибутом __attribute__((section(".ccm"))). Секция объявлена как NOLOAD, поэтому такие переменные не обнуляются кодом запуска и должны инициализироваться программой.
6. Счётчик тактов и аварийная остановка
Счётчик тактов DWT
Блок трассировки ядра содержит 32-битный счётчик CYCCNT, который увеличивается на единицу каждый такт ядра. На частоте 168 МГц его разрешение около 6 нс, а переполнение наступает примерно через 25,6 секунды. Счётчиком измеряется время выполнения фрагментов кода: разность двух показаний, вычисленная в беззнаковой арифметике, верна и при переполнении.
Обработчик аварийной остановки
При обращении по недопустимому адресу, делении на ноль с включённой проверкой или ошибке выравнивания ядро вызывает обработчик HardFault_Handler. Перед вызовом ядро сохраняет в стеке восемь регистров, среди них адрес инструкции, на которой произошла авария.
Файл hal/stm32f4/clock_stm32.c
volatile uint32_t g_fault_regs[8]; /* R0–R3, R12, LR, PC, xPSR в момент аварии */
void HardFault_Handler(void)
{
uint32_t *sp;
/* Бит 2 в LR показывает, какой стек использовался: MSP или PSP. */
__asm volatile ("tst lr, #4\n ite eq\n mrseq %0, msp\n mrsne %0, psp"
: "=r"(sp));
for (int i = 0; i != 8; i++) g_fault_regs[i] = sp[i];
__BKPT(0); /* остановка в отладчике: смотрим g_fault_regs[6] (PC) */
for (;;) { }
}
Шестой элемент массива g_fault_regs — регистр PC. Команда info line *0x08000abc в отладчике GDB показывает строку исходного кода по этому адресу.
7. Практика
Задание S3.1. Расчёт умножителя частоты
Продолжительность — 30 минут, выполняется на компьютере.
Реализовать функции pll_calc и flash_wait_states в файле core/clock_calc.c. Функция должна подбирать коэффициенты для кварцев 8, 16 и 25 МГц и возвращать false, если получить ровно 48 МГц для USB невозможно.
Файл tests/unit/test_level1.c
void test_pll_8mhz_crystal_gives_168mhz_and_usb_48mhz(void)
{
pll_cfg_t c;
TEST_ASSERT_TRUE(pll_calc(8000000u, 168000000u, &c));
TEST_ASSERT_EQUAL_UINT32(4, c.m); /* вход VCO 2 МГц */
TEST_ASSERT_EQUAL_UINT32(168, c.n);
TEST_ASSERT_EQUAL_UINT32(2, c.p);
TEST_ASSERT_EQUAL_UINT32(7, c.q);
TEST_ASSERT_EQUAL_UINT32(168000000u, c.sysclk_hz);
TEST_ASSERT_EQUAL_UINT32(48000000u, c.usb_hz);
TEST_ASSERT_EQUAL_UINT32(5, flash_wait_states(c.sysclk_hz));
}
Критерий автоматической проверки: тесты test_pll_* пройдены.
Задание S3.2. Тактирование 168 МГц на плате
Продолжительность — 50 минут.
Подключить clock_init и dwt_init в функции main. Измерить счётчиком CYCCNT число тактов одного прохода пустого цикла задержки из урока S2 до и после настройки тактирования. Записать результаты в отчёт docs/s3_report.md.
Критерий автоматической проверки: в модели Renode переменная SystemCoreClock после вызова clock_init равна 168 000 000.
Задание S3.3. Намеренная авария
Продолжительность — 40 минут.
Добавить в программу чтение по адресу 0x30000000, где нет памяти. Убедиться, что программа останавливается в HardFault_Handler, найти по значению регистра PC строку с ошибкой и приложить снимок экрана отладчика к отчёту.
8. Как это сделано в больших прошивках
| Прошивка | Версия | Файл в исходном коде |
|---|---|---|
| Betaflight | 2026.6.2 |
src/platform/STM32/startup/startup_stm32f40xx.s, src/platform/STM32/link/stm32_flash_f405.ld, src/platform/STM32/system_stm32f4xx.c |
| INAV | 9.1.0 |
src/main/startup/startup_stm32f40xx.s, src/main/drivers/system_stm32f4xx.c |
| ArduPilot | Copter-4.7.2 |
libraries/AP_HAL_ChibiOS/hwdef/scripts/chibios_hwdef.py |
| PX4 | v1.17.0 |
ROMFS/px4fmu_common/init.d/rcS |
- Betaflight использует собственный сценарий компоновщика
stm32_flash_f405.ld. В нём область CCM получает два псевдонима:STACKRAMиFASTRAM: в CCM размещаются стек и данные, помеченные макросамиFAST_DATAиFAST_DATA_ZERO_INIT. В файлеbuild_config.hпрямо указано, что на F4 прямой доступ к памяти в CCM невозможен. - Файл
system_stm32f4xx.cв Betaflight и INAV основан на файле производителя и содержит ту же последовательность: такты ожидания, умножитель, переключение на него. - ArduPilot не хранит настройку тактирования в коде вручную: сценарий
chibios_hwdef.pyгенерирует её из строкиOSCILLATOR_HZ 8000000файлаhwdef.dat. - В PX4 после запуска операционной системы NuttX выполняется сценарий
rcS, который запускает модули прошивки по очереди. Это пример того, что в крупной прошивке функцияmain— лишь начало длинной цепочки запуска.
9. Проверьте себя
- Какие два слова читает ядро Cortex-M4 из начала флеш-памяти после сброса?
- Почему в функции
SystemInitнельзя сохранять значения в глобальные переменные? - Рассчитайте коэффициенты умножителя частоты для кварца 8 МГц и частоты ядра 168 МГц.
- Почему буфер приёма с прямым доступом к памяти нельзя размещать в CCM?
- С какой частотой тактируются таймеры шины APB1 при частоте шины 42 МГц и почему?
- Как найти строку кода, вызвавшую аварийную остановку?
10. Чек-лист самопроверки
11. Источники
- Справочное руководство RM0090, разделы 2 «Memory and bus architecture», 3 «Embedded Flash memory interface» и 7 «Reset and clock control».
- Техническое описание STM32F405RG.
- Руководство программиста PM0214 по ядру Cortex-M4, разделы об исключениях и регистре CPACR.
- Заголовочные файлы и файлы запуска CMSIS для STM32F4.
- CMSIS версии 5.
12. Что дальше
- Следующий урок: S4. Язык C для встраиваемых систем и архитектура BeeFC.
- Полный перечень уроков приведён в статье «Курс BeeFC: содержание уровня „Старт“».
Проверено: 13.10.2026
↑ К оглавлению- мод. : Суббота, 17 октября 2026