Когда в 2011 году я переходил c atmega8 на stm32, меня очень вдохновил проект opencm3. Но вдохновил не на его изучение, а на написание похожего. На сегодня в моём варианте почти библиотеки есть макросы регистров для микроконтроллеров серий stm32f10x и stm32f40x, stm8s003, nrf51, nrf52, rp2040, и cc2640/1310. Реально же протестирована из этого списка только stm32f103. Кроме регистров для 103-й я написал базовые функции для включения/выключения тактирования периферии и управления портами ввода-вывода. А также написаны примеры для USB профилей HID gamepad, HID keyboard и USB serial port. В этом же посте задокументирую функции портов и тактирования.

Речь идёт о файлах stm32/rcc.c, stm32/gpio.c и stm32/delay.c моей библиотеки для микроконтроллеров.

Тактирование периферии (rcc.h)

Как известно, периферия в STM32, как и во всех ARM, раскидана по нескольким шинам: AHB, APB1 и APB2. Потому, когда по быстрому хочешь накидать скетч, немного неудобно каждый раз вспоминать, на какой же шине находится нужная тебе периферия. Этот тупой вопрос решён тупым switch-case. Теперь достаточно вспомнить название периферии (например: DMA1, TIM2, ADC1, USART2, SPI1, IOPA, USB, I2C1) и передать его в функцию enablePeriphClock(uint16_t periph) или resetPeriphClock(uint16_t periph) для включения или отключения периферии. Полный список названий периферии можно подсмотреть там же, в заголовочнике rcc.h в enum. Единственное неудобство - это то, что файл содержит код одновременно для всех микроконтроллеров 100й серии и не предупредит, если вы захотите включить несуществующую в данном варианте периферию.

Порты ввода - вывода (gpio.h)

Во всех библиотеках для STM32 есть очень удобные функции вида gpio_set(port, pin) для установки и сброса любого количества пинов. Я же решил продолжить эту традицию ещё и для конфигурации. Таким образом, кроме традиционных gpioSet и gpioReset у меня имеется:

// тип порта (аналоговый ввод, цифровой ввод, вывод, вывод с открытым стоком) 
void gpioSetAnalogue(uint32_t port, uint32_t pin);
void gpioSetInput(uint32_t port, uint32_t pin);
void gpioSetPushPull(uint32_t port, uint32_t pin);
void gpioSetOpenDrain(uint32_t port, uint32_t pin);
// ручное управление или для периферии (сперва нужно выбрать тип порта) 
void gpioSetAlternativeF(uint32_t port, uint32_t pin);
// 20кОм резисторы подтяжки
void gpioSetPullUp(uint32_t port, uint32_t pin);
void gpioSetPullDown(uint32_t port, uint32_t pin);
// частота тактирования порта (максимальная по умолчанию).
void gpioSetOutput2M(uint32_t port, uint32_t pin);
void gpioSetOutput10M(uint32_t port, uint32_t pin);
void gpioSetOutput50M(uint32_t port, uint32_t pin);

Как вы видите, все они имеют такой же синтаксис port, pin . Таким образом не надо передавать никаких настроек, нужная конфигурация уже содержится в названии функции, а также можно инициализировать несколько пинов сразу.

Теперь об особенностях использования. При инициализации порта на выход (push pull или open drain) выбирается максимальная частота тактирования - 50МГц. Таким образом для конфигурации пина на выход или вход достаточно вызвать всего одну из четырёх верхних функций. Если же частота тактирования нужна другая, после дополнительно нужно вызвать функцию выбора частоты, но не перед. Также после выбора типа порта (аналоговый вход, цифровой вход, выход, выход с открытым стоком), можно вызвать функции активации периферии на этом порту (gpioSetAlternativeF) и функции выбора подтяжки к питанию или земле для входов (gpioSetPullUp/Down). Если вы думаете, что вы что то напутали ранее, тогда есть вариант gpioToDefault(GPIOx, GPIOALL) .

Также новый подход дал и новую фичу. Кроме классических gpioSet/Reset и getPort, есть функция gpioIsActive, возвращает единицу если хотя бы один из выбранных пинов активен.

Но новый подход имеет и новые недостатки. Учитывая тот факт, что конфигурация порта разбросана по двум регистрам с четырёхбитными полями, то функция инициализации к простым условным операциям уже не сводится. Для неё необходим цикл, проходящий по всем 16 вероятным пинам порта. Короче, функция получилась слишком большой и медленной для реализации программных интерфейсов с постоянным переключением с выхода на вход, хотя для большинства остальных задач подходит.

UPD: В комментариях упоминалась атомарность. В моей библиотеке, как и в libopencm3 установка и сброс пинов производится одновременно, при том любого количества. Для этого нужно их указать через или, например: gpioSet(GPIOA, GPIO1 | GPIO3 | GPIO4); . По сути функции gpioSet и gpioReset всего лишь оберточные функции регистров bit set/reset register и bit reset register. А вот конфигурация уже не одновременная, при конфигурации каждый из выбранных через или пинов будет конфигурироваться отдельной командой записи в регистр, в порядке возрастания номера.

Для реализации программных интерфейсов пригодились же дубликаты этих функций, но работающие только для одного пина. Причём пин необходимо указывать не как обычно через макрос GPIOn, означающий (1<<n), а просто передавая целочисленный номер пина. Все эти функции доступны в этом же файле и называются так же, но с заменой gpio на gpion. Мне они пригодились для написания мегагерцового интерфейса SWD, потому как там нужно было постоянно переключать пин данных SWDIO с выхода на вход и обратно.

Задержки (delay.h)

Куда же без задержек, мой вариант задержек отличается от вариантов любых других библиотек убогостю. Т.к. в нём нет ассемблерных вставок, а количество тактов в микросекунду подбиралось вручную на частоте 1 Гц. Есть задержки для микросекунд, миллисекунд и секунд (rough_delay_us, delay_ms, delay_s) название функции намекает на то, что точность задержки грубая.

Так же есть и таймерные функции задержки для stm32f103, работающие на втором таймере (timDelayNs, timDelayUs, timDelayMs) . Естественно, требуют инициализации перед использованием (timDelayInit() ).

Проверка

Функции испытаны в боевых условиях, здесь просто классический hello world, мигание портом PA1. Берём папку stm32, пишем в main.c вот это:

#include "gpio.h"
#include "delay.h"
#include "rcc.h"


int main(void) {
    enablePeriphClock(IOPA);
    gpioSetPushPull(GPIOA, GPIO1);

    while(1){
        gpioSet(GPIOA, GPIO1);
        delay_ms(500);
        gpioReset(GPIOA, GPIO1);
        delay_ms(500);
    }
}

Запускаем make, потом st-flash write bin/example.bin 0x8000000. Под виндой нео бессудьте.

Итог

Наверное все микроконтроллеры не приспособлены под какой либо общий API. Вспомните сами, скольких любителей среды arduino вы ткнули в libavr в своё время. STM32 хоть и намного сложнее восьмибиток, но по прежнему ей лучше управлять вручную регистрами. А какие либо стандартные функции бесполезны и мешают, либо вовсе невозможны. Я уже достаточно повозился с STM32, попробовал и АЦП по DMA и UART и SPI и I2C и таймеры в качестве ШИМ. И в итоге я подумал, что есть только четыре случая, когда стандартные функции действительно нужны и пригождаются, три из них и были указаны в этом посте. Позже добавлю функции записи flash, помню, приходилось хранить настройки в памяти программ stmки. Возможно будет и пятый случай - функции конфигурации портов, которые я уже не представляю, как буду писать.

Комментарии (17)


  1. anoldman25
    11.10.2024 20:25

    А что плохого в opencm3? Это просто вопрос. Я как раз подумал его использовать.


    1. dltex Автор
      11.10.2024 20:25

      Да ничего, макросы чуть чуть длиннее, код usb не так хорошо структурирован. Мейк у него тоже какой то длинный и необрезанный. В остальном хорошая библиотека, много стандартных функций на всю периферию. А свою начал писать просто потому что хотелось высказаться, "as free as speech" как говориться. В общем мой вариант может меньше, но он и короче, у cm3 возможностей больше, но и объём кода значительней, что может усложнить изучение.


  1. alcotel
    11.10.2024 20:25

    Я как-то давно подзабил на все эти "хардваре абстракшын". Тем более, что у разных процов инициализация функций GPIO разная. И один фиг вся эта инициализация по факту - просто запись пачки констант в пачку регистров.

    Поэтому давно пользуюсь табличкой типа такой. Изначально я для FPGA такие "кодогенераторы" делал, где ног немеряно. Потом прикрутил к микроконтроллерам.

    Распиновку при этом можно скопировать из нетлиста CAD, или сравнить со схемой уже не вручную. Ошибок меньше выходит, когда выводов много. Ну и прошивка получается меньше, заливается моментально.


    1. dltex Автор
      11.10.2024 20:25

      Я тут мысленно меряюсь с opencm3 и hal количеством функций тихо комплексную. А ты мне, забей, все равно под капотом регистры, с которыми даже уже и человеку не обязательно работать.


  1. HEXFFFFFFFF
    11.10.2024 20:25

    Я занимаюсь микроконтроллерами/микропроцессорам больше 30ти лет. Когда то для z80 писал даже не на asm а прямо на машинных кодах)))) Но я обсалютно не понимаю зачем все эти изощерения в 2024г? Есть куча готовых библиотек для работы с низким уровнем. Есть arduino sdk, используя которую вы сможете одними и тем же кодом работать с переферией кучи разных контроллеров. Тот же delay там реализован для всего.. Да бывают специфические задачи где нужно реализовать что то не стандартное и надо лезть в asm и регистры, но это бывает очень-очень редко. Да, нормальный программист должен знать как это все работает на уровне регистров.

    Но писать с нуля взаимодействие с регистрами, вместо того что бы взять готовое sdk, растягивая задачу которую можно решить за несколько часов на месяц, это, в 2024г показатель крайнего непроффесианализма.


    1. dltex Автор
      11.10.2024 20:25

      Как быстро летит время, камушек то совсем свежий был, дата стоит 2007 год в datasheet. А вы уже говорите опоздал.


    1. kenomimi
      11.10.2024 20:25

      это, в 2024г показатель крайнего непроффесианализма

      Или начинающего, которых хочет детально понять, как оно работает. В наше время чел, который начинает с такого изучения, а не с тупого копипаста из примеров, заслуживает реального уважения.

      А к arduino sdk массовые претензии больше потому, что нет некоего стандартного файла описания, какие ресурсы какой либой используются. В результате имеем одно большое минное поле...


    1. Goron_Dekar
      11.10.2024 20:25

      Гибкость.

      Дело в том, что библиотеки дают универсальность, теряя в гибкости.

      Как и в представленной тут библиотеке, во многих проектах подобных абстракций не реализуется

      1) атомарность переключения состояния пинов

      2) верная последовательность переключения состояния.

      3) множественная настройка пинов одного порта одновременно

      Это не важно, если вы настраиваете пин один раз за всю свою программу. Но если вам посреди критической секции вам надо перевести 3-4 пина из ввода с поддяжкой вверх на вывод и 0, избегая вывода 1 или рассинхронизации пинов (например, ШИМ силовой), то лучше это делать напрямую через регистры. А для этого через регистры надо уметь.

      В своё время долго пытался написать хорошую компил-тайм библиотеку для пинов, но быстро понял, что регистры писали люди весьма умные, и когда реализуешь в абстракции все их возможности, абстракция получается гораздо более громоздкой, чем сами регистры.


      1. HEXFFFFFFFF
        11.10.2024 20:25

        Гибкость вы теряете если вы ардуинщик, и не понимаете как это работает внутри. А так беруться исходники и правяться под себя если уже возникла такая необходиость. Кстати у stm32 есть специальные режимы для формирования ШИМ, они все прекрастно доступны через hal, cmsis, arduino, нет необходимости дергать пины напрямую, да и не правильно это.

        Да, во всех этих библиотеках попадаются ошибки, или недочеты, что то бывает не реализовано. Но поправить/добавить на порядок быстрее чем писать с нуля, и тратить месяцы на то что за вас уже давно сделали.


        1. Goron_Dekar
          11.10.2024 20:25

          Вот чтобы не быть ардуинщиком отличный путь это хоть раз написать свою абстракцию.

          ШИМ режимы stm32 весьма ущербны, и хорошо работают только если у тебя трансформатор или простой коллекторник. Для чего-то более сложного всё равно не хватает. Я уж не говорю про "аварийные" режимы, где из поведения - только хай-З. А мне может быть нужен аварийный сброс тока со всех катушек в резисторы торможения, да ещё и на бесколлекторном моторе.

          Короче, я не спорю, что если ты проффессионально пишешь прошивки для детских игрушек, получая деньги за то, как быстро ты реализуешь необходимый функционал, то ты должен использовать самый быстрый фреймворк. Если же у тебя медоборудование, сертификации, прописаны все аварийные режимы, да ещё и батарейное питание с экономией каждого микроваттчаса то не стоит экономить пару месяцев вылизывания всего до тактов, всё равно сертификация займёт года два.


          1. NutsUnderline
            11.10.2024 20:25

            для повышенной безопасности как раз вроде есть специальные фрейворки с кучей проверок и тестов

            в stm32 вроде не простые таймеры и много их разных, неужели не хватает, неужели в каком нибуть К1921 (заточены под управление движками) таймеры круче сделаны?


            1. Goron_Dekar
              11.10.2024 20:25

              Фреймворки есть. Но они реализуют алгоритмы, а не конфигурацию GPIO.

              Да, в К1921 таймеры интереснее чем в stm32. Но всегда найдётся такой кейс, когда их функционала не достаточно. Для таких моментов или FPGA, или напрямую с регистрами. А если вспомнить про батарейное питание, то FPGA отпадает, и остаются только регистры.


              1. dltex Автор
                11.10.2024 20:25

                Есть там один прикол, можно в регистры управления портами через DMA писать. Официально заявленная точность такого способа ~5 тактов. То есть нужную последовательность состояний портов сохраняем в массив, а потом заставляем DMA последовательно этот массив писать в порт. Причём DMA может пинать таймер с заданной вами частотой.
                Мне такой бред помог сделать блок питания по полумостовой схеме, где по очерёдности нужно активировать транзисторы верхнего и нижнего плеча, да ещё и скважность при этом регулировать.


                1. Goron_Dekar
                  11.10.2024 20:25

                  О, да! Я помню как я пускал DMA в регистры управления и в регистры вывода. Даже помню, как специально проектировал плату так, чтобы делать сложный ногодрыг через выводы по DMA.

                  Но DMA внезапно очень много жрёт и очень долго запускается, чтобы использовать этот трюк для аварийного поведения. Только для штатного.


    1. NutsUnderline
      11.10.2024 20:25

      как раз вот это самое "готовое sdk" в 2024 году бывает представляет собой кучку глючного кода, небольшая правка параметров может вызвать исключение в ядре которое в либе/rom и и без исходников, отладчиком только дизасм показывает - тадам, программист уже реверсер.

      Тот же delay может использовать какой нить таймер, а значит это надо учитывать.

      В общем допиливание напильником все равно быстрее чем разработка с 0, но чуть что то менее стандартное - приходится углубляться.


  1. Brrastak
    11.10.2024 20:25

    Одна из вещей, которые мне нравятся в rust, это библиотека интерфейсов embedded-hal, которая реализована для кучи популярных чипов


    1. ponikrf
      11.10.2024 20:25

      А толку то, в отношении того же STM32 у которого есть генератор кода для СИ? Вобще для МК на данный момент гибкая работа с памятью это все таки необходимость. Хотя если брать уже более современные МК типа F7 или тот же ESP32, то там это уже не так актуально и можно в целом потерять в оптимизации некоторых процессов в замен использования более тяжелых библиотек. Тема мутная