80. Прототип на Arduino: плата, среда, первый скетч
Arduino — не микроконтроллер, а платформа: плата с микроконтроллером,
библиотека функций вроде digitalWrite и среда, которая собирает и
загружает программу одной кнопкой. Для прототипа (раздел 79.4) это
быстрый путь: датчик на макетке, двадцать строк — и видно, работает ли
замысел.
Разработчику на STM32 Arduino стоит знать по трём причинам: с него часто начинают, на нём приносят прототипы, и код с него переносят на STM32. В разделе — что внутри платы, чем она отличается от STM32 так, что это ломает перенесённый код, как правильно читать кнопку и как проще всего убить обе платы.
Факты об ATmega328P проверены компилятором avr-gcc с заголовками
avr-libc, факты об STM32 — armclang и файлами описания паков.
Как это называется в документации
Оригинал Перевод sketch скетч — программа в среде Arduino board, shield плата, плата расширения core ядро Arduino — библиотека под конкретный микроконтроллер bootloader загрузчик serial monitor монитор последовательного порта pull-up, INPUT_PULLUPподтяжка к питанию, вход с подтяжкой floating input «висящий» вход contact bounce, debouncing дребезг, подавление дребезга rotary (quadrature) encoder поворотный (квадратурный) энкодер absolute maximum ratings предельно допустимые режимы reverse polarity переполюсовка back-powering питание «с обратной стороны»
80.1 Что внутри: Uno против STM32
Классическая плата Uno построена на 8-битном микроконтроллере ATmega328P с питанием 5 В. Для сравнения — Blue Pill на F103C8 и F407VG:
| Uno (ATmega328P) | Blue Pill (F103C8) | F407VG | |
|---|---|---|---|
| ядро | AVR, 8 бит | Cortex-M3, 32 бита | Cortex-M4F, 32 бита |
| питание | 5 В | 3,3 В | 3,3 В |
| Flash | 32 КБ | 64 КБ | 1 МБ |
| ОЗУ | 2 КБ | 20 КБ | 128 + 64 КБ |
| EEPROM | 1 КБ | нет — хранят во Flash (раздел 11.5) | нет |
int |
2 байта | 4 байта | 4 байта |
double |
4 байта, как float |
8 байт | 8 байт |
| указатель | 2 байта | 4 байта | 4 байта |
| пошаговая отладка | из коробки нет | SWD | SWD |
Память Uno — из заголовка avr-libc (FLASHEND, RAMSTART, RAMEND,
E2END), память STM32 — из файлов описания паков Keil. У Blue Pill
Flash вдвое, а ОЗУ вдесятеро больше, чем у Uno; про частоты и периферию
STM32 — разделы 2 и 6.
Разрядность — главная ловушка переноса. Код, написанный «под Arduino», и код для STM32 расходятся на самых простых строках:
int minute_ms = 1000 * 60; /* переполнение: компилятор предупреждает */
int big = 40000; /* а здесь молчит — и хранит −25536 */
long minute_ok = 1000L * 60; /* так правильно */
На AVR int 16-битный, до 32 767. 1000 * 60 вычисляется в int и
переполняется — avr-gcc выдаёт integer overflow in expression. А
int big = 40000 собирается без единого предупреждения с -Wall и
хранит −25 536. На Cortex-M обе строки дают правильные 60 000 и 40 000.
Эту ловушку проверка раздела поймала на себе самой. Размер Flash Uno
проверялся строкой _Static_assert(FLASHEND + 1 == 32768, …) — и она не
прошла: FLASHEND равен 0x7FFF, а прибавление единицы переполняет
16-битный int. Правильно — FLASHEND + 1L. Привычка к 32-битному int
сильнее, чем кажется.
Отсюда правило для кода, который живёт на двух платформах: типы
фиксированной ширины — int16_t, uint32_t (раздел 64), константы с
суффиксом, где нужен запас.
То же с double: на AVR это 4 байта, та же точность, что у float.
Расчёт, который «работал на Arduino», на STM32 даст другие последние
знаки — уже потому, что там double честный 64-битный (раздел 69).
80.2 Среда: скетч — это C++ со своей main
В скетче нет main — есть две функции:
setup()выполняется один раз после сброса;loop()вызывается снова и снова, пока есть питание.
main среда подставляет сама: она настраивает таймер для millis() и
delay(), вызывает setup() и затем loop() в бесконечном цикле.
Скетч — это C++: среда добавляет к нему нужные заголовки и объявления и
собирает avr-gcc вместе с библиотекой ядра. Та же схема есть и для
STM32: существуют ядра Arduino и для этих камней, включая Blue Pill.
Вот тот же «скетч» без среды — на STM32F103 регистрами CMSIS (раздел 8).
Он собран armclang без предупреждений:
#include "stm32f10x.h"
void setup(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; /* такт порта C */
GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | (0x2u << 20); /* PC13 — выход, 2 МГц */
}
void loop(void)
{
GPIOC->ODR ^= 1u << 13; /* светодиод Blue Pill */
for (volatile uint32_t i = 0; i < 100000; i++)
;
}
int main(void)
{
setup();
for (;;)
loop();
}
Загрузка в Uno идёт через загрузчик в самом микроконтроллере: среда передаёт прошивку по USB, микросхема на плате превращает USB в UART. Поэтому перед загрузкой выбирают плату и порт. У дешёвых копий Uno микросхема USB–UART другая, и ей нужен свой драйвер — «порт не появился» чаще всего означает именно это. У STM32 путей больше: SWD, встроенный загрузчик по UART или USB (раздел 4.9).
80.3 Первый скетч: светодиод, кнопка, энкодер
Светодиод — через резистор (раздел 49.3). Резистор 220 Ом при красном светодиоде (около 2 В на нём) даёт 13,6 мА от 5 В Uno и только 5,9 мА от 3,3 В STM32. Схема, перенесённая с Uno как есть, будет светить заметно тусклее.
Кнопка. Первая попытка обычно такая: кнопка между выводом и питанием,
вывод настроен как простой вход. Работает непредсказуемо — потому что,
пока кнопка отпущена, вход ни к чему не подключён. Это «висящий»
вход: он ловит наводки и читается как угодно. Лечится подтяжкой —
резистором к питанию или к земле, внешним или встроенным:
INPUT_PULLUP у Arduino, GPIO_PULLUP у STM32 (раздел 8). С подтяжкой
к питанию кнопку ставят между выводом и землёй: отпущена — 1, нажата — 0.
⚠️ Подтяжка не лечит дребезг. Это два разных явления. Висящий вход — когда сигнала нет вовсе; дребезг — когда сигнал есть, но при замыкании несколько раз прыгает, прежде чем установиться (раздел 50.7). Первое лечат только резистором, второе — RC-цепью или программой.
Энкодер — поворотная ручка с двумя контактами, которые замыкаются со сдвигом по фазе: по порядку замыканий определяют направление. У энкодера на 20 импульсов за оборот один щелчок — 18°. Его контакты дребезжат так же, как у кнопки. У STM32 таймер умеет считать энкодер сам, без программы (раздел 13).
80.4 Дребезг: что работает, а что нет
Конденсатор в пикофарады бесполезен. RC-цепь сглаживает дребезг, если её постоянная времени сравнима с ним, а дребезг длится от сотен микросекунд до нескольких миллисекунд, у изношенных кнопок и дольше. С резистором 10 кОм конденсатор 5 пФ даёт 50 нс — в двадцать тысяч раз меньше, чем 1 мс со 100 нФ (раздел 50.7).
delay(300) между двумя чтениями останавливает всю программу на
треть секунды и почти ничего не доказывает: два совпавших отсчёта с
интервалом 300 мс могут прийтись на дребезг обоих нажатий.
История отсчётов — хороший способ: раз в 10 мс сдвигать в байт очередное чтение вывода, и кнопка нажата, когда все восемь бит — нули. Функция не читает вывод и не знает времени сама — ей передают миллисекунды и уровень, поэтому её можно проверить тестом (раздел 79.5):
#define PERIOD_MS 10
typedef struct {
uint32_t last; /* время последнего отсчёта */
uint8_t history; /* восемь последних отсчётов, младший бит — свежий */
} btn_t;
/* Вернёт 1, пока кнопка нажата: восемь отсчётов подряд — ноль. */
int btn_poll(btn_t *b, uint32_t now, int level)
{
if (now - b->last >= PERIOD_MS) { /* «>=», а не «>»: иначе период 11 мс */
b->last = now;
b->history = (uint8_t)((b->history << 1) | (level & 1));
}
return b->history == 0x00;
}
Начальное значение history — 0xFF, то есть «отпущена восемь раз
подряд»: btn_t b = { 0, 0xFF };. Обнулённая структура первые 10 мс,
до первого отсчёта, читается как нажатая кнопка — ложное нажатие при
каждом включении.
На Arduino now — это millis(), на STM32 — счётчик, который
увеличивает прерывание SysTick. Что показал тест:
| Что подано | Распознано через |
|---|---|
| кнопку нажали и держат | 80 мс — восемь отсчётов по 10 мс |
то же, но с > вместо >= |
88 мс — отсчёт раз в 11 мс |
| дребезг 25 мс, попавший в два отсчёта | 100 мс |
| касание на 50 мс | не распознано |
| за 10 мс до переполнения счётчика | 80 мс — как обычно |
Два вывода. Первый: условие > 10 при целых миллисекундах срабатывает
на одиннадцатой — мелочь, из-за которой период расходится с задуманным.
Второй: восемь отсчётов по 10 мс — это 80 мс, и быстрое касание короче
этого пропадёт. Число отсчётов и период выбирают под требование
(раздел 79.2), а не «как в примере». Состояние кнопки — 8 байт: 4 + 1
и три байта выравнивания (раздел 70).
Переполнение millis(). 32-битный счётчик миллисекунд переполняется
через 49,7 суток. Разность now - last беззнаковая и переживает переход
через ноль — тест это подтвердил (раздел 7). А вот сравнение со «сроком»
не переживает:
uint32_t deadline = now + 100; /* за 10 мс до переполнения: deadline = 90 */
if (now > deadline) { … } /* сработает сразу, на 100 мс раньше */
if (now - start >= 100) { … } /* правильно */
80.5 Как убить плату — и как не убить
Предельно допустимые режимы — граница разрушения, а не рабочая точка (разделы 49.7 и 62). У двух плат они разные:
| Uno (ATmega328P) | STM32F103 | |
|---|---|---|
| ток одного вывода, предел | 40 мА | 25 мА |
| через выводы питания, вместе с самим камнем | 200 мА | 150 мА |
| напряжение на выводе | до питания + 0,5 В, то есть 5,5 В | до 3,6 В; толерантные — до 5,5 В, только при поданном питании |
| питание, абсолютный предел | 6 В | 4,0 В |
Способы вывести плату из строя — от вывода до всей платы:
- Выход накоротко на землю или на другой выход. У вывода Uno выходное сопротивление порядка 25 Ом: 5 В / 25 Ом = 200 мА — впятеро выше предела. Два выхода в противофазе, соединённые проводом, — то же самое, но сразу для двух выводов.
- Напряжение выше питания на выводе. Открывается встроенный защитный диод, а он рассчитан на миллиамперы (раздел 62.2). У STM32 ловушка тоньше: 5 В от датчика на нетолерантный вывод, например аналоговый, — за пределом; на толерантный — можно, но только пока плата включена (раздел 62.1).
- Переполюсовка питания. Вход питания на плате может не иметь защиты от обратной полярности — тогда перепутанные провода убивают сразу всё.
- Внешнее напряжение на выход стабилизатора. Стабилизатор не понижает то, что подали прямо на его выход: больше 6 В на вывод 5 В Uno или 5 В на вывод 3,3 В Blue Pill идут прямо на микроконтроллер — и заодно в USB компьютера.
- Питание «с обратной стороны». Плата питается через вывод 5 В, а на входе стабилизатора что-то подключено — ток идёт через стабилизатор назад и сжигает его.
- Суммарный ток. Десять светодиодов по 20 мА — это уже весь суммарный ток Uno, 200 мА; у F103 предел наступает на пяти: через питание идёт 150 мА, и до 50 из них берёт сам камень (раздел 62.6). Поштучный предел соблюдён, а камень перегружен.
- Индуктивная нагрузка без диода — реле или мотор прямо от вывода (раздел 52).
- Статика — касание вывода пальцем после прогулки по ковру (раздел 62.8).
80.6 От прототипа к STM32
Что переносится, а что нет:
| Переносится | Не переносится |
|---|---|
| логика, если она отделена от выводов и времени | номера выводов и вызовы digitalWrite, analogRead |
| алгоритмы и таблицы | int и double — у них другая ширина |
| протоколы обмена | уровни 5 В: датчик с питанием 5 В отдаёт 5 В на выходе (раздел 62.5) |
| тесты функций на компьютере | задержки delay() вместо таймеров и прерываний |
Порядок переноса: сначала убедиться, что логика собрана в функции без
обращения к выводам, как btn_poll; затем заменить типы на
фиксированные; затем пересчитать уровни и токи под 3,3 В; и только
потом писать слой, который читает и пишет выводы STM32.
80.7 Грабли
Кнопка «нажимается сама». Вход без подтяжки.
Добавили подтяжку, а кнопка по-прежнему срабатывает дважды. Подтяжка не лечит дребезг.
Конденсатор на кнопке ничего не изменил. Единицы пикофарад — это наносекунды; нужны десятки или сотни нанофарад.
Программа «замирает» при нажатии. delay() в опросе кнопки.
Короткие нажатия теряются. Опрос по восьми отсчётам требует 80 мс удержания — параметры не под требование.
Раз в полтора месяца устройство ведёт себя странно. Сравнение
millis() > deadline не пережило переполнения счётчика.
Код с Arduino на STM32 считает иначе. 16-битный int и 32-битный
double на AVR; на STM32 — 32 и 64.
Светодиоды после переноса светят тускло. Тот же резистор при 3,3 В вместо 5.
Порт платы не появляется в системе. Нет драйвера микросхемы USB–UART на копии платы.
После подключения датчика вывод STM32 умер. 5 В на нетолерантный вывод или на толерантный при выключенной плате.
80.8 Итоги
- Arduino — платформа: плата, библиотека ядра и среда. Uno — ATmega328P, 5 В, Flash 32 КБ, ОЗУ 2 КБ; Blue Pill — вдвое больше Flash и вдесятеро больше ОЗУ при 3,3 В.
- На AVR
int— 2 байта иdouble— 4:1000 * 60переполняется,int big = 40000молча хранит −25 536. Для общего кода — типы фиксированной ширины. - Скетч — это C++ с подставленной
main, которая вызываетsetup()один раз иloop()бесконечно; на STM32 то же пишется явно. - Висящий вход и дребезг — разные явления: первое лечит подтяжка, второе
— RC-цепь с постоянной порядка миллисекунды или опрос по истории
отсчётов. Условие
>=, а не>; параметры — под требование. - Счётчик миллисекунд переполняется через 49,7 суток: сравнивают разности, а не сроки.
- Предельные режимы Uno и STM32 разные: 40 и 25 мА на вывод, 200 и 150 мА через питание, 5,5 В и 3,6 В на выводе. Плату убивают короткое замыкание, напряжение выше питания, переполюсовка, питание в обход стабилизатора и суммарный ток.
- Перенос на STM32 начинают с логики, отделённой от выводов, и с типов.
Дальше: раздел 81 «Датчики на прототипе и борьба с шумом».