AVR vs STM32: як перейти і чому варто
Серія «STM32 з нуля без HAL»
Я починав з Arduino. Потім ESP32. Потім подивився на STM32 і подумав — навіщо? Arduino працює, бібліотек купа, все просто.
А потім спробував написати bare-metal код для STM32 і зрозумів що Arduino — це як їздити на машині з автоматом не знаючи що таке зчеплення. Їздити можна. Але коли щось піде не так — ти не знаєш що робити.
Ця стаття для тих хто знає AVR/Arduino і хоче зрозуміти STM32. Не «ось бібліотека підключи і працює» — а що реально відбувається всередині і чому STM32 влаштований саме так.
Перше здивування: де PORTB?
На AVR все просто:
|
1 2 3 4 5 6 7 8 9 10 11 |
// AVR ATmega328P — мигаємо PB5 (це D13 на Arduino Uno) #include <avr/io.h> int main(void) { DDRB |= (1 << PB5); // пін як вихід while (1) { PORTB ^= (1 << PB5); // toggle _delay_ms(500); } } |
Три рядки. DDRB, PORTB — просто змінні. Магія.
На STM32:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// STM32F103 — мигаємо PC13 (вбудований LED на Blue Pill) #include <stdint.h> #define RCC_APB2ENR (*(volatile uint32_t *)0x40021018) #define GPIOC_CRH (*(volatile uint32_t *)0x40011004) #define GPIOC_ODR (*(volatile uint32_t *)0x4001100C) int main(void) { RCC_APB2ENR |= (1 << 4); // тактування GPIOC GPIOC_CRH &= ~(0xF << 20); // очищаємо PC13 GPIOC_CRH |= (0x2 << 20); // output push-pull 2MHz while (1) { GPIOC_ODR ^= (1 << 13); for (volatile int i = 0; i < 500000; i++); } } |
Більше коду. Магічні адреси. Незрозумілий RCC. Навіщо це все?
Чому на AVR простіше
ATmega328P — маленький мікроконтролер. 32 КБ Flash, 2 КБ SRAM, 23 GPIO. Периферії мало — UART один, SPI один, I2C один.
Регістри периферії на AVR — це буквально змінні в пам’яті даних. Компілятор знає де вони знаходяться і дає їм імена:
|
1 2 3 4 |
// AVR — DDRB це просто адреса 0x24 в пам'яті // але avr/io.h ховає це від тебе DDRB = 0xFF; // виглядає як звичайна змінна |
Плюс — на AVR немає тактування периферії. Вмикаєш живлення — все вже працює. Не треба думати про RCC.
AVR простіший бо менший. Менше периферії → менше регістрів → менше налаштувань.
Чому на STM32 складніше але краще
STM32F103 — зовсім інший рівень. 64 КБ Flash, 20 КБ SRAM, 37 GPIO, 3 UART, 2 SPI, 2 I2C, 7 таймерів, ADC, USB, CAN.
1. Карта адрес (Memory Map)
На AVR регістри периферії живуть в окремому I/O просторі. На STM32 — все в одному 32-бітному адресному просторі:
0x08000000 Flash (твій код)
0x20000000 SRAM (твої змінні)
0x40000000 Периферія APB1 (таймери, I2C, UART2/3)
0x40010000 Периферія APB2 (GPIO, SPI1, UART1, ADC)
0xE0000000 Системна периферія (NVIC, SysTick)
Це означає що будь-який модуль — DMA, відладчик, Linux через UART — може читати і писати в будь-який регістр периферії напряму.
2. Тактування (RCC)
На AVR периферія завжди тактується. На STM32 — за замовчуванням вимкнена. Економія енергії.
// STM32: спочатку вмикаємо тактування
RCC_APB2ENR |= (1 << 4); // GPIOC on
// тільки після цього регістри GPIOC реагують на запис
Забув RCC — периферія мовчить. Без підказок. Просто мовчить. Це найпопулярніша помилка новачків на STM32.
3. Більше можливостей GPIO
AVR: пін або вхід або вихід. STM32: вісім режимів — input floating, pull-up, pull-down, output push-pull 2/10/50MHz, open-drain, alternate function, analog.
// input floating: 0x4
// input pull-up: 0x8
// output push-pull 2MHz: 0x2
// AF push-pull 50MHz: 0xB ← для UART TX, SPI
// AF open-drain 50MHz: 0xF ← для I2C
Порівняння: UART
AVR ATmega328P
|
1 2 3 4 5 6 7 8 |
void uart_init(uint32_t baud) { uint16_t ubrr = F_CPU / 16 / baud - 1; UBRR0H = (ubrr >> 8); UBRR0L = ubrr; UCSR0B = (1 << TXEN0) | (1 << RXEN0); UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); // 8-bit } |
STM32F103
|
1 2 3 4 5 6 7 8 9 |
void uart_init(uint32_t baud) { RCC_APB2ENR |= (1 << 14); // USART1 clock // PA9 TX → AF push-pull 50MHz GPIOA_CRH &= ~(0xF << 4); GPIOA_CRH |= (0xB << 4); USART1_BRR = 8000000UL / baud; // простіше ніж на AVR! USART1_CR1 = (1<<13)|(1<<3)|(1<<2); // UE+TE+RE } |
BRR на STM32 простіший — просто ділимо частоту на baud. На AVR формула складніша і залежить від режиму.
Порівняння: переривання
AVR — переривання через макрос
|
1 2 3 4 5 6 7 8 9 |
ISR(INT0_vect) { // макрос приховує все // твій код } int main(void) { EICRA |= (1 << ISC01); // falling edge EIMSK |= (1 << INT0); // увімкнути INT0 sei(); // глобальні переривання } |
STM32 — переривання через таблицю векторів
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void EXTI1_IRQHandler(void) { // точне ім'я з RM0008 if (EXTI_PR & (1 << 1)) { // твій код EXTI_PR = (1 << 1); // скидаємо прапорець } } int main(void) { AFIO_EXTICR1 &= ~(0xF << 4); // Port A для EXTI1 EXTI_FTSR |= (1 << 1); // falling edge EXTI_IMR |= (1 << 1); // unmask NVIC_ISER0 |= (1 << 7); // IRQ7 = EXTI1 } |
На STM32 видно весь ланцюжок: GPIO → AFIO → EXTI → NVIC → ISR. Ця архітектура однакова на будь-якому Cortex-M — STM32, nRF52, LPC, SAMD. Вивчив один — розумієш всі.
Де AVR виграє
Чесно — є речі де AVR зручніший:
1. Простота старту. П’ять рядків і LED мигає.
2. Менше документації. Datasheet ATmega328P — 450 сторінок. RM0008 — 1136 сторінок.
3. Передбачуваність тактування. AVR завжди запускається без налаштувань.
4. Програматори дешевші. USBasp — $2. ST-Link v2 — $3-5.
Де STM32 виграє
1. Продуктивність. 16MHz 8-bit vs 72MHz 32-bit. В 10-20 разів більше обчислень.
2. Периферія. Три UART, два SPI, два I2C, USB, CAN — на одному чіпі.
3. Ціна. Blue Pill — $2-3. ATmega328P окремо — $2-4 тільки за чіп.
4. Переносимість знань. Вивчив Cortex-M3 — розумієш Cortex-M4, Cortex-A (Raspberry Pi, Android). Вивчив AVR — знаєш тільки AVR.
5. Промислове застосування. ARM домінує в embedded. Вакансій набагато більше.
Покроковий план переходу
Крок 1 — Середовище (1 день): встановити arm-none-eabi-gcc, купити Blue Pill + ST-Link v2.
Крок 2 — Перший LED (1 день): написати мигання без бібліотек. Зрозуміти RCC, CRH, ODR.
Крок 3 — UART (1 день): вивести текст в термінал. Зрозуміти BRR, CR1, Alternate Function.
Крок 4 — Переривання (2-3 дні): EXTI від кнопки. Таблиця векторів, NVIC, AFIO. Це найважче.
Крок 5 — Периферія (1-2 тижні): SysTick, TIM2, PWM, SPI, I2C, ADC. По одному протоколу на день.
Крок 6 — Власний HAL (1 тиждень): загорнути все в зручний API. Тепер розумієш що роблять ST HAL і CubeMX.
Порівняльна таблиця
| Параметр | AVR ATmega328P | STM32F103C8T6 |
| Ядро | AVR 8-bit | ARM Cortex-M3 32-bit |
| Частота | 16 MHz | до 72 MHz |
| Flash | 32 KB | 64 KB |
| SRAM | 2 KB | 20 KB |
| GPIO | 23 | 37 |
| UART | 1 | 3 |
| SPI | 1 | 2 |
| I2C | 1 | 2 |
| Таймери | 3 | 7 |
| ADC | 10-bit, 6 каналів | 12-bit, 10 каналів |
| USB | немає | є (FS) |
| CAN | немає | є |
| Ціна плати | Arduino Uno ~$5-25 | Blue Pill ~$2-3 |
| Документація | 450 стор | 1136 стор |
| Складність старту | низька | середня |
| Переносимість | тільки AVR | весь ARM світ |
Головна відмінність — рівень абстракції
Arduino — це digitalWrite(13, HIGH). Зручно. Але що під капотом — невідомо.
AVR bare-metal — це PORTB |= (1 << PB5). Вже краще. Але I/O простір прихований.
STM32 bare-metal — це *(volatile uint32_t *)0x4001100C |= (1 << 13). Бачиш все. Адреса, розмір, volatile — нічого не приховано.
І це не недолік STM32. Це перевага для розуміння.
Якщо знаєш AVR — знаєш Arduino. Якщо знаєш STM32 bare-metal — знаєш як працює будь-який мікроконтролер на ARM. А ARM — це 90% embedded ринку.
Код на GitHub: github.com/pipicosim800-maker/stm32F103
Серія «STM32 з нуля без HAL»