Работа с портами ввода-вывода микроконтроллеров на Си++

При разработке программ для микроконтроллеров (МК) работа с внутренний и внешней периферией является очень важной частью (а иногда и единственной) программы. Это своего рода фундамент, на котором основывается более высокоуровневая логика программы. От эффективности взаимодействия с периферией напрямую зависит эффективность программы в целом. Под эффективностью здесь следует понимать не только скорость выполнения и минимальный размер кода, но и эффективность написания и сопровождения кода.
Многие внешние устройства подключаются к МК через порты ввода-вывода общего назначения (GPIO). Эффективность взаимодействия с этими устройствами во многом зависит от способа работы с портами ввода-вывода.

Тут возникают два, на первый взгляд, противоречивых требования:

  • 1)Драйвера внешней периферии хочется писать максимально абстрагировавшись от конкретного способа подключения к микроконтроллеру, а ещё лучше независимо от типа микроконтроллера. Переписывать «библиотечный» код для каждого проекта не очень хорошо.
  • 2)Скорость и размер кода в большинстве случаев имеют большое значение.


Ситуация осложняется огромным количеством способов подключения одного внешнего устройства к одному контроллеру и в большинстве случаев определяется удобством изготовления печатной платы наличием свободных линий в порту, необходимостью использовать некоторые линии портов для их специальных возможностей и т.д. Устройство может быть подключено как к одному порту, так и к нескольким, линии порта могут быть как упорядочены по их логическому весу в подключенном устройстве, так и нет. Оно вообще может быть подключено через какой-нибудь расширитель ввода-вывода, например, сдвиговый регистр. При таком разнообразии очень сложно, чтобы драйвер устройства не был заточен под конкретный способ подключения этого устройства, или-же не был слишком громоздким и медленным в случае более универсального решения.

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

Давайте разберемся, какие методы работы с портами ввода-вывода традиционно применяются при программировании на чистом Си.
Можно выделить следующие подходы (вариант с жестко заданными в коде именами портов и номерами ножек не рассматриваем):

  • 1) Определение портов и линий ввода-вывода с помощью препроцессора.
  • 2) Передача порта в код, который его использует, посредствам указателя.
  • 3) Виртуальные порты.

Примеры приведены для МК семейства AVR. Компилятор avr-gcc, но описываемые подходы могут быть применены к любым другим МК, для которых имеется стандартный Си/Си++ компилятор.

1. Препроцессор
Способов использования препроцессора для работы с портами в МК существует великое множество. В самом простом и самом распространенном случае просто объявляем порт и номера ножек, к которым подключено наше устройство с помощью директивы #define, не забыв, конечно, про DDR и PIN регистры, если они нужны.
Нет ничего проще, чем помигать светодиодом, подключенным к одному из выводов МК:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <avr/io.h>
#include <util/delay.h>
#define LED_PORT PORTA
#define LED_PIN 5
 
int main()
{
	while(1)
	{
		LED_PORT |= 1 << LED_PIN; //зажечь 
		_delay_ms(100);
		LED_PORT &= ~(1 << LED_PIN);
		_delay_ms(100);
	}
}

Строчка

1
LED_PORT |= 1 << LED_PIN;

после компиляции превращается в одну команду процессора:

1
sbi PORTA, 5

также как и

1
LED_PORT &= ~(1 << LED_PIN);

компилируется в:

1
cbi PORTA, 5

Выглядит всё очень просто и эффективно. А что, если нам надо управлять несколькими линиями сразу?
Вот пример из хорошо известной библиотеки для работы с дисплеем HD44780 Scienceprog.com Lcd Lib:

1
2
3
4
5
6
7
8
9
10
11
#define LCD_RS	0 	//define MCU pin connected to LCD RS
#define LCD_RW	1 	//define MCU pin connected to LCD R/W
#define LCD_E	2	//define MCU pin connected to LCD E
#define LCD_D4	4	//define MCU pin connected to LCD D3
#define LCD_D5	5	//define MCU pin connected to LCD D4
#define LCD_D6	6	//define MCU pin connected to LCD D5
#define LCD_D7	7	//define MCU pin connected to LCD D6
#define LDP PORTD	//define MCU port connected to LCD data pins
#define LCP PORTD	//define MCU port connected to LCD control pins
#define LDDR DDRD	//define MCU direction register for port connected to LCD data pins
#define LCDR DDRD	//define MCU direction register for port connected to LCD control pins

А вот так там выглядит функция инициализации дисплея:

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
void LCDinit(void)//Initializes LCD
{
	_delay_ms(15);
	LDP=0x00;
	LCP=0x00;
	LDDR|=1<<LCD_D7|1<<LCD_D6|1<<LCD_D5|1<<LCD_D4;
	LCDR|=1<<LCD_E|1<<LCD_RW|1<<LCD_RS;
 
   //---------one------
	LDP=0<<LCD_D7|0<<LCD_D6|1<<LCD_D5|1<<LCD_D4; //4 bit mode
	LCP|=1<<LCD_E|0<<LCD_RW|0<<LCD_RS;		
	_delay_ms(1);
	LCP&=~(1<<LCD_E);
	_delay_ms(1);
 
	//-----------two-----------
	LDP=0<<LCD_D7|0<<LCD_D6|1<<LCD_D5|1<<LCD_D4; //4 bit mode
	LCP|=1<<LCD_E|0<<LCD_RW|0<<LCD_RS;		
	_delay_ms(1);
	LCP&=~(1<<LCD_E);
	_delay_ms(1);
 
	//-------three-------------
	LDP=0<<LCD_D7|0<<LCD_D6|1<<LCD_D5|0<<LCD_D4; //4 bit mode
	LCP|=1<<LCD_E|0<<LCD_RW|0<<LCD_RS;		
	_delay_ms(1);
	LCP&=~(1<<LCD_E);
	_delay_ms(1);
 
	//--------4 bit--dual line---------------
	LCDsendCommand(0b00101000);
   //-----increment address, cursor shift------
	LCDsendCommand(0b00001110);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
void LCDsendCommand(uint8_t cmd)	//Sends Command to LCD
{
	LDP=(cmd&0b11110000);
	LCP|=1<<LCD_E;		
	_delay_ms(1);
	LCP&=~(1<<LCD_E);
	_delay_ms(1);
	LDP=((cmd&0b00001111)<<4);	
	LCP|=1<<LCD_E;		
	_delay_ms(1);
	LCP&=~(1<<LCD_E);
	_delay_ms(1);
}

В этом случае правильнее было-бы применить побитовый вывод в порт:

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
void LCDwrite4(uint8_t cmd)
{
	LDP &= ~(1<<LCD_D7|1<<LCD_D6|1<<LCD_D5|1<<LCD_D4); //clear data bus
 
	if(cmd & (1 << 0))
		LDP |= LCD_D4;
	if(cmd & (1 << 1))
		LDP |= LCD_D5;
	if(cmd & (1 << 2))
		LDP |= LCD_D6;
	if(cmd & (1 << 3))
		LDP |= LCD_D7;
}
 
void LCDsendCommand(uint8_t cmd)	//Sends Command to LCD
{
	LCDwrite4(cmd);
	LCP|=1<<LCD_E;		
	_delay_ms(1);
	LCP&=~(1<<LCD_E);
	_delay_ms(1);
	LCDwrite4(cmd);	
	LCP|=1<<LCD_E;		
	_delay_ms(1);
	LCP&=~(1<<LCD_E);
	_delay_ms(1);
}

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

1
2
3
#define LCD_RS	PORTA, 0 	//define MCU pin connected to LCD RS
#define LCD_RW	PORTB, 1 	//define MCU pin connected to LCD R/W
#define LCD_E	PORTB, 2	//define MCU pin connected to LCD E

Добавим к этому средства для манипуляции линией и получим так называемые макросы Аскольда Волкова:

1
2
3
4
5
6
7
#define _setL(port,bit)         do { port &= ~(1 << bit); } while(0)
#define _setH(port,bit)         do { port |= (1 << bit); } while(0)
#define _clrL(port,bit)         do { port |= (1 << bit); } while(0)
#define _clrH(port,bit)         do { port &= ~(1 << bit); } while(0)
#define _bitL(port,bit)         (!(port & (1 << bit)))
#define _bitH(port,bit)         (port & (1 << bit))
#define _cpl(port,bit,val)      do {port ^= (1 << bit); } while(0)

Этот подход, в отличии от предыдущих, уже можно сделать переносимым на разные аппаратные платформы. Достаточно только определить эти макросы для целевой платформы соответствующим образом. Однако, у нас по-прежнему остались нерешенными некоторые проблемы. Во-первых, большой размер и низкое быстродействие кода, ведь мы используем побитовый вывод в порты. Даже если часть линий находятся в одном порту и идут подряд, определить эту ситуацию с помощью макросов невозможно. И если, например, размер кода окажется слишком большим, то уже написанную и отлаженную библиотеку придется подгонять под конкретный проект, плодя многочисленные ее версии и, возможно, внося ошибки.
Во-вторых, подключение нескольких однотипных устройств возможно только путём дублирования кода. Опять-же результирующий размер программы неоправданно увеличивается. А потом в две версии одного и того-же кода вносятся изменения независимо друг от друга и каждая из них начинает жить своей жизнью. Спасает здесь только относительно малый размер программ для встроенных систем, ведь если что не так, то можно всё быстренько переписать заново :)

Многие компиляторы для AVR поддерживают побитовый доступ к портам на уровне специальных расширений компилятора (CVAVR, MicroC AVR) или встроенных библиотек (IAR C/C++ Compiler for AVR). Такой побитовый доступ несложно реализовать и в avr-gcc с помощью битовых полей (собственно в IAR примерно так и это и реализовано):

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
typedef struct Bits_t
{
	uint8_t Bit0 :1;
	uint8_t Bit1 :1;
	uint8_t Bit2 :1;
	uint8_t Bit3 :1;
	uint8_t Bit4 :1;
	uint8_t Bit5 :1;
	uint8_t Bit6 :1;
	uint8_t Bit7 :1;
}Bits;
 
#define PortaBits (*((volatile Bits*)&PORTA))
#define LedPin PortaBits.Bit5
 
int main()
{
	DDRA = 1 << 5;
	while(1)
	{
		LedPin = 1;//зажечь 
		_delay_ms(100);
		LedPin = 0;//выключить
_delay_ms(100);
	}
}

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

В итоге при использовании препроцессора для манипуляций с портами ввода-вывода мы получаем:

  • Простоту и ясность для простых вещей – очень просто написать пару макросов, чтоб поморгать светодиодом.
  • Высокую скорость и компактность кода при отказе от универсальности (все ножки в одном порту и желательно по порядку).
  • Не расходуется дополнительная память.
  • Содержащий большое количество битовых операций код достаточно сложно читать.
  • Можно сделать универсально и относительно переносимо пожертвовав размером и скоростью кода (побитовый вывод).
  • Для управления несколькими однотипными устройствами придется дублировать код.

2. Передача порта через указатель.
Как уже выяснили один из основных недостатков пи работе с портами ввода-вывода с помощью препроцессора это сложность использования однотипных устройств. Такой сложности не возникает если передавать порт в код, который его использует через указатель.
Порты ввода-вывода в большинстве МК есть не что иное как просто ячейка памяти или регистр в пространстве ввода-вывода и естественно к нему можно обратиться по адресу через указатель.
Удобно запаковать указатель на порт и битовые маски нужных ножек в одну структуру, чтоб потом ее передавать в функцию, которая что-то с ними будет делать. Здесь лучше использовать именно битовые маски, а не битовые позиции, иначе сдвиги вида (1 << some_bit_position) не могут быть вычислены на этапе компиляции (потому, что some_bit_position не константа, а переменная) и будут честно выполнится в каждом месте где встретятся. Возьмём сдвиговый регистр-защёлку, например 74HC595, который часто используется для экономии выводов МК при подключении многовыводной периферии.

1
2
3
4
5
6
7
8
//ShiftReg.h
typedef struct ShiftReg_t
{
	volatile uint8_t *port;
	uint8_t data_pin_bm;
	uint8_t clk_pin_bm;
	uint8_t latch_pin_bm;
}ShiftReg;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//ShiftReg.c
void WriteShiftReg(ShiftReg *reg, uint8_t value)
{
	for(uint8_t i=0; i<8; i++)
	{
		if(value & 1)	//выводим данные
			*reg->port |= reg->data_pin_bm;
		else
			*reg->port &= ~reg->data_pin_bm;
		//тактовый импульс
		*reg->port |= reg->clk_pin_bm;
		value >>= 1;
		*reg->port &= ~reg->clk_pin_bm;
	}
	//защёлкиваем данные в регистр
	*reg->port |= reg->latch_pin_bm;
	*reg->port &= ~reg->latch_pin_bm;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//main.c
 
#include <avr/io.h>
//вывода data и clk могут быть общие.
ShiftReg reg1 = {&PORTA, 1<<1, 1<<2, 1<<3};
ShiftReg reg2 = {&PORTA, 1<<1, 1<<2, 1<<4};
 
int main()
{
	DDRA = 0xff;
	DDRB = 0xff;
	WriteShiftReg(&reg1, 0xff);
	WriteShiftReg(&reg2, 0x55);
	while(1)
	{
 
	}
}

От дублирования кода мы избавились, одна функция WriteShiftReg используется для записи во много сдвиговых регистров. Читаемость кода не пострадала. К тому-же появилась возможность менять порт и ножки к которым подключен регистр во время выполнения программы. Полезность такой возможности, правда, сомнительна особенно для маленьких МК. Таким способом удобно работать с перифирией требующей немного линий ввода-вывода и подключенноу к МК во множественном числе, в том числе подключенные с использованием каких-либо последовательных протоколов USART, SPI (если не хватает аппаратных) 1-Wire и т.д.

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

1
*reg->port |= reg->latch_pin_bm;

Фрагмент ассемблерного листинга WriteShiftReg

1
2
3
4
5
6
7
8
9
	LD	r30, X+
	LD	r31, X
	SBIW	r26, 0x01	; 1
	LD	r24, Z
	ADIW	r26, 0x04	; 4
	LD	r25, X
	SBIW	r26, 0x04	; 4
	OR	r24, r25
	ST	Z, r24

Как видно, компилятор не может теперь сопримизировать обращение к порту и установка бита теперь занимает 9 инструкций вместо одной.

Чтобы несколько оптимизировать код можно ввести дополнительные ограничения, например, задать номера ножек константами, и выкинуть соответствующие им битовые маски. В примере со сдвиговым регистром можно заменить константами data_pin_bm и clk_pin_bm и исключить их из структуры, а latch_pin_bm оставить как есть для универсальности:

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
typedef struct ShiftReg_t
{
	volatile uint8_t *port;
	uint8_t latch_pin_bm;
}ShiftReg;
 
enum {clk_pin_bm = 1 << 0, data_pin_bm = 1 << 1};
...
//ShiftReg.c
void WriteShiftReg(ShiftReg *reg, uint8_t value)
{
	for(uint8_t i=0; i<8; i++)
	{
		if(value & 1)	//выводим данные
			*reg->port |= data_pin_bm;
		else
			*reg->port &= ~>data_pin_bm;
		//тактовый импульс
		*reg->port |= clk_pin_bm;
		value >>= 1;
		*reg->port &= clk_pin_bm;
	}
	//защёлкиваем данные в регистр
	*reg->port |= reg->latch_pin_bm;
	*reg->port &= ~reg->latch_pin_bm;
}

Такая оптимизация сократит код WriteShiftReg примерно на 25 % с незначительной потерей в удобстве.

Итого:

  • Удобно использовать для подключения многих однотипных устройств, требующих не много линий ввода-вывода.
  • Нет необходимости в дублировании кода.
  • Можно менять порт и линии подключения устройства во время выполнения программы.
  • Низкая скорость доступа к портам.
  • Большой размер кода.
  • Требуется дополнительная память для хранения указателя на порт и битовых масок.
  • Неудобно и неэффективно работать с большим количеством линий ввода-вывода.

3. Виртуальные порты
Нужно подключить к МК несколько устройств требующих достаточно много линий ввода-вывода, драйвер которых обладает достаточно сложной и объёмной логикой, например, тот-же дисплей HD44780 (при использовании 4х битного интерфейса требует 7 линий). К тому-же устройства могут быть подключены различными способами – к разным линиям портов, или через сдвиговый регистр. Дублировать код драйвера и подгонять его под каждый способ подключения устройства – нет уж, спасибо. Да и размер скомпилированного кода рискует не поместится в целевой МК. Передавать порты драйверу в через указатели? Слишком большие накладные расходы при работе с портами через указатели, много памяти, медленно и громоздко.
Здесь лучше применить, так называемые виртуальные порты. На языке Си они могут быть реализованы как группа функций, принимающих входное значение и выполняющих соответствующие операции ввода-вывода:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void VPort1Write(uint8_t value)
{
	PORTA = (PORTA & 0xf0) | (value & 0x0f);
	PORTB = (PORTB & 0x0f) | (value & 0xf0) >> 4;
}
 
void VPort1DirWrite(uint8_t value)
{
	DDRA = (DDRA & 0xf0) | (value & 0x0f);
	DDRB = (DDRB & 0x0f) | (value & 0xf0) >> 4;
}
 
uint8_t VPort1Read()
{
	return (PORTA & 0xf0) | (PORTB & 0x0f) << 4;
}
 
uint8_t VPort1PinRead()
{
	return (PINA & 0xf0) | (PINB & 0x0f) << 4;
}

В этом примере входное значение из 8-ми бит распределено между 4-мя младшими битами портов PORTA и PORTB.
Реализация функции для вывода команды в дисплей HD44780 с использованием такого виртуального порта будет выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//объявляем тип указателя на функцию
typedef void (*WriteFunc)(uint8_t);
 
void LCDwrite4(uint8_t value, WriteFunc write)
{
	enum{LCD_E=4, LCD_RS=5, LCD_RW=6};
	uint8_t tmp;
tmp = (value & 0x0f) | (1 << LCD_E);//совмещаем вывод тетрады 
	write(tmp);					//и установку LCD_E
	_delay_ms(1);
	tmp &= ~(1 << LCD_E);
	write(tmp);
	_delay_ms(1);
}
 
void LCDsendCommand(uint8_t cmd, WriteFunc write)	//Sends Command to LCD
{
	LCDwrite4(cmd >> 4, write); //старшая тетрада
	LCDwrite4(cmd, write);		//младшая тетрада
}

1
2
3
4
5
6
7
void DoSomthing()
{
	...
	VPort1DirWrite(0xff);
	LCDsendCommand(0x30, VPort1Write);
	...
}

Теперь становится не важно как именно подключен дисплей к МК. Всё тяжёлая работа по манипуляции портами возложена на функцию VPort1Write. Для каждого способа подключения дисплея нам нужно только написать соответствующую реализацию виртуального порта, тут уже насколько фантазии хватит. Используя, например, уже написанную функцию WriteShiftReg, легко подключить дисплей через сдвиговый регистр:

1
2
3
4
5
6
ShiftReg reg1 = {&PORTA, 1};
 
void VPort2Write(uint8_t value)
{
	WriteShiftReg(&reg1, value);
}

Такой подход достаточно эффективен для вывода многобитного значения. Из накладных расходов только вызов функции по указателю, но за один вызов выводится сразу много бит. Это эффективнее побитового вывода в порт, но менее эффективно вывода в обычный порт (когда все линии устройства подключены по логическому порядку к одному порту). Тут всё уже зависит от того как реализована функция виртуального порта. Однако если нужно изменить только один бит, то придется запомнить предыдущее значение (или прочитать его из порта), наложить на него соответствующую битовую маску и записать в виртуальный порт. То есть изменения одного бита в этом порту будет дороже записи всех бит порта. Поэтому, например, в функции LCDwrite4 запись тетрады совмещена с установкой бита LCD_E.
Если необходимо не только писать в виртуальный порт, но и читать из него и управлять направлением, подтяжкой или ещё что-то, то функций реализующих виртуальный порт будет много и все их надо писать .

Чего-же мы добились с помощью виртуальных портов:

  • Логика работы с устройством полностью отделена от способа подключения устройства.
  • Нет необходимости в дублировании кода.
  • Сравнительно небольшие накладные расходы на вывод в порт.
  • Чистота и понятность кода.
  • Необходимо вручную писать реализацию виртуального порта – много однотипных функций (совсем зажрались, подумают некоторые товарищи).
  • Манипуляции с отдельными битами неэффективны.

Подход Си++ к работе с портами.
Что может нам предложить язык Си++ при работе с портами ввода-вывода по сравнению чистым Си? Давайте сначала сформулируем, что мы хотим получить в результате наших изысканий:

  • 1. Логика работы с устройством должна быть отделена от способа его подключения.
  • 2. Не должно быть дублирования кода при подключении многих однотипных устройств.
  • 3. Эффективно работать с отдельными битами.
  • 4. Эффективно работать с многобитовыми значениями.
  • 5. Решение должно быть переносимо на разные аппаратные платформы.
  • 6. Не должно использоваться дополнительная память.
  • 7. Легкость написания и сопровождения кода.
  • 8. Реализация полностью на стандартном Си++.

От динамической конфигурации линий ввода-вывода сразу отказываемся из-за необходимости доступа к портам через указатель со всеми вытекающими последствиями.
Удобно было бы описать линию ввода-вывода в виде отдельной сущности, т.е. класса. В Си++ даже если в классе не объявлено ни одного поля, переменная этого класса всё равно будет иметь размер как минимум один байт, потому, что переменная должна иметь адрес. Значит, нам не надо создавать объекты этого класса, а все функции в нем сделать статическими. А как тогда различать разные линии? Можно сделать этот класс шаблоном, а порт и номер бита передавать в виде параметров шаблона. С номером бита всё ясно – это целое число, его легко передать как нетиповой параметр шаблона. А как быть с портом? Посмотрим, как определены порты ввода-вывода в заголовочных файлах avr-gcc:

1
2
3
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
#define _SFR_MEM8(mem_addr) _MMIO_BYTE(mem_addr + __SFR_OFFSET)
#define PORTB	_SFR_IO8(0x18)

Параметры шаблона могут быть только типовыми или целочисленными константными выражениями, вычисляемыми во время компиляции. Ни указатель, ни ссылку нельзя предать как параметр шаблона:

1
2
3
template<uint8_t *PORT, uint8_t PIN> //ошибка
class Pin
{...};

Может быть попробовать передавать адрес порта в виде целого числа, а потом его преобразовывать в указатель:

1
2
3
4
5
6
7
8
9
10
template<unsigned PORT, uint8_t PIN>  
class Pin
{
public:
	static void Set()
	{
		*(volatile uint8_t*)(PORT + __SFR_OFFSET) |= (1 << PIN);
	}
...
};

Это уже работает, но адрес порта придется задавать вручную в виде целого числа, что неудобно:

1
2
3
typedef Pin<0x18, 1> Pin1;
...
Pin1::Set();	//sbi 0x18, 1

Взять адрес PORTB по имени не получится потому, что операция взятия адреса не может появляться в константных выражениях, коими должны быть параметры шаблона:

1
typedef Pin<(unsigned)&PORTB, 1> Pin1; // ошибка

Однако, нам нужно передавать не только адрес порта, ещё нужны PINx и DDRx регистры. К тому-же, в таком виде о переносимости не может быть и речи. Можно, конечно, написать макрос, которому передаём соответствующие имена регистров, и он генерирует соответствующий класс. Но тогда Pin будет слишком жестко завязан на конкретную реализацию портов.
Можно написать перечисление в котором объявлены все порты с удобочитаемыми именами и функцию, которая возвращает нужный порт в зависимости от переданного параметра шаблона.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum Ports {Porta, Portb, Portc};
 
template<Ports PORT, uint8_t PIN>  
class Pin
{
public:
	static volatile uint8_t & GetPort()
	{
		switch(PORT)
		{
			case Porta: return PORTA;
			case Portb: return PORTB;
			case Portc: return PORTC;
		}
	}
	static volatile uint8_t & GetPin(){...}
	static volatile uint8_t & GetDDR(){...}
 
	static void Set()
	{
		GetPort() |= (1 << PIN);
	}
...
};

Функцию GetPort можно объявить как внутри класса, так и снаружи. Реализовать её можно, например, с помощью оператора switch или специализаций шаблонной функции:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<Ports PORT>
volatile uint8_t & GetPort();
 
template<>
volatile uint8_t & GetPort<Porta>()
{
	return PORTA;
}
 
template<>
volatile uint8_t & GetPort<Portb>()
{
	return PORTB;
}
...
template<Ports PORT, uint8_t PIN>  
class Pin
{
public:
	static void Set()
	{
		GetPort<PORT>() |= (1 << PIN);
	}
};

Однако, такой подход всё равно ограничен. В первую очередь потому, что мы жестко завязываемся на конкретную реализацию портов. Во многих семействах МК для управления портами ввода-вывода имеются дополнительные регистры для быстрого сброса/установки/переключения отдельных бит и много чего ещё. Вот, например, структура, описывающая порт в МК семейства Atmel XMega:

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
typedef struct PORT_struct
{
    register8_t DIR;  	/* I/O Port Data Direction */
    register8_t DIRSET;  /* I/O Port Data Direction Set */
    register8_t DIRCLR;  /* I/O Port Data Direction Clear */
    register8_t DIRTGL;  /* I/O Port Data Direction Toggle */
    register8_t OUT;  	/* I/O Port Output */
    register8_t OUTSET;  /* I/O Port Output Set */
    register8_t OUTCLR;  /* I/O Port Output Clear */
    register8_t OUTTGL;  /* I/O Port Output Toggle */
    register8_t IN;  	/* I/O port Input */
    register8_t INTCTRL;  /* Interrupt Control Register */
    register8_t INT0MASK;  /* Port Interrupt 0 Mask */
    register8_t INT1MASK;  /* Port Interrupt 1 Mask */
    register8_t INTFLAGS;  /* Interrupt Flag Register */
    register8_t reserved_0x0D;
    register8_t reserved_0x0E;
    register8_t reserved_0x0F;
    register8_t PIN0CTRL;  /* Pin 0 Control Register */
    register8_t PIN1CTRL;  /* Pin 1 Control Register */
    register8_t PIN2CTRL;  /* Pin 2 Control Register */
    register8_t PIN3CTRL;  /* Pin 3 Control Register */
    register8_t PIN4CTRL;  /* Pin 4 Control Register */
    register8_t PIN5CTRL;  /* Pin 5 Control Register */
    register8_t PIN6CTRL;  /* Pin 6 Control Register */
    register8_t PIN7CTRL;  /* Pin 7 Control Register */
} PORT_t;

Чтобы изолировать класс Pin от конкретной реализации портов ввода-вывода введём дополнительный уровень абстракции. Добавление нового уровня абстракции вовсе не обязательно влечёт за собой какие-то накладные расходы.
С классом описывающим порт ввода-вывода у нас возникает та-же проблема, что и с классом Pin: как связать класс с конкретными регистрами? Можно конечно попытаться сделать это с помощью перечислений и частичной специализации, но в данном случае это всё-таки лучше сделать с помощью препроцессора:

1
2
3
4
#define MAKE_PORT(portName, ddrName, pinName, className, ID) \
		class className{\
		...
		};

Теперь объявим портов на все случаи жизни:

1
2
3
4
5
6
7
#ifdef PORTA
	MAKE_PORT(PORTA, DDRA, PINA, Porta, 'A')
	#endif
	...
	#ifdef PORT
	MAKE_PORT(PORTR, DDRR, PINR, Portr, 'R')
	#endif

Проанализировав реализацию портов ввода-вывода различных семейств МК составим минимальный интерфейс для эффективного управления портами (управление режимами подтяжки пока опустим):

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
// Псевдоним для типа данных порта.
// Для ARM, например, это будет uint32_t.
typedef uint8_t DataT;
 
// Записать значение в порт PORT = value
static void Write(DataT value);
 
// Прочитать значение записанное в порт
static DataT Read();
 
//Записать значение направления линий В/В
static void DirWrite(DataT value);
 
// прочитать направление линий В/В
static DataT DirRead();
 
//Установить биты в порту PORT |= value;
static void Set(DataT value);
 
// Очистить биты в проту PORT &= ~value;
static void Clear(DataT value);
 
// Очистить по маске и установить PORT = (PORT & ~clearMask) | value;
static void ClearAndSet(DataT clearMask, DataT value);
 
// Переключить биты PORT ^= value;
static void Togle(DataT value);
 
// Установить биты направления
static void DirSet(DataT value);
 
// Очистиь биты направления
static void DirClear(DataT value);
 
// Переключить биты направления
static void DirTogle(DataT value);
 
// прочитать состояние линий В/В
static DataT PinRead();
 
// Уникальный идентификотор порта 
enum{Id = ID};
 
// Разрядность порта (бит)
enum{Width=sizeof(DataT)*8};

Реализация этого интерфейса для семейств Tiny и Mega AVR будет выглядеть так:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#define MAKE_PORT(portName, ddrName, pinName, className, ID) \
		class className{\
		public:\
			typedef uint8_t DataT;\
		private:\
			static volatile DataT &data()\
			{\
				return portName;\
			}\
			static volatile DataT &dir()\
			{\
				return ddrName;\
			}\
			static volatile DataT &pin()\
			{\
				return pinName;\
			}\
		public:\
			static void Write(DataT value)\
			{\
				data() = value;\
			}\
			static void ClearAndSet(DataT clearMask, DataT value)\
			{\
				data() = (data() & ~clearMask) | value;\
			}\
			static DataT Read()\
			{\
				return data();\
			}\
			static void DirWrite(DataT value)\
			{\
				dir() = value;\
			}\
			static DataT DirRead()\
			{\
				return dir();\
			}\
			static void Set(DataT value)\
			{\
				data() |= value;\
			}\
			static void Clear(DataT value)\
			{\
				data() &= ~value;\
			}\
			static void Togle(DataT value)\
			{\
				data() ^= value;\
			}\
			static void DirSet(DataT value)\
			{\
				dir() |= value;\
			}\
			static void DirClear(DataT value)\
			{\
				dir() &= ~value;\
			}\
			static void DirTogle(DataT value)\
			{\
				dir() ^= value;\
			}\
			static DataT PinRead()\
			{\
				return pin();\
			}\
			enum{Id = ID};\
			enum{Width=sizeof(DataT)*8};\
		};

Поскольку в семействе XMega все регистры порта сгруппированы в одну структуру и есть специальные регистры чтобы быстро устанавливать/очищать/переключать отдельные биты порта, реализация нашего интерфейса будет несколько проще:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#define MAKE_PORT(portName, className, ID) \
	class className{\
	public:\
		typedef uint8_t DataT;\
	public:\
		static void Write(DataT value)\
		{\
			portName.OUT = value;\
		}\
		static void ClearAndSet(DataT clearMask, DataT value)\
		{\
			Clear(clearMask);\
			Set(value);\
		}\
		static DataT Read()\
		{\
			return portName.OUT;\
		}\
		static void DirWrite(DataT value)\
		{\
			portName.DIR = value;\
		}\
		static DataT DirRead()\
		{\
			return portName.DIR;\
		}\
		static void Set(DataT value)\
		{\
			portName.OUTSET = value;\
		}\
		static void Clear(DataT value)\
		{\
			portName.OUTCLR = value;\
		}\
		static void Togle(DataT value)\
		{\
			portName.OUTTGL = value;\
		}\
		static void DirSet(DataT value)\
		{\
			portName.DIRSET = value;\
		}\
		static void DirClear(DataT value)\
		{\
			portName.DIRCLR = value;\
		}\
		static DataT PinRead()\
		{\
			return portName.IN;\
		}\
		static void DirTogle(DataT value)\
		{\
			portName.DIRTGL = value;\
		}\
		enum{Id = ID};\
		enum{Width=8};\
	};
 
#ifdef PORTA
MAKE_PORT(PORTA, Porta, 'A')
#endif
...
#ifdef PORTR
MAKE_PORT(PORTR, Portr, 'R')
#endif

Анологично можно определить порты В\В для других семейств МК.
Порты В/В теперь инкапсулированы в классы, и мы можем их использовать как типовые параметры шаблонов. Приступим к реализации класса для линии ввода-вывода:

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
template<class PORT, uint8_t PIN>
	class TPin
	{
	public:
		typedef PORT Port;
		enum{Number = PIN};
		static void Set()
		{
			PORT::Set(1 << PIN);
		}
		static void Set(uint8_t val)
		{
			if(val)
				Set();
			else Clear();
		}
		static void SetDir(uint8_t val)
		{
			if(val)
				SetDirWrite();
			else SetDirRead();
		}
		static void Clear()
		{
			PORT::Clear(1 << PIN);
		}
		static void Togle()
		{
			PORT::Togle(1 << PIN);
		}
		static void SetDirRead()
		{
			PORT::DirClear(1 << PIN);
		}
		static void SetDirWrite()
		{
			PORT::DirSet(1 << PIN);
		}
		static uint8_t IsSet()
		{
			return PORT::PinRead() & (uint8_t)(1 << PIN);
		}	
	};

Протестируем полученный класс:

1
2
3
4
5
6
7
8
9
10
11
12
typedef TPin<Porta, 1> Pa1;
...
Pa1::Set();
//sbi	0x1b, 1	; 27
Pa1::Clear();
//cbi	0x1b, 1	; 27
 
Pa1::Togle();
//in	r24, 0x1b	; 27
//ldi	r25, 0x02	; 2
//eor	r24, r25
//out	0x1b, r24	; 27

Для удобства определим короткие имена для всех возможных линий В/В:

1
2
3
4
5
6
7
8
9
10
11
#ifdef PORTA
	typedef TPin<Porta, 0> Pa0;
	...
	typedef TPin<Porta, 7> Pa7;
	#endif
	...
	#ifdef PORTR
	typedef TPin<Portr, 0> Pr0;
	...
	typedef TPin<Portr, 7> Pr7;
	#endif

Как видно, никаких накладных расходов нет, эффективность получилась на уровне того, что можно получит с помощью препроцессора. Те кто давно пишут на Си могут возразить – стоило ли писать какие-то непонятные классы на две страницы вместо того, чтобы написать пару однострочных #define-ов и получить тоже самое?

Конечно-же стоило. Ведь получили мы далеко не тоже самое. Во-первых, класс TPin объединяет в себе все операции применимые к линии В/В. Во- вторых, он жестко типизирован и его можно использовать как параметр шаблона. Например, класс для записи значения в сдвиговый регистр:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class ClockPin, class DataPin, class LatchPin, class T = uint8_t>
	class ThreePinLatch
	{
	public:
		typedef T DataT;
		enum{Width=sizeof(DataT)*8};
 
		static void Write(T value)
		{	
			for(uint8_t i=0; i < Width; ++i)
			{
				DataPin::Set(value & 1);
				ClockPin::Set();
				value >>= 1;
				ClockPin::Clear();
			}
			LatchPin::Set();
			LatchPin::Clear();
		}
	};

Постой пример его использования:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef ThreePinLatch<Pa0, Pb3, Pc2> Reg1;
 
int main()
{
	Pa0::SetDirWrite();
	Pb3::SetDirWrite();
	Pc2::SetDirWrite();
 
	while(1)
	{
		Reg1::Write(PORTD);
	}
}

Вызов Reg1::Write компилируется в следующий ассемблерный листинг:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Reg1::Write:
	ldi	r25, 0x00
	sbrs	r24, 0
	rjmp	.+4
	sbi	0x18, 3
	rjmp	.+2
 
	cbi	0x18, 3
	sbi	0x1b, 0
 
	cbi	0x1b, 0
	subi	r25, 0xFF
	cpi	r25, 0x08
	breq	.+4
 
	lsr	r24
	rjmp	.-24
 
	sbi	0x15, 2
 
	cbi	0x15, 2
	ret

Сгенерированный листинг не уступает написанному вручную на ассемблере. И кто говорит, что Си++ избыточен при программировании для МК? Попробуйте переписать этот пример на чистом Си, сохранив чистоту и понятность кода, разделение логики работы устройства от конкретной реализации портов В/В и такую-же эффективность.

Списки линий ввода-вывода
Это только начало. Теперь нам предстоит самое интересное — реализовать эффективный вывод многобитных значений. Для этого нам нужна сущность объединяющая группу линий В/В – своеобразный список линий В/В. Поскольку и порты и отдельные линии у нас представлены различными классами, то логично реализовывать список линий с помощью шаблонов. Но здесь есть одна проблема: список линий может содержать различное число линий, а шаблоны в Си++ имеют фиксированное число параметров (а стандарте Cxx03, в следующей версии появятся Variadic templates). Нам поможет библиотека Loki, написанная Андреем Александреску. В ней реализовано множество шаблонных алгоритмов для манипуляций со списками типов произвольной длинны. Это нам подойдёт – списки типов превращаются в списки линий ввода-вывода. Что, собственно, такое списки типов лучше всего почитать у их автора Андрея Александреску в книге Современное проектирование на С++. Очень рекомендую прочитать, хотя-бы мельком, главу «Списки типов» в этой книге. Без этого будет мало понятно, что происходит дальше.

Не во всех МК предусмотрены команду для манипуляций с отдельными битами в портах В/В. В семействе MegaAVR тоже есть порты для которых недоступны битовые операции. Поэтому чтобы сделать операции с портами максимально эффективными нам нужно отказаться от побитового вывода – одно чтение, модификация значения и запись.

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

В упрощенном виде алгоритм записи значения в произвольный список линий В/В будет выглядеть так:
1. Определить список портов к которых подключены линии из списка.
2. Для каждого порта:

  • Определить список линий к нему подключенный.
  • По этому списку сгенерировать битовую маску для битов, которые не нужно менять.
  • Спроецировать биты из входного значения на соответствующие им места в регистре порта во временный буфер.
  • Наложить битовую маску на регистр порта (т.е. очистить в нем те биту, куда будем записывать новое значение)
  • Записать значение из временного буфера в регистр порта.

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

У каждой линии в списке есть два номера:

  • 1. Номер бита во входном значении
  • 2. Номер бита в порту, куда он отображается

Оба они понадобятся для того, чтобы спроецировать биты из входного значения в порт.
Второй номер класс TPin помнит сам, оно хранится в enum-е:

1
enum{Number = PIN};

Чтобы запомнить первый номер понадобится дополнительный шаблон:

1
2
3
4
5
6
template<class TPIN, uint8_t POSITION>
struct PW	//Pin wrapper
{
	typedef TPIN Pin;
	enum{Position = POSITION};
};

Хотя можно было этого и не делать, а вычислять битовыю позицию потом с помощью алгоритма IndexOf.

Этот шаблон хранит тип линии В/В и ее битовую позицию в списке. Теперь приступим к генерации собственно списка линий. Для определённости ограничим длину списка 16-ю линиями, ели надо можно добавить и больше, потом. Для этого возьмём шаблон MakeTypelist из библиотеки Loki и модифицируем его под свои нужды:

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
template
     <	
	int Position, // стартовая битовая позиция, сначала это 0
typename T1  = NullType, typename T2  = NullType, typename T3  = NullType,
typename T4  = NullType, typename T5  = NullType, typename T6  = NullType,
typename T7  = NullType, typename T8  = NullType, typename T9  = NullType,
typename T10 = NullType, typename T11 = NullType, typename T12 = NullType,
typename T13 = NullType, typename T14 = NullType, typename T15 = NullType,
typename T16 = NullType, typename T17 = NullType
> 
struct MakePinList
{
private:
		// рекурсивно проходим все параметры
		// на следующей итерации Position увеличится на 1,
		// а T2 превратится в T1 и так далее
typedef typename MakePinList
<
		Position + 1,
T2 , T3 , T4 , 
T5 , T6 , T7 , 
T8 , T9 , T10, 
T11, T12, T13,
T14, T15, T16, 
T17
>::Result TailResult;
		enum{PositionInList = Position};
public:
		// Result это и есть требуемый список линий
typedef Typelist< PW<T1, PositionInList>, TailResult> Result;
};
 
//конец списка
	//конец рекурсии, когда список пуст	
template<int Position>
struct MakePinList<Position>
{
typedef NullType Result;
};

В результате на выходе имеем «голый» список типов наших линий ввода вывода. Мы уже можем объявить список из нескольких линий, сделать с ним пока ничего нельзя – для него не определены никакие операции:

1
typedef MakePinList<Pa1, Pa2, Pa3, Pb2, Pb3>::Result MyList;

Зато его можно передать в другой шаблон как один параметр. Воспользуемся этим и напишем класс реализующий операции с этим списком:

1
2
3
4
5
template<class PINS>
struct PinSet
{
...
};

Далее напишем алгоритм для преобразования списка линий в список соответствующих им портам:

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
	//шаблон принимает список линий в качестве параметра
template <class TList> struct GetPorts;
 
// для пустого списка результат – пустой тип
template <> struct GetPorts<NullType>
{
typedef NullType Result;
};
 
	// для непустого списка 
	// конкретизируем, что это должен быть список типов
	// содержащий голову Head и хвост Tail
template <class Head, class Tail>
struct GetPorts< Typelist<Head, Tail> >
{
private:
	// класс TPin помнит свой порт
// запоминаем этот тип порта
		typedef typename Head::Pin::Port Port;
		//рекурсивно генерируем хвост
typedef typename GetPorts<Tail>::Result L1;
public:
	// определяем список портов из текущего порта (Port) и хвоста (L1)
typedef Typelist<Port, L1> Result;
};

Теперь мы можем конвертировать список линий в соответствующий список портов, однако один и тот-же порт может содержаться в нем несколько раз. Нам нужны не повторяющиеся порты, по этому воспользуемся алгоритмом NoDuplicates из библиотеки Loki:

1
2
3
4
// конвертируем список линий в соответствующий список портов
typedef typename GetPorts<PINS>::Result PinsToPorts;
// генерируем список портов без дудликатов
typedef typename NoDuplicates<PinsToPorts>::Result Ports;

Чтобы организовать рекурсивный проход по списку портов понадобится еще один шаблон класса. Назовём его PortWriteIterator. В качестве шаблонных параметров он принимает список портов и исходный список линий:

1
template <class PortList, class PinList> struct PortWriteIterator;

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

1
2
3
4
5
6
7
template <class PinList> struct PortWriteIterator<NullType, PinList>
{
	// DataType может быть uint8_t или uint16_t (а может и uint32_t в дальнейшем)
	template<class DataType>
	static void Write(DataType value)
	{ /*ничего не делаем тут*/ }
};

Далее необходимо выбрать из списка линий, те которые принадлежат определённому порту.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// шаблон принимает два параметра:
// TList - список линий
// T – тип порта дл якоторого
template <class TList, class T> struct GetPinsWithPort;
 
// для пустого списка результат – пустой тип (т.е. тоже пустой список)
template <class T>
struct GetPinsWithPort<NullType, T>
{
typedef NullType Result;
};
// если TList это список типов, голова в котором это PW<TPin<T, N>, M>, т.е. линия в заданном порту T с битовыми позициями N и M в порту и во входном значении соответственно, то вставляем её в голову нового списка. Рекурсивно обрабатываем хвост.
template <class T, class Tail, uint8_t N, uint8_t M>
struct GetPinsWithPort<Typelist<PW<TPin<T, N>, M>, Tail>, T>
{
typedef Typelist<PW<TPin<T, N>, M>, 
typename GetPinsWithPort<Tail, T>::Result> Result;
};
// если голова списка - любой другой тип, то вставляем на её место рекурсивно обработанный хвост.
template <class Head, class Tail, class T>
struct GetPinsWithPort<Typelist<Head, Tail>, T>
{
typedef typename GetPinsWithPort<Tail, T>::Result Result;
};

Теперь вычислим битовую маску для порта.

1
2
3
4
5
6
7
8
9
10
11
12
13
//Параметр TList должен быть список линий
template <class TList> struct GetPortMask;
// Для пустого списка возвращаем 0.
template <> struct GetPortMask<NullType>
{
enum{value = 0};
};
 
template <class Head, class Tail>
struct GetPortMask< Typelist<Head, Tail> >
{	//value =	битовая маска для головы  | битовая маска оставшейся части списка
enum{value = (1 << Head::Pin::Number) | GetPortMask<Tail>::value};
};

Теперь напишем реализацию для функции записи в порт:

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
// Head – голова списка портов – текущий порт
// Tail – оставшийся список
// PinList – исходный список линий.
template <class Head, class Tail, class PinList>
struct PortWriteIterator< Typelist<Head, Tail>, PinList>
{
//Определим линии принадлежащие текущему порту.
typedef typename GetPinsWithPort<PinList, Head>::Result Pins;
// Посчитаем битовую маску для порта
enum{Mask = GetPortMask<Pins>::value};
typedef Head Port;
 
template<class DataType>
	static void Write(DataType value)
{   
	// проецируем биты из входного значения в соответствующие биты порта
	// как это реализованно увидим дальше
		uint8_t result = PinWriteIterator<Pins>::UppendValue(value);
		// если кол-во бит в записываемом значении совпадает с шириной порта,
		// то записываем порт целиком.
		// это условие вычислится во время компиляции
		if((int)Length<Pins>::value == (int)Port::Width)
			Port::Write(result);
		else
		{
			// PORT = PORT & Mask | result;
			Port::ClearAndSet(Mask, result);
		}
		// рекурсивно обрабатываем остальные порты в списке
		PortWriteIterator<Tail, PinList>::Write(value);
}
}

Функция PinWriteIterator::UppendValue отображает биты во входном значении в соответствующие им биты в порту. Эффективность списков линий в целом зависит в первую очередь именно от реализации этого отображения, поскольку оно, в общем случае, должно выполняться динамически и не всегда может быть вычислено во время компиляции. В зависимости от распределения записываемых бит в результирующем порту применим несколько стратегий отображения бит:

  • если записываемые биты в порту расположены последовательно, спроецируем их все сразу с помощью сдвига на нужное число бит и соответствующей битовой маски
  • если одиночный бит в исходном значении и в порту имеют одну позицию, спроецируем его с помощью побитного ИЛИ.
  • в остальных случаях, если бит во входном значении равен 1, то устанавливаем в 1 соответствующий ему бит в регистре.
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
// Tlist – список линий принадлежащих одному порту
template <class TList> struct PinWriteIterator;
// специализация для пустого списка – возвращаем 0
template <> struct PinWriteIterator<NullType>
{
template<class DataType>
	static uint8_t UppendValue(const DataType &value)
	{
		return 0; 
	}
};
 
// специализация для непустого списка
template <class Head, class Tail>
struct PinWriteIterator< Typelist<Head, Tail> >
{
template<class DataType>
static inline uint8_t UppendValue(const DataType &value)
{
	// проверяем, если линии в порту расположены последовательно
// если часть линий в середине списка будет расположена последовательно, то 
// это условие не выполнется, так, что есть ещё простор для оптимизации.
	if(IsSerial<Typelist<Head, Tail> >::value)
	{	
		// сдвигаем значение на нужное число бит и накладываем не него маску
		if((int)Head::Position > (int)Head::Pin::Number)
			return (value >> ((int)Head::Position - (int)Head::Pin::Number)) & 
				GetPortMask<Typelist<Head, Tail> >::value;
		else
			return (value << ((int)Head::Pin::Number - (int)Head::Position)) & 
				GetPortMask<Typelist<Head, Tail> >::value;
	}
 
	uint8_t result=0;
 
	if((int)Head::Position == (int)Head::Pin::Number)
		result |= value & (1 << Head::Position);
	else 
		// это условие будет вычисляться во время выполнения программы
		if(value & (1 << Head::Position))
			result |= (1 << Head::Pin::Number);
	// рекурсивно обрабатываем оставшиеси линии в списке
	return result | PinWriteIterator<Tail>::UppendValue(value);
}
};

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

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
template <class TList> struct IsSerial;
// специализация для пустого списка
template <> struct IsSerial<NullType>
{
	// пустой список последователен
enum{value = 1};
// номер текущей линии
	enum{PinNumber = -1};
	// признак конца списка
	enum{EndOfList = 1};
};
// для непустого списка
template <class Head, class Tail>
struct IsSerial< Typelist<Head, Tail> >
{	
	// последовательна ли оставшаяся часть списка
typedef IsSerial<Tail> I;
// запоминаем номер текущей линии в её порту
enum{PinNumber = Head::Pin::Number};
// не конец списка
enum{EndOfList = 0};
// список последователен если
// номер текущей линии равен номеру следующей - 1 И
// оставшаяся часть списка последовательна ИЛИ 
// текущая линия последняя в списке
enum{value = ((PinNumber == I::PinNumber - 1) && I::value) || I::EndOfList};
};

С учётом всего выше написанного класс PinSet будет выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<class PINS>
struct PinSet
{
private:
	// конвертируем список линий в соответствующий список портов
	typedef typename GetPorts<PINS>::Result PinsToPorts;
public:
	typedef PINS PinTypeList;
// генерируем список портов без дудликатов
	typedef typename NoDuplicates<PinsToPorts>::Result Ports; 
	// длинна списка линий
	enum{Length = Length<PINS>::value};
	// выбираем тип данных записываемый в список линий
	// если длинна списка меньше или равна 8 берём тип uint8_t,
	// если больше – uint16_t
	typedef typename IoPrivate::SelectSize<Length <= 8>::Result DataType;
	//записать значение в список линий
	static void Write(DataType value)
	{
		PortWriteIterator<Ports, PINS>::Write(value);
	}
};

Собственно списки линий уже должны работать:

1
2
3
4
typedef MakePinList<0, Pa1, Pa2, Pa3, Pb3, Pb4>::Result MyList;
typedef PinSet<MyList> MyPins;
...
MyPins::Write(0x55);

Однако, пользоваться ими пока не очень удобно. Поэтому сделаем вокруг нашей реализации списков линий прозрачную и удобную обёртку:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template
<	
typename T1  = NullType, typename T2  = NullType, typename T3  = NullType,
typename T4  = NullType, typename T5  = NullType, typename T6  = NullType,
typename T7  = NullType, typename T8  = NullType, typename T9  = NullType,
typename T10 = NullType, typename T11 = NullType, typename T12 = NullType,
typename T13 = NullType, typename T14 = NullType, typename T15 = NullType,
typename T16 = NullType, typename T17 = NullType
> 
struct PinList: public PinSet
	<
		typename MakePinList
		<	0,	T1,
			T2 , T3 , T4 , 
			T5 , T6 , T7 , 
			T8 , T9 , T10, 
			T11, T12, T13,
			T14, T15, T16, T17
		>::Result
	>
{
// тело этого класса пусое, весь функционал наследован от PinSet
};

Теперь можно объявлять списки линий следующим образом:

1
2
typedef PinList<Pa1, Pa2, Pa3, Pb3, Pb4> MyPins;
MyPins::Write(0x55);

Стало достаточно удобно – можно один раз объявить такой список линий, а потом использовать его где угодно, передавать его как шаблонный параметр в классы и функции реализующие управление периферией. А для того, чтобы изменить способ подключения достаточно подправить только одну строку.

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

1
2
3
4
5
6
7
8
9
10
//вывод в PORTA
	in	r24, 0x1b
	andi	r24, 0xF1
	ori	r24, 0x0A
	out	0x1b, r24
//вывод в PORTB
	in	r24, 0x18
	andi	r24, 0xE7
	ori	r24, 0x10
	out	0x18, r24

Как видно, компилятору все значения были известны и он благополучно посчитал все битовые маски и логические операции, не оставив ничего лишнего на время выполнения. А как он поведёт себя если записываемое значение не известно во время компиляции? Рассмотрим следующий пример (список линий тот-же самый):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// MCU AtMega16
MyPins::Write(PORTC);
 
// читаем PORTC
	in	r18, 0x15	; 21
 
//вывод в PORTA
	in	r25, 0x1b	; 27
	mov	r24, r18
	add	r24, r24
	andi	r24, 0x0E	; 14
	andi	r25, 0xF1	; 241
	or	r24, r25
	out	0x1b, r24	; 27
 
//вывод в PORTB
	in	r24, 0x18	; 24
	andi	r18, 0x18	; 24
	andi	r24, 0xE7	; 231
	or	r18, r24
	out	0x18, r18	; 24

В качестве значения неизвестного используем PORTC. Результат впечатляет, неправда-ли? Даже на ассемблере сложно написать эффективнее. К тому-же если вручную считать все эти битовые маски, то очень велик риск ошибиться.
Рассмотрим тот-же пример скомпилированный для AtXMega128a1. В этом МК порты В/В находятся в расширенном пространстве ввода-вывода и недоступны для операций in и out. Реализация портов, учитывающая особенности семейства XMega у нас уже написана.

1
2
typedef PinList<Pa1, Pa2, Pa3, Pb3, Pb4> MyPins;
MyPins::Write(PORTC.IN);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// читаем PORTC.IN
 
	lds	r18, 0x0648
 
	mov	r25, r18
	add	r25, r25
	andi	r25, 0x0E	; 14
 
//вывод в PORTA
	ldi	r30, 0x00	; 0
	ldi	r31, 0x06	; 6
	ldi	r24, 0x0E	; 14
	std	Z+6, r24	; 0x06
	std	Z+5, r25	; 0x05
	andi	r18, 0x18	; 24
 
//вывод в PORTB
	ldi	r30, 0x20	; 32
	ldi	r31, 0x06	; 6
	ldi	r24, 0x18	; 24
	std	Z+6, r24	; 0x06
	std	Z+5, r18	; 0x05

Как видно, говорить об избыточности и накладных расходах, которые может повлечь использование Си++ здесь не приходится. Аналогичным образом реализуем запись направления линий. Единственное отличие это то, что теперь значение записывается в регистры управления направлением линий (DDRx).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class PINS>
struct PinSet
{
	...
	static void Write(DataType value)
	{
		PortWriteIterator<Ports, PINS>::Write(value);
	}
	//запись направления
static void DirWrite(DataType value)
	{
		PortWriteIterator<Ports, PINS>::DirWrite(value);
	}
};

И соответствующие дополнения в класс PortWriteIterator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <class Head, class Tail, class PinList>
struct PortWriteIterator< Typelist<Head, Tail>, PinList>
{
	...
 
template<class DataType>
static void DirWrite(DataType value)
{   
		uint8_t result = PinWriteIterator<Pins>::UppendValue(value);
		if((int)Length<Pins>::value == (int)Port::Width)
		Port::DirWrite(result);
		else
		{
			Port::DirClear(Mask);
			Port::DirSet(result);
		}
			PortWriteIterator<Tail, PinList>::DirWrite(value);
	}
};

Для полноты добавим, аналогичным образом операции Set, Clear, DirSet, DirClear для установки и обнуления битов соответственно в регистрах порта и регистрах направления.

Остаётся реализовать чтение состояния порта. Это несколько проще, чем запись:

  • Читаем значение из каждого порта,
  • Проецируем биты из порта в выходное значение (в обратном порядке, чем при записи),
  • Объединяем значения со всех портов в одно выходное значение.

Подробно на чтении останавливаться не будем.

Итак у нас уже есть средство для эффективной манипуляции портами ввода-вывода МК. Но ведь задача была изначально шире – полностью изолировать драйвера от с способа подключения устройств. И эта цель уже достигнута. Класс TPin, реализующий линию ввода-вывода, как и списки линий полностью изолированы от реализации портов В/В. Это было видно на том примере, что у нас уже реализованы порты как для семейств Tiny/Mega AVR, так и для XMega. Так вот, достаточно написать класс, реализующий интерфейс порта В/В, и списки линий будут с ним прекрасно работать. Для примера возьмём, уже рассмотренный ранее, сдвиговый регистр и будем его использовать для расширения количества доступных линий В/В.

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
template<class ClockPin, class DataPin, class LatchPin, unsigned ID, class T = uint8_t>
class ThreePinLatch
{
	public:
 
	typedef T DataT;
	// нужен для сортировки портов в списках линий
	enum{Id = ID};
	//разрядность регистра
	enum{Width=sizeof(DataT)*8};
	// запись значения
	static void Write(T value)
	{	
		_currentValue = value;
		for(uint8_t i=0; i < Width; ++i)
		{
			DataPin::Set(value & 1);
			ClockPin::Set();
			value >>= 1;
			ClockPin::Clear();
		}
		LatchPin::Set();
		LatchPin::Clear();
	}
	static DataT Read()
	{
		return _currentValue;
	}
	static void ClearAndSet(DataT clearMask, DataT value)
	{
		Write(_currentValue = (_currentValue & ~clearMask) | value);
	}
	static void Set(DataT value)
	{
		Write(_currentValue |= value);
	}
	static void Clear(DataT value)
	{
		Write(_currentValue &= ~value);
	}
	static void Togle(DataT value)
	{
		Write(_currentValue ^= value);
	}
	static void DirWrite(DataT value)
	{ /*не можем менять направление*/ }
	static DataT DirRead()
	{
		return 0xff; //всегда выход
	}
	static void DirSet(DataT value)
	{/* не можем менять направление */}
	static void DirClear(DataT value)
	{/* не можем менять направление */	}
	static void DirTogle(DataT value)
	{/* не можем менять направление */}
	protected:
		// текущее значение в регистре
		static DataT _currentValue; 
	};
 
	template<class ClockPin, class DataPin, class LatchPin, unsigned ID, class T>
	T ThreePinLatch<ClockPin, DataPin, LatchPin, ID, T>::_currentValue = 0;

Мы не можем прочитать состояние выходных линий регистра – он всегда работает на выход, поэтому функцию чтения состояния не реализуем и не объявляем. Попытка прочитать состояние такого пора вызовет ошибку компиляции. Зато на запись нет никаких ограничения. И мы свободно можем использовать этот «порт» со списками линий.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef ThreePinLatch<Pa0, Pa1, Pa3, 'R1'> ShiftReg1;
 
typedef TPin<ShiftReg1, 0> Rg0;
typedef TPin<ShiftReg1, 1> Rg1;
typedef TPin<ShiftReg1, 2> Rg2;
typedef TPin<ShiftReg1, 3> Rg3;
typedef TPin<ShiftReg1, 4> Rg4;
typedef TPin<ShiftReg1, 5> Rg5;
typedef TPin<ShiftReg1, 6> Rg6;
typedef TPin<ShiftReg1, 7> Rg7;
 
typedef PinList<Rg4, Rg5, Rg6, Rg7, Pb0, Pb1, Pb2, Pb3> MyPins;
...
MyPins::Write(PORTC);

Причем нет никаких ограничений на состав списка линий – можно смешивать в одном списке линии сдвигового регистра и линии обычных портов.

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

1
2
3
template<uint8_t PIN>
class Pin :public TypeAt<PINS, PIN>::Result::Pin
{};

Здесь мы воспользовались алгоритмом TypeAt из библиотеки Loki, который возвращает из списка типов указанную в параметре позицию. Пользоваться этим достаточно просто:

1
2
3
4
5
// Установить линию с индексом 1 в единичное состояние.
// индексация начинается с 0
MyPins::Pin<1>::Set();
...
MyPins::Pin<1>::Clear();

Обобщая доступ к отдельным битам в списке линий приходим к концепции среза. Не буду вдаваться в подробности их реализации, приведу лишь пример использования:

1
2
// берём срез из MyPins начиная с 4-го бита, длинной 4 бита.
typedef MyPins::Slice<4, 4> OtherPins;

В результате получаем новый список линий OtherPins, который будет содержать:

1
<Pb0, Pb1, Pb2, Pb3>

Этот механизм удобен для того, чтобы изменить значения отдельной группы линий, не изменяя остальных. Компилятор для этого случая может сгенерировать более эффективный код. В приведенном примере вызов OtherPins::Write(что-нибудь) запишет значение в 4 бита PORTB, не затронув линии в сдвиговом регистре.

Подведём итоги
Цели поставленные перед реализацией ввода-вывода для МК на Си++ были достигнуты в полной мере. Достигнута как эффективность манипуляций с многобитовыми значениями, так и отдельными линиями ввода-вывода. Списки линий, как и код их использующий, полностью изолированы от конкретной реализации портов. Чтобы добавить новый способ подключения (например через SPI расширитель портов) устройства использующего списки линий, или портировать его на другое семейство МК, достаточно написать соответствующий класс реализующий интерфейс порта ввода-вывода.
Единственными существенными недостатками этого подхода является относительная сложность реализации списков линий (но ведь они уже написаны) и необходимость изучения языка Си++.

Архив с примерами для AVR Studio

© Константин Чижов
Email: klen1@mail.ru Сентябрь 2010

P.S.
Наиболее актуальную версию списков линий (PinList) можно найти по адресу:
github.com/KonstantinChizhov/AvrProjects/tree/master/avrcpp/

75 thoughts on “Работа с портами ввода-вывода микроконтроллеров на Си++”

  1. Очень и очень интересная статья! Я не настолько силен в С++, чтобы даже применить в полной мере тот великолепный инструмент для работы с портами, который предлагает нам автор. Скорее, я понимаю смысл поставленной задачи, ограничения существующих подходов и тот путь, который избрал автор.
    А задача была поставлена крутая! И решена в полной мере. В результате можно получить именно код, не зависящий от конкретной реализации портов, работать как с битами, так и с полями — и при этом всю унылую (и чреватую ошибками) работу делает компилятор! Более того, получаем совершенно изумительно эффективный код!

    Вот сейчас оторвался для отзыва — как раз из отладки программы, где порты использовались через дефайны. Затем несколько линий поменялись местами, изменили назначение — и программа полетела ко всем чертям. Понадобилось много времени, чтобы вернуь все к работающему состоянию.

    Огромный респект автору и пожелание успехов. Хотелось бы узнать (я же оговорился, что слаб в С++), насколько общим было бы описание класса, «реализующего интерфейс порта ввода-вывода», для, скажем, всех АТмег или для конкретной модели? Т.е., нельзя ли теперь от общего к частному — дать готовые шаблоны для использования не слишком высококвалифицированными программистами? Или все слишком завязано на конкретный проект с заданными списками?

    1. Оо, как оперативно выложил ( я не автор, я только наводчик:) )
      Автора можно найти тут: http://electronix.ru/forum/index.php?s=&showtopic=78757&view=findpost&p=808966
      И статья действительно ОЧЕНЬ сильная!
      Я даже не думал, что на плюсах можно получить такого уровня одновременно удобство/гибкость в сочетании с таким качественным и компактным кодом на выходе.

    2. Описание портов максимально обобщено и должно работать для Tiny/Mega AVR и для XMega.
      Со стальёй были коротенькие примеры:
      http://forum.easyelectronics.ru/viewtopic.php?f=4&t=2173
      В конце статьи ссылка на Git репозиторий — там есть всё и даже больше.
      Там есть пример побольше:
      http://github.com/KonstantinChizhov/AvrProjects/tree/master/TestCppIo/

  2. Решение на C++ бесспорно впечатляет, но. С самого начала ставится задача сделать код переносимым.
    Это на практике означает следующее. У нас есть, например, МК ATmega32 в 40-выводном корпусе и МК ATmega328p в 28-выводном. И например, мы делаем LCD-терминал с RS232 входом, LCD подключается по параллельному интерфейсу.
    Поскольку нам нужен последовательный интерфейс, то мы не можем использовать единственный полностью 8-битный порт ATmega328p для передачи данных в LCD. То есть реализацию придётся переделывать, если мы хотим, чтобы код был достаточно оптимизирован.
    Соответственно, код должен быть достаточно простым для переносимости. Что, если мы захотим добавить в библиотеку поддержку ещё одного типа LCD, раз уж у нас есть теперь виртуальные порты? Или ещё проще — добавить поддержку ещё одного протокола терминала.
    Казалось бы, нет ничего проще — в C++ есть наследование. Но наследование — это доступ к методам объектов через указатель. И код сразу же потеряет свою эффективность, более того — сильно разрастётся в объёме, но главное — сразу же начнёт требовать RAM (под объекты и стек вызовов), которого в МК традиционно не слишком много.

    Поэтому делается это всё на самом деле не так. Есть традиционный для C метод доступа к внешним устройствам — через таблицу файловых дескрипторов и функции open, close, read, write и fcntl. Реализация поддержки устройств при этом довольно простая, код — переносимей некуда, и понятный любому С-программисту.
    C++ соответственно это то же самое, только на шаблонах методом потоков iostream.
    По-моему нет смысла изобретать велосипед в данном случае.

    1. Цитирую: «Поэтому делается это всё на самом деле не так»

      Вы совершенно правы, уважаемый! Реально делается «пер анус». Нет, Вы-то знаете ноты и делаете, думаю, именно так, как пишете. Но посмотрите кучу примеров на всех сайтах по МК — что делается! Как раз упомянутым выше способом! Причем, замечу, хорошие программы и знающие программисты.
      Поэтому, если Вам известна альтернатива — в студию, плз! И покажите, как там легко будет переносить и, самое главное, какой оптимальный код сгенерится :)

      И тогда мы уже все вместе сможем понять Ваше крутое заявление, что «нет смысла изобретать велосипед». Илм есть :)

      Мы с Вами, ИМХО, присутствуем при настоящем открытии. С чем автора и поздравляю!

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

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

        Но. То же самое на C++ выдаёт на выходе код, примерно в два с половиной раза больший по размеру, а главное — съедающий при этом весь RAM. Так что то, что на сях помещается в 168-ю или 16-ю мегу, на плюсах требует 328-ю или 32-ю.

    2. Цитата: Но наследование — это доступ к методам объектов через указатель. И код сразу же потеряет свою эффективность, более того — сильно разрастётся в объёме, но главное — сразу же начнёт требовать RAM (под объекты и стек вызовов), которого в МК традиционно не слишком много.

      Это распространённое заблуждение и далеко не свегда происходит так. Помимо динамического полиморфизма — виртуальных функций и иже сними, есть ещё полиморфизм статический на основе всё тех-же шаблонов. Но это отдельный разговор.

      1. Шаблоны хороши как средство отделить алгоритм от конкретного типа данных.

        Чтобы проиллюстрировать то, что использование C++ в данной статье — явное излишество, попробуйте представить себе реализацию на основе данного переносимого кода доступа к портам реализацию например светодиода.
        Задача проста. К мк может быть подключен одно или несколько устройств — светодиодов. Мы хотим управлять светодиодом следующими методами — включить, выключить, мигнуть( кол-во раз, длительность импульса ).
        Решение этой простой задачи методами ООП, а ведь C++ предполагает именно такой подход, сводится к созданию класса, например, Led.
        Что получается на практике. Помимо методов управления, добавляются конструктор и деструктор, а также в RAM занимается место под экземпляры класса. Кроме этого выясняется, что нам нужен ещё один класс — таймер, для того, чтобы светодиодом можно было мигать. Переносимый класс таймера — это уже разделение кода на переносимую реализацию таймеров микроконтроллера и небольшой класс таймера для светодиодов или системного таймера (эдакий мини-RTOS).

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

        И наконец, самое страшное. В случае AVR, поскольку AVR — RISC-процессор, то компилятор генерит для него код, в котором регистры сохраняет не вызываемая функция, а вызывающая функция. На практике это означает, что если обработчик прерывания вызывает функцию, то все регистры в обработчике прерывания будут сохранены. 62 такта в заголовке обработчика и 62 такта перед возвратом из обработчика.
        Такова будет плата за возможность выключить или включить светодиод по прерыванию при помощи переносимого кода. Что на си, что на плюсах.

        1. Вы, товаришь, читали невнимательно. Объекты типов PortX и TPin не создаются вовсе — эти типы статические, в них нет никаких данных, это просто группа функций, объёдинённая одним именем типа, чтобы её(эту группу функций) можно было передавать как аргумент шаблонам. На счёт таймеров и прочего, найду время — напишу ещё.

  3. Спасибо за статью. Я правда полный 0 в с++ но общую суть уловил. Щас скачал книгу с++ для чайников дабы начать постигать это вуду.
    Вопрос к знающим людям: я пользуюсь gcc из win-avr и пакетом gcc из набора debian. Для того чтобы начать работать с C++ этих инструментов достаточно, или надо качать что-то еще?

    1. Достаточно WinAvr и AvrStudio.
      AvrStudio по умолчанию создает в проекте .c файл. Его нужно удалить из проекта, нати его на и заменить расширение на .cpp , после чего добавить этот файл (уже с расширением .cpp) снова в проект. И всё, можно писать на Си++.

    1. class в Си++ практически не отличается от struct, за исключением того, что в структурах все члены по умолчанию отрыты (public:), а в классах закрыты (private:).
      В остальном они идентичны:
      class A
      {
      public:

      };
      тоже самое, что:
      struct A
      {

      };

  4. Так называемые макросы Аскольда Волкова написаны безграмотно и содержат в себе классическую засаду, а именно — используют операнды без скобок:

    #define _setL(port,bit) do { port &= ~(1 << bit); } while(0)

    Положим, что операнд bit в каком-нибудь сценарии использования может быть задан как номер бита в канале (и на это есть макрос), а каналов в одном порту несколько, и номер реального бита вычисляется от номера канала и кол-ва бит на канал:

    _setL(SOME_PORT, SOME_BIT + (channel * CHAN_BITS))

    В таком случае макрос будет преобразован препроцессором в

    do { SOME_PORT &= ~(1 << SOME_BIT + (channel * CHAN_BITS)); } while(0);

    В итоге при, например, SOME_BIT=1, channel=3, CHAN_BITS=2, вместо

    SOME_PORT &= ~(0x80);
    получится
    SOME_PORT &= ~(0x08);

    Аналогичная фигня может произойти и с номером порта тоже при определенных обстоятельствах.

    Решение — ВСЕГДА и ВЕЗДЕ в макросах, не надеясь на их «правильное» применение, указывать подстановочные параметры в скобках:

    #define _setL(port,bit) do { (port) &= ~(1 << (bit)); } while(0)

      1. Глобальные переменные — это страшное зло. Их использование нужно сводить к минимуму. В идеале Data Coupling должен быть нулевым, во избежание страшных и ужасных багов, которые замучаешься ловить.

        А каждый вызов функции, если она не inline, — это лишняя нагрузка на процессор и лишнее расходование памяти (вместо двух инструкций будет минимум 3).
        В зависимости от типа процессора нагрузка (расход времени впустую) может быть разной, но в случае с функциями типа таких _setL(), она всегда будет бессмысленной.

        Только это понимают не все, к сожалению. А только те, кто по этим граблям ходил.

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

              1. Ну вот, дискуссия превратилась в обычный треп блондинов. Теперь мы все поумничаем по поводу памяти. Потом начнем холивар по поводу языков программирования микроконтроллеров.
                Да, конечно, упомянутый прокол в макросах Аскольда Волкова важен для учета этого теми, кто воспользуется ими. Да, существует культура использования локальных и глобальных переменных. И здесь имеют значение, кстати, именно особенности архитектуры и системы команд процессора. Об этом и у DI HALT’а рассказывается, и на других сайтах тоже…
                Но! Какое отношение все это имеет к обсуждаемой статье?

                Ладно я лично — в плюсах очень даже начинающий (лет 15 назад начал и прекратил). Далее использования простейших классов (да и то по шаблонам) как-то не приходилось забираться. Однако у меня создается впечатление, что нам всем, здесь присутствующим, нечего сказать по сути безусловно талантливой статьи.

                Чтобы вернуться к «телу», позволю себе высказать дилетантское суждение — а уж вы поправьте или дополняйте, если будете так добры.

                Итак, я думаю, что в результате использования предлагаемого подхода решаются В ПОЛНОЙ МЕРЕ И БЕЗ ОГОВОРОК поставленные уважаемым автором задачи:
                — программы имеют хорошую переносимость,
                — пользование портами как в режиме слов (набранных из разных бит разных портов), так и в побитовом режиме является понятным и ошибкоутойчивым,
                — вся ужасная муторная работа по объединению полей и их разбитию делается компилятором,
                — мы получаем код, такой же компактности и быстродействия, как у ХОРОШЕГО программиста на ассемблере. Что, кстати, вносит некоторые новые моменты в холивар АСМ-СИ :)

                Прав ли я? Мне важно знать, ибо самому не хватает квалификации. Но, если прав — придется глубоко (для моего уровня) полезть в плюсы, чтобы научиться хотя бы использовать то, что гениальные люди предлагают нам, сирым…

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

                  1. Так в том-то и дело! не знаю, как у Вас, но я вот провозился день со сменой назначения парочки портов — как раз из-за примитивного подхода к их использованию. Скажите, а для простого проетка день бессмысленной траханины — не слишком ли большая роскошь?
                    Это мне напомнило об RTOS-ах. Тоже, казалось бы, излишество. Но освоив инструмент, новый проект реального времени (а у нас все такие) создаешь быстро и качественно — значит, не удорожая его расходом своего драгоценного времени.
                    Ну. с организацией отработки событий в реальном времени у меня проблем нет, есть подходы. А вот именно о портах… Да, хотелось бы прочувствовать, что есть смысл «очень сильно изменить образ мышления» :)

                    1. «Скажите, а для простого проетка день бессмысленной траханины — не слишком ли большая роскошь?»

                      Я не в теме — сказать, было ли это бессмысленным или нет, увы, не могу.

                      «Но освоив инструмент, новый проект реального времени (а у нас все такие) создаешь быстро и качественно — значит, не удорожая его расходом своего драгоценного времени.»

                      Выбор между стоимостью и временем — классика. Короче, каждому — свое (с) Освенцим.

  5. Спрашивается, на хрена писать на Си, если компиляторы пишут тоже люди (я о человеческом факторе) и на элементарной операции с вводом-выводом компилятор может выплюнуть что угодно?…………………………

    1. А спрашивается, нахрена, вообще пользоваться компьютерами и микроконтроллерами в частности, если шальной нейтрон космического излучения высокой энергии может попасть в процессор и исказить ячейку памяти?

    2. Применительно к AVR, очень многие хвалят тот же IAR. А лексические анализаторы и прочие дела — вещи, формально доказываемые, так что, качественный код вполне возможен. Правда, стоить такой продукт будет денег.

      1. У IAR, на мой взгляд, нет каких либо серьёзных приемуществ (особенно учитывая сколько он стоит) перед тем-же GCC. На одних тестах он выигрывает, на других отстаёт — средняя температура по больнице одинаковая.

  6. Статья большая… очень большая. Настолько, что сразу не удалось найти той простоты которой добивались. Прийдётся почитать ещё раз, подумать…
    Тем не менее уважение автору — работа проделана большая.
    И ещё (так мысли в слух) почему-то большая статья у многих вызывает восторг причём и у тех, кто до конца не разобрался. Это видно из некоторых отзывов общий смысл которых: «Я в C++ полный ноль, но статья хорошая!» (по каким критериям определили хорошесть?)

    1. Ну, раз камень в мой огород, то отвечу.

      1) Я не сказал и не думал говорить, что я в С++ «полный ноль».
      2) Определил «хорошесть» потому, что автор достаточно четко сформулировал задачу — и мне хватило квалификации, чтобы понять, что задачу он выполнил. А актуальность именно той задачи, что сформулирована автором, мне ясна из собственного опыта, т.к. работаю с контроллерами давно. Не обязательно разбираться до конца, чтобы дать оценку. Вы же высказали респект автору — хотя Вам предстоит еще думать, как Вы заметили…
      3) У меня нет желания просто потрындеть здесь. Как я уже заявил, считаю тему актуальной для себя лично — и очень хочу увидеть конструктивное обсуждение для того, чтобы (со своей невысокой колокольни) разобраться получше и понять, нужно ли мне напрягаться для освоения предложенного инструмента. А напрягаться мне нужно, увы! — сходу применить не смогу.

      Ну-с, оправдался за участие :) Так что теперь у нас с конструктивным обсуждением, специалисты ненулевые?

    1. Конечно проводились, и даже не минимальные.
      Но тут есть вопрос, что с чем сравнивать? И в какой конфигурации?
      Пусть это будет «списки линий VS препроцессор».
      В большинстве случаев размер кода на Си++ меньше или равен аналогичному по конфигурации коду на чистом Си. А выполняется быстрее или также, соответственно.
      Смотрите ассемблерные листинги в статье, поэкспериментируйте с примерами.

  7. Интересный подход, хоть и очень громоздкий — но результат того стоит.

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

    Поясню на примере. Возьмём тот же многострадальный hd44780, у него имеется некоторое количество разрозненных бит (RS, RW, E) и четыре бита данных, объединённых в один «виртуальный порт». Можно написать что-то типа (пишу на ходу, возможны ошибки):

    <>:

    #if defined __AVR__
    #include
    #elif defined __PIC__
    #include
    #endif

    <>:

    class HD44780
    {
    public:
    static inline void SetRS ()
    { PORTA |= _BV (PA3); }
    static inline void ClearRS ()
    { PORTA &= ~_BV (PA3); }
    static inline void SetDATA (uint8_t x)
    { PORTC = (PINC & 0x3c) | (x << 2); }
    };

    class RedLED
    {
    public:
    static inline void ON ()
    { PORTD |= _BV (PD5); }
    static inline void OFF ()
    { PORTD &= ~_BV (PD5); }
    };

    … ну и так далее.

    При переносе (библиотеки, приложения) на другую аппаратную платформу придётся всего лишь сделать альтернативный вариант hal.h, в котором определить базовые низкоуровневые операции с имеющейся аппаратной периферией — наиболее оптимальным способом для данной платформы и имеющейся конфигурации аппаратной части. Фактически это некий аналог hardware abstraction layer — некоего аппаратно-зависимого микроядра.

    Такой подход существенно проще для понимания и проще в реализации, даёт код максимально возможного качества.

      1. А для кого сделали кодовый редактор? Вот туда бы и постил, там и подсветка и форматирование. А вордпресс это чистый хтмл, причем из соображений безопасности ряд тэгов отключен вовсе.

    1. И у вас будут благополучно плодиться и размножеться разные версии одного и тогоже драйвера для разных платформ, разных способов подключения…
      Чего я, собственно, ставил целью избежать.
      А как с вашим подходом подключить два дисплея HD44780? Видимо скопипастить и подправить класс HD44780.
      Да, релизация у моего метода нетривиальна и сложна для понимания, особенно для тех кто давно пишет на Си (я даже не говорю про ассемблерщиков), однако пользоваться её вполне просто и удобно. Один толко минус — Си++ надо понимать.
      Мало того, я крайне не рекомендую использовать свой метод начинающим, которые чистого Си не знают толком. Надо пройти весь путь изучения вопроса сначала:
      Ассемблер -> макросы на Си -> виртуальные порты и т.д. А только потом переходить на Си++, в полной мере понимая какие возможности это даст, какие проблемы это решит и зачем всё это нужно.

      Посмотрите пример тогоже HD44780:
      http://github.com/KonstantinChizhov/AvrProjects/blob/master/avrcpp/HD44780.h
      Тестовое приложение — инициализация и вывод строки «Hello world!» имеет размер 316 байт (скомпилированно для мега16) из которых 114 байт — это вектор прерываний и стартап код (причём код не оптимален, его можно ещё ужать).

      1. Ну, а при Вашем подходе точно так же будут плодиться методы доступа для разных платформ :) И опять же, при Вашем подходе точно так же будет невозможно одним кодом управлять двумя разными HD44780 — надо будет либо отказаться от статических методов и генерировать обьекты (тогда высокоуровневый драйвер HD44780 будет один, а низкоуровневых драйверов — два), либо два инстанса темплейта высокоуровнего драйвера HD44780 (а его размер может быть значителен).

        Чтобы не плодить классы для одной платформы, если есть необходимость, можно написать темплейт-прародитель HD44780, и точно так же сделать два производных наследуемых темплейта, в которых параметрически задавать только различающиеся параметры (в общем случае — списки портов). Тогда можно будет писать примерно так же:

        typedef hd44780_t my_hd44780_1;
        typedef hd44780_t my_hd44780_2;

        1. Смотрите пример его использования:
          http://github.com/KonstantinChizhov/AvrProjects/blob/master/LcdTest/LcdTest.cpp
          Один дисплей подключен к порту А, другой через сдвиговый регистр-защёлку, подключенный к порту Б.
          Цитата:
          И опять же, при Вашем подходе точно так же будет невозможно одним кодом управлять двумя разными HD44780 — надо будет либо отказаться от статических методов и генерировать обьекты
          конец цитаты.

          У каждого класса сгенерированного из шаблона класса будет свой собственный набор статических функций и полей. Так, что всё работает.

  8. по мне давно уже надо придумать нормальный язык(оболочку) для микроконтроллеров
    С не подходит потому-что он под архитектуру фон Неймана заточен, а контроллеры по Гарвардской архитектуре . А ассемблерные кострукции морально устарели(типа кушайте- не обляпайтесь). Ну с по-блоковой памятью и своебразным переходом. Наверняка существует что-то переходное и с готовой оболочкой. Типа ООП кросс-ассемблера. Создаются же сильные продукты.

  9. Neiver, спасибо за столь детальное описание концепций. Код весьма недурственный получился. Увидел в вашем репозитории файлик mcucpp/impl/iopin.h, а в нем вот такую строчку:

    PORT::template Set<1 <();

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

      1. В классе реализующем порт ввода-вывода есть две версии функции Set (установки битов в порте). Одна обычная, которая принимает битовую маску для установки бит как обычный параметр. Вторая — шаблонная, она принимает эту маску как шаблонный параметр — константу. У других функций так-же есть обычный и шаблонный варианты. Шаблонные варианты нужны для вывода константных значений в порты — они с большой вероятностью будут встроены по месту вызова.
        Так вот, в шаблонный класс TPin, передаётся класс Port, как шаблонный параметр. У порта есть шаблонная функция-член Set. Чтоб её вызвать, нужно явно указать, что она является шаблоном, поэтому нужно ключевое слово template.

  10. Neiver, большую работу проделали, за что вам спасибо! Давненько, еще в универе шаблоны использовал в своих работах, но конечно не на таком джедайском уровне :)
    Пытаюсь скормить ваш пример С++ компилятору от IAR, но что-то грабли какие-то. Он вообще способен такое переварить?
    ошибки примерно такие:
    Error[Pe276]: name followed by «::» must be a class or namespace name pinlist.h 71
    detected during:
    instantiation of class «IO::GetPorts<Loki::Typelist> [with Head=IO::PW<, (uint8_t)’00’>, Tail=Loki::Typelist<IO::PW<, (uint8_t)’01’>, Loki::Typelist<IO::PW<, (uint8_t)’02’>,
    Loki::Typelist<IO::PW<, (uint8_t)’03’>, Loki::Typelist<IO::PW<, (uint8_t)’04’>, Loki::NullType>>>>>]» at line 578 of «D:\MK\BerinLab\CORE\io\pinlist.h»
    T7=Loki::NullType, T8=Loki::NullType, T9=Loki::NullType, T10=Loki::NullType, T11=Loki::NullType, T12=Loki::NullType, T13=Loki::NullType, T14=Loki::NullType, T15=Loki::NullType, T16=Loki::NullType, T17=Loki::NullType]» at line 10 of main.cpp

    line 10 гласит: MyPins::Write(0x55);

  11. Расскажу историю: понадобилось скопировать бит из PIN в PORT. Вроде задача простая, даже тривиальная. Но оказалось не все так просто.
    Тонкость в том, что нужно это было делать гарантированно, и желательно за меньшее число команд.
    Традиционные пути выглядели непривлекательными: мы можем либо сбрасывать бит командой POPTB &= ~(1<<bit), либо устанавливать его командой PORTB |= (1<<bit). Но копировать бит не можем, придется городить что-то вроде
    if (bit_is_set(PINB,bit_in)) //Если бит установлен
    {PORTB |= (1<<bit_out); //Устанавливаем бит в порту
    }else
    {PORTB &= ~(1<<bit_out); //Иначе — сбрасываем
    };
    Согласитесь — выглядит по идиотски: вместо простого копирования мы городим черт знает что, это наверняка сьест кучу команд.
    И новичка такая ситуация сбивает. Очень сильно сбивает — в ассемблере-то копирование было, есть для этого специальные команды, значит и в Си должно быть, как бы к этим командам достучатся?
    Начинаем рыть интернет, и… ничего не находим. Никто не знает про копирование? Почему?
    И, как затравка, натыкаемся на упоминание DiHalt'a о некой библиотеке, которая может копировать биты, но которая недоделана, доступна только в одной версии компилятора, и сильно раздувает код.
    Новичок в ступоре: мистика, никто не знает о копировании, никто им не пользуется. Что же тогда делать? Оставлять это убожество с if? Нет, мы так не сдадимся. Пишем свою реализацию копирования.
    В ассемблере для этого был специальный бит Т, и две команды, для копирования определенного бита из регистра в Т, и наоборот, из Т в регистр. Все было просто:
    in r24, 0x16 //temp=PINB
    bst r24, 0 //SREG.T=temp.0
    in r24, 0x18 //temp=PORTB
    bld r24, 4 //temp.4=SREG.T
    out 0x18, r24 //PORTB=temp
    Т.е. загружаем порт-PIN в регистр, копируем бит, загружаем порт в регистр, переписываем бит. Никаких сравнений, вроде как должны выиграть по размеру.
    Повторяем на Си:
    uint8_t temp;
    temp = PINB;
    asm volatile("BST %0, %1 \r\n" : "=r" (temp) : "M" (bit_in));
    temp = PORTB;
    asm volatile("BLD %0, %1 \r\n" : "=r" (temp) : "M" (bit_out));
    PORTB = temp;
    Компилируем, смотрим листинг — идеально.
    Теперь пробуем повторить тоже с if, смотрим листинг:
    112: b0 9b sbis 0x16, 0 // if PINB.bit_in == 1
    114: 48 c0 rjmp .+144 ; 0x1a6
    116: c4 9a sbi 0x18, 4 // then set PORTB.bit_out
    118: //продолжаем программу
    1a6: c4 98 cbi 0x18, 4 // else clear PORTB.bit_out
    1a8: b7 cf rjmp .-146 ; 0x118
    Итого тоже 5 команд. Только исполнятся всего 3: сравнение, прыжок и снятие/установка бита.
    Почему так получилось? Засада в том, что команды bst/bld работают только с регистрами, потому приходится вводить три лишних команды по работе с портами. А во втором варианте у нас есть три уникальные команды (sbis, sbi/cbi), которые, напротив, прекрасно работают с портами, и потому экономят нам от 6 до 9 команд. В итоге получается ничья по размеру — оба варианта по 5 команд, и выигрыш традиционного подхода по скорости — вариант с Т всегда исполняется за 5 циклов. а вариант с if всего за 3.
    Теперь все ясно, ясно почему никто не знает про копирование, и никто им не пользуется — копирование невыгодно, более того, вариант с if не требует внедрения ассемблерных ставок, а потому более универсален. Вот так вот все просто оказалось. Только надо было это хоть в одном месте в интернете описать, иначе каждого новичка будут грызть аналогичные сомнения "Почему никто не использует копирование? Почему о нем вообще нигде не упоминается? Зачем городить лишние сравнения? Разве нельзя просто скопировать и все?", а ему никто не говорит, почему нигде это не описано, в итоге рано или поздно он тоже захочет проверить, и убьет кучу времени на безуспешный поиск в интернете.
    Так что на будущее совет всем — в статьях для новичков по работе с портами нужно не только расписывать команды чтения из порта, и установки/сброса битов, но и упоминать, почему вместо прямого копирования занимаются сравнением, чтоб сомнений в правильности такого подхода даже не возникало.

  12. Как то, не совсем ясен момент с этим кодом:

    void LCDwrite4(uint8_t cmd)
    {
    LDP &= ~(1<<LCD_D7|1<<LCD_D6|1<<LCD_D5|1<<LCD_D4); //clear data bus

    if(cmd & (1 << 0)) <--До этого момента все понятно
    LDP |= LCD_D4; (LCD_D4 - это номер ноги на порту = #define LCD_D4 = 4)
    --/--

    Так почему он пишется в порт???

  13. На STM32(F103)
    typedef PinList List1;
    typedef PinList List2;

    List1::Write(1);
    List1::Write(0);
    Диодик на Pc0 мигает.

    List2::Write(1);
    List2::Write(0);
    И диодик повешанный на Pc13 НЕ МИГАЕТ.
    (естественно клок порта разрешен, пины сконфигурены на out)

    При этом
    Pc13.Set();
    Pc13.Clear();
    Работают!

    В общем в pinlist.h есть одна пичалька, которую я целый вечер вылавливал!

    Очень жаль, что автор забросил эту библиотеку(на гитхабе последние обновления 4года назад).
    Задумка то очень годная!

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