Перейти к содержимому

Устройство ядра Cortex-M4F: тактирование, память и запуск программы … ↩

Устройство ядра Cortex-M4F: тактирование, память и запуск программы mod. Четверг, 15 октября 2026 22:55 . См. 9

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.

  1. Установить указатель стека на конец ОЗУ.
  2. Вызвать функцию SystemInit.
  3. Скопировать начальные значения инициализированных переменных из флеш-памяти в ОЗУ.
  4. Обнулить неинициализированные переменные.
  5. Вызвать конструкторы стандартной библиотеки функцией __libc_init_array.
  6. Вызвать функцию main.
Глобальные переменные в SystemInit: В файле запуска производителя функция 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();
}
Plain text
↑ К оглавлению

4. Тактирование

Дерево тактовых сигналов

Частота ядра STM32F405 после сброса — 16 МГц от внутреннего генератора. Максимальная частота — 168 МГц. Для её получения сигнал кварцевого резонатора проходит через умножитель частоты, а затем распределяется по шинам.

ШинаДелительЧастотаТаймеры шиныПримеры периферии
AHB1168 МГц—порты ввода-вывода, контроллер прямого доступа к памяти
APB1442 МГц84 МГцTIM2–TIM5, USART2, USART3, SPI2, SPI3
APB2284 МГц168 МГцTIM1, TIM8–TIM11, USART1, SPI1, АЦП

Если делитель шины больше единицы, таймеры этой шины тактируются удвоенной частотой шины. Это правило понадобится в уроке S8 при расчёте частоты ШИМ.

Тактирование STM32F405 от кварцевого резонатора 8 МГц: делитель M, генератор, управляемый напряжением (ГУН), делители P и Q и частоты шин.
Тактирование STM32F405 от кварцевого резонатора 8 МГц: делитель M, генератор, управляемый напряжением (ГУН), делители P и Q и частоты шин.

Расчёт умножителя частоты

Умножитель частоты описывается четырьмя коэффициентами: 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;
}
Plain text
Такты ожидания флеш-памяти: Флеш-память не успевает выдавать данные на частоте 168 МГц. При напряжении питания 2,7–3,6 В на каждые 30 МГц частоты добавляется один такт ожидания, и на 168 МГц их требуется пять. Такты ожидания устанавливаются до повышения частоты, иначе программа остановится на первой же выборке инструкции.

Настройка тактирования

Функция 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;             /* счётчик тактов ядра */
}
Plain text
↑ К оглавлению

5. Память

ОбластьНачальный адресОбъёмОсобенности
флеш-память0x080000001 МБкод, константы, начальные значения переменных
SRAM1 и SRAM20x20000000128 КБпеременные, стек, буферы прямого доступа к памяти
CCM0x1000000064 КБдоступна только ядру, недоступна для прямого доступа к памяти
постоянная память загрузчика0x1FFF000030 КБзагрузчик производителя

Всего у STM32F405 192 КБ ОЗУ, но только 128 КБ доступны контроллеру прямого доступа к памяти. Буфер, в который приём по последовательному порту выполняется без участия процессора, нельзя размещать в CCM: передача не произойдёт, ошибки при этом не возникнет. Зато CCM хорошо подходит для данных фильтров и регуляторов, к которым ядро обращается на каждом проходе цикла.

Карта памяти STM32F405: где находятся код, константы, переменные и стек BeeFC. Масштаб не соблюдён.
Карта памяти STM32F405: где находятся код, константы, переменные и стек BeeFC. Масштаб не соблюдён.

Сценарий компоновщика

Файл 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                /* «горячие» данные: фильтры, ПИД */
}
Plain text

Сценарий определяет символы _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 (;;) { }
}
Plain text

Шестой элемент массива 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));
}
Plain text

Критерий автоматической проверки: тесты 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 использует собственный сценарий компоновщика 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. Проверьте себя

  1. Какие два слова читает ядро Cortex-M4 из начала флеш-памяти после сброса?
  2. Почему в функции SystemInit нельзя сохранять значения в глобальные переменные?
  3. Рассчитайте коэффициенты умножителя частоты для кварца 8 МГц и частоты ядра 168 МГц.
  4. Почему буфер приёма с прямым доступом к памяти нельзя размещать в CCM?
  5. С какой частотой тактируются таймеры шины APB1 при частоте шины 42 МГц и почему?
  6. Как найти строку кода, вызвавшую аварийную остановку?
↑ К оглавлению

10. Чек-лист самопроверки

Результат: Все пункты выполнены. Можно переходить к уроку S4.
↑ К оглавлению

11. Источники

  1. Справочное руководство RM0090, разделы 2 «Memory and bus architecture», 3 «Embedded Flash memory interface» и 7 «Reset and clock control».
  2. Техническое описание STM32F405RG.
  3. Руководство программиста PM0214 по ядру Cortex-M4, разделы об исключениях и регистре CPACR.
  4. Заголовочные файлы и файлы запуска CMSIS для STM32F4.
  5. CMSIS версии 5.
↑ К оглавлению

12. Что дальше

  • Следующий урок: S4. Язык C для встраиваемых систем и архитектура BeeFC.
  • Полный перечень уроков приведён в статье «Курс BeeFC: содержание уровня „Старт“».

Проверено: 13.10.2026

↑ К оглавлению
  • мод. : Четверг, 15 октября 2026