STM32 з нуля без HAL: Місяць 5, Тиждень 1: FreeRTOS на Blue Pill. Частина 15

Місяць 4 закрився Частиною 14 — драйвером для HC-SR04 і загадковою помилкою −517, яка ламала старі туторіали на сучасному ядрі. Місяць 5 за планом це “FreeRTOS багатозадачність на Blue Pill F103 + Python-автотести”. Сьогодні усе просто, LED і кнопка.

◆ Що вже було готово

Мій HAL до цього моменту (Місяці 1-4) — це власний GPIO/UART/SysTick/I2C/Timer, написаний з нуля без CubeMX, ще з Частини 1, де я вперше писав pinMode/digitalWrite через прямий доступ до регістрів. Працював стабільно на 8MHz HSI, бо Blue Pill стартує саме на цій частоті без додаткового налаштування.

FreeRTOS якщо використовуєш vTaskDelay то одразу впираєшся в питання: а на якій частоті взагалі рахувати ці мілісекунди?

◆ Крок 1 — перевірка заліза

Перш ніж лізти в код, я перевірив реальний Flash на моїй платі:

⚠️ Знов таки, на хабрахабрі та інших ресурсах ардуіно зустрічав твердження, що “у китайських Blue Pill насправді 128KB Flash замість заявлених 64KB”, але в мене можливо внаслідок того, що мої платки придбані років 10 тому тут не підтвердилась. Рівно 64KB, як і написано в даташиті. Сумно, але маємо, що маємо, може так навіть цікавіше буде. Але, що якщо не вистачить?

◆ Крок 2 — клонування FreeRTOS-Kernel

Версія V11.2.0 (LTS, MIT). З репозиторію взяв тільки те, що потрібно для Cortex-M3 без корутин (для F103 вони зайві):

Свідомо не брав croutine.c — корутини для ultra-low-memory систем, F103 в цьому не потребує.

◆ Крок 3 — де тримати два проєкти

Місяці 1-4 і Місяць 5 мають різну частоту ядра (8MHz проти 72MHz), тож потрібні два окремі build-конфіги. Але HAL — один. Правити hal_uart.c у двох копіях, таке собі задоволення.

Рішення: окремий каталог stm32_freertos/, а hal/ в ньому — symlink на оригінал:

Один hal/, дві точки використання. Правки видно одразу в обох проєктах, ізоляція — на рівні Makefile.

◆ Ґуля №1 — хардкод, який пощастило не помітити 4 місяці

Мій hal.h мав:

Простий план — перевизначити це на 72000000UL для FreeRTOS-збірки. Але перш ніж чіпати клок, я вирішив пошукати, де ще в HAL сидять приховані залежності від частоти. Знахідка виявилась серйознішою, ніж очікував.

Периферія STM32F103 тактується не від SYSCLK напряму, а від шин APB1/APB2. На 8MHz HSI усі прескалери дорівнюють 1, тому випадково всі шини рівні CPU_HZ — і хардкод 8000000 “працював” скрізь. На 72MHz це вирішується по-різному:

Шина 8 MHz 72 MHz Периферія
APB2 (PCLK2) 8 MHz 72 MHz = CPU_HZ USART1, SPI1, ADC1
APB1 (PCLK1) 8 MHz 36 MHz ≠ CPU_HZ (÷2) I2C1
Такт таймерів APB1 8 MHz 72 MHz = CPU_HZ (×2 quirk) TIM2..7

Мій hal_uart.c мав окремий, повністю незалежний від CPU_HZ хардкод — той самий регістр BRR, математику якого я розбирав ще в Частині 2 серії, коли з’ясовував чому software UART видає “крякозябри” на високих швидкостях:

Змінити тільки CPU_HZ і залишити це тоді UART почне видавати сміття одразу після переходу на 72MHz.

Необхідні правки це три похідні макроси в hal.h, кожен зі своїм #ifndef:

І відповідні правки в трьох файлах:

⚠️ Окрема проблема в середині проблеми: TIM2_PSC на 72MHz дає PSC=71999, а це 16-бітний регістр з межею 65535.

◆ Ґуля №2 — #ifndef замало

Мій план був: #define CPU_HZ 72000000UL перед #include "hal.h" у новому main.c. Здавалось логічним. Не спрацювало.

Причина проста, коли розумієш, що кожен .c файл це окремий translation unit. Makefile компілює hal_uart.c, hal_i2c.c, hal_tim.c окремо від main.c. #define у main.c просто не долітає до інших файлів, які взагалі не бачать цього рядка.

Робочий варіант — прапорець компілятора, спільний для всіх translation units:

#ifndef у hal.h — це саме те, що робить -D робочим. Форма “#define до #include” лишається валідною, але діє лише в межах одного файлу.

Перевірка, що стара збірка (без -D) не зламалась:

Байт-у-байт ідентичний закоміченій версії. HAL лишився зворотньо сумісним.

◆ Ґуля №3 — SysTick має двох власників одночасно

FreeRTOSConfig.h вимагає маперинг хендлерів:

Цей рядок буквально каже port.c визначити функцію з іменем SysTick_Handler. Але ж, мій hal_systick.c вже мав власний SysTick_Handler, interrupt-based, для рахунку _ticks:

Лінкер:

SysTick може мати тільки одного власника. Без FreeRTOS — це HAL. З FreeRTOS — це port.c, і HAL має делегувати йому керування.

Рішення — #ifdef FREERTOS розділення в hal_systick.c:

І -DFREERTOS до CFLAGS у stm32_freertos/Makefile (тільки там, не в основному HAL-проєкті).

◆ Ґуля №4 — FreeRTOS не bare-metal-панк, на відміну від мого HAL

Знахідка Лінкер:

Мій Makefile з самого початку серії використовує -nostdlib  ми йдемо без стандартної бібліотеки C, бо суть в тому, що HAL пишеться “з нуля”. Але heap_4.c і tasks.c у FreeRTOS використовують memset/memcpy з <string.h>, а без libc де їх взяти.

Два шляхи: дозволити мінімальний libc через --specs=nano.specs, або написати власні реалізації. Обрав друге — зберігає принцип серії:

◆ Ґуля №5 — конфіг-макроси

V11.2.0 вимагає явних відповідей на питання, яких я не бачив перед цим. Кожна помилка компіляції додавала по одному-два макроси:

⚠️ configMAX_SYSCALL_INTERRUPT_PRIORITY — не довільне число. F103 має лише 4 біти пріоритету в NVIC (не всі 8), тому зсув << (8 - configPRIO_BITS) обов’язковий. Без нього завищений або занижений пріоритет тихо ламає ISR-safe.

◆ Ґуля №6 — st-flash не бачить таргет після зміни коду

st-info --probe бачив пристрій нормально, а st-flash write — ні. MCU, схоже, встиг перейти в стан, де звичайний halt через SWD не проходить.

Спрацювало одразу. Заніс у Makefile як стандартний flash target.

◆ Ґуля №7 — git stash і build-артефакти в репозиторії

Під час паралельної роботи над HAL (спільний файл через symlink) заплутався, в якому каталозі редагую Makefile, і зробив git stash щоб розібратись. git stash pop відмовився спрацювати:

Причина: .elf/.bin файли були в git. Кожен make перезаписує їх новим build-артефактом — і це постійно створює “брудні” локальні зміни, які конфліктують з будь-якою git-операцією.

Бінарники мали бути в .gitignore з першого дня, а не з п’ятого місяця, але що вже поробиш

◆ Ґуля №8 — обіцянка компілятору

Зібрав, прошив два таски (LED-блимання 500мс + кнопка на PA1). Перший запуск:

LED блимав з інтервалом ~5 секунд замість очікуваної 1 секунди.

configCPU_CLOCK_HZ = 72000000UL у FreeRTOSConfig.h — це лише число для розрахунку SysTick->LOAD. Воно нічого не каже реальному апаратному генератору тактової частоти. Я задав FreeRTOS “рахуй так, ніби 72MHz”, а RCC фізично досі стояв на HSI 8MHz — PLL ніхто не вмикав.

Математика сходиться ідеально: 72000000 / 8000000 = 9. Кожен запланований 1мс-тік реально тривав 9мс. vTaskDelay(500) → 4500мс замість 500мс. Період блимання ~5 секунд — рівно те, що я побачив на платі.

Число в конфізі це просто лише обіцянка компілятору, що так буде, а от PLL-код це вже фізична дія безпосередньо на залізі.

◆ PLL: HSE × 9 = 72MHz

На платі є кварц HSE 8MHz (маркування “8.000” біля USB-роз’єму). Стандартний шлях для Blue Pill.

Крок Регістр (адреса) Біти Дія
1. HSE ON RCC_CR (0x40021000) HSEON = біт 16 → 1, чекаємо HSERDY = біт 17 Запуск зовнішнього кварца, busy-wait з таймаутом
2. Flash latency FLASH_ACR (0x40022000) LATENCY [2:0] = 0b010 (2 WS), PRFTBE = біт 4 = 1 Обов’язково до підняття частоти: 0 WS ≤24MHz, 1 WS ≤48MHz, 2 WS ≤72MHz
3. Прескалери шин RCC_CFGR (0x40021004) HPRE [7:4] = 0000 (AHB /1), PPRE1 [10:8] = 100 (APB1 /2), PPRE2 [13:11] = 0xx (APB2 /1) APB1 max 36MHz — ділимо. AHB і APB2 тримають 72MHz
4. Конфіг PLL RCC_CFGR PLLSRC = біт 16 = 1 (HSE), PLLXTPRE = біт 17 = 0, PLLMUL [21:18] = 0111 (×9) 8MHz × 9 = 72MHz
5. PLL ON RCC_CR PLLON = біт 24 → 1, чекаємо PLLRDY = біт 25 Локінг PLL, таймаут з fallback
6. Switch SYSCLK RCC_CFGR SW [1:0] = 10, чекаємо SWS [3:2] == 10 Апаратне підтвердження реального переключення

Кожен таймаут це лічильник, а не while(1). При збої функція вимикає щойно ввімкнене (HSEON/PLLON), при цьому не перемикає SW, і плата лишається на HSI 8MHz — працює, хоч і повільніше.

Прошив, оп-ля  інтервал точно 1 секунда. PLL реально на 72MHz, vTaskDelay рахує правильно.

Замкнув PA1 на GND імітиуючи натискання кнопки і  LED почав швидко мигтіти 10 разів, потім повернувся до звичайного ритму, як і задумано.

◆ Бюджет пам’яті

Зайнято Доступно %
Flash 5837 байт 65536 байт 8.9%
SRAM 12532 байт 20480 байт 61.2%

Flash — величезний запас, майже 60KB вільно на весь код серії наперед.

SRAM — інша картина. heap_4.c виділяє статичний масив ucHeap[] одним шматком, розміром configTOTAL_HEAP_SIZE (12*1024) на перший погляд може здатися, що щось пішло не так, але це не “витрачені 12KB, а стеки десь ще” — навпаки: стеки обох тасків (xTaskCreate(..., 128, ...) — 128 слів × 4 байти = 512 байт мінімум кожен) виділяються з цього самого 12KB-бюджету через pvPortMalloc, так само як і будь-яка черга чи семафор. 12KB — це запас під усе майбутнє динамічне виділення одразу, а не порожня трата.

⚠️ Історична довідка. Vanilla FreeRTOS (репозиторій FreeRTOS-Kernel з GitHub, ARM_CM3 GCC порт, який використовується) визначає usStackDepth у словах, а не байтах. Це стандартна поведінка, яка задокументована офіційно: StackType_t на 32-бітній архітектурі = uint32_t = 4 байти, і xTaskCreate(..., 128, ...) виділяє 128 × 4 = 512 байт.

Єдиний виняток — ESP-IDF-форк FreeRTOS (Espressif для ESP32), ось там розробники свідомо змінили конвенцію на байти замість слів, і це джерело плутанини в інтернеті (купа заплутаних форумних постів).

Залишок SRAM поза цим бюджетом (20480 – 12288 = 8192 байт) — стек main() до старту планувальника, глобальні змінні HAL, вектор переривань. Тому основна мораль для початківця в RTOS  “плануй heap заздалегідь, бо на F103 з 20KB SRAM резерв під RTOS — це відчутна частка всього бюджету, і краще знати про це на старті, ніж з’ясувати постфактум через pvPortMalloc що повертає NULL”.

Для порівняння: весь HAL без FreeRTOS (Місяці 1-4) важив 1859 байт Flash і 4 байти SRAM. Ціна входу в RTOS на цьому мікроконтролері — реальна, і її варто знати наперед.

Попередня Частині 14 , ну а наступна стаття — черги й семафори, де: UART-таск читає команди, LED-таск виконує, mutex захищає спільний ресурс. Можна освіжити пам’ять перечитавши Частину 2 UART. Отже, поступову і нудно плавно вибудовується архітектура, яка знадобиться шостому місяці для TDOA-мікрофонної мережі.

Опубліковано в STM32

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *