Місяць 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 на моїй платі:
|
1 2 3 4 5 6 |
st-info --probe flash: 65536 (pagesize: 1024) sram: 20480 chipid: 0x0410 descr: F1xx Medium-density |
⚠️ Знов таки, на хабрахабрі та інших ресурсах ардуіно зустрічав твердження, що “у китайських Blue Pill насправді 128KB Flash замість заявлених 64KB”, але в мене можливо внаслідок того, що мої платки придбані років 10 тому тут не підтвердилась. Рівно 64KB, як і написано в даташиті. Сумно, але маємо, що маємо, може так навіть цікавіше буде. Але, що якщо не вистачить?
◆ Крок 2 — клонування FreeRTOS-Kernel
|
1 |
git clone https://github.com/FreeRTOS/FreeRTOS-Kernel.git FreeRTOS-Kernel |
Версія V11.2.0 (LTS, MIT). З репозиторію взяв тільки те, що потрібно для Cortex-M3 без корутин (для F103 вони зайві):
|
1 2 3 4 5 6 7 8 9 |
mkdir -p freertos/include cp FreeRTOS-Kernel/tasks.c freertos/ cp FreeRTOS-Kernel/queue.c freertos/ cp FreeRTOS-Kernel/list.c freertos/ cp FreeRTOS-Kernel/portable/GCC/ARM_CM3/port.c freertos/ cp FreeRTOS-Kernel/portable/GCC/ARM_CM3/portmacro.h freertos/ cp FreeRTOS-Kernel/portable/MemMang/heap_4.c freertos/ cp -r FreeRTOS-Kernel/include/* freertos/include/ |
Свідомо не брав croutine.c — корутини для ultra-low-memory систем, F103 в цьому не потребує.
◆ Крок 3 — де тримати два проєкти
Місяці 1-4 і Місяць 5 мають різну частоту ядра (8MHz проти 72MHz), тож потрібні два окремі build-конфіги. Але HAL — один. Правити hal_uart.c у двох копіях, таке собі задоволення.
Рішення: окремий каталог stm32_freertos/, а hal/ в ньому — symlink на оригінал:
|
1 2 3 4 5 6 |
mkdir -p stm32_freertos/src stm32_freertos/ld cd stm32_freertos ln -s ../HAL_stm32/hal hal ln -s ../../HAL_stm32/src/startup.c src/startup.c ln -s ../../HAL_stm32/ld/stm32f103.ld ld/stm32f103.ld mv ../HAL_stm32/freertos ./freertos |
Один hal/, дві точки використання. Правки видно одразу в обох проєктах, ізоляція — на рівні Makefile.
◆ Ґуля №1 — хардкод, який пощастило не помітити 4 місяці
Мій hal.h мав:
|
1 |
#define CPU_HZ 8000000UL |
Простий план — перевизначити це на 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 видає “крякозябри” на високих швидкостях:
|
1 |
USART1_BRR = 8000000UL / baud; |
Змінити тільки CPU_HZ і залишити це тоді UART почне видавати сміття одразу після переходу на 72MHz.
Необхідні правки це три похідні макроси в hal.h, кожен зі своїм #ifndef:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#ifndef CPU_HZ #define CPU_HZ 8000000UL #endif #ifndef PCLK1_HZ #define PCLK1_HZ CPU_HZ // APB1: I2C1, TIM2..7 #endif #ifndef PCLK2_HZ #define PCLK2_HZ CPU_HZ // APB2: USART1, SPI1, ADC1 #endif #ifndef TIMER_CLK_HZ #define TIMER_CLK_HZ CPU_HZ #endif |
І відповідні правки в трьох файлах:
|
1 2 3 4 5 6 7 8 9 10 |
// hal_uart.c USART1_BRR = PCLK2_HZ / baud; // hal_i2c.c I2C1_CR2 = PCLK1_HZ / 1000000UL; I2C1_CCR = PCLK1_HZ / (2UL * 100000UL); I2C1_TRISE = (PCLK1_HZ / 1000000UL) + 1; // hal_tim.c TIM2_PSC = (TIMER_CLK_HZ / 1000UL) - 1; |
⚠️ Окрема проблема в середині проблеми:
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:
|
1 2 3 4 |
CFLAGS = -mcpu=cortex-m3 -mthumb -O0 -g \ -Wall -ffunction-sections -fdata-sections \ -Ihal -Ifreertos -Ifreertos/include -I. \ -DCPU_HZ=72000000UL -DPCLK1_HZ=36000000UL |
#ifndef у hal.h — це саме те, що робить -D робочим. Форма “#define до #include” лишається валідною, але діє лише в межах одного файлу.
Перевірка, що стара збірка (без -D) не зламалась:
|
1 2 |
make clean && make md5sum hal_blink.bin |
Байт-у-байт ідентичний закоміченій версії. HAL лишився зворотньо сумісним.
◆ Ґуля №3 — SysTick має двох власників одночасно
FreeRTOSConfig.h вимагає маперинг хендлерів:
|
1 |
#define xPortSysTickHandler SysTick_Handler |
Цей рядок буквально каже port.c визначити функцію з іменем SysTick_Handler. Але ж, мій hal_systick.c вже мав власний SysTick_Handler, interrupt-based, для рахунку _ticks:
|
1 2 3 |
void SysTick_Handler(void) { _ticks++; } |
Лінкер:
|
1 |
multiple definition of `SysTick_Handler' |
SysTick може мати тільки одного власника. Без FreeRTOS — це HAL. З FreeRTOS — це port.c, і HAL має делегувати йому керування.
Рішення — #ifdef FREERTOS розділення в hal_systick.c:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#ifdef FREERTOS #include "FreeRTOS.h" #include "task.h" void systick_init(uint32_t cpu_hz) { (void) cpu_hz; // FreeRTOS сам налаштує SysTick через port.c } void delay_ms(uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); } uint32_t get_ticks(void) { return xTaskGetTickCount(); } #else // старий interrupt-based код, без змін #endif |
І -DFREERTOS до CFLAGS у stm32_freertos/Makefile (тільки там, не в основному HAL-проєкті).
◆ Ґуля №4 — FreeRTOS не bare-metal-панк, на відміну від мого HAL
Знахідка Лінкер:
|
1 |
undefined reference to `memset' |
Мій Makefile з самого початку серії використовує -nostdlib ми йдемо без стандартної бібліотеки C, бо суть в тому, що HAL пишеться “з нуля”. Але heap_4.c і tasks.c у FreeRTOS використовують memset/memcpy з <string.h>, а без libc де їх взяти.
Два шляхи: дозволити мінімальний libc через --specs=nano.specs, або написати власні реалізації. Обрав друге — зберігає принцип серії:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// freertos/freertos_string.c #include <stddef.h> void *memset(void *dst, int val, size_t len) { unsigned char *d = dst; while (len--) *d++ = (unsigned char)val; return dst; } void *memcpy(void *dst, const void *src, size_t len) { unsigned char *d = dst; const unsigned char *s = src; while (len--) *d++ = *s++; return dst; } |
◆ Ґуля №5 — конфіг-макроси
V11.2.0 вимагає явних відповідей на питання, яких я не бачив перед цим. Кожна помилка компіляції додавала по одному-два макроси:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#define configUSE_16_BIT_TICKS 0 // 32-біт лічильник тіків #define INCLUDE_vTaskDelay 1 // без цього vTaskDelay не компілюється #define configUSE_IDLE_HOOK 0 #define configUSE_TICK_HOOK 0 #define configUSE_MALLOC_FAILED_HOOK 0 #define configCHECK_FOR_STACK_OVERFLOW 0 #define configPRIO_BITS 4 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 #define configKERNEL_INTERRUPT_PRIORITY \ ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) ) #define configMAX_SYSCALL_INTERRUPT_PRIORITY \ ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) ) |
⚠️ configMAX_SYSCALL_INTERRUPT_PRIORITY — не довільне число. F103 має лише 4 біти пріоритету в NVIC (не всі 8), тому зсув << (8 - configPRIO_BITS) обов’язковий. Без нього завищений або занижений пріоритет тихо ламає ISR-safe.
◆ Ґуля №6 — st-flash не бачить таргет після зміни коду
|
1 |
ERROR common.c: Can not connect to target. Please use 'connect under reset' and try again |
st-info --probe бачив пристрій нормально, а st-flash write — ні. MCU, схоже, встиг перейти в стан, де звичайний halt через SWD не проходить.
|
1 |
st-flash --connect-under-reset write freertos_blink.bin 0x08000000 |
Спрацювало одразу. Заніс у Makefile як стандартний flash target.
◆ Ґуля №7 — git stash і build-артефакти в репозиторії
Під час паралельної роботи над HAL (спільний файл через symlink) заплутався, в якому каталозі редагую Makefile, і зробив git stash щоб розібратись. git stash pop відмовився спрацювати:
|
1 2 |
error: Ваші локальні зміни у наступних файлах будуть перезаписані під час злиття: hal_blink.elf |
Причина: .elf/.bin файли були в git. Кожен make перезаписує їх новим build-артефактом — і це постійно створює “брудні” локальні зміни, які конфліктують з будь-якою git-операцією.
|
1 2 3 4 5 6 |
git checkout -- hal_blink.elf git stash pop echo "*.elf" >> .gitignore echo "*.bin" >> .gitignore git rm --cached hal_blink.elf hal_blink.bin |
Бінарники мали бути в .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 2 |
rcc_set_pll_72mhz(); // ПЕРЕД hal_init() hal_init(); |
Прошив, оп-ля інтервал точно 1 секунда. PLL реально на 72MHz, vTaskDelay рахує правильно.
Замкнув PA1 на GND імітиуючи натискання кнопки і LED почав швидко мигтіти 10 разів, потім повернувся до звичайного ритму, як і задумано.
◆ Бюджет пам’яті
|
1 2 |
text data bss dec hex 5837 8 12532 18377 47c9 |
| Зайнято | Доступно | % | |
|---|---|---|---|
| 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-мікрофонної мережі.
