Embedded HandbookПриложения
Все разделы › X. Проектирование, сети и IoT
STM32F103, STM32F407 · уровень базовый · 12 мин чтения
Открыть в интерактивном справочнике

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).

Три строки с числами больше 32 767 на AVR: где компилятор предупреждает, а где молчит


80.2 Среда: скетч — это C++ со своей main

В скетче нет main — есть две функции:

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 мс — как обычно

История отсчётов btn_poll: дребезг 25 мс попадает в два отсчёта, и нажатие распознано через 100 мс

Два вывода. Первый: условие > 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 В

Способы вывести плату из строя — от вывода до всей платы:

  1. Выход накоротко на землю или на другой выход. У вывода Uno выходное сопротивление порядка 25 Ом: 5 В / 25 Ом = 200 мА — впятеро выше предела. Два выхода в противофазе, соединённые проводом, — то же самое, но сразу для двух выводов.
  2. Напряжение выше питания на выводе. Открывается встроенный защитный диод, а он рассчитан на миллиамперы (раздел 62.2). У STM32 ловушка тоньше: 5 В от датчика на нетолерантный вывод, например аналоговый, — за пределом; на толерантный — можно, но только пока плата включена (раздел 62.1).
  3. Переполюсовка питания. Вход питания на плате может не иметь защиты от обратной полярности — тогда перепутанные провода убивают сразу всё.
  4. Внешнее напряжение на выход стабилизатора. Стабилизатор не понижает то, что подали прямо на его выход: больше 6 В на вывод 5 В Uno или 5 В на вывод 3,3 В Blue Pill идут прямо на микроконтроллер — и заодно в USB компьютера.
  5. Питание «с обратной стороны». Плата питается через вывод 5 В, а на входе стабилизатора что-то подключено — ток идёт через стабилизатор назад и сжигает его.
  6. Суммарный ток. Десять светодиодов по 20 мА — это уже весь суммарный ток Uno, 200 мА; у F103 предел наступает на пяти: через питание идёт 150 мА, и до 50 из них берёт сам камень (раздел 62.6). Поштучный предел соблюдён, а камень перегружен.
  7. Индуктивная нагрузка без диода — реле или мотор прямо от вывода (раздел 52).
  8. Статика — касание вывода пальцем после прогулки по ковру (раздел 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 Итоги

Дальше: раздел 81 «Датчики на прототипе и борьба с шумом».

Открыть в интерактивном справочнике