STM32 без магії: переривання

Минулого разу ми написали свій HAL — gpio_init, uart_printf, delay_ms. Все працювало, Blue Pill моргав, UART виводив текст, і здавалось що life is good.

А потім я захотів зробити кнопку.

Не polling кнопку — де ти в while(1) постійно питаєш «ну що там, натиснута?». А нормальну — щоб процесор сам дізнавався коли щось сталось. Як дзвінок у двері замість того щоб кожну секунду виглядати у вікно.

Це називається переривання (interrupt). І ось тут почалось справжнє навчання.

Що таке переривання — без занудства

Уяви що процесор читає книгу (while(1)). Ти натискаєш кнопку — це як дзвінок у двері. Процесор закладає сторінку, йде відчиняти (виконує ISR — Interrupt Service Routine), і повертається читати з того ж місця.

main(): ████████████████░░░░████████████████

↑↑↑↑

ISR: ████ ← кнопка!

Головна перевага — main не витрачає час на постійну перевірку. Він займається своїми справами, а переривання само прилетить коли треба.

На STM32F103 зовнішні переривання від GPIO обробляє модуль EXTI (External Interrupt Controller). І щоб кнопка запрацювала — треба налаштувати цілий ланцюжок.

Ланцюжок: від кнопки до функції

Кнопка натиснута → PA1 впав до 0

AFIO — маршрутизатор: який порт слухає лінію EXTI1?

EXTI — детектор: побачив falling edge, виставив прапорець

NVIC — менеджер переривань: отримав IRQ7, зупинив main

EXTI1_IRQHandler() — твій код

Чотири блоки, чотири налаштування. Пропустив один — не працює. Без підказок.

 

Налаштування крок за кроком

1. RCC — тактування

Перш за все — вмикаємо тактування. Без цього регістри периферії просто не реагують на запис. Це як вмикати вимикач перед тим як крутити ручки.

AFIO — це окремий модуль і окремий біт в RCC. Якщо забути — EXTI просто не підключений до пінів. Тиша.

2. GPIO — пін як вхід з підтяжкою

Кнопка підключена між PA1 і GND. В спокої — HIGH (підтяжка тримає). Натиснута — LOW. Саме це і ловимо.

3. AFIO — маршрутизатор

Ось де стає цікаво. У STM32F103 є 16 ліній EXTI (EXTI0-EXTI15), але на кожну лінію можуть претендувати піни з різних портів:
PA1, PB1, PC1 — всі йдуть на лінію EXTI1

Процесор не може слухати їх одночасно. AFIO — це мультиплексор який вирішує хто саме.

Правило просте: номер піна = номер лінії EXTI. PA1 → EXTI1, PB5 → EXTI5, PC13 → EXTI13. Порт вибираємо в AFIO.

4. EXTI — детектор події

FTSR — Falling Trigger Selection Register. Реагуємо на спад напруги (кнопка натиснута).
IMR — Interrupt Mask Register. Без цього переривання замасковане і не пройде далі.

5. NVIC — головний менеджер

NVIC (Nested Vectored Interrupt Controller) — це вбудований в Cortex-M3 менеджер всіх переривань. Він вирішує хто важливіший, ставить в чергу, і каже процесору куди стрибати.

ISER0 — Interrupt Set Enable Register 0. Керує IRQ0-31. Записуємо 1 у відповідний біт — переривання дозволено.

Обробник переривання (ISR)

 

Два важливих моменти:

Чому EXTI_PR = (1 << 1) а не |=?

Цей регістр скидається записом 1, не 0. Звучить контрінтуїтивно але саме так. Якщо забути скинути — переривання буде викликатись нескінченно, main ніколи не продовжиться, і ти довго дивитимешся на завислу програму.

Ім’я функції — не випадкове.

EXTI1_IRQHandler — це точне ім’я яке має збігатись з таблицею векторів. Помилився в одній літері — функція не викликається. Тиша.

Таблиця векторів — найбільший сюрприз

Ось де я застряг найдовше.

Написав код, прошив, натиснув кнопку — нічого. LED мовчить. Код компілюється. Схема правильна. Що не так?

Відповідь: таблиця векторів у startup.c закінчувалась на SysTick і не мала жодного периферійного IRQ.

Коли стається EXTI1 — процесор іде на позицію 23 в таблиці векторів. А там… нуль. Або сміття. Або Default_Handler. Результат — тиша або HardFault.

Що таке таблиця векторів насправді

Це не регістри. Це масив адрес у Flash-пам’яті за адресою 0x08000000.
Регістри (RCC, GPIO, NVIC) — ти керуєш ними вручну, читаєш і пишеш коли треба.
Таблицю векторів — процесор читає сам, автоматично, коли стається переривання.
Подія: натиснута кнопка (EXTI1)
Процесор: іду до позиції 23 в таблиці
Таблиця: тут адреса EXTI1_IRQHandler
Процесор: стрибаю туди

Аналогія: таблиця векторів — це список номерів екстрених служб на стіні. Якщо пожежа — пілот не шукає кнопку «гасити», він дивиться на список і дзвонить пожежникам.

Де взяти правильні позиції?

RM0008, розділ 10.1.2, Table 63 — «Vector table for other STM32F10xxx devices».

Важливо: є ще Table 61 — для connectivity line (STM32F105/107). Не плутати!

Позиція 0 → початок стеку (_estack)
Позиція 1 → Reset_Handler
Позиція 2 → NMI_Handler

Позиція 15 → SysTick_Handler
Позиція 16 → IRQ0 (WWDG)
Позиція 17 → IRQ1 (PVD)

Позиція 22 → IRQ6 (EXTI0)
Позиція 23 → IRQ7 (EXTI1) ← наша функція

Позиція 37+16 → IRQ37 (USART1)

Критичне правило: таблиця — це фіксований масив з точними позиціями. Між SysTick (позиція 15) і EXTI1 (позиція 23) є 7 записів які не можна пропустити. Навіть якщо IRQ не використовується — його місце треба заповнити (хоча б Default_Handler).

 

EXTI має 16 ліній але векторів менше — чому?

Це питання виникло коли я порівняв Figure 21 з RM0008 (AFIO схема — 16 ліній) і Table 63 (таблиця векторів).

Figure 21 показує скільки ліній є фізично.

Table 63 показує скільки векторів під них виділено.

ST зекономив: EXTI5-9 і EXTI10-15 йдуть в групові handler’и:
EXTI0 → окремий IRQ6 → EXTI0_IRQHandler
EXTI1 → окремий IRQ7 → EXTI1_IRQHandler
EXTI2 → окремий IRQ8 → EXTI2_IRQHandler
EXTI3 → окремий IRQ9 → EXTI3_IRQHandler
EXTI4 → окремий IRQ10 → EXTI4_IRQHandler
EXTI5-9 → спільний IRQ23 → EXTI9_5_IRQHandler
EXTI10-15 → спільний IRQ40 → EXTI15_10_IRQHandler

Якщо кнопка на PA7 — пишеш EXTI9_5_IRQHandler і всередині перевіряєш який саме біт в EXTI_PR.

NVIC_ISER0 vs NVIC_ISER1

Ще одна граблина яку я зловив при UART RX через переривання.

weak і alias — як це все не падає

В startup.c є магічний рядок:

 

weak — ця функція «слабка». Якщо де-інде є «сильна» версія з тим самим іменем — лінкер візьме її.

alias — якщо «сильної» немає — використай Default_Handler.

Результат: якщо ти написав void EXTI1_IRQHandler(void) у main.c — лінкер підставить твою функцію в таблицю векторів автоматично. Не написав — летить у Default_Handler (нескінченний цикл, але не падіння).

Це саме те що ST робить у своєму startup_stm32f103xb.s — тільки на асемблері.

Шпаргалка: пін → IRQ → NVIC

Пін Лінія EXTI IRQ NVIC регістр Назва функції
Px0 EXTI0 6 ISER0 біт 6 EXTI0_IRQHandler
Px1 EXTI1 7 ISER0 біт 7 EXTI1_IRQHandler
Px2 EXTI2 8 ISER0 біт 8 EXTI2_IRQHandler
Px3 EXTI3 9 ISER0 біт 9 EXTI3_IRQHandler
Px4 EXTI4 10 ISER0 біт 10 EXTI4_IRQHandler
Px5-9 EXTI9_5 23 ISER0 біт 23 EXTI9_5_IRQHandler
Px10-15 EXTI15_10 40 ISER1 біт 8 EXTI15_10_IRQHandler
USART1 37 ISER1 біт 5 USART1_IRQHandler
TIM2 28 ISER0 біт 28 TIM2_IRQHandler

Повний код BUTTON_EXTI

Частина 2 — Таймери: SysTick, TIM2 і PWM

Чому polling — це погано

Наш старий delay_ms виглядав так:

Процесор буквально нічого не робить — крутиться в циклі і чекає. Хочеш мигати LED і одночасно читати кнопку? Не вийде. Поки висиш в delay_ms — кнопка ігнорується.

Рішення — SysTick через переривання.

SysTick IRQ — системний таймер Cortex-M3

SysTick — це вбудований таймер ядра Cortex-M3. Він є на будь-якому Cortex-M незалежно від виробника. Три регістри:

STK_LOAD — до якого числа рахуємо вниз
STK_VAL — поточне значення
STK_CTRL — керування

Раніше ми використовували STK_CTRL_COUNTFLAG — polling. Тепер вмикаємо STK_CTRL_TICKINT — один біт який перетворює таймер на джерело переривань:

 

Тепер delay_ms не вимикає таймер і не блокує переривання. Поки він чекає — ISR від кнопки все одно спрацює.

Чому volatile?

_ticks змінюється в SysTick_Handler — а читається в main. Без volatile компілятор може вирішити що значення не змінюється і закешує його в регістр. Тоді while (_ticks – start < ms) буде вічним циклом. volatile каже компілятору: «не оптимізуй, завжди читай з пам’яті».

Чому _ticks – start а не _ticks >= start + ms?

Overflow-safe патерн. _ticksuint32_t, і через ~49 днів він переповниться і стане 0. Якщо порівнювати через >= — після переповнення умова ніколи не виконається. Різниця _ticks – start працює правильно навіть при переповненні завдяки беззнаковій арифметиці.

SysTick — системний вектор, не периферійний

Важлива деталь: SysTick_Handler вже є в базовій таблиці векторів на позиції 15. Ніяких додаткових IRQ не треба — просто пишеш функцію і вона автоматично підставляється через WEAK_ALIAS.

// в startup.c вже є:

 

TIM2 — периферійний таймер ST

SysTick — простий таймер ядра, тільки рахує. TIM2 — повноцінний периферійний таймер STM32 з купою можливостей. Нас цікавлять дві: переривання по переповненню і PWM.

Головна відмінність від SysTick:
SysTick — завжди є, тактується від ядра, один на всіх
TIM2 — периферія ST, треба вмикати RCC, є prescaler і ARR

 

TIM2 переривання — мигаємо LED без delay

Два ключових регістри:

PSC (Prescaler) — ділить тактову частоту
ARR (Auto Reload) — до якого числа рахуємо

Коли лічильник досягає ARR — виставляється прапорець UIF (Update Interrupt Flag) і якщо дозволено — викликається переривання.

ISR:

 

Увага: TIM2_SR скидається записом 0, а EXTI_PR — записом 1. Різна периферія, різна логіка. Це не баг — так задумано в RM0008.

TIM2 на APB1, не APB2

Різні таймери сидять на різних шинах. TIM1 — на APB2. TIM2, TIM3, TIM4 — на APB1. Переплутав шину — таймер мовчить, регістри не реагують.

 

PWM — плавне дихання LED

PWM (Pulse Width Modulation) це швидке перемикання піна між HIGH і LOW. Якщо робити це достатньо швидко — людське око бачить проміжну яскравість.

Period: |←————————————→|

25% duty: ██░░░░░░░░░░░░░

50% duty: ██████░░░░░░░░░

75% duty: ██████████░░░░░

 

Два регістри:

ARR → визначає період (частоту PWM)
CCR1 → визначає duty cycle (скважність)

Змінюєш CCR1 від 0 до ARR — яскравість змінюється від 0% до 100%.

Який пін може виводити PWM?

Не будь-який. Тільки той що має Alternate Function прив’язку до каналу таймера.

PC13 — звичайний GPIO, немає AF до жодного таймера → PWM не вийде
PA0 — TIM2_CH1 → PWM тут!
PA1 — TIM2_CH2
PA2 — TIM2_CH3
PA3 — TIM2_CH4

Це визначено в datasheet STM32F103 — таблиця Alternate Function mapping.
Тому пін треба ініціалізувати як OUTPUT_AF_FAST (0xB) — Alternate Function Push-Pull 50MHz:

Без AF мод пін залишається під контролем GPIO, а не TIM2. PWM не буде навіть якщо таймер налаштований правильно.

Налаштування TIM2 PWM

 

Що таке PWM mode 1?

Коли лічильник менший за CCR1 — пін HIGH. Коли більший — LOW. Просто і елегантно:

CNT: 0 ────────────────────► ARR

|←── CCR1 ──►|

OUT: ████████████░░░░░░░░░░░

HIGH LOW

Змінюй TIM2_CCR1 в while(1) — і отримаєш плавне дихання:

 

Ключова фраза про PWM

Коли ми пишемо TIM2_CCER = (1 << 0) — апаратно TIM2 починає керувати піном PA0 сам, без участі процесора. Саме тому PWM не навантажує CPU — таймер генерує сигнал автономно, процесор займається іншим.

Порівняння: SysTick vs TIM2

SysTick TIM2
Де живе Ядро Cortex-M3 Периферія ST
RCC не потрібен APB1 біт 0
Таблиця векторів позиція 15 (системний) позиція 44 (IRQ28)
Prescaler немає є (PSC)
PWM не вміє вміє (4 канали)
Використання системний тік, delay_ms точні затримки, PWM, захоплення

 

Повний код TIM2_BLINK

Частина 3 — Протоколи: SPI і Nokia 5110, I2C і MPU6050

SPI — чотири дроти і повний контроль

SPI (Serial Peripheral Interface) — синхронний протокол. На відміну від UART де кожна сторона сама рахує час, тут є окремий дріт тактування. Master диктує темп, slave слухає.

Чотири дроти:
SCK — тактовий сигнал (master генерує)
MOSI — Master Out Slave In (дані від master до slave)
MISO — Master In Slave Out (відповідь від slave)
NSS — Chip Select, активний LOW

Nokia 5110 — тільки приймає, MISO не використовується. Плюс два додаткових GPIO якими керуємо вручну:
DC — 0 = команда, 1 = дані (пікселі)
RST — скидання дисплея

Як виглядає передача одного байта

NSS ‾‾‾‾\___________________________/‾‾‾‾

SCK _____/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\__

MOSI —–[7][6][5][4][3][2][1][0]———-

NSS тягнеш LOW → надсилаєш 8 біт старшим вперед (MSB first) → NSS назад HIGH. SCK генерує SPI периферія автоматично поки ти пишеш байт в DR.

Налаштування SPI1

SSM + SSI — програмне керування NSS. Ми самі опускаємо і піднімаємо CE через GPIOA_BSRR. Апаратний NSS (PA4) не використовуємо — він підходить тільки якщо один slave.

Відправка байта:

TXE — TX Empty, можна писати наступний байт.
BSY — SPI зайнятий, чекаємо до кінця передачі.

Nokia 5110 — ініціалізація і виведення

Nokia 5110 (контролер PCD8544) — 84×48 пікселів, організованих в 6 рядків по 8 пікселів кожен (так звані «pages»). Всього 504 байти відеопам’яті.

DC = LOW → команда. DC = HIGH → дані (пікселі).

 

Залити весь екран:

Вивести символ зі шрифту 5×8:

Шрифт — просто масив байт де кожен байт це колонка 8 пікселів:

 

I2C — два дроти і адреси

I2C (Inter-Integrated Circuit) — ще один протокол зв’язку, але принципово інший.

SPI: master керує CLK, окремий CS для кожного пристрою
4 дроти, але швидко і просто
I2C: тільки 2 дроти, кожен пристрій має унікальну адресу
менше проводів, але складніший протокол

Два дроти:
SCL — тактування (clock)
SDA — дані (bidirectional — і master і slave пишуть в один дріт!)

Важливо: I2C потребує зовнішніх підтягуючих резисторів на SCL і SDA до VCC. Зазвичай 4.7кОм. Без них шина просто не працює — лінії «плавають» і пристрої один одного не чують. Багато модулів MPU6050 вже мають резистори на платі.

Послідовність транзакції I2C

START → [addr+W] → ACK → [reg] → ACK → REPEATED START → [addr+R] → ACK → [data] → NACK → STOP

START — master тягне SDA LOW поки SCL HIGH
addr+W/R — 7-бітна адреса + біт напрямку (0=write, 1=read)
ACK — slave підтверджує кожен байт (тягне SDA LOW)
REPEATED START — повторний старт без STOP між записом і читанням
NACK — master не підтверджує останній байт читання → slave відпускає шину
STOP — master відпускає SDA

Чому Repeated Start?

Типова операція — записати адресу регістра, потім прочитати дані. Можна зробити STOP між ними, але деякі пристрої це не люблять — між STOP і START вони «забувають» яку адресу ти вказав. Repeated Start вирішує це елегантно.

Налаштування I2C1

 

CCR — Clock Control Register. Визначає швидкість SCL:
      CCR = fPCLK1 / (2 * fSCL)
      CCR = 8MHz / (2 * 100kHz) = 40

TRISE — максимальний час наростання фронту. Для 100кГц режиму:
TRISE = (tRISE_max / tPCLK1) + 1 = (1000нс / 125нс) + 1 = 9

Читання регістра MPU6050

Чому (void)I2C1_SR2?

Прапорець ADDR скидається тільки послідовним читанням SR1 і SR2. Якщо не прочитати SR2 — прапорець залишається, I2C зависає в очікуванні. (void) — щоб компілятор не ругався на невикористане значення.

MPU6050 — акселерометр і гіроскоп

MPU6050 — популярний IMU (Inertial Measurement Unit). Адреса 0x68 (якщо AD0 підтягнутий до GND).

Перевірка зв’язку — читаємо WHO_AM_I (регістр 0x75). Має повернути 0x68:
    uint8_t who = i2c_read(0x68, 0x75);
// якщо who == 0x68 — MPU6050 знайдений

Виводимо з режиму сну:
i2c_write(0x68, 0x6B, 0x00); // PWR_MGMT_1 = 0

Читаємо акселерометр — 6 байт (X, Y, Z по 2 байти кожен):

 

Перевести в одиниці g (при налаштуванні ±2g за замовчуванням):

      float ax_g = ax / 16384.0f; // 16384 = 2^14 LSB/g

Результат в терміналі:

      AX=-12840 AY=1120 AZ=-9704
AX=-12904 AY=1164 AZ=-9628

Від’ємні значення — нормально. Означає що вісь спрямована проти гравітації.

SPI vs I2C — коли що використовувати

SPI I2C
Дроти 4 (+ CS для кожного slave) 2 (для будь-якої кількості)
Швидкість до 50+ MHz 100кГц / 400кГц / 1MHz
Адресація CS пін 7-бітна адреса
Підтяжки не потрібні обов’язкові (4.7кОм)
Підтвердження немає (fire and forget) ACK від кожного байта
Складність простий складніший протокол
Коли дисплеї, Flash, SD-карти сенсори, RTC, EEPROM

Nokia 5110 — SPI. Бо дисплей тільки приймає і потребує швидкості.

MPU6050 — I2C. Бо сенсор і швидкість не критична, але зручність адресації важлива.

Підключення

Nokia 5110 (SPI)

Blue Pill Nokia 5110

─────────────────────────

PA5 → CLK (SCK)
PA7 → DIN (MOSI)
PA4 → CE (CS)
PA3 → DC
PA2 → RST
3.3V → VCC
GND → GND
3.3V → BL (підсвітка)

MPU6050 (I2C)

Blue Pill MPU6050

─────────────────────

PB6 → SCL
PB7 → SDA
3.3V → VCC
GND → GND
GND → AD0 (адреса 0x68)

 

Частина 4 — ADC, пульсометр на OLED і власний HAL

ADC — коли цифровому процесору потрібен аналоговий світ

Всі попередні периферії працювали з цифровими сигналами — HIGH або LOW, 0 або 1. Але реальний світ аналоговий. Пульс сенсор видає напругу яка плавно змінюється — від ~1.8V до ~3.3V залежно від кровотоку під шкірою.

ADC (Analog-to-Digital Converter) перетворює напругу в число. На STM32F103 — 12-бітний ADC: від 0 до 4095. 0 = 0V, 4095 = 3.3V.

Налаштування ADC1

Читання:

 

Калібрування — навіщо?

ADC має внутрішні похибки через технологічні відхилення при виробництві. Калібрування вимірює і компенсує їх. Без калібрування — результати можуть бути неточними на кілька одиниць. Для пульсометра це некритично, але для точного вимірювання напруги — важливо.

Pulse Sensor — як він працює

Pulse Sensor — оптичний сенсор. Всередині є LED (зазвичай зелений) і фотодіод. LED підсвічує шкіру, фотодіод вимірює відбите світло. Коли серце б’ється — кровотік під шкірою змінюється, змінюється кількість поглиненого і відбитого світла.

Результат — аналоговий сигнал який «пульсує» в такт серцебиттю.

Спочатку дивимось що видає ADC:
    Без пальця: ~2030 (базовий рівень)
    З пальцем: пік ~2136, потім падає до ~1854

 

Різниця між піком і базовим рівнем — всього ~100 одиниць з 4095. Сенсор вимірює дуже малі зміни.

Алгоритм підрахунку BPM

Ловимо піки — моменти коли сигнал перетинає поріг знизу вгору:

 

THRESHOLD — не магічне число

Поріг підбирається під конкретний сенсор і умови освітлення. Алгоритм:
1. Запускаємо без пальця — дивимось базовий рівень
2. Прикладаємо палець — дивимось максимум піку
3. Поріг = десь між ними, ближче до піку

У нас вийшло 2100 (базовий 2030, пік 2136). При затримці дихання — м’язи напружуються, кровотік змінюється, сенсор ловить артефакти. Це відоме обмеження простого алгоритму.

 

OLED SSD1306 — більший брат Nokia

SSD1306 — OLED дисплей 128×64 пікселі. Підключення через SPI — ті ж самі піни що і Nokia 5110. Тільки протокол ініціалізації інший.

        D0 → PA5 (SCK)
        D1 → PA7 (MOSI)
        RES → PA2 (Reset)
        DC → PA3
        CS → PA4

 

Ініціалізація SSD1306 довша ніж Nokia — більше параметрів:

 

Двоколірний дисплей

Наш модуль фізично двоколірний: перші 2 рядки (16 пікселів) — жовті, решта 6 рядків — сині. Це не програмується — так зроблено фізично. Використовуємо це: жовта зона — індикатор пульсу, синя зона — великі цифри BPM.

Пульсуючий індикатор

При кожному ударі серця — жовта смуга вмикається і вимикається:

Виклик в ISR пульсу — і жовта смуга мигає в такт серцебиттю.

Власний HAL — навіщо і як

Після того як всі приклади запрацювали bare-metal — саме час загорнути все в HAL. Не тому що «так правильно», а тому що ми вже розуміємо що під капотом.

Структура HAL:

 

PACK_PIN — найцікавіший трюк

Як передавати пін у функцію? Пін — це порт (адреса) + номер. Два параметри.

Рішення: пакуємо обидва в один uint32_t:

Це працює бо адреси портів вирівняні по 0x400 — молодші 8 біт завжди нулі. Туди і пакуємо номер піна.

uart_printf без stdlib

Стандартний printf тягне за собою купу коду — malloc, locale, floating point. Нам це не треба. Пишемо свій:

 

stdarg.h — єдиний системний заголовок який використовуємо. Він не тягне stdlib, просто дає доступ до змінних аргументів через va_list.

Принцип: кожен модуль робить своє

 

hal_gpio — тільки про піни. hal_tim — тільки про таймер. Не мішаємо.

RCC — один раз визначаємо, скрізь використовуємо

 

Якщо визначити RCC_APB2ENR ще раз в іншому файлі — компілятор дасть warning про перевизначення. Не помилку, але варто прибрати.

Повна таблиця векторів — один startup.c для всіх

Найважливіше рішення — повний startup.c з усіма IRQ з Table 63 RM0008. Не мінімальний під кожен приклад, а один повний для всього HAL:

 

Хочеш використати USART1 IRQ — просто пишеш void USART1_IRQHandler(void) у main.c. Лінкер автоматично підставить твою функцію замість дефолтної. Не треба кожного разу лізти в startup.c.

Що вийшло в підсумку

За місяць 1 зроблено:

Bare-metal приклади — кожен показує як периферія працює без абстракцій:

     BLINK_stm32 ← перше мигання, 272 байти
     GPIO_stm32 ← порти, BSRR vs ODR
     RUNNING_FIRE_stm32← біжучий вогонь
     UART_stm32 ← hardware UART
     BUTTON_EXTI_stm32 ← EXTI переривання
     UART_RX_IRQ ← UART через ring buffer
     SYS_TICK_BLINK ← SysTick IRQ
     TIM2_BLINK ← TIM2 переривання
     TIM2_PWM_BLINK ← PWM дихання LED
     SPI_NOKIA5110 ← Nokia 5110
     I2C_MPU6050 ← акселерометр
     OLED_PULSE ← пульсометр на OLED

 

HAL з 8 модулів:

 

Порівняй з bare-metal версією де кожен рядок — це робота з регістрами напряму. HAL не магія — це просто зручна обгортка над тим що ми вже розуміємо.

Головне що зрозуміли

Таблиця векторів — не регістри. Масив адрес у Flash. Процесор читає її сам при кожному перериванні. Пропустив позицію — процесор стрибає не туди.

RCC завжди першим. Забув тактування — периферія мовчить. Без підказок. Просто мовчить.

EXTI_PR скидається записом 1, TIM2_SR — записом 0. Різна периферія, різна логіка. Читай RM0008.

ISER0 vs ISER1. IRQ0-31 → ISER0. IRQ32-63 → ISER1. USART1 = IRQ37 → ISER1, біт 5.

Alternate Function — не кожен пін може виводити PWM або SPI. Тільки ті що мають AF прив’язку до периферії. Дивись datasheet, таблиця Alternate Functions.

volatile для змінних в ISR. Без нього компілятор оптимізує і main ніколи не побачить зміну.

Що далі

Місяць 2 — STM32 підключається до Linux. Blue Pill збирає дані з MPU6050 і пульс сенсора, відправляє через UART в Luckfox Pico. Python скрипт на Linux стороні парсить дані і виводить графік в реальному часі.

Bare-metal + Linux = перший справжній embedded міст.

Код на GitHub: github.com/pipicosim800-maker/stm32F103

 

Додаток — Протокол однаковий, діалект різний

Чому не можна просто перемкнути дроти

Ми використовували і Nokia 5110 і SSD1306 OLED — обидва через SPI, обидва мають DC, RST, CS. Здається — поміняй дроти і готово.

Не готово.

SPI — це тільки транспорт. Він каже як передати байти: тактування, фази, порядок бітів. Але що ці байти означають — вирішує кожен контролер дисплея самостійно.

        Ти надсилаєш: 0x21

        Nokia 5110 (PCD8544): «О, це команда увімкнути extended instruction set»
SSD1306: «О, це команда встановити нижній ніби колонки»
        ST7735 (TFT): «О, це NOP»

Один і той самий байт — три різних реакції. Протокол транспортний однаковий, протокол прикладний — у кожного свій.

Що реально відрізняється між дисплеями:

Параметр Nokia 5110 SSD1306 OLED ST7735 TFT
Контролер PCD8544 SSD1306 ST7735
Роздільність 84×48 128×64 128×160
Кольори монохром монохром 65536 (16-bit)
Команда ініц. 6 байт ~20 байт ~100 байт
Буфер відео 504 байти 1024 байти 40960 байт
Datasheet PCD8544.pdf SSD1306.pdf ST7735.pdf

Те саме з сенсорами на I2C — MPU6050 і BMP280 обидва на I2C, але команди, регістри і формат даних абсолютно різні.

Висновок: міняєш пристрій — переписуєш драйвер. Протокол залишається, логіка — ні.

Додаток: популярні модулі для STM32 і Arduino

Шпаргалка по найпопулярніших модулях — протокол, піни на Blue Pill, типове застосування.

📺 Дисплеї

Модуль Протокол Піни Blue Pill Роздільність Нотатки
Nokia 5110 SPI PA5/PA7/PA4/PA3/PA2 84×48 монохром, дешевий, 3.3V
SSD1306 OLED SPI або I2C SPI: PA5/PA7 або I2C: PB6/PB7 128×64 самосвітний, чудовий контраст
ST7735 TFT SPI PA5/PA7/PA4/PA3/PA2 128×160 кольоровий, потребує більше RAM
ILI9341 TFT SPI PA5/PA7/PA4/PA3/PA2 240×320 популярний 2.8″ дисплей
HD44780 LCD паралельний або I2C (з PCF8574) PB6/PB7 (I2C варіант) 16×2 або 20×4 символьний, класика
MAX7219 SPI PA5/PA7/PA4 8×8 LED матриця можна каскадувати

🌡 Сенсори температури і тиску

Модуль Протокол Піни Blue Pill Що вимірює Нотатки
DS18B20 1-Wire будь-який GPIO температура -55°C до +125°C, до 127 на одному дроті
DHT11 / DHT22 1-Wire будь-який GPIO температура + вологість DHT22 точніший
BMP180 I2C PB6/PB7 тиск + температура висотомір
BMP280 I2C або SPI PB6/PB7 або PA5/PA7 тиск + температура точніший за BMP180
BME280 I2C або SPI PB6/PB7 або PA5/PA7 тиск + температура + вологість все в одному
LM35 Analog PA0-PA7 температура простий аналоговий

🏃 IMU — акселерометри і гіроскопи

Модуль Протокол Піни Blue Pill Що вимірює Нотатки
MPU6050 I2C PB6/PB7 акселерометр + гіроскоп найпопулярніший, є DMP
MPU9250 I2C або SPI PB6/PB7 або PA5/PA7 акцел + гіро + магнітометр 9DOF
ADXL345 I2C або SPI PB6/PB7 або PA5/PA7 акселерометр до ±16g
HMC5883L I2C PB6/PB7 магнітометр (компас) часто в парі з MPU6050
ICM-42688-P SPI PA5/PA7 акцел + гіро точніший сучасний варіант

📡 Зв’язок

Модуль Протокол Піни Blue Pill Що робить Нотатки
HC-05 / HC-06 UART PA9/PA10 Bluetooth 2.0 AT команди
ESP8266 UART PA9/PA10 WiFi AT команди або прошивка
ESP32 UART або SPI PA9/PA10 WiFi + BT потужніший за ESP8266
NRF24L01 SPI PA5/PA7/PA4 + GPIO 2.4GHz радіо до 1Mbps, до 100м
LoRa Ra-02 SPI PA5/PA7/PA4 + GPIO LoRa 433MHz великі відстані, малий трафік
SIM800L UART PA9/PA10 GSM/GPRS потребує 4V, багато струму

📏 Дистанція і рух

Модуль Протокол Піни Blue Pill Що вимірює Нотатки
HC-SR04 GPIO trigger/echo будь-які 2 GPIO відстань (2-400 см) ультразвук
VL53L0X I2C PB6/PB7 відстань (до 2м) лазер ToF, точніший
PIR HC-SR501 GPIO будь-який GPIO рух датчик присутності
IR модуль GPIO будь-який GPIO відстань 2-30 см відбите ІЧ
Encoder GPIO interrupt EXTI піни оберти мотора квадратурний або однофазний

❤️ Біометрія

Модуль Протокол Піни Blue Pill Що вимірює Нотатки
Pulse Sensor Analog PA0-PA7 пульс оптичний, потребує фільтрації
MAX30102 I2C PB6/PB7 пульс + SpO2 точніший, вбудований ADC
AD8232 Analog PA0-PA7 ЕКГ м’язовий сенсор ECG
Muscle Sensor v3 Analog PA0-PA7 ЕМГ (м’язи) для жестового управління

💾 Пам’ять і зберігання

Модуль Протокол Піни Blue Pill Об’єм Нотатки
AT24C256 EEPROM I2C PB6/PB7 256 Кбіт повільна запис, довге зберігання
W25Q32 Flash SPI PA5/PA7/PA4 32 Мбіт швидка, для прошивок і даних
SD карта SPI PA5/PA7/PA4 до 32 ГБ потрібен FAT32 або FATFS

⚙️ Актуатори і керування

Модуль Протокол Піни Blue Pill Що робить Нотатки
Сервопривід PWM PA0-PA3 (TIM2) кут 0-180° 50Гц, 1-2мс імпульс
L298N GPIO + PWM будь-які DC мотор до 2А, два мотори
A4988 GPIO STEP + DIR кроковий мотор мікрокроки
PCA9685 I2C PB6/PB7 16 каналів PWM ідеальний для сервоприводів
Реле GPIO будь-який комутація 220V оптоізоляція обов’язкова

🔊 Звук

Модуль Протокол Піни Blue Pill Що робить Нотатки
Buzzer GPIO або PWM будь-який звуковий сигнал активний (просто GPIO) або пасивний (PWM)
MAX9814 Analog PA0-PA7 мікрофон з підсилювачем для запису звуку
VS1053 SPI PA5/PA7/PA4 MP3 декодер програє MP3 з SD
I2S DAC (PCM5102) I2S спеціальні піни якісний аудіо вихід для STM32 з I2S

Як швидко зрозуміти новий модуль

1. Знайди datasheet — шукай «[назва чіпа] datasheet pdf». Не datasheet модуля — а саме чіпа (PCD8544, SSD1306, MPU6050).

2. Знайди register map — таблиця всіх регістрів. Це і є «словник» пристрою.

3. Знайди initialization sequence — послідовність команд для старту. Зазвичай є в datasheet або application note.

4. Перевір I2C адресу — для I2C пристроїв є утиліта i2c_scan яка знаходить всі пристрої на шині.

5. Починай з мінімуму — для дисплея: ініціалізація → залити екран одним кольором → вивести один піксель. Для сенсора: WHO_AM_I → одне вимірювання.

 

Повний список прикладів і HAL: [github.com/pipicosim800-maker/stm32F103](https://github.com/pipicosim800-maker/stm32F103)

Серія «STM32 з нуля без HAL»

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

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

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