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

Материал из eeWiki - открытая энциклопедия по электронике
Перейти к: навигация, поиск

Введение

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

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

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

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

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

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

Давайте разберемся, какие методы работы с портами ввода-вывода традиционно применяются при программировании на чистом Си.

Можно выделить следующие подходы (вариант с жестко заданными в коде именами портов и номерами ножек не рассматриваем):

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

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

Препроцессор.

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

Нет ничего проще, чем помигать светодиодом, подключенным к одному из выводов МК:

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

Строчка

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

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

  1. void LCDinit(void)//Initializes LCD
  2. {
  3. _delay_ms(15);
  4. LDP=0x00;
  5. LCP=0x00;
  6. LDDR|=1<<LCD_D7|1<<LCD_D6|1<<LCD_D5|1<<LCD_D4;
  7. LCDR|=1<<LCD_E|1<<LCD_RW|1<<LCD_RS;
  8. //---------one------
  9. LDP=0<<LCD_D7|0<<LCD_D6|1<<LCD_D5|1<<LCD_D4; //4 bit mode
  10. LCP|=1<<LCD_E|0<<LCD_RW|0<<LCD_RS;
  11. _delay_ms(1);
  12. LCP&=~(1<<LCD_E);
  13. _delay_ms(1);
  14. //-----------two-----------
  15. LDP=0<<LCD_D7|0<<LCD_D6|1<<LCD_D5|1<<LCD_D4; //4 bit mode
  16. LCP|=1<<LCD_E|0<<LCD_RW|0<<LCD_RS;
  17. _delay_ms(1);
  18. LCP&=~(1<<LCD_E);
  19. _delay_ms(1);
  20. //-------three-------------
  21. LDP=0<<LCD_D7|0<<LCD_D6|1<<LCD_D5|0<<LCD_D4; //4 bit mode
  22. LCP|=1<<LCD_E|0<<LCD_RW|0<<LCD_RS;
  23. _delay_ms(1);
  24. LCP&=~(1<<LCD_E);
  25. _delay_ms(1);
  26. //--------4 bit--dual line---------------
  27. LCDsendCommand(0b00101000);
  28. //-----increment address, cursor shift------
  29. LCDsendCommand(0b00001110);
  30. }

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

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

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

  1. void LCDwrite4(uint8_t cmd)
  2. {
  3. LDP &= ~(1<<LCD_D7|1<<LCD_D6|1<<LCD_D5|1<<LCD_D4); //clear data bus
  4.  
  5. if(cmd & (1 << 0))
  6. LDP |= LCD_D4;
  7. if(cmd & (1 << 1))
  8. LDP |= LCD_D5;
  9. if(cmd & (1 << 2))
  10. LDP |= LCD_D6;
  11. if(cmd & (1 << 3))
  12. LDP |= LCD_D7;
  13. }
  14. void LCDsendCommand(uint8_t cmd) //Sends Command to LCD
  15. {
  16. LCDwrite4(cmd);
  17. LCP|=1<<LCD_E;
  18. _delay_ms(1);
  19. LCP&=~(1<<LCD_E);
  20. _delay_ms(1);
  21. LCDwrite4(cmd);
  22. LCP|=1<<LCD_E;
  23. _delay_ms(1);
  24. LCP&=~(1<<LCD_E);
  25. _delay_ms(1);
  26. }
  27.  
  28.  

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

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

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

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

  1. #define _setL(port,bit) do { port &= ~(1 << bit); } while(0)
  2. #define _setH(port,bit) do { port |= (1 << bit); } while(0)
  3. #define _clrL(port,bit) do { port |= (1 << bit); } while(0)
  4. #define _clrH(port,bit) do { port &= ~(1 << bit); } while(0)
  5. #define _bitL(port,bit) (!(port & (1 << bit)))
  6. #define _bitH(port,bit) (port & (1 << bit))
  7. #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. typedef struct Bits_t
  3. {
  4. uint8_t Bit0 :1;
  5. uint8_t Bit1 :1;
  6. uint8_t Bit2 :1;
  7. uint8_t Bit3 :1;
  8. uint8_t Bit4 :1;
  9. uint8_t Bit5 :1;
  10. uint8_t Bit6 :1;
  11. uint8_t Bit7 :1;
  12. }Bits;
  13.  
  14. #define PortaBits (*((volatile Bits*)&PORTA))
  15. #define LedPin PortaBits.Bit5
  16.  
  17. int main()
  18. {
  19. DDRA = 1 << 5;
  20. while(1)
  21. {
  22. LedPin = 1; //зажечь
  23. _delay_ms(100);
  24. LedPin = 0; //выключить
  25. _delay_ms(100);
  26. }
  27. }
  28.  

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

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

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

Передача порта через указатель.

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

Порты ввода-вывода в большинстве МК есть не что иное как просто ячейка памяти или регистр в пространстве ввода-вывода и естественно к нему можно обратиться по адресу через указатель.

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

  1.  
  2. typedef struct ShiftReg_t
  3. {
  4. volatile uint8_t *port;
  5. uint8_t data_pin_bm;
  6. uint8_t clk_pin_bm;
  7. uint8_t latch_pin_bm;
  8. }ShiftReg;
  9. ...
  10. //ShiftReg.c
  11. void WriteShiftReg(ShiftReg *reg, uint8_t value)
  12. {
  13. for(uint8_t i=0; i<8; i++)
  14. {
  15. if(value & 1) //выводим данные
  16. *reg->port |= reg->data_pin_bm;
  17. else
  18. *reg->port &= ~reg->data_pin_bm;
  19. //тактовый импульс
  20. *reg->port |= reg->clk_pin_bm;
  21. value >>= 1;
  22. *reg->port &= ~reg->clk_pin_bm;
  23. }
  24. //защёлкиваем данные в регистр
  25. *reg->port |= reg->latch_pin_bm;
  26. *reg->port &= ~reg->latch_pin_bm;
  27. }
  28.  
  29. //main.c
  30. ...
  31. #include <avr/io.h>
  32. //вывода data и clk могут быть общие.
  33. ShiftReg reg1 = {&PORTA, 1<<1, 1<<2, 1<<3};
  34. ShiftReg reg2 = {&PORTA, 1<<1, 1<<2, 1<<4};
  35.  
  36. int main()
  37. {
  38. DDRA = 0xff;
  39. DDRB = 0xff;
  40. WriteShiftReg(&reg1, 0xff);
  41. WriteShiftReg(&reg2, 0x55);
  42. while(1)
  43. {
  44.  
  45. }
  46. }
  47.  

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

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

  1. *reg->port |= reg->latch_pin_bm;
  1.  
  2. ld r30, X+
  3. ld r31, X
  4. sbiw r26, 0x01  ; 1
  5. ld r24, Z
  6. adiw r26, 0x04  ; 4
  7. ld r25, X
  8. sbiw r26, 0x04  ; 4
  9. or r24, r25
  10. st Z, r24
  11.  

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

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

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

  1. typedef struct ShiftReg_t
  2. {
  3. volatile uint8_t *port;
  4. uint8_t latch_pin_bm;
  5. }ShiftReg;
  6.  
  7. enum {clk_pin_bm = 1 << 0, data_pin_bm = 1 << 1};
  8. ...
  9. //ShiftReg.c
  10. void WriteShiftReg(ShiftReg *reg, uint8_t value)
  11. {
  12. for(uint8_t i=0; i<8; i++)
  13. {
  14. if(value & 1) //auaiaei aaiiua
  15. *reg->port |= data_pin_bm;
  16. else
  17. *reg->port &= ~>data_pin_bm;
  18. //oaeoiaue eiioeun
  19. *reg->port |= clk_pin_bm;
  20. value >>= 1;
  21. *reg->port &= clk_pin_bm;
  22. }
  23. //cau?eeeaaai aaiiua a ?aaeno?
  24. *reg->port |= reg->latch_pin_bm;
  25. *reg->port &= ~reg->latch_pin_bm;
  26. }

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

Итого:

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

Виртуальные порты.

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

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

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

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

  1.  
  2. void LCDwrite4(uint8_t value, WriteFunc write)
  3. {
  4. enum{LCD_E=4, LCD_RS=5, LCD_RW=6};
  5. uint8_t tmp;
  6. tmp = (value & 0x0f) | (1 << LCD_E); //совмещаем вывод тетрады
  7. //и установку LCD_E
  8. write(tmp);
  9. _delay_ms(1);
  10. tmp &= ~(1 << LCD_E);
  11. write(tmp);
  12. _delay_ms(1);
  13. }
  14.  
  15. void LCDsendCommand(uint8_t cmd, WriteFunc write) //Sends Command to LCD
  16. {
  17. LCDwrite4(cmd >> 4, write); //старшая тетрада
  18. LCDwrite4(cmd, write); //младшая тетрада
  19. }
  20. ...
  21. void DoSomthing()
  22. {
  23. ...
  24. VPort1DirWrite(0xff);
  25. LCDsendCommand(0x30, VPort1Write);
  26. ...
  27. }

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

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

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

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

Подход Си++ к работе с портами.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1.  
  2. typedef struct PORT_struct
  3. {
  4. register8_t DIR; /* I/O Port Data Direction */
  5. register8_t DIRSET; /* I/O Port Data Direction Set */
  6. register8_t DIRCLR; /* I/O Port Data Direction Clear */
  7. register8_t DIRTGL; /* I/O Port Data Direction Toggle */
  8. register8_t OUT; /* I/O Port Output */
  9. register8_t OUTSET; /* I/O Port Output Set */
  10. register8_t OUTCLR; /* I/O Port Output Clear */
  11. register8_t OUTTGL; /* I/O Port Output Toggle */
  12. register8_t IN; /* I/O port Input */
  13. register8_t INTCTRL; /* Interrupt Control Register */
  14. register8_t INT0MASK; /* Port Interrupt 0 Mask */
  15. register8_t INT1MASK; /* Port Interrupt 1 Mask */
  16. register8_t INTFLAGS; /* Interrupt Flag Register */
  17. register8_t reserved_0x0D;
  18. register8_t reserved_0x0E;
  19. register8_t reserved_0x0F;
  20. register8_t PIN0CTRL; /* Pin 0 Control Register */
  21. register8_t PIN1CTRL; /* Pin 1 Control Register */
  22. register8_t PIN2CTRL; /* Pin 2 Control Register */
  23. register8_t PIN3CTRL; /* Pin 3 Control Register */
  24. register8_t PIN4CTRL; /* Pin 4 Control Register */
  25. register8_t PIN5CTRL; /* Pin 5 Control Register */
  26. register8_t PIN6CTRL; /* Pin 6 Control Register */
  27. register8_t PIN7CTRL; /* Pin 7 Control Register */
  28. } PORT_t;
  29.  

Чтобы изолировать класс Pin от конкретной реализации портов ввода-вывода введём дополнительный уровень абстракции. Добавление нового уровня абстракции вовсе не обязательно влечёт за собой какие-то накладные расходы.

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

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

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

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

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

  1.  
  2. // Псевдоним для типа данных порта.
  3. // Для ARM, например, это будет uint32_t.
  4. typedef uint8_t DataT;\
  5. // Записать значение в порт PORT = value
  6. static void Write(DataT value);
  7. // Прочитать значение записанное в порт
  8. static DataT Read();
  9. //Записать значение направления линий В/В
  10. static void DirWrite(DataT value);
  11. // прочитать направление линий В/В
  12. static DataT DirRead();
  13. //Установить биты в порту PORT |= value;
  14. static void Set(DataT value);
  15. // Очистить биты в проту PORT &= ~value;
  16. static void Clear(DataT value);
  17. // Очистить по маске и установить PORT = (PORT & ~clearMask) | value;
  18. static void ClearAndSet(DataT clearMask, DataT value);
  19. // Переключить биты PORT ^= value;
  20. static void Togle(DataT value);
  21. // Установить биты направления
  22. static void DirSet(DataT value);
  23. // Очистиь биты направления
  24. static void DirClear(DataT value);
  25. // Переключить биты направления
  26. static void DirTogle(DataT value);
  27. // прочитать состояние линий В/В
  28. static DataT PinRead();
  29. // Уникальный идентификотор порта
  30. enum{Id = ID};
  31. // Разрядность порта (бит)
  32. enum{Width=sizeof(DataT)*8};

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

  1.  
  2. #define MAKE_PORT(portName, ddrName, pinName, className, ID) \
  3.   class className{ \
  4.   public: \
  5.   typedef uint8_t DataT; \
  6.   private: \
  7.   static volatile DataT &data() \
  8.   { \
  9.   return portName; \
  10.   } \
  11.   static volatile DataT &dir() \
  12.   { \
  13.   return ddrName; \
  14.   } \
  15.   static volatile DataT &pin() \
  16.   { \
  17.   return pinName; \
  18.   } \
  19.   public: \
  20.   static void Write(DataT value) \
  21.   { \
  22.   data() = value; \
  23.   } \
  24.   static void ClearAndSet(DataT clearMask, DataT value) \
  25.   { \
  26.   data() = (data() & ~clearMask) | value; \
  27.   } \
  28.   static DataT Read() \
  29.   { \
  30.   return data(); \
  31.   } \
  32.   static void DirWrite(DataT value) \
  33.   { \
  34.   dir() = value; \
  35.   } \
  36.   static DataT DirRead() \
  37.   { \
  38.   return dir(); \
  39.   } \
  40.   static void Set(DataT value) \
  41.   { \
  42.   data() |= value; \
  43.   } \
  44.   static void Clear(DataT value) \
  45.   { \
  46.   data() &= ~value; \
  47.   } \
  48.   static void Togle(DataT value) \
  49.   { \
  50.   data() ^= value; \
  51.   } \
  52.   static void DirSet(DataT value) \
  53.   { \
  54.   dir() |= value; \
  55.   } \
  56.   static void DirClear(DataT value) \
  57.   { \
  58.   dir() &= ~value; \
  59.   } \
  60.   static void DirTogle(DataT value) \
  61.   { \
  62.   dir() ^= value; \
  63.   } \
  64.   static DataT PinRead() \
  65.   { \
  66.   return pin(); \
  67.   } \
  68.   enum{Id = ID}; \
  69.   enum{Width = sizeof(DataT)*8}; \
  70.   };

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

  1.  
  2. #define MAKE_PORT(portName, className, ID) \
  3.   class className{ \
  4.   public: \
  5.   typedef uint8_t DataT; \
  6.   public: \
  7.   static void Write(DataT value) \
  8.   { \
  9.   portName.OUT = value; \
  10.   } \
  11.   static void ClearAndSet(DataT clearMask, DataT value) \
  12.   { \
  13.   Clear(clearMask); \
  14.   Set(value); \
  15.   } \
  16.   static DataT Read() \
  17.   { \
  18.   return portName.OUT; \
  19.   } \
  20.   static void DirWrite(DataT value) \
  21.   { \
  22.   portName.DIR = value; \
  23.   } \
  24.   static DataT DirRead() \
  25.   { \
  26.   return portName.DIR; \
  27.   } \
  28.   static void Set(DataT value) \
  29.   { \
  30.   portName.OUTSET = value; \
  31.   } \
  32.   static void Clear(DataT value) \
  33.   { \
  34.   portName.OUTCLR = value; \
  35.   } \
  36.   static void Togle(DataT value) \
  37.   { \
  38.   portName.OUTTGL = value; \
  39.   } \
  40.   static void DirSet(DataT value) \
  41.   { \
  42.   portName.DIRSET = value; \
  43.   } \
  44.   static void DirClear(DataT value) \
  45.   { \
  46.   portName.DIRCLR = value; \
  47.   } \
  48.   static DataT PinRead() \
  49.   { \
  50.   return portName.IN; \
  51.   } \
  52.   static void DirTogle(DataT value) \
  53.   { \
  54.   portName.DIRTGL = value; \
  55.   } \
  56.   enum{Id = ID}; \
  57.   enum{Width=8}; \
  58.   };
  59.  
  60. #ifdef PORTA
  61. MAKE_PORT(PORTA, Porta, 'A')
  62. #endif
  63. ...
  64. #ifdef PORTR
  65. MAKE_PORT(PORTR, Portr, 'R')
  66. #endif
  67.  

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

  1.  
  2. template<class PORT, uint8_t PIN>
  3. class TPin
  4. {
  5. public:
  6. typedef PORT Port;
  7. enum{Number = PIN};
  8. static void Set()
  9. {
  10. PORT::Set(1 << PIN);
  11. }
  12. static void Set(uint8_t val)
  13. {
  14. if(val)
  15. Set();
  16. else Clear();
  17. }
  18. static void SetDir(uint8_t val)
  19. {
  20. if(val)
  21. SetDirWrite();
  22. else SetDirRead();
  23. }
  24. static void Clear()
  25. {
  26. PORT::Clear(1 << PIN);
  27. }
  28. static void Togle()
  29. {
  30. PORT::Togle(1 << PIN);
  31. }
  32. static void SetDirRead()
  33. {
  34. PORT::DirClear(1 << PIN);
  35. }
  36. static void SetDirWrite()
  37. {
  38. PORT::DirSet(1 << PIN);
  39. }
  40. static uint8_t IsSet()
  41. {
  42. return PORT::PinRead() & (uint8_t)(1 << PIN);
  43. }
  44. };

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

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

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

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

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

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

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

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

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

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

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

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

Списки линий ввода-вывода.

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

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

Spicok.GIF

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

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

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

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

Приступим.

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

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

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

  1. enum{Number = PIN};

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

  1.  
  2. template<class TPIN, uint8_t POSITION>
  3. struct PW //Pin wrapper
  4. {
  5. typedef TPIN Pin;
  6. enum{Position = POSITION};
  7. };
  8.  
  9. <p>Хотя можно было этого и не делать, а вычислять битовыю позицию потом с помощью алгоритма IndexOf.</p>
  10. <p>Этот шаблон хранит тип линии В/В и ее битовую позицию в списке. Теперь приступим к генерации собственно списка линий. Для определённости ограничим длину списка 16-ю линиями, ели надо можно добавить и больше, потом. Для этого возьмём шаблон MakeTypelist из библиотеки Loki и модифицируем его под свои нужды:</p>
  11. <code lang='c'>
  12. template
  13. <
  14. int Position, // стартовая битовая позиция, сначала это 0
  15. typename T1 = NullType, typename T2 = NullType, typename T3 = NullType,
  16. typename T4 = NullType, typename T5 = NullType, typename T6 = NullType,
  17. typename T7 = NullType, typename T8 = NullType, typename T9 = NullType,
  18. typename T10 = NullType, typename T11 = NullType, typename T12 = NullType,
  19. typename T13 = NullType, typename T14 = NullType, typename T15 = NullType,
  20. typename T16 = NullType, typename T17 = NullType
  21. >
  22. struct MakePinList
  23. {
  24. private:
  25. // рекурсивно проходим все параметры
  26. // на следующей итерации Position увеличится на 1,
  27. // а T2 превратится в T1 и так далее
  28. typedef typename MakePinList
  29. <
  30. Position + 1,
  31. T2 , T3 , T4 ,
  32. T5 , T6 , T7 ,
  33. T8 , T9 , T10,
  34. T11, T12, T13,
  35. T14, T15, T16,
  36. T17
  37. >::Result TailResult;
  38. enum{PositionInList = Position};
  39. public:
  40. // Result это и есть требуемый список линий
  41. typedef Typelist< PW<T1, PositionInList>, TailResult> Result;
  42. };
  43.  
  44. //конец списка
  45. //конец рекурсии, когда список пуст
  46. template<int Position>
  47. struct MakePinList<Position>
  48. {
  49. typedef NullType Result;
  50. };

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

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

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

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

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

  1.  
  2. //шаблон принимает список линий в качестве параметра
  3. template <class TList> struct GetPorts;
  4.  
  5. // для пустого списка результат – пустой тип
  6. template <> struct GetPorts<NullType>
  7. {
  8. typedef NullType Result;
  9. };
  10.  
  11. // для непустого списка
  12. // конкретизируем, что это должен быть список типов
  13. // содержащий голову Head и хвост Tail
  14. template <class Head, class Tail>
  15. struct GetPorts< Typelist<Head, Tail> >
  16. {
  17. private:
  18. // класс TPin помнит свой порт
  19. // запоминаем этот тип порта
  20. typedef typename Head::Pin::Port Port;
  21. //рекурсивно генерируем хвост
  22. typedef typename GetPorts<Tail>::Result L1;
  23. public:
  24. // определяем список портов из текущего порта (Port) и хвоста (L1)
  25. typedef Typelist<Port, L1> Result;
  26. };

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

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

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

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

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

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

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

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

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

  1.  
  2. //Параметр TList должен быть список линий
  3. template <class TList> struct GetPortMask;
  4. // Для пустого списка возвращаем 0.
  5. template <> struct GetPortMask<NullType>
  6. {
  7. enum{value = 0};
  8. };
  9.  
  10. template <class Head, class Tail>
  11. struct GetPortMask< Typelist<Head, Tail> >
  12. { //value = битовая маска для головы | битовая маска оставшейся части списка
  13. enum{value = (1 << Head::Pin::Number) | GetPortMask<Tail>::value};
  14. };
  15.  
  16. Теперь напишем реализацию для функции записи в порт:
  17.  
  18. // Head – голова списка портов – текущий порт
  19. // Tail – оставшийся список
  20. // PinList – исходный список линий.
  21. template <class Head, class Tail, class PinList>
  22. struct PortWriteIterator< Typelist<Head, Tail>, PinList>
  23. {
  24. //Определим линии принадлежащие текущему порту.
  25. typedef typename GetPinsWithPort<PinList, Head>::Result Pins;
  26. // Посчитаем битовую маску для порта
  27. enum{Mask = GetPortMask<Pins>::value};
  28. typedef Head Port;
  29.  
  30. template<class DataType>
  31. static void Write(DataType value)
  32. {
  33. // проецируем биты из входного значения в соответствующие биты порта
  34. // как это реализованно увидим дальше
  35. uint8_t result = PinWriteIterator<Pins>::UppendValue(value);
  36. // если кол-во бит в записываемом значении совпадает с шириной порта,
  37. // то записываем порт целиком.
  38. // это условие вычислится во время компиляции
  39. if((int)Length<Pins>::value == (int)Port::Width)
  40. Port::Write(result);
  41. else
  42. {
  43. // PORT = PORT & Mask | result;
  44. Port::ClearAndSet(Mask, result);
  45. }
  46. // рекурсивно обрабатываем остальные порты в списке
  47. PortWriteIterator<Tail, PinList>::Write(value);
  48. }
  49. }

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

  • если записываемые биты в порту расположены последовательно, спроецируем их все сразу с помощью сдвига на нужное число бит и соответствующей битовой маски
  • если одиночный бит в исходном значении и в порту имеют одну позицию, спроецируем его с помощью побитного ИЛИ.
  • в остальных случаях, если бит во входном значении равен 1, то устанавливаем в 1 соответствующий ему бит в регистре.
  1. // Tlist – список линий принадлежащих одному порту
  2. template <class TList> struct PinWriteIterator;
  3. // специализация для пустого списка – возвращаем 0
  4. template <> struct PinWriteIterator<NullType>
  5. {
  6. template<class DataType>
  7. static uint8_t UppendValue(const DataType &value)
  8. {
  9. return 0;
  10. }
  11. };
  12.  
  13. // специализация для непустого списка
  14. template <class Head, class Tail>
  15. struct PinWriteIterator< Typelist<Head, Tail> >
  16. {
  17. template<class DataType>
  18. static inline uint8_t UppendValue(const DataType &value)
  19. {
  20. // проверяем, если линии в порту расположены последовательно
  21. // если часть линий в середине списка будет расположена последовательно, то
  22. // это условие не выполнется, так, что есть ещё простор для оптимизации.
  23. if(IsSerial<Typelist<Head, Tail> >::value)
  24. {
  25. // сдвигаем значение на нужное число бит и накладываем не него маску
  26. if((int)Head::Position > (int)Head::Pin::Number)
  27. return (value >> ((int)Head::Position - (int)Head::Pin::Number)) &
  28. GetPortMask<Typelist<Head, Tail> >::value;
  29. else
  30. return (value << ((int)Head::Pin::Number - (int)Head::Position)) &
  31. GetPortMask<Typelist<Head, Tail> >::value;
  32. }
  33.  
  34. uint8_t result=0;
  35.  
  36. if((int)Head::Position == (int)Head::Pin::Number)
  37. result |= value & (1 << Head::Position);
  38. else
  39. // это условие будет вычисляться во время выполнения программы
  40. if(value & (1 << Head::Position))
  41. result |= (1 << Head::Pin::Number);
  42. // рекурсивно обрабатываем оставшиеси линии в списке
  43. return result | PinWriteIterator<Tail>::UppendValue(value);
  44. }
  45. };

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

  1. template <class TList> struct IsSerial;
  2. // специализация для пустого списка
  3. template <> struct IsSerial<NullType>
  4. {
  5. // пустой список последователен
  6. enum{value = 1};
  7. // номер текущей линии
  8. enum{PinNumber = -1};
  9. // признак конца списка
  10. enum{EndOfList = 1};
  11. };
  12. // для непустого списка
  13. template <class Head, class Tail>
  14. struct IsSerial< Typelist<Head, Tail> >
  15. {
  16. // последовательна ли оставшаяся часть списка
  17. typedef IsSerial<Tail> I;
  18. // запоминаем номер текущей линии в её порту
  19. enum{PinNumber = Head::Pin::Number};
  20. // не конец списка
  21. enum{EndOfList = 0};
  22. // список последователен если
  23. // номер текущей линии равен номеру следующей - 1 И
  24. // оставшаяся часть списка последовательна ИЛИ
  25. // текущая линия последняя в списке
  26. enum{value = ((PinNumber == I::PinNumber - 1) && I::value) || I::EndOfList};
  27. };

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

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

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

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

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

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

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

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

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

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

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

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

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



Оригинал, печатается с разрешения автора

Продолжу позже.