Удобная работа с GPIO на STM32

Долгое время работая с 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 все очень однообразно везде.

16 thoughts on “Удобная работа с GPIO на STM32”

      1. Можно. Более того, у хал лицензия GPL, а у планировщика — коммерческая. Но можно прикрутить другое ядро (FreeRTOS, например), а можно вообще без ядра

  1. > Самое главное, чтобы порядок расположения строк в массиве точно следовал порядку имен в Enum.
    > За этим надо следить ручками, если знаете более красивое решение, то набросьте

    Загуглите X macros.

      1. Юзаю эту либу с прошлой статьи про шаговики, я вот так прикрутил:

        #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
        };

  2. А я так делаю (конструктор класса 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();

    };

    1. А за что ее любить? ТАм порты ничего не умеют. Люби уж тогда MCS-51 там у порта только одна опция 0 или 1 :)))

  3. Метр, на мой взгляд, близок к написанию краткой и наглядной раскопытации, но предлагаю еще немного упростить подход:

    #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 — */
    );

Добавить комментарий

Ваш e-mail не будет опубликован.

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.