Долгое время работая с STM32 все никак не мог определиться как же удобней мне обращаться с портами GPIO. Всякие SPL методы мне категорически не прикалывают, как то громоздко, хотя и читаемо. Опять же помнить как там параметры эти зовутся. Вручную порты тасовать можно, но опять же громоздко выходит. В дефайны оборачивать… тоже чет не то. Пока в одном из проектов не увидел годную реализацию, немного ее усовершенствовал и теперь с удовольствием использую.
Главное достоинство ее в том, что теперь неимоверно просто менять порты с ноги на ногу. Тасовать как угодно. Что особенно приятно когда пилишь пилишь девайс, хотелки постепенно растут, а потом не влезаешь в корпус и надо ног побольше. Берешь и пересаживаешься в корпус пожирней, а матрицу выводов переписываешь за пять минут и все взлетает правильно с первой же компиляции.
В общем, суть такова:
Библиотечка IO.h состоит из хидера с именами и сишника, с парой функций. В хидере кроме прототипов есть еще и enum с именами:
Вроде такого, скопипащен из реального проекта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | typedef enum { io_Blink = 0, io_DIR1 = 1, io_EN1, io_STEP1, io_DIR2, io_EN2, io_STEP2, io_DIR3, io_EN3, io_STEP3, io_DIR4, io_EN4, io_STEP4, io_DIR5, io_EN5, io_STEP5, io_EndstopX, io_EndstopY, io_EndstopB, io_EndstopF, io_LEDX, io_LEDY, io_RX1, io_TX1, io_RX2, io_TX2, io_RX3, io_TX3, io_RX4, io_TX4, io_RS_DIR4 } tIOLine; |
А также есть enum с состояними, чисто для удобства и наглядности.
1 2 3 4 5 6 7 | typedef enum { OFF = 0, ON = 1, LOW = 0, HIGH =1 } tIOState; |
Там же находятся битовые маски определения состояний в которых может быть вывод порта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #define IN (0x00) #define OUT_10MHz (0x01) #define OUT_2MHz (0x02) #define OUT_50MHz (0x03) #define OUT_PP (0x00) #define OUT_OD (0x04) #define OUT_APP (0x08) #define OUT_AOD (0x0C) #define IN_ADC (0x00) #define IN_HIZ (0x04) #define IN_PULL (0x08) |
Они взяты просто из таблицы:
И фактически кодируют вот этот четырехбитный блок.
Т.е. если нам надо вывод перевести в состояние входа с подтяжкой, то нам нужна битмаска IN_PULL. 0x08 это b1000, что развернется в CNF=10, MODE=00.
Если же надо, например, выход с Open Drain и частотой в 2Мгц, то берем сумму двух масок: OUT_2MHz+OUT_OD = b0110. Остается только это битмаску задвинуть на нужное место.
Ну,а обозначения простые IN и OUT это вход и выход соответственно, PP — PushPull, OD — OpenDrain, APP — Альтернативная функция PushPull, AOD — альтернативная функция OpenDrain. Ну и ADC — вход АЦП, HIZ — выскоимпендансный вход. PULL — вход с подтяжкой. Направление подтяжки задается битом в ODR.
Теперь эту всю красоту надо как то упорядочить, чтобы было удобно пользоваться.
В IO.с у меня живет вот такая структура:
1 2 3 4 5 6 7 | typedef struct { GPIO_TypeDef* GPIOx; // Имя порта uint16_t GPIO_Pin; // Номер порта uint8_t MODE; // Режим uint8_t DefState; // Стартовое значение (уйдет в бит ODR при инициализации) } tGPIO_Line; |
Ну и типом этой структуры мы создаем массивчик:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | const tGPIO_Line IOs[] = {{ GPIOD, 11, OUT_10MHz + OUT_PP, HIGH}, // Blink 1 * { GPIOB, 9, OUT_10MHz + OUT_PP, HIGH}, // Dir1 XR 2 * { GPIOE, 1, OUT_10MHz + OUT_PP, HIGH}, // En1 3 * { GPIOE, 0, OUT_10MHz + OUT_PP, HIGH}, // Step1 4 * { GPIOB, 6, OUT_10MHz + OUT_PP, HIGH}, // Dir2 YL 5 * { GPIOB, 8, OUT_10MHz + OUT_PP, HIGH}, // En2 6 * { GPIOB, 7, OUT_10MHz + OUT_PP, HIGH}, // Step2 7 * { GPIOD, 6, OUT_10MHz + OUT_PP, HIGH}, // Dir3 F 8 * { GPIOB, 5, OUT_10MHz + OUT_PP, LOW}, // En3 9 * { GPIOD, 7, OUT_10MHz + OUT_PP, HIGH}, // Step3 10 * { GPIOD, 3, OUT_10MHz + OUT_PP, HIGH}, // Dir4 B 11 * { GPIOD, 5, OUT_10MHz + OUT_PP, LOW}, // En4 12 * { GPIOD, 4, OUT_10MHz + OUT_PP, HIGH}, // Step4 13 * { GPIOA, 8, OUT_10MHz + OUT_PP, HIGH}, // Dir5 S 14 * { GPIOA, 12, OUT_10MHz + OUT_PP, LOW}, // En5 15 * { GPIOA, 11, OUT_10MHz + OUT_PP, HIGH}, // Step5 16 * { GPIOD, 13, IN + IN_PULL, HIGH}, // Endstop x 17 * { GPIOD, 12, IN + IN_PULL, HIGH}, // Endstop y 18 * { GPIOC, 6, IN + IN_PULL, HIGH}, // Endstop B 19 * { GPIOD, 15, IN + IN_PULL, HIGH}, // Endstop F 20 * { GPIOA, 5, OUT_10MHz + OUT_PP, LOW}, // LedX 21 * DEBUG { GPIOA, 6, OUT_10MHz + OUT_PP, LOW}, // LedY 22 * DEBUG { GPIOA, 10, IN + IN_PULL, HIGH}, // Rx1 23 * { GPIOA, 9, OUT_10MHz + OUT_APP, HIGH}, // Tx1 24 * { GPIOA, 3, IN + IN_PULL, HIGH}, // Rx2 25 * { GPIOA, 2, OUT_10MHz + OUT_APP, HIGH}, // Tx2 26 * { GPIOB, 11, IN + IN_PULL, HIGH}, // Rx3 27 * { GPIOB, 10, OUT_10MHz + OUT_APP, HIGH}, // Tx3 28 * { GPIOC, 11, IN + IN_PULL, HIGH}, // Rx4 27 * { GPIOC, 10, OUT_10MHz + OUT_APP, HIGH}, // Tx4 28 * { GPIOD, 0, OUT_10MHz + OUT_PP, LOW} // RS DIR4 }; const uint32_t cIO_COUNT = sizeof(IOs)/sizeof(tGPIO_Line); |
Где по каждому значению, записями вида
GPIOB, 9, OUT_10MHz + OUT_PP, HIGH
определяем нога какого порта, под каким номером в каком режиме работает. Ну и дефолтное значение при старте. Высокое или низкое. Подтяжка вверх или вниз (для входа с подтяжкой). В результате у нас будет обращение к нужному порту GPIO просто по номеру его в массиве, а номера обозваны через enum. Самое главное, чтобы порядок расположения строк в массиве точно следовал порядку имен в Enum. За этим надо следить ручками, если знаете более красивое решение, то набросьте. Да, использования (+) для склейки битмасок является дурным тоном, т.к. в случае ошибки перехлеста равностоящих битов вы получите лажу. Лучше применять ИЛИ (|). Но мне так норм, выглядит красивее. :)
Осталось теперь только сделать обработчики нашего массива, который позволит дрыгать ногами и настраивать. Первым делом идет функция инициализации. Она просто пробежит по массиву и все настроит как задано:
Функция конфигурации линии, тут все просто:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void IO_ConfigLine(tIOLine Line, uint8_t Mode, uint8_t State) { if(IOs[Line].GPIO_Pin < 8) // Определяем в старший или младший регистр надо запихивать данные. { IOs[Line].GPIOx->CRL &= ~(0x0F << (IOs[Line].GPIO_Pin * 4)); // Стираем биты IOs[Line].GPIOx->CRL |= Mode<<(IOs[Line].GPIO_Pin * 4); // Вносим нашу битмаску, задвинув ее на нужное место. } else { IOs[Line].GPIOx->CRH &= ~(0x0F << ((IOs[Line].GPIO_Pin - 8)* 4)); // Аналогично для старшего регистра. IOs[Line].GPIOx->CRH |= Mode<<((IOs[Line].GPIO_Pin - 8)* 4); } IOs[Line].GPIOx->ODR &= ~(1<<IOs[Line].GPIO_Pin); // Прописываем ODR, устанавливая состояние по умолчанию. IOs[Line].GPIOx->ODR |= State<<IOs[Line].GPIO_Pin; } |
Линии порта надо проинициализировать вначале:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | void IO_Init(void) { uint32_t IOCNT = cIO_COUNT; // Включаем тактирование нужных портов. Можно конечно автоматизировать, но я не стал. RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; RCC->APB2ENR |= RCC_APB2ENR_IOPDEN; RCC->APB2ENR |= RCC_APB2ENR_IOPEEN; //RCC->APB2ENR |= RCC_APB2ENR_IOPFEN; //RCC->APB2ENR |= RCC_APB2ENR_IOPGEN; RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; // А дальше в цикле пробегаемся по нашему массиву и конфигурируем. for (int Line = 0; Line < cIO_COUNT; Line++) { IO_ConfigLine(Line, IOs[Line].MODE, IOs[Line].DefState); } } |
И три функции управления портом:
Установить линию
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void IO_SetLine(tIOLine Line, bool State) { // Кстати, пришла в голову сейчас идея как можно избавиться от этого условия. У нас же BSRR может и ставить и сбрасывать. // А значит State можно намазать через умножение на сдвиг и в зависимости от того 0 он или 1 двигать бит // на нужную половину для сброса или установки. Осталось только проверить даст ли это выгоду. if (State) { IOs[Line].GPIOx->BSRR = 1 << IOs[Line].GPIO_Pin; } else { IOs[Line].GPIOx->BRR = 1 << IOs[Line].GPIO_Pin; } } |
Инвертировать
1 2 3 4 | void IO_InvertLine(tIOLine Line) { IOs[Line].GPIOx->ODR ^= 1 << IOs[Line].GPIO_Pin; } |
Считать
1 2 3 4 5 6 7 | bool IO_GetLine(tIOLine Line) { if (Line < cIO_COUNT) return ((IOs[Line].GPIOx->IDR) & (1<<IOs[Line].GPIO_Pin)); else return false; } |
Так как линии все поименованы, то пользоваться ими становится легко и приятно, IDE сама подсказывает имя в автозамену и возможные состояния которые она может принимать:
1 2 | IO_SetLine(io_Blink,ON); IO_SetLine(io_Blink,OFF); |
Ну и в таком духе. Получается очень удобно, читаемо, легко использовать и, если надо, в махом переконфигурируется как угодно в одном месте. Такой же способ на типах-массивах можно применить и для группировки и размножения чего угодно. Например серводвигателей. Тогда добавление еще одного движка будет делаться буквально в пару копипастов — просто добавить еще одну запись в массив. Та же концепция прокатит и с таймерами например, благо в стм32 все очень однообразно везде.
Оригинально. Свежо.
Можно подавать на патент.
Да не, что то похожее я где то видел уже. Правда реализовано было средствами плюсов.
Похожее было в либе pio.h для Atmel Sam7
const Pin res2 = {1 << 26, AT91C_BASE_PIOA, AT91C_ID_PIOA, PIO_OUTPUT_1, PIO_DEFAULT};
const Pin miso = {1 << 24, AT91C_BASE_PIOA, AT91C_ID_PIOA, PIO_PERIPH_B, PIO_DEFAULT};
const Pin mosi = {1 << 23, AT91C_BASE_PIOA, AT91C_ID_PIOA, PIO_PERIPH_B, PIO_DEFAULT};
const Pin sck = {1 << 22, AT91C_BASE_PIOA, AT91C_ID_PIOA, PIO_PERIPH_B, PIO_DEFAULT};
const Pin cs0 = {1 << 21, AT91C_BASE_PIOA, AT91C_ID_PIOA, PIO_PERIPH_B, PIO_DEFAULT};
const Pin pins[] = { res2, miso, mosi, sck, cs0};
auto result = PIO_Configure (pins, PIO_LISTSIZE(pins));
На chibios Hal не смотрели?
Когда то смотрел саму чибиос, но чет не понравилось. Хал там можно отдельно оторвать?
Можно. Более того, у хал лицензия GPL, а у планировщика — коммерческая. Но можно прикрутить другое ядро (FreeRTOS, например), а можно вообще без ядра
> Самое главное, чтобы порядок расположения строк в массиве точно следовал порядку имен в Enum.
> За этим надо следить ручками, если знаете более красивое решение, то набросьте
Загуглите X macros.
Хм…. даже не знал что оно так умеет О_о.
Юзаю эту либу с прошлой статьи про шаговики, я вот так прикрутил:
#define IO_TABLE\
X_IO(io_LED, GPIOA, 15, OUT_2MHz, OUT_PP, LOW) \
X_IO(io_RS485_Switch, GPIOA, 8, OUT_2MHz, OUT_PP, LOW) \
…………………………………..
typedef enum
{
#define X_IO(a,b,c,d,e,f) a,
IO_TABLE
#undef X_IO
NUM_IO //count
} tIOLine;
…………………………………..
const tGPIO_Line IOs[NUM_IO] =
{
#define X_IO(a,b,c,d,e,f) {b,c,d+e,f},
IO_TABLE
#undef X_IO
};
А я так делаю (конструктор класса Bluetooth для примера)
class Bluetooth: public PinIRQ{
public:
Bluetooth(): at_mode_pin(PORTB,PIN10), /*state_pin(PORTC,PIN7),*/ reset_pin(PORTC,PIN4), usart(9600){
self_pointer = this;
vSemaphoreCreateBinary(xBluetoothSemaphore);
vSemaphoreCreateBinary(xOkReceivedSemaphore);
xTaskCreate( vListeningTask,/* ( signed char * )*/ «ListeningTask», configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2,
( xTaskHandle * ) NULL);
at_mode_pin.switchToOutput();
at_mode_pin.reset();
state_pin.switchToInput();
state_pin.setCallback((TPIN_IRQ_CALLBACK) this);
state_pin.enableExti(true);
reset_pin.switchToOutput();
reset_pin.set();
usart.iqr_enable();
};
Ну это плюсы за собой тащить надо. А я предпочитаю голый Си.
Зря.
А теперь с плюсов не слезу ни за что.
В такие моменты я начинаю горячо любить Ардуинку.
А за что ее любить? ТАм порты ничего не умеют. Люби уж тогда MCS-51 там у порта только одна опция 0 или 1 :)))
есть ещё к580
Это слишком олдфажно даже для меня.
Метр, на мой взгляд, близок к написанию краткой и наглядной раскопытации, но предлагаю еще немного упростить подход:
#define PIN_CFG(PIN, MODE) ((uint64_t)(MODE) << ((PIN) * 4))
/*
In input mode (MODE[1:0]=00):
00: Analog mode
01: Floating input (reset state)
10: Input with pull-up / pull-down
11: Reserved
*/
#define I_ANALOG (0ULL << 2)
#define I_FLOAT (1ULL << 2)
#define I_PULL (2ULL < 00):
00: General purpose output push-pull
01: General purpose output Open-drain
10: Alternate function output Push-pull
11: Alternate function output Open-drain
*/
#define O_PP (0ULL << 2)
#define O_OD (1ULL << 2)
#define O_AF (2ULL <BSRR = CAT(STATE, PIN)
Теперь, чтобы проинитить порт целиком, достаточно будет записи вида:
GPIOA->BSRR = GPIO_BSRR_BS13 | GPIO_BSRR_BR14; /* CONFIGURE SWD PINS PULL-UP/PULL-DOWN */
*(uint64_t *) GPIOA = (
PIN_CFG(0, O_2MHZ) | /* PA0: OUTPUT (PUSH-PULL, LOW SPEEED) */
PIN_CFG(1, O_2MHZ) | /* PA1: OUTPUT (PUSH-PULL, LOW SPEEED) */
PIN_CFG(2, O_2MHZ) | /* PA2: OUTPUT (PUSH-PULL, LOW SPEEED) */
PIN_CFG(3, O_2MHZ) | /* PA3: OUTPUT (PUSH-PULL, LOW SPEEED) */
PIN_CFG(4, O_2MHZ) | /* PA4: OUTPUT (PUSH-PULL, LOW SPEEED) */
PIN_CFG(5, O_2MHZ) | /* PA5: OUTPUT (PUSH-PULL, LOW SPEEED) */
PIN_CFG(6, O_2MHZ) | /* PA6: OUTPUT (PUSH-PULL, LOW SPEEED) */
PIN_CFG(7, O_2MHZ) | /* PA7: OUTPUT (PUSH-PULL, LOW SPEEED) */
PIN_CFG(8, I_ANALOG) | /* — unused — */
PIN_CFG(9, O_AF + O_10MHZ) | /* PA9: USART1 TX (PUSH-PULL, MEDIUM SPEEED) */
PIN_CFG(10, I_ANALOG) | /* — unused — */
PIN_CFG(11, I_ANALOG) | /* — unused — */
PIN_CFG(12, I_ANALOG) | /* — unused — */
PIN_CFG(13, I_PULL) | /* PA13: SWDIO (INPUT, PULL-UP) */
PIN_CFG(14, I_PULL) | /* PA14: SWCLK (INPUT, PULL-DOWN) */
PIN_CFG(15, I_ANALOG) /* — unused — */
);
Доброго времени суток. Подскажите, как в данном формате удобнее сделать Переинициализацию пина (например для перевода пина 1-wire на вход, а затем обратно на выход). Тупой вариант «в лоб» — завести второй массив структур, с другой конфигурацией пинов, и запускать процедуру его инициализации. Но это как правило нужно сделать для единичных пинов, и нет смысла переинициализировать все остальные пины заново. Есть более элегантное решение ?
Можно сделать что-то типа функцию инициализации с аргументом-указателем на элемент массива структур. А если несколько (например переинициализировать 8 выводов из 40) ?
Всё отлично работает но ровно до тех пор пока программист помнит порядок аргументов. Через месяц-другой вызовете вместо IO_SetLine(io_Blink,OFF) функцию IO_SetLine(OFF,io_Blink). А прекрасный Си компилятор успешно скомпилирует не выдав ни единого варнинга даже в самом жестком режиме. Конечно можно вылечить макросами с ## или плагином фронтенда, но всё-же проще с enum просто не работать.
Так иде подскажет порядок.
Разве компилятор не ругнется? Первый параметр типа «tIOLine», а второй — bool.
Я тоже такой фигнёй страдал. Теперь СТ-шники выпустили свою CubeIDE с графической инициализацией. Плюс библиотека LL позволяет генерировать высокопроизводительный код. Имена пинов можно задавать прямо на микросхеме, Пара макросов типа PinUp() PinDown(). И никакой запары ручками с инициализацией периферии.
Куб гененирует такой фарш, что ну его нахер. Лучше бы они просто генерилку кусков сделали, чем это убожество. ЛЛ юзаю, но не в этом случае, неудобно там реализована работа с портами. Мне моя матрица всего на свете нравится куда больше, уж сколько раз благодаря ей мощные изменения в распиновке делал в пару движений. Особенно когда в отладке тебе надо взять и перекинуть группу пинов куда-нибудь в другое место, чтобы просто посмотреть что там дрыгается, а потом вернуть обратно. Матрицу поправил, посмотрел, вернул как было. А с кубовски/лл/спл инициализациями задолбаешься строки тасовать. Я уж не говорю о том, что этот куб меняется постоянно, они то одно придумают, то другое, то старое похерят, не удивлюсь если завтра они скажут — нахер куб ,вот вам новая шняга, изучайте ее. Как было когда то с спл.
Мне понравилась реализация на c++ и метапрограммирование:
Pinlist<Pin, …> list; // любой список пинов
list.Init();
Создается список используемых портов и за 1 операцию конфигурируются все пины, которые относятся к этому порту.
Получается быстрее и памяти расходуется меньше. Например, для пинов, указанных в статье, получилось на 152 байта меньше.
При этом, на этапе компиляции, проверяется правильность порта, номера пина(не больше 15), конфигурация, а также уникальность пина — нельзя случайно или специально использовать один и тот же пин. Это все бесплатно — никаких накладных расходов.
Также можно изменить состояние пинов:
list.Write(3) — 2 младших пина в 1, остальные в 0.
Причем совершенно неважно к каким портам относятся пины и их последовательность. Удобно использоовать в параллельных интерфейсах, типа дисплея HD44780.
Если пойти дальше, то можно интегрировать класс в остальную периферию и в итоге писать примерно так:
Pinlist::Init() — и все необходимые пины сконфигурируются как надо.
Мне понравилась реализация на c++17 и метапрограммирование:
Pinlist<Pin, …> list; // любой список пинов
list.Init();
Создается список используемых портов и за 1 операцию конфигурируются все пины, которые относятся к этому порту.
Получается быстрее и памяти расходуется меньше. Например, для пинов, указанных в статье, получилось на 152 байта меньше.
При этом, на этапе компиляции, проверяется правильность порта, номера пина(не больше 15), конфигурация, а также уникальность пина — нельзя случайно или специально использовать один и тот же пин. Это все бесплатно — никаких накладных расходов.
Также можно изменить состояние пинов:
list.Write(3) — 2 младших пина в 1, остальные в 0.
Причем совершенно неважно к каким портам относятся пины и их последовательность. Удобно использоовать в параллельных интерфейсах, типа дисплея с HD44780.
Если пойти дальше, то можно интегрировать класс в остальную периферию и в итоге писать примерно так:
Pinlist::Init() — и все необходимые пины сконфигурируются как надо.
Последний код:
» Pinlist::Init() «
А можно ссылку на эту библиотеку?