2ж. BlackPill на STM32F411: Cortex-M4 с FPU по цене BluePill — плата, загрузка по USB и три примера
Опорных плат в справочнике две (0.2): дешёвая BluePill на F103 для регистрового уровня и Discovery на F407 для плавающей точки, DSP и быстрого DMA. Между ними есть третья, которая встречается на столе не реже первой, — BlackPill от WeAct на STM32F411CE или STM32F401CC. Размером и ценой она как BluePill, а внутри Cortex-M4 с FPU, как у F407. Её любят за то, что прошивается по USB без программатора: у F4 в системной памяти есть заводской загрузчик с USB DFU, а у F103 его нет.
Здесь — что стоит на плате, чем её кристаллы отличаются от F103 и F407, как настроить такт от кварца 25 МГц, как прошить по USB и по SWD, первый проект регистрами CMSIS и три законченных примера: мигалка с кнопкой, консоль по UART и уход в загрузчик по команде. Все примеры собраны из одних исходников под три платы — BlackPill F411, BlackPill F401 и Discovery F407; что при этом меняется, показано в 2ж.8.
Опоры: раздел 2 (портфель, линейка F4, замена камня по выводам), 2а
(клоны), 0.2 (опорные платы), 4в (сборка без среды), 6.6 (такт F407 на
168 МГц), 48д (заводской загрузчик). Документы: даташиты STM32F411xC/xE
(DocID026289 Rev 7) и STM32F401xB/xC (DS9716 Rev 11), справочные
руководства RM0383 Rev 3 (F411), RM0368 Rev 5 (F401) и RM0090 Rev 19
(F407), AN2606 Rev 21 (заводской загрузчик), база кристаллов CubeMX
DB.6.0.181, заголовки CMSIS из пакета STM32F4xx_DFP 2.17.1. Сведения о
самой плате — описание платформы BlackPill и настройки двух плат
BLACKPILL_STM32_F401/F411 в QMK (github.com/qmk/qmk_firmware,
коммит b1aea2556040, лицензия GPL-2.0; код оттуда не брался, только
числа и назначение выводов). Схема платы производителя
(github.com/WeActStudio/WeActStudio.MiniSTM32F4x1) с текстом не сверялась.
Сборка — arm-none-eabi-gcc 12.2.1, опыты — в QEMU 7.2.22 (машина
netduinoplus2, это STM32F405 с тем же ядром M4F, USART1, SysTick и
SYSCFG), логика — на компьютере, gcc 12.2.0. Живой платы в опытах не
было.
Как это называется в документации
Оригинал Перевод system memory, embedded bootloader системная память, заводской загрузчик ST DFU, Device Firmware Upgrade обновление прошивки по USB, класс устройства USB boot mode selection pins BOOT0, BOOT1 выводы выбора области запуска HSE, LSE, HSI кварц или внешний генератор, часовой кварц 32,768 кГц, внутренний RC 16 МГц main PLL, PLLM/N/P/Q, VCO главная ФАПЧ, делители и множитель, генератор внутри PLL regulator voltage scaling, VOS, Scale 1/2/3 ступени напряжения ядра под частоту wait states, LATENCY задержки чтения Flash в тактах coprocessor access control, CPACR разрешение доступа к FPU NOCP, UsageFault ошибка «нет сопроцессора»: команда FPU при запрещённом FPU FT, TC I/O вывод, терпящий 5 В, и обычный вывод 3,3 В VCAP_1 вывод под конденсатор стабилизатора ядра
2ж.1 Что на плате
Плата одна, кристаллов два: STM32F411CEU6 (дороже и быстрее) и
STM32F401CCU6. Корпус у обоих один — UFQFPN48, выводы по номерам совпадают;
у F401 выводы 8 и 9 в базе CubeMX называются VSSA/VREF- и VREF+, у F411 —
VSSA и VDDA. Что стоит вокруг кристалла:
| Что | Куда | Откуда сведения |
|---|---|---|
| кварц 25 МГц | PH0, PH1 (HSE) |
STM32_HSECLK 25000000U в настройках платы QMK |
| часовой кварц 32,768 кГц | PC14, PC15 (LSE) |
STM32_LSECLK 32768U там же |
| светодиод | PC13, анодом к 3,3 В |
описание платформы в QMK: «connected to +3.3V» |
| кнопка KEY | PA0 |
там же: «shared with the User Key» |
| кнопки BOOT0 и NRST | вывод 44 и вывод 7 | назначение выводов — база CubeMX; кнопки — по описанию платы |
| место под флешку SOIC-8 | PA4 — выбор, PA5…PA7 — SCK, MISO, MOSI |
описание платформы в QMK |
| разъём USB-C | PA11, PA12 |
назначение выводов OTG_FS — DocID026289 |
| разъём SWD | PA13 SWDIO, PA14 SWCLK |
там же |
Схема платы с этим не сверялась: стабилизатор, резисторы и то, к чему именно
подключены кнопки, здесь известны по описаниям. Два вывода по описанию в QMK
заняты: PB2 — это BOOT1 (вывод 20 по DocID026289), его не трогают; PA9
у F411 — ещё и OTG_FS_VBUS.
Светодиод на PC13 горит при нуле. Так и должно быть: по даташиту
(DocID026289, примечание к таблице выводов) PC13…PC15 питаются через ключ
резервного домена, он пропускает только 3 мА, частота — не выше 2 МГц при
30 пФ, и источником тока для светодиода эти выводы быть не должны.
Значит, вывод только принимает ток: светодиод от 3,3 В к выводу. То же было у
BluePill (2.6), и регистровый код светодиода переносится с неё без правок —
кроме адреса порта.
5 В терпят не все. У F411 два вывода из 36 — PA0 и PB5 — помечены
в даташите TC, «Standard 3.3 V I/O»; остальные FT. У F401 по DS9716 и
PA0, и PB5 — FT. Кнопка KEY висит как раз на PA0: если её вывод
занять под датчик на 5 В, на F411 это ошибка, а на F401 — нет.
2ж.2 F411 и F401 против F103 и F407
Сводка по базе кристаллов CubeMX: что есть в этом корпусе — экземпляры
периферии, частота, память, число линий ввода-вывода (chips.py раздела,
вывод сохранён целиком):
| F103C8 (BluePill) | F401CC (BlackPill) | F411CE (BlackPill) | F407VG (Discovery) | |
|---|---|---|---|---|
| корпус | LQFP48 | UFQFPN48 | UFQFPN48 | LQFP100 |
| ядро | M3 | M4F | M4F | M4F |
| частота, МГц | 72 | 84 | 100 | 168 |
| Flash, КБ | 64 (CB — 128) | 256 (CB — 128) | 512 (CC — 256) | 1024 (VE — 512) |
| ОЗУ, КБ | 20 | 64 | 128 | 128 + 64 CCM |
| линий ввода-вывода | 37 | 36 | 36 | 82 |
| USART/UART | 3 | 3 | 3 | 6 |
| SPI / I²S | 2 / 0 | 3 / 2 | 5 / 5 | 3 / 2 |
| I²C | 2 | 3 | 3 | 3 |
| таймеры | 4 | 8 | 8 | 14 |
| АЦП | 2 | 1 | 1 | 3 |
| ЦАП | — | — | — | 1 |
| CAN | 1 | — | — | 2 |
| Ethernet | — | — | — | 1 |
| USB | FS, только устройство | OTG FS | OTG FS | OTG FS + OTG HS |
| SDIO | — | — | 1 | 1 |
| FSMC, RNG, камера | — | — | — | есть |
Строки «Flash» и «ОЗУ» сходятся с таблицами 2 обоих даташитов: у F411xE — 512 КБ и 128 КБ, у F401xC — 256 КБ и 64 КБ. Главное из таблицы:
- Нет CAN, ЦАП и Ethernet. Разделы 15, 19 и 105–106а на BlackPill не повторить; CAN — только внешним контроллером по SPI.
- USART3 нет, зато есть USART6. Код с BluePill, где консоль на
USART3(PB10/PB11), переносится не сразу:PB11в этом корпусе вообще нет. - SPI у F411 пять, у F401 в этом корпусе три. При этом заголовок
stm32f401xc.hобъявляет иSPI4, иSDIO: они есть у F401 в корпусах от 64 выводов (DS9716, таблица 2), а в UFQFPN48 — нет. Заголовок говорит о семействе, а не о корпусе. - Таймеров 8: TIM1–TIM5 и TIM9–TIM11. Базовых TIM6/TIM7, которыми в справочнике часто запускают АЦП, нет.
Что объявлено в самих заголовках CMSIS (тот же скрипт ищет #define с
именем блока):
stm32f401xc.h: SPI4 USB_OTG_FS SDIO CRC I2S2ext
нет: CAN1 DAC1 ETH RNG DCMI FSMC_Bank1 TIM6 TIM8 USART3 UART4 SPI5 USB_OTG_HS
stm32f411xe.h: SPI4 SPI5 USB_OTG_FS SDIO CRC I2S2ext
нет: CAN1 DAC1 ETH RNG DCMI FSMC_Bank1 TIM6 TIM8 USART3 UART4 USB_OTG_HS
stm32f407xx.h: CAN1 DAC1 ETH RNG DCMI FSMC_Bank1 TIM6 TIM8 USART3 UART4 USB_OTG_HS USB_OTG_FS SDIO CRC I2S2ext
нет: SPI4 SPI5
Так ошибка переноса видна ещё при сборке: строка CAN1->MCR = … под
stm32f411xe.h не соберётся.
Выводы по номерам. Корпуса у BluePill и BlackPill разные (LQFP и UFQFPN, 2.9), но нумерация выводов 48-выводного корпуса у ST общая. Сравнение по базе CubeMX — где на том же номере стоит другое:
F411CE: 5:PD0>PH0 6:PD1>PH1 22:PB11>VCAP1
F401CC: 5:PD0>PH0 6:PD1>PH1 8:VSSA>VSSA/VREF- 9:VDDA>VREF+ 22:PB11>VCAP1
Выводы кварца у F4 называются PH0/PH1 вместо PD0/PD1, а на месте
PB11 — VCAP1, вывод стабилизатора ядра: на нём висит конденсатор, и
ничего другого туда подключать нельзя. Это та же история, что в 2.1 с
F103RB и F401RE в LQFP64.
2ж.3 Такт: кварц 25 МГц, 100 или 96 МГц и напряжение ядра
У Discovery кварц 8 МГц, у BlackPill — 25 МГц. Первое следствие —
константа: по умолчанию system_stm32f4xx.c считает HSE_VALUE = 25 МГц
(2.8), и на BlackPill это значение совпадает с платой. Ошибка здесь
обратная: код, перенесённый с Discovery вместе с -DHSE_VALUE=8000000,
на BlackPill ошибётся в 3,125 раза.
Второе следствие — делители PLL. Частота ядра — HSE / M × N / P, выход
для USB — HSE / M × N / Q, и пределы у кристаллов свои: вход VCO 1–2 МГц,
выход VCO 100–432 МГц (у F401 — 192–432), выход Q не выше 48 МГц, а для USB
нужно ровно 48 (RM0383 и RM0368, 6.3.2; RM0090, 7.3.2). Перебор всех
сочетаний M, N, P, Q на компьютере (pll.c):
F411 hse=25000000: configs=108510 usb48_configs=28
max_sysclk=100.000 MHz (m=13 n=104 p=2)
max_sysclk_usb48=96.000 MHz (m=25 n=192 p=2 q=4, vco_in=1.000 MHz)
F401 hse=25000000: configs=38683 usb48_configs=19
max_sysclk=84.000 MHz (m=25 n=336 p=4)
max_sysclk_usb48=84.000 MHz (m=25 n=336 p=4 q=7, vco_in=1.000 MHz)
F407 hse=8000000: configs=47128 usb48_configs=130
max_sysclk=168.000 MHz (m=4 n=168 p=2)
max_sysclk_usb48=168.000 MHz (m=4 n=168 p=2 q=7, vco_in=2.000 MHz)
F411_hse8 hse=8000000: configs=41218 usb48_configs=110
max_sysclk=100.000 MHz (m=4 n=100 p=2)
max_sysclk_usb48=96.000 MHz (m=4 n=96 p=2 q=4, vco_in=2.000 MHz)
F411 не бывает одновременно на 100 МГц и с USB. Это не свойство
кварца: последняя строка — тот же F411 с кварцем 8 МГц, и потолок с USB у
него тоже 96 МГц. Причина в арифметике: при 100 МГц на ядре VCO = 100 × P,
то есть 200 или 400 МГц (800 уже за пределом), а для USB VCO должен
делиться на 48 нацело — ни 200, ни 400 не делятся. Поэтому F411 с USB
работает на 96 МГц, а F401 на своих 84 МГц получает USB без потерь. Так же
делает и QMK: в настройках платы BLACKPILL_STM32_F411 стоят PLLM 25,
PLLN 384, PLLP 4, PLLQ 8 — это 96 МГц и 48 МГц, у F401 — 25, 336, 4,
7, то есть 84 и 48.
У кварца 25 МГц есть и цена: точные 48 МГц получаются только при M = 25 (25 · N = 48 · M · Q, а 25 и 48 взаимно просты), то есть вход VCO — 1 МГц, нижний край диапазона. RM0383 советует 2 МГц «to limit PLL jitter», но с таким кварцем и USB это недостижимо. Без USB F411 берёт 100 МГц и при 1,923 МГц на входе (M = 13).
Всё, что зависит от платы, собрано в board.h. Для F411:
#if defined(BOARD_BLACKPILL_F411)
#define BOARD_NAME "BlackPill F411CE"
#define CHIP_HEADER "stm32f411xe.h"
#define HSE_HZ 25000000u /* кварц 25 МГц на плате */
#if defined(CLOCK_USB) /* для USB нужен ровно 48 МГц с выхода Q */
#define PLL_M 25u /* 25 / 25 = 1 МГц на входе VCO */
#define PLL_N 384u /* VCO 384 МГц */
#define PLL_P 4u /* SYSCLK 96 МГц */
#define PLL_Q 8u /* 48 МГц — USB годится */
#else
#define PLL_M 25u
#define PLL_N 400u /* VCO 400 МГц */
#define PLL_P 4u /* SYSCLK 100 МГц — предел F411 */
#define PLL_Q 9u /* 44,4 МГц — не выше 48, но для USB не годится */
#endif
#define FLASH_WS 3u /* RM0383, таблица 5: 90 < HCLK <= 100 МГц при 2,7–3,6 В */
#define VOS_BITS 3u /* PWR_CR.VOS = 11, Scale 1, до 100 МГц; после сброса 10 — до 84 */
#define APB1_DIV 2u /* APB1 не выше 50 МГц */
#define APB2_DIV 1u /* APB2 не выше 100 МГц */
Напряжение ядра. У F411 поле VOS в PWR_CR двухбитное, и после сброса
там 10 — Scale 2, до 84 МГц (RM0383, 5.4.1). Для 100 МГц нужно 11 — Scale 1, и
записать его можно только при выключенной PLL. У F401 Scale 2 и есть потолок
84 МГц, а значение 11 зарезервировано. У F407 VOS — один бит 14, и после
сброса он уже 1, Scale 1 (RM0090, 5.4.1). Вот почему код с F407, который
VOS не трогает, на F407 работает, а F411 на 100 МГц оставляет на ступени
до 84 МГц — вне паспорта.
Задержки Flash при 2,7–3,6 В: у F411 3 такта для 90–100 МГц (RM0383,
таблица 5), у F401 2 такта для 60–84 МГц (RM0368, таблица 6), у F407 5 для
150–168 МГц (RM0090, таблица 10). Проверка test_board.c собирается под
каждую плату и сверяет числа board.h с этими таблицами и пределами шин:
board=BlackPill F411CE chip=F411 hse=25000000 m=25 n=400 p=4 q=9
vco_in=1000000 vco=400000000 sysclk=100000000 pclk1=50000000 pclk2=100000000 q_out=44444444 ws_need=3 ws=3
Вся настройка — одна функция. Порядок по RM0383 (3.4.1 и раздел 6): кварц, напряжение ядра, PLL, задержки Flash с проверкой, делители шин, переключение. Каждое ожидание ограничено: если кварц не запустился, функция возвращает код, а ядро остаётся на HSI 16 МГц — консоль сообщит об этом сама.
int clock_init(void)
{
RCC->CR |= RCC_CR_HSEON;
if (wait_set(&RCC->CR, RCC_CR_HSERDY)) {
RCC->CR &= ~RCC_CR_HSEON;
return CLOCK_NO_HSE; /* кварца нет или он не завёлся */
}
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
(void)RCC->APB1ENR; /* дождаться включения такта PWR */
PWR->CR = (PWR->CR & ~(3u << 14)) | VOS_BITS << 14; /* VOS меняется только при выключенной PLL */
RCC->PLLCFGR = (RCC->PLLCFGR & ~CLK_PLLCFGR_FIELDS) /* бит 29 после сброса — 1, он резервный */
| clk_pllcfgr(PLL_M, PLL_N, PLL_P, PLL_Q, CLK_SRC_HSE);
RCC->CR |= RCC_CR_PLLON;
if (wait_set(&RCC->CR, RCC_CR_PLLRDY))
return CLOCK_NO_PLL;
FLASH->ACR = FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_PRFTEN | FLASH_WS;
if ((FLASH->ACR & FLASH_ACR_LATENCY) != FLASH_WS) /* RM0383, 3.4.1: прочитать и убедиться */
return CLOCK_NO_WS;
RCC->CFGR = (RCC->CFGR & ~(RCC_CFGR_HPRE | RCC_CFGR_PPRE1 | RCC_CFGR_PPRE2))
| clk_cfgr_div(1u, APB1_DIV, APB2_DIV); /* делители — до переключения */
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL;
for (uint32_t n = 0; (RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL; n++)
if (n >= WAIT_LOOPS)
return CLOCK_NO_SW;
SystemCoreClock = SYSCLK_HZ;
return CLOCK_OK;
}
Резервный бит в RCC_PLLCFGR. После сброса там 0x24003010 (RM0383,
6.3.2): M = 16, N = 192, P = 2, Q = 4 — и ещё единица в бите 29, который
описан как резервный. Запись «всего регистра» его сбросила бы, а резервные
биты положено оставлять как есть. Поэтому поля собирает clk_pllcfgr, а
остальное сохраняется маской:
clk: reset=0x24003010 fields(16,192,2,4,HSI)=0x04003010 reserved=0x20000000 with_reserved=0x24003010 src=0 sysclk=16000000
Тот же файл clk.c разбирает регистры обратно в частоты. Его зовёт
команда clk консоли (2ж.6), а проверка на компьютере собирает значения
регистров по board.h и сверяет разбор с ожидаемыми частотами:
RCC_PLLCFGR=0x09416419 RCC_CFGR=0x00001008 -> sysclk=100000000 hclk=100000000 pclk1=50000000 pclk2=100000000 pll48=44444444
| Плата | SYSCLK | APB1 / APB2 | USB | RCC_PLLCFGR (поля) |
задержки | VOS |
|---|---|---|---|---|---|---|
| BlackPill F411 | 100 МГц | 50 / 100 МГц | 44,4 МГц — нельзя | 0x09416419 |
3 | 11 |
BlackPill F411, -DCLOCK_USB |
96 МГц | 48 / 96 МГц | 48 МГц | 0x08416019 |
3 | 11 |
| BlackPill F401 | 84 МГц | 42 / 84 МГц | 48 МГц | 0x07415419 |
2 | 10 |
| Discovery F407 | 168 МГц | 42 / 84 МГц | 48 МГц | 0x07405408 |
5 | бит 14 = 1 |
На кристалле в регистре будет на 0x20000000 больше: бит 29 остаётся
единицей. Все четыре сборки прошли проверку целиком (result_…=PASS).
2ж.4 Как прошить: USB DFU без программатора и SWD
По USB, заводским загрузчиком. У F401 и F411 в системной памяти с
адреса 0x1FFF0000 записан загрузчик ST; у F411 он занимает 30424 байта
(AN2606, таблица 56). Вход — комбинацией «pattern1»: BOOT0 = 1 и
BOOT1 = 0 во время сброса (AN2606, 26.1; RM0383, таблица 2). BOOT1 —
это PB2, и на плате он уже в нуле, поэтому на BlackPill достаточно двух
кнопок:
- держать BOOT0;
- нажать и отпустить NRST;
- отпустить BOOT0.
Загрузчик сам выбирает интерфейс — тот, где первым увидит признак жизни (AN2606, рис. 33). Из списка для F411 (таблица 56) в корпусе UFQFPN48 есть не все выводы:
bootloader_F411CE: USART1=есть USART2=нет I2C1=есть I2C2=есть I2C3=есть SPI1=есть SPI2=есть SPI3=нет DFU=есть
USART2 загрузчика — на PD5/PD6, у SPI3 — PC10…PC12; в этом корпусе
таких выводов нет. Для USB загрузчик требует внешний кварц с частотой,
кратной 1 МГц, от 4 до 26 МГц; он меряет его таймером TIM11 и переводит
ядро на 60 МГц от PLL. 25 МГц в этот ряд попадают. Внешний резистор
подтяжки на D+ не нужен. Компьютер увидит устройство 0483:DF11, и прошивка
пишется так (строка из настроек загрузчика stm32-dfu в QMK):
dfu-util -d 0483:DF11 -a 0 -s 0x08000000:leave -D console_f411.bin
Ключ :leave после записи запускает прошивку. Ещё три подробности из
таблицы 56 AN2606, которые объясняют поведение загрузчика:
- ОЗУ. Первые 12 КБ с
0x20000000занимает сам загрузчик; командами записи доступно ОЗУ с0x20003000(таблица 68). - Напряжение. Загрузчик ставит диапазон 1,62–2,1 В, а в нём Flash пишется только по байту. Диапазон меняется командами загрузчика.
- Кварц под надзором. Для DFU включается CSS: пропал кварц — сброс.
Опознать камень через загрузчик можно по номеру устройства и версии
(таблица 68): F401xB/C — PID 0x423, номер загрузчика 0xD1; F411 —
0x431 и 0xD0; F407 — 0x413. Это те же числа, что читает функция
опознания из 2а; там же — что делать, если номер не тот.
Сколько ждать после сброса, прежде чем говорить с загрузчиком: для F401 и F411 AN2606 (таблица 69) даёт не меньше 74,5 мс на запуск и 85 мс на поиск кварца.
По UART тем же загрузчиком. USART1 на PA9/PA10 — 8 бит, чётность,
1 стоп-бит, скорость определяется по 0x7F. Это то же, что у F407 в 48д.4–48д.5,
и своя утилита оттуда подойдёт без правок. Консоль примеров раздела стоит на
тех же выводах: один переходник USB–UART и для консоли, и для прошивки.
По SWD. Разъём SWD на плате — SWDIO, SWCLK, земля и 3,3 В; подойдёт
любой ST-LINK (2.7). Команды — как в 35б, файл камня тот же, что у F407:
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program console_f411.elf verify reset exit"
Ни dfu-util, ни openocd в разделе с платой не запускались: живой платы
не было. Команды взяты из QMK и раздела 35б.
2ж.5 Первый проект: стартовый код и мигалка с кнопкой
Проект — без HAL и без библиотек, как в 4в: стартовый код на C, скрипт компоновщика, заголовки CMSIS. Отличий от F103 три: таблица векторов та же, но FPU надо включить до первой плавающей команды; скрипт компоновщика — под объём своего кристалла; и в конце ОЗУ оставлено место под признак «в загрузчик» (2ж.7).
void Reset_Handler(void)
{
boot_early(); /* до .data и .bss: признак лежит вне них */
for (uint32_t *s = &_sidata, *d = &_sdata; d < &_edata;)
*d++ = *s++;
for (uint32_t *d = &_sbss; d < &_ebss;)
*d++ = 0u;
#ifndef NO_FPU_ENABLE
*(volatile uint32_t *)0xE000ED88u |= 0xFu << 20; /* SCB->CPACR: CP10 и CP11 — полный доступ */
__asm volatile("dsb\n isb" ::: "memory"); /* до первой команды FPU */
#endif
main();
for (;;) {
}
}
Скрипт компоновщика — общая часть и по файлу на кристалл. Признаку отданы последние 16 байт ОЗУ, стек растёт вниз от него:
/* sections.ld — раздел 2ж: общая часть скрипта компоновщика для F401CC, F411CE и F407VG.
* Память задаёт файл кристалла (f411ce.ld и др.), он же подключает этот файл.
* Последние 16 байт ОЗУ — не стек и не .bss: там признак «в загрузчик», он переживает сброс. */
ENTRY(Reset_Handler)
_boot_flag = ORIGIN(RAM) + LENGTH(RAM) - 16;
_estack = _boot_flag; /* стек растёт вниз от признака */
/* f411ce.ld — раздел 2ж: STM32F411CE — 512 КБ Flash, 128 КБ ОЗУ (DocID026289, таблица 2). */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
INCLUDE sections.ld
В собранных образах признак оказался там, где ждали: у F411 — 0x2001FFF0,
у F401 с его 64 КБ — 0x2000FFF0.
Ключи сборки — под Cortex-M4 с FPU: -mcpu=cortex-m4 -mthumb
-mfpu=fpv4-sp-d16 -mfloat-abi=hard -Os, плюс -fno-math-errno, чтобы
корень из sqrtf стал одной командой, и -fno-tree-loop-distribute-patterns:
без него GCC заменил циклы копирования .data и обнуления .bss вызовами
memcpy и memset, а библиотеки в сборке нет — компоновщик ответил
undefined reference to 'memcpy'.
Выводы — регистрами MODER, PUPDR, BSRR, IDR; порт GPIO у F401, F411 и
F407 устроен одинаково (RM0383 и RM0090, 8.4), меняются только номер порта
и вывода:
static inline void pin_mode(GPIO_TypeDef *g, uint32_t pin, uint32_t mode, uint32_t pupd)
{
port_clock(g);
g->MODER = (g->MODER & ~(3u << 2 * pin)) | mode << 2 * pin; /* 00 вход, 01 выход, 10 альт. функция */
g->PUPDR = (g->PUPDR & ~(3u << 2 * pin)) | pupd << 2 * pin; /* 00 нет, 01 вверх, 10 вниз */
}
Пример 1 — мигалка с кнопкой. Короткое нажатие KEY переключает скорость (500, 250, 100 мс), долгое — пауза. Антидребезг — по времени: уровню верят, когда он 20 мс не менялся; долгое нажатие — 800 мс.
enum key_event key_step(struct key *k, int pressed, uint32_t now_ms)
{
uint8_t r = pressed ? 1u : 0u;
if (r != k->raw) { /* дребезг или настоящее изменение — ждём */
k->raw = r;
k->t_change = now_ms;
return KEY_NONE;
}
if (r != k->stable && now_ms - k->t_change >= KEY_STABLE_MS) {
k->stable = r;
if (r) { /* нажатие подтвердилось */
k->t_down = now_ms;
k->long_sent = 0u;
return KEY_NONE;
}
return k->long_sent ? KEY_NONE : KEY_SHORT; /* отпустили: короткое, если долгого не было */
}
if (k->stable && !k->long_sent && now_ms - k->t_down >= KEY_LONG_MS) {
k->long_sent = 1u;
return KEY_LONG;
}
return KEY_NONE;
}
Главный цикл мигалки знает о плате только имена из board.h:
switch (key_step(&k, pin_read(KEY_PORT, KEY_PIN) == KEY_DOWN, now)) {
case KEY_SHORT: mode = (mode + 1u) % 3u; next = now; break;
case KEY_LONG: paused = !paused; next = now; break;
default: break;
}
У BlackPill KEY_DOWN = 0 (кнопка на землю, подтяжка внутри), у Discovery
— 1 (кнопка B1 к питанию, подтяжка вниз на плате, как в 198л). На
компьютере кнопку прогнали через 200 нажатий с дребезгом до 8 мс и
удержанием 30–700 или 900–1500 мс, а затем через 1000 одиночных помех
по 1–15 мс:
key: presses=200 want_short=110 got_short=110 want_long=90 got_long=90 worst_confirm_ms=28 glitches=1000 glitch_events=0
Все 110 коротких и 90 долгих распознаны, ни одна помеха не стала нажатием, нажатие подтверждается не позже 28 мс от первого касания: 8 мс дребезга и 20 мс тишины. Мигалку в QEMU не гоняли: у машины netduinoplus2 нет модели GPIO, порты там читаются нулями.
2ж.6 Пример 2: консоль по UART
Консоль на USART1 (PA9 TX, PA10 RX, альтернативная функция 7),
115 200 бит/с. У Discovery — USART2 на PA2/PA3, как в разделе 16.
Делитель считается не из констант, а из того такта, что вышел на самом
деле: если кварц не запустился и ядро осталось на HSI, консоль всё равно
заговорит на верной скорости.
static void uart_init(void)
{
#if CON_APB == 2u
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
#else
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
#endif
pin_af(GPIOA, CON_TX_PIN, 7u); /* AF7 — USART1 и USART2 на всех трёх кристаллах */
pin_af(GPIOA, CON_RX_PIN, 7u);
GPIOA->PUPDR |= 1u << 2 * CON_RX_PIN; /* RX с подтяжкой: без переходника не ловит шум */
struct clk_info ci;
clock_now(&ci); /* делитель — от того такта, что вышел на самом деле */
CON_USART->BRR = clk_brr(CON_APB == 2u ? ci.pclk2 : ci.pclk1, BAUD);
CON_USART->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}
Делитель при 16-кратной выборке — частота шины, делённая на скорость, с округлением. Ошибка скорости на всех частотах раздела меньше 0,12 %:
brr: pclk=16000000 brr=139 baud=115108 err=-0.080%
brr: pclk=42000000 brr=365 baud=115068 err=-0.114%
brr: pclk=84000000 brr=729 baud=115226 err=+0.023%
brr: pclk=96000000 brr=833 baud=115246 err=+0.040%
brr: pclk=100000000 brr=868 baud=115207 err=+0.006%
Строка 42 МГц — USART2 у F407, и её BRR = 365 совпадает с таблицей 16.
Команды: help, ver, clk (такты по регистрам), id (номер кристалла,
объём Flash и уникальный номер), led on|off|blink, fpu (корень из
двух на FPU), dfu (в загрузчик, 2ж.7). Разбор строки — отдельный файл
без доступа к железу; на компьютере он прошёл 13 случаев, в том числе
забой обоими кодами (0x08 и 0x7F), лишнее слово и строку из 40 символов:
cmd led on 5 -> BADARG arg=1 buf="led on 5" ok
cmd ledon -> UNKNOWN arg=0 buf="ledon" ok
cmd dfx<BS>u -> DFU arg=0 buf="dfu" ok
cmd fpz<DEL>u, LF -> FPU arg=0 buf="fpu" ok
cmd 40 x -> TOOLONG arg=0 buf="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ok
Команда clk берёт частоты из RCC_CFGR и RCC_PLLCFGR тем же clk_decode:
static void cmd_clk(void)
{
static const char *const src[] = {"HSI", "HSE", "PLL"};
struct clk_info ci;
clock_now(&ci);
puts_("ядро от ");
puts_(src[ci.src]);
if (ci.src == CLK_SRC_PLL) {
puts_(" (вход ");
puts_(src[ci.pll_src]);
puts_(")");
}
put_mhz(": SYSCLK ", ci.sysclk);
put_mhz(", HCLK ", ci.hclk);
put_mhz(", APB1 ", ci.pclk1);
put_mhz(", APB2 ", ci.pclk2);
if (ci.src == CLK_SRC_PLL)
put_mhz(", USB ", ci.pll48);
puts_("\nFLASH_ACR.LATENCY ");
put_dec(FLASH->ACR & FLASH_ACR_LATENCY);
puts_(", PWR_CR.VOS ");
put_dec(PWR->CR >> 14 & 3u);
puts_("\n");
}
Прогон в QEMU. Образ тот же, что для платы, — console_f411.elf без
особых ключей. У машины netduinoplus2 нет блоков RCC, PWR и интерфейса Flash,
их регистры читаются нулями. Поэтому кварц «не запускается», и прошивка честно
это говорит — так же она поведёт себя на плате с непропаянным кварцем:
BlackPill F411CE 2zh-1.0
такт: кварц не запустился, работаю от HSI 16 МГц
> help
help ver clk id led on|off|blink fpu dfu
> ver
BlackPill F411CE 2zh-1.0
> clk
ядро от HSI: SYSCLK 16 МГц, HCLK 16 МГц, APB1 16 МГц, APB2 16 МГц
FLASH_ACR.LATENCY 0, PWR_CR.VOS 0
> led on
ok
> led blink
ok
> led on 5
? аргумент
> fpu
sqrt(2) = 1.414214
> foo
? help — список команд
> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
? строка длиннее 31 символа
Корень посчитан одной командой: в дизассемблере console_f411.elf
команда vsqrt.f32 встречается 1 раз. Образ F401 в QEMU ответил так же,
только с именем BlackPill F401CC. Что напечатает clk на плате, здесь не
проверено. Если всё сошлось, это будут 100 МГц, APB1 50, APB2 100, USB 44,4
и LATENCY 3.
id в QEMU падает — и это полезно увидеть. Номер устройства в
DBGMCU_IDCODE машина отдаёт нулём, а области с объёмом Flash и
уникальным номером (0x1FFF7A10…) у неё нет совсем. Чтение даёт
BusFault, и обработчик печатает регистры отказа:
> id
DEV_ID 0x000, REV_ID 0x0000, Flash
HardFault: CFSR 0x00008200, HFSR 0x40000000, BFAR 0x1FFF7A22
CFSR = 0x00008200 — точная ошибка шины (PRECISERR) с верным адресом
(BFARVALID), BFAR — адрес FLASHSIZE_BASE, HFSR = 0x40000000 —
отказ поднят до HardFault (FORCED): обработчика BusFault нет. На плате
id должен напечатать DEV_ID 0x431 у F411 или 0x423 у F401, объём
Flash в килобайтах и 96 бит номера; на плате это не проверялось.
2ж.7 Пример 3: в загрузчик по команде и что нужно для USB
BOOT0 удобна на столе, но в корпусе до неё не добраться. Команда dfu
уводит камень в заводской загрузчик сама: кладёт признак в конец ОЗУ и
делает программный сброс. Первой строкой Reset_Handler, пока ничего не
включено, признак проверяется, и управление уходит в системную память.
Сброс вернул такты и периферию сам — подробно оба способа (со сбросом и без)
разобраны в 48д.3, здесь — короткий вариант для F401 и F411:
void boot_request(void)
{
_boot_flag[0] = BOOT_MAGIC;
_boot_flag[1] = ~BOOT_MAGIC; /* второе слово: мусор в ОЗУ после включения не сойдёт за признак */
__DSB();
NVIC_SystemReset();
}
void boot_early(void)
{
if (_boot_flag[0] != BOOT_MAGIC || _boot_flag[1] != ~BOOT_MAGIC)
return;
_boot_flag[0] = 0u; /* прочитали — стёрли, иначе после выхода из загрузчика снова туда */
_boot_flag[1] = 0u;
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
(void)RCC->APB2ENR;
SYSCFG->MEMRMP = 1u; /* MEM_MODE = 01: по адресу 0 — системная память */
SCB->VTOR = SYSMEM;
__DSB();
__ISB();
uint32_t sp = ((const volatile uint32_t *)SYSMEM)[0];
uint32_t pc = ((const volatile uint32_t *)SYSMEM)[1];
__asm volatile("msr msp, %0\n"
"bx %1\n"
:
: "r"(sp), "r"(pc)
: "memory");
for (;;) {
}
}
Адрес признака задаёт скрипт компоновщика — у каждого кристалла свой, а
код один. Перед сбросом консоль ждёт флаг TC: иначе последний байт
сообщения не успеет уйти в линию.
Опыт в QEMU. Системной памяти у машины нет, поэтому сборка
-DQEMU_FAKE_SYSMEM подкладывает свою таблицу из двух слов — указатель стека
0x20001000 и адрес обработчика, — а обработчик печатает, в каком состоянии
получил камень:
> dfu
ухожу в загрузчик: dfu-util -d 0483:df11
[системная память] MSP 0x20000FF8, VTOR 0x08001200, признак 0x00000000, PRIMASK 0x00000000
После сброса QEMU оставил ОЗУ как было, признак нашёлся, и boot_early
его стёр. VTOR смотрит на подложенную таблицу, прерывания разрешены
(PRIMASK = 0). MSP взят из таблицы; на 8 байт ниже 0x20001000 он
потому, что пролог обработчика успел сохранить два регистра. С настоящим
загрузчиком на плате эта цепочка не проверялась.
USB CDC на BlackPill. Своё USB-устройство с виртуальным COM-портом и клавиатурой целиком собрано в 198ш на U585 — у него тот же блок OTG_FS. Полностью переносить его сюда раздел не стал, но по документам видно, что придётся поменять:
| U585 (198ш) | F411 и F401 | |
|---|---|---|
| точки, кроме нулевой | 5 IN и 5 OUT | 3 IN и 3 OUT (RM0383, 22) — 198ш занимает как раз три номера: 1, 2, 3 |
| память FIFO | 1,25 КБ | 1,25 КБ, та же раскладка |
| 48 МГц | HSI48 без кварца | только от PLLQ: кварц обязателен (20.2), F411 — 96 МГц ядра |
| VBUS | свой бит включения | бит NOVBUSSENS в OTG_FS_GCCFG: «VBUS всегда есть» — так делает QMK (BOARD_OTG_NOVBUSSENS) |
| выводы | PA11, PA12 |
PA11, PA12, альтернативная функция 10 |
Остальное — дескрипторы, точка 0, CDC — от кристалла не зависит. На F411
для этого собирают с -DCLOCK_USB: такт 96 МГц и ровно 48 МГц на USB
(2ж.3). Консоль и мигалка с таким тактом не меняются — делитель UART
считается от фактической частоты.
2ж.8 Перенос примера с Discovery F407
Одни и те же blink.c и console.c собраны под три платы — меняется
только ключ -DBOARD_… и скрипт компоновщика. Всё, что при переносе
приходится трогать, собрано в board.h:
| Discovery F407 | BlackPill F411 | BlackPill F401 | |
|---|---|---|---|
| заголовок | stm32f407xx.h |
stm32f411xe.h |
stm32f401xc.h |
| кварц | 8 МГц | 25 МГц | 25 МГц |
| PLL M, N, P, Q | 8, 336, 2, 7 | 25, 400, 4, 9 (с USB 25, 384, 4, 8) | 25, 336, 4, 7 |
| SYSCLK | 168 МГц | 100 МГц (96) | 84 МГц |
| задержки Flash | 5 | 3 | 2 |
| VOS | бит 14 = 1 (уже после сброса) | 11 — записать | 10 (после сброса) |
| APB1 / APB2, делители | /4, /2 | /2, /1 | /2, /1 |
| светодиод | PD12, горит при единице |
PC13, горит при нуле |
PC13, горит при нуле |
| кнопка | PA0, нажатие — 1, подтяжка на плате |
PA0, нажатие — 0, подтяжка внутри |
так же |
| консоль | USART2, PA2/PA3, APB1 |
USART1, PA9/PA10, APB2 |
так же |
| Flash / ОЗУ в скрипте | 1024 К / 128 К | 512 К / 128 К | 256 К / 64 К |
Адреса блоков у всех трёх одни: GPIO с 0x40020000 на AHB1, RCC, USART1 и
USART2 на тех же местах. Регистровый код периферии переносится без правок,
а меняются такт, номера выводов и то, чего на новом кристалле нет. Размеры
сборок:
1116 4 4 1124 464 blink_f411.elf
1116 4 4 1124 464 blink_f401.elf
1116 4 4 1124 464 blink_f407.elf
3416 4 8 3428 d64 console_f411.elf
3416 4 8 3428 d64 console_f401.elf
3408 4 8 3420 d5c console_f407.elf
Столбцы — код с константами, .data, .bss. Мигалка на всех трёх платах —
1116 байт, образ .bin — 1120 байт. Консоль — 3416 байт на BlackPill и 3408
на Discovery, образ — 3420 и 3412 байт. Сборка F411 с -DCLOCK_USB по размеру
та же: 3416. Все девять сборок раздела прошли без предупреждений
(-Wall -Wextra -Werror).
Что делать с кодом, которого на BlackPill нет:
- CAN, ЦАП, Ethernet, камера, FSMC, RNG — не соберутся, и это хорошо: компилятор покажет каждое место. Замена — внешние микросхемы или другая плата.
- TIM6/TIM7 как запуск АЦП — заменить на TIM2…TIM5 с выходом TRGO.
- USART3 — перенести на USART1, USART2 или USART6.
- Кварц и HSE_VALUE — 25 МГц, а не 8; в проекте с HAL — в
stm32f4xx_hal_conf.h(2.8). - Объём — 128 КБ ОЗУ у F411 хватает для примеров с Discovery, у F401 ОЗУ 64 КБ, и большие буферы из примеров с F407 могут не войти; CCM у F4x1 нет вовсе.
2ж.9 Грабли
Плата «не видна» по USB в режиме загрузчика. По описанию в QMK вывод
PA10, к которому что-то подключено, мешает загрузчику выбрать DFU: нужен
подтягивающий резистор около 22 кОм. По рис. 33 AN2606 причина понятна —
загрузчик слушает и USART1, и USB и уходит туда, где первым что-то
увидел. Там же сказано: из-за кварца 25 МГц плата может не входить в
загрузчик, и помогает её прогреть. Оба наблюдения — QMK, в разделе не
проверялись.
Код с Discovery врёт со временем в 3,125 раза. Остался
-DHSE_VALUE=8000000, а кварц на BlackPill — 25 МГц. Задержки HAL и
скорость UART уйдут; функция настройки такта из этого раздела этого не
заметит, потому что считает не по HSE_VALUE, а по board.h.
100 МГц и USB не работают вместе. С выхода Q при VCO 400 МГц — 44,4 МГц,
и устройство не опознается. Для USB F411 — на 96 МГц (-DCLOCK_USB),
2ж.3.
F411 на 100 МГц без записи VOS. После сброса VOS = 10 — ступень до
84 МГц. Код с F407, где после сброса уже Scale 1, этот шаг пропускает.
На столе может и работать, но это вне паспорта; записывать VOS до включения
PLL.
HardFault на первой же плавающей команде. Забыли включить FPU в
CPACR. Сборка -DNO_FPU_ENABLE в QEMU на команде fpu:
> fpu
HardFault: CFSR 0x00080000, HFSR 0x40000000, BFAR 0x00000000
CFSR = 0x00080000 — бит NOCP (UsageFault, «нет сопроцессора»),
поднятый до HardFault. На F103 такой ошибки не бывает: FPU там нет, и
компилятор с -mfloat-abi=soft плавающих команд не выдаёт. Без включения
FPU образ меньше на 24 байта (3392 против 3416) — ровно на это включение.
memcpy в программе без библиотек. GCC при -Os узнаёт в цикле
копирования .data вызов memcpy, а в обнулении .bss — memset. Без
библиотеки сборка падает на компоновке, лечится
-fno-tree-loop-distribute-patterns (2ж.5).
Заголовок обещает то, чего нет в корпусе. stm32f401xc.h объявляет
SPI4 и SDIO, stm32f411xe.h — ещё и все пять SPI на всех выводах
семейства. В UFQFPN48 у F401 SPI три, SDIO нет, а выводов PB11, PC10…PC12,
PD5/PD6 нет ни у одного из двух. Сверять с таблицей выводов даташита или с
CubeMX, а не с заголовком.
Светодиод «наоборот». PC13 горит при нуле, а у Discovery PD12 — при
единице. В board.h это одна строка LED_ON, в разбросанном по
проекту коде — десяток мест.
Датчик на 5 В к PA0. У F411 PA0 и PB5 не терпят 5 В (2ж.1). На F401
по DS9716 терпят — плата одна, а ответ разный.
Мусор после включения принят за признак загрузчика. Одного слова мало: ОЗУ после подачи питания не обнулено. Поэтому признак — два слова, магия и её дополнение, и стирается сразу после чтения. Иначе прибор после выхода из загрузчика уходил бы в него снова.
2ж.10 Итоги
- BlackPill — F411CE (100 МГц, 512 КБ Flash, 128 КБ ОЗУ) или F401CC (84 МГц,
256 и 64 КБ) в UFQFPN48, Cortex-M4F, кварцы 25 МГц и 32,768 кГц, светодиод
PC13(горит при нуле), кнопка KEY наPA0, USB-C, кнопки BOOT0 и NRST. - Против F103: FPU, такт выше в 1,2–1,4 раза, ОЗУ больше в 3,2–6,4 раза, три I²C и OTG FS. Против F407: нет CAN, ЦАП, Ethernet, RNG, FSMC, камеры, USART3 и TIM6/TIM7; линий ввода-вывода 36 против 82.
- На выводах 5, 6 и 22 у 48-выводных F4 стоят
PH0,PH1иVCAP1— там, где у F103C8PD0,PD1иPB11. - Такт: F411 — 100 МГц без USB или 96 МГц с USB (M 25, N 384, P 4, Q 8),
100 МГц и 48 МГц вместе недостижимы ни с каким кварцем; F401 — 84 и 48 МГц.
Для 100 МГц —
VOS= 11 и 3 такта задержки Flash. - Прошивка без программатора: BOOT0 + NRST, устройство
0483:DF11,dfu-util … -s 0x08000000:leave; загрузчику нужен кварц 4–26 МГц, кратный 1 МГц, он тактирует ядро от PLL на 60 МГц и пишет Flash по байту. - Три примера из одних исходников под три платы: мигалка — 1116 байт кода,
консоль — 3416 байт (3408 на F407); в QEMU консоль ответила на все
команды,
fpuдал1.414214одной командойvsqrt.f32, без включения FPU —NOCPвCFSR(0x00080000). - Логика на компьютере: кнопка — 200 нажатий без единой ошибки, 1000 помех без ложных нажатий; разбор команд — 13 случаев; делитель UART — ошибка меньше 0,12 % на всех частотах раздела.
- Уход в загрузчик по команде: признак в последних 16 байтах ОЗУ (
0x2001FFF0у F411,0x2000FFF0у F401), сброс, прыжок первой строкойReset_Handler.
Полные исходники
Всё, чтобы собрать и повторить примеры: настройки плат, такт, стартовый
код и скрипты компоновщика, три примера и перебор PLL. Проверки на
компьютере (test_board.c, test_logic.c), подложенная системная память
для QEMU, разбор базы CubeMX и скрипты прогонов — в архиве исходников
раздела. Заголовки CMSIS — из пакета STM32F4xx_DFP.
board.h — всё, чем отличаются три платы: такт, задержки, VOS, светодиод, кнопка, консоль.
/* board.h — раздел 2ж: всё, чем три платы отличаются друг от друга, в одном месте.
* Собирать с одним из -DBOARD_BLACKPILL_F411, -DBOARD_BLACKPILL_F401, -DBOARD_DISC_F407.
* Здесь только числа и имена: файл читает и прошивка, и проверка на компьютере.
* Такты — по RM0383 (F411), RM0368 (F401), RM0090 (F407); выводы — по описанию плат. */
#ifndef BOARD_H
#define BOARD_H
#if defined(BOARD_BLACKPILL_F411)
#define BOARD_NAME "BlackPill F411CE"
#define CHIP_HEADER "stm32f411xe.h"
#define HSE_HZ 25000000u /* кварц 25 МГц на плате */
#if defined(CLOCK_USB) /* для USB нужен ровно 48 МГц с выхода Q */
#define PLL_M 25u /* 25 / 25 = 1 МГц на входе VCO */
#define PLL_N 384u /* VCO 384 МГц */
#define PLL_P 4u /* SYSCLK 96 МГц */
#define PLL_Q 8u /* 48 МГц — USB годится */
#else
#define PLL_M 25u
#define PLL_N 400u /* VCO 400 МГц */
#define PLL_P 4u /* SYSCLK 100 МГц — предел F411 */
#define PLL_Q 9u /* 44,4 МГц — не выше 48, но для USB не годится */
#endif
#define FLASH_WS 3u /* RM0383, таблица 5: 90 < HCLK <= 100 МГц при 2,7–3,6 В */
#define VOS_BITS 3u /* PWR_CR.VOS = 11, Scale 1, до 100 МГц; после сброса 10 — до 84 */
#define APB1_DIV 2u /* APB1 не выше 50 МГц */
#define APB2_DIV 1u /* APB2 не выше 100 МГц */
#define LED_PORT GPIOC
#define LED_PIN 13u
#define LED_ON 0u /* светодиод от 3,3 В к PC13: горит при нуле на выводе */
#define KEY_PORT GPIOA
#define KEY_PIN 0u
#define KEY_DOWN 0u /* кнопка KEY замыкает PA0 на землю */
#define KEY_PUPD 1u /* PUPDR = 01: подтяжка вверх внутри кристалла */
#define CON_USART USART1 /* PA9 TX, PA10 RX — те же, что у заводского загрузчика */
#define CON_APB 2u
#define CON_TX_PIN 9u
#define CON_RX_PIN 10u
#elif defined(BOARD_BLACKPILL_F401)
#define BOARD_NAME "BlackPill F401CC"
#define CHIP_HEADER "stm32f401xc.h"
#define HSE_HZ 25000000u
#define PLL_M 25u /* 1 МГц на входе VCO */
#define PLL_N 336u /* VCO 336 МГц: у F401 не ниже 192 */
#define PLL_P 4u /* SYSCLK 84 МГц — предел F401 */
#define PLL_Q 7u /* 48 МГц — USB годится на той же частоте */
#define FLASH_WS 2u /* RM0368, таблица 6: 60 < HCLK <= 84 МГц при 2,7–3,6 В */
#define VOS_BITS 2u /* VOS = 10, Scale 2, до 84 МГц — значение после сброса */
#define APB1_DIV 2u /* APB1 не выше 42 МГц */
#define APB2_DIV 1u /* APB2 не выше 84 МГц */
#define LED_PORT GPIOC
#define LED_PIN 13u
#define LED_ON 0u
#define KEY_PORT GPIOA
#define KEY_PIN 0u
#define KEY_DOWN 0u
#define KEY_PUPD 1u
#define CON_USART USART1
#define CON_APB 2u
#define CON_TX_PIN 9u
#define CON_RX_PIN 10u
#elif defined(BOARD_DISC_F407)
#define BOARD_NAME "Discovery F407VG"
#define CHIP_HEADER "stm32f407xx.h"
#define HSE_HZ 8000000u /* кварц 8 МГц на Discovery */
#define PLL_M 8u /* 1 МГц на входе VCO */
#define PLL_N 336u /* VCO 336 МГц */
#define PLL_P 2u /* SYSCLK 168 МГц */
#define PLL_Q 7u /* 48 МГц */
#define FLASH_WS 5u /* RM0090, таблица 10: 150 < HCLK <= 168 МГц при 2,7–3,6 В */
#define VOS_BITS 1u /* у F405/F407 VOS — один бит 14: 1 = Scale 1, значение после сброса */
#define APB1_DIV 4u /* APB1 42 МГц */
#define APB2_DIV 2u /* APB2 84 МГц */
#define LED_PORT GPIOD
#define LED_PIN 12u /* зелёный LD4 */
#define LED_ON 1u
#define KEY_PORT GPIOA
#define KEY_PIN 0u
#define KEY_DOWN 1u /* синяя кнопка B1: нажатие — высокий уровень */
#define KEY_PUPD 0u /* подтяжка вниз стоит на плате */
#define CON_USART USART2 /* PA2 TX, PA3 RX, как в разделе 16 */
#define CON_APB 1u
#define CON_TX_PIN 2u
#define CON_RX_PIN 3u
#else
#error "BOARD_BLACKPILL_F411, BOARD_BLACKPILL_F401 или BOARD_DISC_F407"
#endif
#define SYSCLK_HZ (HSE_HZ / PLL_M * PLL_N / PLL_P)
#define HCLK_HZ SYSCLK_HZ
#define PCLK1_HZ (HCLK_HZ / APB1_DIV)
#define PCLK2_HZ (HCLK_HZ / APB2_DIV)
#define CON_PCLK_HZ (CON_APB == 2u ? PCLK2_HZ : PCLK1_HZ)
#endif
clk.h — арифметика тактов: поля RCC_PLLCFGR и RCC_CFGR.
/* clk.h — раздел 2ж: арифметика тактов STM32F4 без доступа к железу.
* Поля RCC_PLLCFGR и RCC_CFGR одинаковы у F401, F411 и F407 (RM0368 и RM0383 — 6.3.2–6.3.3, RM0090 — 7.3.2–7.3.3),
* поэтому один и тот же код и собирает значение регистра, и разбирает его обратно в частоты. */
#ifndef CLK_H
#define CLK_H
#include <stdint.h>
#define CLK_SRC_HSI 0u
#define CLK_SRC_HSE 1u
#define CLK_SRC_PLL 2u
#define CLK_PLLCFGR_FIELDS 0x0F437FFFu /* M, N, P, SRC, Q; остальные биты резервные — не трогать */
struct clk_info {
uint32_t src; /* что сейчас тактирует ядро: CLK_SRC_* по полю SWS */
uint32_t pll_src; /* вход PLL: CLK_SRC_HSI или CLK_SRC_HSE */
uint32_t sysclk, hclk, pclk1, pclk2, pll48; /* Гц; pll48 — выход Q для USB */
};
uint32_t clk_pllcfgr(uint32_t m, uint32_t n, uint32_t p, uint32_t q, uint32_t src);
uint32_t clk_cfgr_div(uint32_t ahb, uint32_t apb1, uint32_t apb2);
void clk_decode(uint32_t cfgr, uint32_t pllcfgr, uint32_t hse_hz, struct clk_info *ci);
uint32_t clk_brr(uint32_t pclk, uint32_t baud);
#endif
clk.c — сборка значений регистров и обратный разбор в частоты, делитель USART.
/* clk.c — раздел 2ж: значения RCC_PLLCFGR и RCC_CFGR и обратный разбор в частоты.
* Поля по RM0383 (6.3.2 RCC_PLLCFGR, 6.3.3 RCC_CFGR); у F401 (RM0368, 6.3) и F407 (RM0090, 7.3) они те же. */
#include "clk.h"
#define HSI_HZ 16000000u
uint32_t clk_pllcfgr(uint32_t m, uint32_t n, uint32_t p, uint32_t q, uint32_t src)
{
return (m & 0x3Fu) /* PLLM: биты 5:0 */
| (n & 0x1FFu) << 6 /* PLLN: биты 14:6 */
| ((p / 2u - 1u) & 3u) << 16 /* PLLP: 00 = 2, 01 = 4, 10 = 6, 11 = 8 */
| (src == CLK_SRC_HSE ? 1u : 0u) << 22 /* PLLSRC */
| (q & 0xFu) << 24; /* PLLQ: биты 27:24 */
}
static uint32_t ahb_code(uint32_t div)
{
static const uint16_t d[] = {2, 4, 8, 16, 64, 128, 256, 512};
for (uint32_t i = 0; i < 8u; i++)
if (d[i] == div)
return 8u + i; /* 1000 = /2 … 1111 = /512 */
return 0u; /* 0xxx = без деления */
}
static uint32_t apb_code(uint32_t div)
{
switch (div) {
case 2u: return 4u; /* 100 */
case 4u: return 5u;
case 8u: return 6u;
case 16u: return 7u;
default: return 0u; /* 0xx = без деления */
}
}
uint32_t clk_cfgr_div(uint32_t ahb, uint32_t apb1, uint32_t apb2)
{
return ahb_code(ahb) << 4 | apb_code(apb1) << 10 | apb_code(apb2) << 13;
}
static uint32_t ahb_div(uint32_t code)
{
static const uint16_t d[] = {2, 4, 8, 16, 64, 128, 256, 512};
return code & 8u ? d[code & 7u] : 1u;
}
static uint32_t apb_div(uint32_t code)
{
return code & 4u ? 2u << (code & 3u) : 1u;
}
void clk_decode(uint32_t cfgr, uint32_t pllcfgr, uint32_t hse_hz, struct clk_info *ci)
{
uint32_t m = pllcfgr & 0x3Fu, n = pllcfgr >> 6 & 0x1FFu;
uint32_t p = ((pllcfgr >> 16 & 3u) + 1u) * 2u, q = pllcfgr >> 24 & 0xFu;
uint32_t in, vco;
ci->pll_src = pllcfgr & (1u << 22) ? CLK_SRC_HSE : CLK_SRC_HSI;
in = ci->pll_src == CLK_SRC_HSE ? hse_hz : HSI_HZ;
vco = m ? in / m * n : 0u; /* делим первым: 25 МГц · 400 не влезает в 32 бита */
ci->pll48 = q ? vco / q : 0u;
switch (cfgr >> 2 & 3u) { /* SWS: чем тактируется ядро на самом деле */
case 1u: ci->src = CLK_SRC_HSE; ci->sysclk = hse_hz; break;
case 2u: ci->src = CLK_SRC_PLL; ci->sysclk = vco / p; break;
default: ci->src = CLK_SRC_HSI; ci->sysclk = HSI_HZ; break;
}
ci->hclk = ci->sysclk / ahb_div(cfgr >> 4 & 0xFu);
ci->pclk1 = ci->hclk / apb_div(cfgr >> 10 & 7u);
ci->pclk2 = ci->hclk / apb_div(cfgr >> 13 & 7u);
}
uint32_t clk_brr(uint32_t pclk, uint32_t baud)
{
return (pclk + baud / 2u) / baud; /* OVER8 = 0: BRR = f / скорость, с округлением */
}
clock.h — коды возврата настройки такта.
/* clock.h — раздел 2ж: настройка тактов платы и чтение того, что вышло на самом деле. */
#ifndef CLOCK_H
#define CLOCK_H
#include <stdint.h>
#include "clk.h"
#define CLOCK_OK 0
#define CLOCK_NO_HSE 1 /* кварц не запустился — работаем от HSI 16 МГц */
#define CLOCK_NO_PLL 2
#define CLOCK_NO_WS 3 /* задержки Flash не записались */
#define CLOCK_NO_SW 4
extern uint32_t SystemCoreClock;
int clock_init(void);
void clock_now(struct clk_info *ci);
#endif
clock.c — такт от кварца через PLL с ограниченными ожиданиями.
/* clock.c — раздел 2ж: такт от кварца через PLL на трёх платах, регистрами CMSIS.
* Порядок — по RM0383, 3.4.1 и 6.3: напряжение ядра (VOS) до включения PLL, задержки Flash до
* переключения на PLL, проверка каждого шага по флагу. Кварц может не запуститься — тогда остаёмся
* на HSI 16 МГц и говорим об этом, а не висим в цикле. */
#include "board.h"
#include CHIP_HEADER
#include "clk.h"
#include "clock.h"
#define WAIT_LOOPS 200000u /* опросов флага; HSE по даташиту встаёт за ~2 мс (DocID026289, таблица 37) */
uint32_t SystemCoreClock = 16000000u;
static int wait_set(volatile uint32_t *reg, uint32_t mask)
{
for (uint32_t n = 0; n < WAIT_LOOPS; n++)
if (*reg & mask)
return 0;
return -1;
}
int clock_init(void)
{
RCC->CR |= RCC_CR_HSEON;
if (wait_set(&RCC->CR, RCC_CR_HSERDY)) {
RCC->CR &= ~RCC_CR_HSEON;
return CLOCK_NO_HSE; /* кварца нет или он не завёлся */
}
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
(void)RCC->APB1ENR; /* дождаться включения такта PWR */
PWR->CR = (PWR->CR & ~(3u << 14)) | VOS_BITS << 14; /* VOS меняется только при выключенной PLL */
RCC->PLLCFGR = (RCC->PLLCFGR & ~CLK_PLLCFGR_FIELDS) /* бит 29 после сброса — 1, он резервный */
| clk_pllcfgr(PLL_M, PLL_N, PLL_P, PLL_Q, CLK_SRC_HSE);
RCC->CR |= RCC_CR_PLLON;
if (wait_set(&RCC->CR, RCC_CR_PLLRDY))
return CLOCK_NO_PLL;
FLASH->ACR = FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_PRFTEN | FLASH_WS;
if ((FLASH->ACR & FLASH_ACR_LATENCY) != FLASH_WS) /* RM0383, 3.4.1: прочитать и убедиться */
return CLOCK_NO_WS;
RCC->CFGR = (RCC->CFGR & ~(RCC_CFGR_HPRE | RCC_CFGR_PPRE1 | RCC_CFGR_PPRE2))
| clk_cfgr_div(1u, APB1_DIV, APB2_DIV); /* делители — до переключения */
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL;
for (uint32_t n = 0; (RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL; n++)
if (n >= WAIT_LOOPS)
return CLOCK_NO_SW;
SystemCoreClock = SYSCLK_HZ;
return CLOCK_OK;
}
void clock_now(struct clk_info *ci)
{
clk_decode(RCC->CFGR, RCC->PLLCFGR, HSE_HZ, ci);
}
startup.c — таблица векторов, Reset_Handler, включение FPU.
/* startup.c — раздел 2ж: стартовый код для STM32F401, F411 и F407 без библиотек.
* Таблица векторов — до SysTick: прерывания периферии примеры не используют.
* После сброса: признак «в загрузчик» (boot_early), копия .data, обнуление .bss, включение FPU, main. */
#include <stdint.h>
#include "boot.h"
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);
void Default_Handler(void)
{
for (;;) {
}
}
void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));
void Reset_Handler(void)
{
boot_early(); /* до .data и .bss: признак лежит вне них */
for (uint32_t *s = &_sidata, *d = &_sdata; d < &_edata;)
*d++ = *s++;
for (uint32_t *d = &_sbss; d < &_ebss;)
*d++ = 0u;
#ifndef NO_FPU_ENABLE
*(volatile uint32_t *)0xE000ED88u |= 0xFu << 20; /* SCB->CPACR: CP10 и CP11 — полный доступ */
__asm volatile("dsb\n isb" ::: "memory"); /* до первой команды FPU */
#endif
main();
for (;;) {
}
}
__attribute__((section(".isr_vector"), used))
const void *const vectors[16] = {
&_estack, /* начальный указатель стека */
Reset_Handler,
NMI_Handler,
HardFault_Handler,
Default_Handler, /* MemManage */
Default_Handler, /* BusFault */
Default_Handler, /* UsageFault */
0, 0, 0, 0,
Default_Handler, /* SVCall */
Default_Handler, /* DebugMon */
0,
Default_Handler, /* PendSV */
SysTick_Handler,
};
sections.ld — общая часть скрипта компоновщика, признак в конце ОЗУ.
/* sections.ld — раздел 2ж: общая часть скрипта компоновщика для F401CC, F411CE и F407VG.
* Память задаёт файл кристалла (f411ce.ld и др.), он же подключает этот файл.
* Последние 16 байт ОЗУ — не стек и не .bss: там признак «в загрузчик», он переживает сброс. */
ENTRY(Reset_Handler)
_boot_flag = ORIGIN(RAM) + LENGTH(RAM) - 16;
_estack = _boot_flag; /* стек растёт вниз от признака */
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text*) *(.rodata*) . = ALIGN(4); } > FLASH
.ARM.exidx : { *(.ARM.exidx*) } > FLASH
_sidata = LOADADDR(.data);
.data : { _sdata = .; *(.data*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
.bss (NOLOAD) : { _sbss = .; *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; } > RAM
}
f411ce.ld — память STM32F411CE.
/* f411ce.ld — раздел 2ж: STM32F411CE — 512 КБ Flash, 128 КБ ОЗУ (DocID026289, таблица 2). */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
INCLUDE sections.ld
f401cc.ld — память STM32F401CC.
/* f401cc.ld — раздел 2ж: STM32F401CC — 256 КБ Flash, 64 КБ ОЗУ (DS9716, таблица 2). */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
INCLUDE sections.ld
f407vg.ld — память STM32F407VG.
/* f407vg.ld — раздел 2ж: STM32F407VG — 1 МБ Flash, 128 КБ основного ОЗУ; CCM 64 КБ не используется. */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
INCLUDE sections.ld
io.h — выводы регистрами CMSIS.
/* io.h — раздел 2ж: выводы F4 регистрами CMSIS. У F401, F411 и F407 порт GPIO устроен одинаково:
* MODER, OTYPER, OSPEEDR, PUPDR, IDR, BSRR, AFR[2] (RM0383, 8.4; RM0090, 8.4). Подключать после CHIP_HEADER. */
#ifndef IO_H
#define IO_H
#include <stdint.h>
static inline void port_clock(GPIO_TypeDef *g)
{
RCC->AHB1ENR |= 1u << (((uint32_t)g - GPIOA_BASE) / 0x400u); /* GPIOAEN — бит 0, GPIOBEN — 1, … */
(void)RCC->AHB1ENR;
}
static inline void pin_mode(GPIO_TypeDef *g, uint32_t pin, uint32_t mode, uint32_t pupd)
{
port_clock(g);
g->MODER = (g->MODER & ~(3u << 2 * pin)) | mode << 2 * pin; /* 00 вход, 01 выход, 10 альт. функция */
g->PUPDR = (g->PUPDR & ~(3u << 2 * pin)) | pupd << 2 * pin; /* 00 нет, 01 вверх, 10 вниз */
}
static inline void pin_af(GPIO_TypeDef *g, uint32_t pin, uint32_t af)
{
pin_mode(g, pin, 2u, 0u);
g->AFR[pin >> 3] = (g->AFR[pin >> 3] & ~(0xFu << 4 * (pin & 7u))) | af << 4 * (pin & 7u);
}
static inline void pin_write(GPIO_TypeDef *g, uint32_t pin, uint32_t level)
{
g->BSRR = level ? 1u << pin : 1u << (pin + 16u); /* одна запись, без чтения порта */
}
static inline uint32_t pin_read(GPIO_TypeDef *g, uint32_t pin)
{
return g->IDR >> pin & 1u;
}
#endif
key.h — кнопка: события и состояние.
/* key.h — раздел 2ж: кнопка с антидребезгом, короткое и долгое нажатие. Без доступа к железу. */
#ifndef KEY_H
#define KEY_H
#include <stdint.h>
#define KEY_STABLE_MS 20u /* столько уровень должен не меняться, чтобы ему поверить */
#define KEY_LONG_MS 800u /* дольше — долгое нажатие */
enum key_event { KEY_NONE, KEY_SHORT, KEY_LONG };
struct key {
uint8_t raw, stable; /* последний прочитанный и подтверждённый уровень: 1 — нажата */
uint8_t long_sent;
uint32_t t_change, t_down; /* когда менялся сырой уровень, когда подтвердилось нажатие */
};
void key_init(struct key *k);
enum key_event key_step(struct key *k, int pressed, uint32_t now_ms);
#endif
key.c — антидребезг, короткое и долгое нажатие.
/* key.c — раздел 2ж: антидребезг по времени. Звать раз в миллисекунду с сырым уровнем кнопки.
* Короткое нажатие сообщается при отпускании, долгое — как только кнопку продержали KEY_LONG_MS. */
#include "key.h"
void key_init(struct key *k)
{
k->raw = k->stable = k->long_sent = 0u;
k->t_change = k->t_down = 0u;
}
enum key_event key_step(struct key *k, int pressed, uint32_t now_ms)
{
uint8_t r = pressed ? 1u : 0u;
if (r != k->raw) { /* дребезг или настоящее изменение — ждём */
k->raw = r;
k->t_change = now_ms;
return KEY_NONE;
}
if (r != k->stable && now_ms - k->t_change >= KEY_STABLE_MS) {
k->stable = r;
if (r) { /* нажатие подтвердилось */
k->t_down = now_ms;
k->long_sent = 0u;
return KEY_NONE;
}
return k->long_sent ? KEY_NONE : KEY_SHORT; /* отпустили: короткое, если долгого не было */
}
if (k->stable && !k->long_sent && now_ms - k->t_down >= KEY_LONG_MS) {
k->long_sent = 1u;
return KEY_LONG;
}
return KEY_NONE;
}
blink.c — пример 1: мигалка и кнопка.
/* blink.c — раздел 2ж, пример 1: мигалка и кнопка регистрами CMSIS — один файл на три платы.
* Короткое нажатие KEY — следующая скорость (500, 250, 100 мс), долгое — пауза и обратно.
* Всё, что зависит от платы (вывод, уровень «горит», подтяжка, такт), — в board.h. */
#include "board.h"
#include CHIP_HEADER
#include "io.h"
#include "clock.h"
#include "key.h"
static volatile uint32_t ms;
void SysTick_Handler(void) { ms++; }
static void led(int on)
{
pin_write(LED_PORT, LED_PIN, on ? LED_ON : !LED_ON);
}
int main(void)
{
static const uint16_t period[] = {500, 250, 100};
struct key k;
uint32_t mode = 0, paused = 0, lit = 0, next = 0, last = 0;
clock_init(); /* не вышло — мигаем от HSI, SysTick считает сам */
SysTick_Config(SystemCoreClock / 1000u);
pin_mode(LED_PORT, LED_PIN, 1u, 0u);
led(0);
pin_mode(KEY_PORT, KEY_PIN, 0u, KEY_PUPD);
key_init(&k);
for (;;) {
uint32_t now = ms;
if (now == last)
continue; /* кнопку — раз в миллисекунду */
last = now;
switch (key_step(&k, pin_read(KEY_PORT, KEY_PIN) == KEY_DOWN, now)) {
case KEY_SHORT: mode = (mode + 1u) % 3u; next = now; break;
case KEY_LONG: paused = !paused; next = now; break;
default: break;
}
if (paused) {
led(0);
} else if ((int32_t)(now - next) >= 0) {
lit = !lit;
led(lit);
next = now + period[mode];
}
}
}
cmd.h — строка консоли и команды.
/* cmd.h — раздел 2ж: строка консоли и разбор команды. Без доступа к железу. */
#ifndef CMD_H
#define CMD_H
#define LINE_MAX 32u /* с завершающим нулём: 31 символ */
enum cmd_id { CMD_EMPTY, CMD_HELP, CMD_VER, CMD_CLK, CMD_ID, CMD_LED, CMD_FPU, CMD_DFU, CMD_UNKNOWN, CMD_BADARG,
CMD_TOOLONG };
struct line {
char buf[LINE_MAX];
unsigned len;
int overflow; /* строка оказалась длиннее буфера — выполнять нельзя */
};
struct cmd {
enum cmd_id id;
int arg; /* led: 0 — off, 1 — on, 2 — blink */
};
void line_init(struct line *l);
int line_feed(struct line *l, char c, char *echo); /* 1 — строка готова; echo — что вернуть в терминал */
struct cmd cmd_parse(const struct line *l);
#endif
cmd.c — строка с эхом и забоем, разбор команды.
/* cmd.c — раздел 2ж: строка консоли с эхом и забоем, разбор «команда [аргумент]».
* Один и тот же файл собирается в прошивку и в проверку на компьютере. */
#include "cmd.h"
void line_init(struct line *l)
{
l->len = 0u;
l->overflow = 0;
}
int line_feed(struct line *l, char c, char *echo)
{
echo[0] = 0;
if (c == '\r' || c == '\n') {
l->buf[l->len < LINE_MAX ? l->len : LINE_MAX - 1u] = 0;
echo[0] = '\r', echo[1] = '\n', echo[2] = 0;
return 1;
}
if (c == '\b' || c == 0x7F) { /* забой: терминалы шлют то одно, то другое */
if (l->len && !l->overflow) {
l->len--;
echo[0] = '\b', echo[1] = ' ', echo[2] = '\b', echo[3] = 0;
}
return 0;
}
if (c < ' ')
return 0; /* прочие управляющие — мимо */
if (l->len < LINE_MAX - 1u) {
l->buf[l->len++] = c;
echo[0] = c, echo[1] = 0;
} else {
l->overflow = 1; /* лишнее не храним, но и строку не выполним */
}
return 0;
}
static int word(const char **p, const char *w)
{
const char *s = *p;
while (*w && *s == *w)
s++, w++;
if (*w || (*s && *s != ' '))
return 0;
while (*s == ' ')
s++;
*p = s;
return 1;
}
struct cmd cmd_parse(const struct line *l)
{
struct cmd c = {CMD_UNKNOWN, 0};
const char *s = l->buf;
if (l->overflow) {
c.id = CMD_TOOLONG;
return c;
}
while (*s == ' ')
s++;
if (!*s)
c.id = CMD_EMPTY;
else if (word(&s, "help"))
c.id = CMD_HELP;
else if (word(&s, "ver"))
c.id = CMD_VER;
else if (word(&s, "clk"))
c.id = CMD_CLK;
else if (word(&s, "id"))
c.id = CMD_ID;
else if (word(&s, "fpu"))
c.id = CMD_FPU;
else if (word(&s, "dfu"))
c.id = CMD_DFU;
else if (word(&s, "led")) {
c.id = CMD_LED;
if (word(&s, "off"))
c.arg = 0;
else if (word(&s, "on"))
c.arg = 1;
else if (word(&s, "blink"))
c.arg = 2;
else
c.id = CMD_BADARG;
if (c.id == CMD_LED && *s)
c.id = CMD_BADARG; /* «led on 5» — лишнее слово */
return c;
}
if (c.id != CMD_UNKNOWN && c.id != CMD_EMPTY && *s)
c.id = CMD_BADARG; /* у остальных команд аргументов нет */
return c;
}
console.c — примеры 2 и 3: консоль по UART и команда dfu.
/* console.c — раздел 2ж, примеры 2 и 3: консоль по UART и уход в USB DFU по команде.
* BlackPill: USART1 на PA9/PA10, Discovery: USART2 на PA2/PA3; 115 200 бит/с, 8N1, опрос без прерываний.
* Команды: help, ver, clk, id, led on|off|blink, fpu, dfu. */
#include "board.h"
#include CHIP_HEADER
#include "io.h"
#include "clock.h"
#include "cmd.h"
#include "boot.h"
#define VERSION "2zh-1.0"
#define BAUD 115200u
static volatile uint32_t ms;
static volatile int blink;
void SysTick_Handler(void)
{
ms++;
if (blink && ms % 250u == 0u)
LED_PORT->ODR ^= 1u << LED_PIN;
}
static void putc_(char c)
{
while (!(CON_USART->SR & USART_SR_TXE)) {
}
CON_USART->DR = (uint8_t)c;
}
static void puts_(const char *s)
{
for (; *s; s++) {
if (*s == '\n')
putc_('\r');
putc_(*s);
}
}
static void raw(const char *s)
{
for (; *s; s++)
putc_(*s);
}
static void put_dec(uint32_t v)
{
char b[11];
int i = 10;
b[i] = 0;
do {
b[--i] = (char)('0' + v % 10u);
v /= 10u;
} while (v);
puts_(b + i);
}
static void put_hex(uint32_t v, int digits)
{
puts_("0x");
while (digits--)
putc_("0123456789ABCDEF"[v >> 4 * digits & 0xFu]);
}
static void put_mhz(const char *name, uint32_t hz)
{
puts_(name);
put_dec(hz / 1000000u);
if (hz % 1000000u) { /* 44,4 МГц и подобные — с одним знаком */
putc_(',');
put_dec(hz % 1000000u / 100000u);
}
puts_(" МГц");
}
static void uart_init(void)
{
#if CON_APB == 2u
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
#else
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
#endif
pin_af(GPIOA, CON_TX_PIN, 7u); /* AF7 — USART1 и USART2 на всех трёх кристаллах */
pin_af(GPIOA, CON_RX_PIN, 7u);
GPIOA->PUPDR |= 1u << 2 * CON_RX_PIN; /* RX с подтяжкой: без переходника не ловит шум */
struct clk_info ci;
clock_now(&ci); /* делитель — от того такта, что вышел на самом деле */
CON_USART->BRR = clk_brr(CON_APB == 2u ? ci.pclk2 : ci.pclk1, BAUD);
CON_USART->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}
static void cmd_clk(void)
{
static const char *const src[] = {"HSI", "HSE", "PLL"};
struct clk_info ci;
clock_now(&ci);
puts_("ядро от ");
puts_(src[ci.src]);
if (ci.src == CLK_SRC_PLL) {
puts_(" (вход ");
puts_(src[ci.pll_src]);
puts_(")");
}
put_mhz(": SYSCLK ", ci.sysclk);
put_mhz(", HCLK ", ci.hclk);
put_mhz(", APB1 ", ci.pclk1);
put_mhz(", APB2 ", ci.pclk2);
if (ci.src == CLK_SRC_PLL)
put_mhz(", USB ", ci.pll48);
puts_("\nFLASH_ACR.LATENCY ");
put_dec(FLASH->ACR & FLASH_ACR_LATENCY);
puts_(", PWR_CR.VOS ");
put_dec(PWR->CR >> 14 & 3u);
puts_("\n");
}
static void cmd_id(void)
{
const volatile uint32_t *uid = (const volatile uint32_t *)UID_BASE;
puts_("DEV_ID ");
put_hex(DBGMCU->IDCODE & 0xFFFu, 3); /* 0x423 — F401xB/C, 0x431 — F411, 0x413 — F407 */
puts_(", REV_ID ");
put_hex(DBGMCU->IDCODE >> 16, 4);
puts_(", Flash ");
put_dec(*(const volatile uint16_t *)FLASHSIZE_BASE);
puts_(" КБ, UID ");
put_hex(uid[2], 8);
put_hex(uid[1], 8);
put_hex(uid[0], 8);
puts_("\n");
}
static void cmd_fpu(void)
{
volatile float x = 2.0f; /* volatile: не дать компилятору сосчитать заранее */
float r = __builtin_sqrtf(x); /* одна команда VSQRT.F32 с -fno-math-errno */
puts_("sqrt(2) = ");
uint32_t u = (uint32_t)(r * 1000000.0f + 0.5f); /* миллионные доли, с округлением */
put_dec(u / 1000000u);
putc_('.');
for (uint32_t d = 100000u; d; d /= 10u) /* шесть знаков, с ведущими нулями */
putc_((char)('0' + u / d % 10u));
puts_("\n");
}
static void run(struct cmd c)
{
switch (c.id) {
case CMD_EMPTY: break;
case CMD_HELP: puts_("help ver clk id led on|off|blink fpu dfu\n"); break;
case CMD_VER: puts_(BOARD_NAME " " VERSION "\n"); break;
case CMD_CLK: cmd_clk(); break;
case CMD_ID: cmd_id(); break;
case CMD_FPU: cmd_fpu(); break;
case CMD_LED:
blink = c.arg == 2;
if (c.arg < 2)
pin_write(LED_PORT, LED_PIN, c.arg ? LED_ON : !LED_ON);
puts_("ok\n");
break;
case CMD_DFU:
puts_("ухожу в загрузчик: dfu-util -d 0483:df11\n");
while (!(CON_USART->SR & USART_SR_TC)) { /* дождаться, пока последний байт уйдёт в линию */
}
boot_request();
break;
case CMD_TOOLONG: puts_("? строка длиннее 31 символа\n"); break;
case CMD_BADARG: puts_("? аргумент\n"); break;
default: puts_("? help — список команд\n"); break;
}
}
void HardFault_Handler(void)
{
puts_("\nHardFault: CFSR ");
put_hex(SCB->CFSR, 8);
puts_(", HFSR ");
put_hex(SCB->HFSR, 8);
puts_(", BFAR ");
put_hex(SCB->BFAR, 8); /* адрес, если в CFSR стоит BFARVALID */
puts_("\n");
for (;;) {
}
}
int main(void)
{
static const char *const why[] = {"", "кварц не запустился", "PLL не захватилась", "задержки Flash", "переключение"};
int rc = clock_init();
SysTick_Config(SystemCoreClock / 1000u);
pin_mode(LED_PORT, LED_PIN, 1u, 0u);
pin_write(LED_PORT, LED_PIN, !LED_ON);
uart_init();
puts_("\n" BOARD_NAME " " VERSION "\n");
if (rc != CLOCK_OK) {
puts_("такт: ");
puts_(why[rc]);
puts_(", работаю от HSI 16 МГц\n");
}
struct line l;
line_init(&l);
puts_("> ");
for (;;) {
if (!(CON_USART->SR & USART_SR_RXNE))
continue;
char echo[4];
if (line_feed(&l, (char)CON_USART->DR, echo)) {
raw(echo);
run(cmd_parse(&l));
line_init(&l);
puts_("> ");
} else {
raw(echo);
}
}
}
boot.h — вход в заводской загрузчик из прошивки.
/* boot.h — раздел 2ж: уйти в заводской загрузчик (USB DFU) из прошивки, без кнопки BOOT0. */
#ifndef BOOT_H
#define BOOT_H
#define BOOT_MAGIC 0xDF00B007u
void boot_request(void); /* из приложения: признак в конце ОЗУ и программный сброс */
void boot_early(void); /* первой строкой Reset_Handler: признак есть — прыжок в системную память */
#endif
boot.c — признак в ОЗУ, сброс и прыжок в системную память.
/* boot.c — раздел 2ж: вход в заводской загрузчик F401/F411 из прошивки — признак в ОЗУ и сброс.
* Сброс возвращает такты и периферию в исходное состояние сам, поэтому прыжок делается первой строкой
* Reset_Handler, пока ничего не включено (подробно оба пути — в разделе 48д.3).
* Системная память — 0x1FFF0000 у F401, F411 и F407 (AN2606, таблица 68). */
#include "board.h"
#include CHIP_HEADER
#include "boot.h"
extern volatile uint32_t _boot_flag[2]; /* два слова в конце ОЗУ, вне .data и .bss (*.ld) */
#ifdef QEMU_FAKE_SYSMEM
extern const uint32_t fake_sysmem[2]; /* опыт в QEMU: «системная память» внутри образа */
#define SYSMEM ((uint32_t)fake_sysmem)
#else
#define SYSMEM 0x1FFF0000u
#endif
void boot_request(void)
{
_boot_flag[0] = BOOT_MAGIC;
_boot_flag[1] = ~BOOT_MAGIC; /* второе слово: мусор в ОЗУ после включения не сойдёт за признак */
__DSB();
NVIC_SystemReset();
}
void boot_early(void)
{
if (_boot_flag[0] != BOOT_MAGIC || _boot_flag[1] != ~BOOT_MAGIC)
return;
_boot_flag[0] = 0u; /* прочитали — стёрли, иначе после выхода из загрузчика снова туда */
_boot_flag[1] = 0u;
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
(void)RCC->APB2ENR;
SYSCFG->MEMRMP = 1u; /* MEM_MODE = 01: по адресу 0 — системная память */
SCB->VTOR = SYSMEM;
__DSB();
__ISB();
uint32_t sp = ((const volatile uint32_t *)SYSMEM)[0];
uint32_t pc = ((const volatile uint32_t *)SYSMEM)[1];
__asm volatile("msr msp, %0\n"
"bx %1\n"
:
: "r"(sp), "r"(pc)
: "memory");
for (;;) {
}
}
pll.c — перебор M, N, P, Q главной PLL.
/* pll.c — раздел 2ж: перебор всех M, N, P, Q главной PLL для кварца платы.
* Ограничения — из описаний RCC_PLLCFGR: PLLM 2…63, вход VCO 1…2 МГц, PLLN 50…432 (у F401 — 192…432),
* выход VCO 100…432 МГц (у F401 — 192…432), PLLP 2/4/6/8, PLLQ 2…15, выход Q не выше 48 МГц.
* Вопрос: какая наибольшая SYSCLK вообще и при ровно 48 МГц для USB. */
#include <stdio.h>
#include <stdint.h>
struct chip { const char *name; uint64_t hse, sys_max, n_min, vco_min; };
static void scan(const struct chip *c)
{
unsigned total = 0, usb = 0;
uint64_t best_num = 0, best_den = 1, busb_num = 0, busb_den = 1;
unsigned bm = 0, bn = 0, bp = 0, um = 0, un = 0, up = 0, uq = 0;
for (unsigned m = 2; m <= 63; m++) {
if (c->hse < 1000000u * m || c->hse > 2000000u * m) /* вход VCO 1…2 МГц: hse/m */
continue;
for (unsigned n = (unsigned)c->n_min; n <= 432; n++) {
uint64_t vco_x_m = c->hse * n; /* VCO · m — без дробей */
if (vco_x_m < c->vco_min * m || vco_x_m > 432000000ull * m)
continue;
for (unsigned p = 2; p <= 8; p += 2) {
if (vco_x_m > c->sys_max * m * p) /* SYSCLK = vco_x_m / (m · p) */
continue;
for (unsigned q = 2; q <= 15; q++) {
if (vco_x_m > 48000000ull * m * q)
continue; /* выход Q выше 48 МГц — нельзя */
total++;
int exact = vco_x_m == 48000000ull * m * q;
usb += exact;
/* сравнить дроби vco_x_m / (m·p) */
if (vco_x_m * best_den > best_num * m * p) {
best_num = vco_x_m, best_den = (uint64_t)m * p;
bm = m, bn = n, bp = p;
}
if (exact && (vco_x_m * busb_den > busb_num * m * p
|| (vco_x_m * busb_den == busb_num * m * p && m < um))) {
busb_num = vco_x_m, busb_den = (uint64_t)m * p;
um = m, un = n, up = p, uq = q;
}
}
}
}
}
printf("%s hse=%llu: configs=%u usb48_configs=%u\n", c->name, (unsigned long long)c->hse, total, usb);
printf(" max_sysclk=%.3f MHz (m=%u n=%u p=%u)\n", (double)best_num / best_den / 1e6, bm, bn, bp);
printf(" max_sysclk_usb48=%.3f MHz (m=%u n=%u p=%u q=%u, vco_in=%.3f MHz)\n",
(double)busb_num / busb_den / 1e6, um, un, up, uq, (double)c->hse / um / 1e6);
}
int main(void)
{
static const struct chip chips[] = {
{"F411", 25000000u, 100000000u, 50, 100000000u},
{"F401", 25000000u, 84000000u, 192, 192000000u},
{"F407", 8000000u, 168000000u, 50, 100000000u},
{"F411_hse8", 8000000u, 100000000u, 50, 100000000u},
};
for (unsigned i = 0; i < sizeof chips / sizeof chips[0]; i++)
scan(&chips[i]);
return 0;
}
Дальше: раздел 3 «Язык C для встраиваемых систем» — типы, битовые операции,
volatile и то, почему компилятор иногда выбрасывает ваш код.