Мне всегда нравилась идея объектно-ориентированного программирования. Это очень удобно и легко, особенно, когда программа раздувается до больших размеров, или есть несколько очень похожих элементов, но с разными настройками. И меня всегда интересовали нестандартные, красивые решения и новинки языка — шаблоны, лямбда-функции, тернарные операторы… К сожалению, я все никак не мог к ним подобраться — то времени не было, то мозг был не готов. В общих чертах знал, что это, но сам никогда не пробовал. Но вдруг в одной из программ для AVR я увидел интересное использование шаблона, которое очень сильно облегчало работу. Мне стало интересно — и время нашлось, и желание… И вот родилась идея этой статьи. Результат — родительский класс для легкой работы с устройствами на базе SPI (сдвиговые регистры, трансиверы, Ethernet etc), в Hardware и Software реализации. Интересно — просьба под кат.
tl;dr — в конце все ссылки из статьи, в том числе готовый код и примеры.
WARNING 1: под катом много букв и о ужас ни одной картинки, а также огромная куча низкокачественного лапшеобразного замечательного кода.
WARNING 2: автор не крут в шаблонах, поэтому извиняется, если что-то реализовано ужасно.
WARNING 3: пока что — только Master, Slave-версия будет через год некоторое время.
WARNING 4: автор обожает варнинги и тег s неправда!!
Зарождение
Когда имеешь дело с большим количеством периферии, начинаешь тихо ненавидеть дефайны с портами и пинами. Их становится столько, что не протолкнуться. Повесим SlaveSelect в один порт с Hardware SPI. Ой, туда тяжело дорожку провести, переставим на другой. И с ним, конечно, сделаем дефайны для DDRx и PINx — мы же хотим, чтобы все само инициализировалось. И так для каждого… А если забивать железно, при любом изменении конструкции приходится лопатить весь код.
У меня возникла идея — почему бы не сделать все классом? Все порты указывать через аргументы конструктора. Удобно, красиво, мало места. Но. PORTx — не функция, не тип, это зубодробительный макрос, который фиг куда запишешь. Споткнувшись на этом, я забросил идею ООП для работы с портами AVR.
В один счастливый день на одном хорошем сайте (там, где живет создатель Pinboard 😉 ) я наткнулся на один прекрасный листинг. Neiver использовал хитрый макрос и шаблон для работы с портами. Он с помощью макроса подставлял значения PORTx etc в методы нового класса, который был объявлен целиком в макросе. При вызове этого чуда создавался класс, который можно передавать куда угодно и который дает доступ до PORT, DDR и PIN, при этом реализуя часть операций типа Read, Set, Toggle и т.д.. Мне так понравилась эта идея, что я оформил ее в маленькую библиотеку, которую довольно часто использую. К сожалению, связаться с автором не было возможности, но я везде указываю его, так что думаю, он не обидится… Код находится тут, я старался хорошо прокомментировать каждый пункт, поэтому подробно рассказывать о работе этой библиотеки не буду.
Проблема с портами решена, теперь можно приступить к самому интересному. Написав пару классов с использованием этой фичи, я подумал: было бы здорово сделать класс-родитель, от которого будет наследоваться работа с интерфейсом. Решено — будем писать такую штуку для SPI.
Реализация Hardware SPI
У меня есть библиотека libSPI (основу взял у Tinkerer) в трех вариантах — Hardware (использует железный SPI), USI-based (работает на базе USI — например, для ATtiny24) и Software (полностью программный вариант). Откинув вторую (USI сейчас довольно редко встречается), я начал с самого легкого — Hardware. Накидал вот такой скелет класса-родителя:
template <class PORT, uint8_t PIN_SS> class SPI_Base_Hardware { public: // в конструкторе инициализируем порт и SPI SPI_Base_Hardware() { spi_init();} ~SPI_Base_Hardware(){;} void init(); // выбор и освобождение устройства (select - SS=>LOW, release - SS=>HIGH) void select(); void release(); // чтение и запись одного байта uint8_t fast_shift(uint8_t dataout); // запись массива данных void transmit_sync(uint8_t *dataout, uint8_t len); // чтение и запись массива данных void transfer_sync(uint8_t *dataout, uint8_t * datain, uint8_t len); };
Для тех, кто не знаком с шаблонами, кратко опишу суть этой штуки (те, кто знают — просьба не пинать за столь краткий и не совсем правильный вариант…). Шаблон позволяет подставлять разные типы в одну и ту же конструкцию. В данном случае при инициализации объекта SPI_Base_Hardware я указываю класс порта ввода-вывода (тот самый, который создаем макросом MAKE_PORT) и номер пина SlaveSelect (далее SS), с которыми потом могу оперировать в классе с помощью слов PORT и PIN_SS соответственно. Вот тут довольно неплохо описано, что это за зверь и с чем его едят, на примерах.
Первые грабли
Я люблю структурированный код. Описание в .h, реализация в .cpp — стараюсь делать так всегда. Первые грабли появились, когда я попытался вынести реализацию за объявление класса. Будучи плохо знакомым с шаблонами, я получил кучу непонятных ошибок, беглое гугление которых мало относилось к текущей проблеме. Мне все же удалось найти немного информации на каком-то форуме, оказавшееся довольно. Вынести реализацию в другой файл нельзя (печалька), но можно вынести ее за пределы объявления, оставив в этом же файле. Для этого нужно сделать следующее:
// тут скелет, который написан выше ... template <class PORT, uint8_t PIN_SS> void SPI_Base_Hardware<PORT, PIN_SS>::_spi_select(){ PORT::Clear(1<<PIN_SS); }
Такая конструкция мне все равно не понравилась, и еще через некоторое время был найден довольно простой вариант. Из основной инклюды (в нашем случае — spi_base_hardware.h) описание класса выносим в spi_base_hardware.hpp, всю реализацию — в spi_base_hardware.cpp. При этом сам файл spi_base_hardware.h становится вот таким:
#include "spi_base_hardware.hpp" #include "spi_base_hardware.cpp"
Вот и все. Описание отдельно, реализация отдельно, я доволен 🙂
Продолжаем писать
Написав реализацию каждой функции, я решил пойти дальше. В Hardware-версии порт и пины интерфейса SPI меняться ну просто не могут (на то он и Hardware). Так давайте заведем на них дефайны, скажете Вы. Да, давайте, отвечу я, но при этом добавлю — только Вы их не увидите. Пускай они будут сами добавляться, для нужного камня — нужные дефайны. Открываем файлик include\io.h и начинаем перебирать все МК — гуглим pinout. Потом в стиле все того же io.h создаем файл, который будет проверять выбранную версию МК и создавать нужный дефайн. Вот что у меня получилось spi_hardware_defs.h. На все МК меня не хватило, список поддерживаемых (все их версии) — в начале того файла. Если хоть кому-то эта штука будет полезна — обязательно доведу до конца.
И что же получилось?
spi_hardware.h
#ifndef _LIB_SPI_HARDWARE_H_ #define _LIB_SPI_HARDWARE_H_ #include "spi_hardware_defs.h" #include "spi_hardware.hpp" #include "spi_hardware.cpp" #endif
spi_hardware.hpp
#ifndef _LIB_SPI_HARDWARE_HPP_ #define _LIB_SPI_HARDWARE_HPP_ #include <avr/io.h> template <class PORT, uint8_t PIN_SS> class SPI_Base_Hardware { public: SPI_Base_Hardware() { init();} ~SPI_Base_Hardware(){;} void init(); void select(); void release(); uint8_t fast_shift(uint8_t dataout); void transmit_sync(uint8_t *dataout, uint8_t len); void transfer_sync(uint8_t *dataout, uint8_t * datain, uint8_t len); }; #endif
spi_hardware.cpp
#include <avr/io.h> #include "spi_hardware_defs.h" #include "spi_hardware.hpp" template <class PORT, uint8_t PIN_SS> void SPI_Base_Hardware<PORT, PIN_SS>:: select(){ PORT::Clear(1<<PIN_SS); } template <class PORT, uint8_t PIN_SS> void SPI_Base_Hardware<PORT, PIN_SS>:: release(){ PORT::Set(1<<PIN_SS); } template <class PORT, uint8_t PIN_SS> void SPI_Base_Hardware<PORT, PIN_SS>:: init(){ SPI_DDR|=1<<SPI_MOSI | 1<<SPI_SCK; SPI_DDR&=~(1<<SPI_MISO); PORT::DirSet(1<<PIN_SS); release(); SPCR = ((1<<SPE)| // SPI Enable (0<<SPIE)| // SPI Interupt Enable (0<<DORD)| // Data Order (0:MSB first / 1:LSB first) (1<<MSTR)| // Master/Slave select (0<<SPR1)|(1<<SPR0)| // SPI Clock Rate (0<<CPOL)| // Clock Polarity (0:SCK low / 1:SCK hi when idle) (0<<CPHA)); // Clock Phase (0:leading / 1:trailing edge sampling) SPSR = (1<<SPI2X); // Double Clock Rate } template <class PORT, uint8_t PIN_SS> uint8_t SPI_Base_Hardware<PORT, PIN_SS>:: fast_shift(uint8_t dataout){ SPDR = dataout; while((SPSR & (1<<SPIF))==0); return SPDR; } template <class PORT, uint8_t PIN_SS> void SPI_Base_Hardware<PORT, PIN_SS>:: transmit_sync(uint8_t *dataout, uint8_t len){ uint8_t i; for (i = 0; i < len; i++) { SPDR = dataout[i]; while((SPSR & (1<<SPIF))==0); } } template <class PORT, uint8_t PIN_SS> void SPI_Base_Hardware<PORT, PIN_SS>:: transfer_sync(uint8_t *dataout, uint8_t * datain, uint8_t len){ uint8_t i; for (i = 0; i < len; i++) { SPDR = dataout[i]; while((SPSR & (1<<SPIF))==0); datain[i] = SPDR; } }
Примеры
Получившийся класс можно использовать несколькими способами — как драйвер для интерфейса SPI, как родитель для другого класса, как родитель для другого класса-шаблона и как драйвер, содержащийся внутри класса-шаблона. Покажу все виды.
Пример 1. Hardware SPI, использование в качестве драйвера
#include <avr/io.h> #include "port.h" #include "spi_hardware.h" MAKE_PORT(PORTB, DDRB, PINB, Portb, 'B'); MAKE_PORT(PORTD, DDRD, PIND, Portd, 'D'); int main(){ // создаем интерфейс Hardware SPI с SlaveSelect на ноге PB0 // при этом мы нигде не указываем ноги MOSI MOSI SCK - они находятся сами SPI_Base_Hardware<Portb, 0> driver1; // он уже готов - ноги инициализированы, SS=HIGH, регистры настроены driver1.fast_shift(0x10); // пишем байт // создаем еще интерфейс, SS на ноге PD5 SPI_Base_Hardware<Portd, 5> driver2; driver2.fast_shift(0x20); while (1) {} return 0; }
Пример 2. Hardware SPI, создание класса-наследника
#include <avr/io.h> #include "port.h" #include "spi_hardware.h" MAKE_PORT(PORTB, DDRB, PINB, Portb, 'B'); class Device : public SPI_Base_Hardware<Portb, 0>{ public: void init(){ fast_shift(0x31); } }; int main(){ // создаем объект Device - Hardware SPI, SlaveSelect на ноге PB0 Device monster; // SPI уже готов - вызван конструктор от SPI_Base_Hardware // вызываем метод нашего класса - "инициализация" monster.init(); while (1) {} return 0; }
И, наконец, самое интересное…
Пример 3. Hardware SPI, создание шаблона класса-наследника
#include <avr/io.h> #include "port.h" #include "spi_hardware.h" MAKE_PORT(PORTB, DDRB, PINB, Portb, 'B'); MAKE_PORT(PORTD, DDRD, PIND, Portd, 'D'); template <class PORT, uint8_t PIN_SS> class Creature : public SPI_Base_Hardware<PORT, PIN_SS>{ public: void init(){ this->fast_shift(0x31); } }; int main(){ // создаем объект Device - Hardware SPI, SlaveSelect на ноге PB0 Creature<Portb, 0> monster1; // SPI уже готов - вызван конструктор от SPI_Base_Hardware // вызываем метод нашего класса - "инициализация" monster1.init(); // проделываем то же самое для монстра с SS на ноге PD5 Creature<Portd, 5> monster2; monster2.init(); // управлялись бы все монстры по SPI... Я бы завоевал мир ;) while (1) {} return 0; }
Тут нужно сделать оговорку. Вы наверняка заметили, что в методе init я использовал this->spi… Если этого не сделать, компилятор будет громко вопить, что он не может найти эти методы. Это касается всех методов, которые есть в классе SPI_Base_Hardware.
У всех предыдущих примеров есть один недостаток — имена методов fast_shift etc заняты, что не критично, но как-то нехорошо… Поэтому я считаю самым оптимальным вариантом…
Пример 4. Hardware SPI, создание шаблона класса, содержащим внутри себя драйвер
#include <avr/io.h> #include "port.h" #include "spi_hardware.h" MAKE_PORT(PORTB, DDRB, PINB, Portb, 'B'); MAKE_PORT(PORTD, DDRD, PIND, Portd, 'D'); template <class PORT, uint8_t PIN_SS> class Creature { private: SPI_Base_Hardware<PORT, PIN_SS> spi; public: void init(){ spi.fast_shift(0x31); } }; int main(){ // создаем объект Device - Hardware SPI, SlaveSelect на ноге PB0 Creature<Portb, 0> monster1; // SPI уже готов - вызван конструктор от SPI_Base_Hardware // вызываем метод нашего класса - "инициализация" monster1.init(); // проделываем то же самое для монстра с SS на ноге PD5 Creature<Portd, 5> monster2; monster2.init(); while (1) {} return 0; }
Драйвер инкапсулирован, теперь все вообще круто 🙂
А что дальше?
Наигравшись с Hardware SPI, я решил заняться программной версией. Тут уже открываются большие горизонты: повесить несколько устройств на разные порты, не обращая внимания на распиновку контроллера — куда легко вести, туда и провел. Красота!
Скелет будет таким же, кроме названия класса и шаблона:
template <class PORT, uint8_t PIN_MOSI, uint8_t PIN_MISO, uint8_t PIN_SCK, uint8_t PIN_SS> class SPI_Base_Software { public: SPI_Base_Software() { spi_init();} ~SPI_Base_Software(){;} void init(); // дальше все то же самое, что в скелете SPI_Base_Hardware };
Теперь передается не только порт и пин SS, а также пины MISO, MOSI и SCK. Но что, если у нас нет свободных ног в одном порту? Правильно, раскидываем по разным — у нас же программная реализация, делаем что хотим. Шаблон меняется до такого вида:
template <class PORT_MOSI, uint8_t PIN_MOSI, class PORT_MISO, uint8_t PIN_MISO, class PORT_SCK, uint8_t PIN_SCK, class PORT_SS, uint8_t PIN_SS> class SPI_Base_Software_Separate { ... };
Он вырос и превратился в огромную строку. В реализации придется писать так:
template <class PORT_MOSI, uint8_t PIN_MOSI, class PORT_MISO, uint8_t PIN_MISO, class PORT_SCK, uint8_t PIN_SCK, class PORT_SS, uint8_t PIN_SS> void SPI_Base_Software_Separate<PORT_MOSI,PIN_MOSI, PORT_MISO,PIN_MISO, PORT_SCK,PIN_SCK, PORT_SS,PIN_SS>:: select(){ PORT_SS::Clear(1<<PIN_SS); }
Жуть… Но если сделать два макроса, то все смотрится очень даже гармонично:
template _SPI_SOFT_SEP_TEMPLATE_ void SPI_Base_Software_Separate _SPI_SOFT_SEP_REALIZATION_ select(){ PORT_SS::Clear(1<<PIN_SS); }
Так ведь лучше? Эти макросы я буду активно использовать в дальнейшем.
Пишем дальше: реализация Software SPI
Я оставил два варианта — Software SPI и Separate Software SPI. Первый висит на одном порту и принимает в шаблоне класс порта и 4 пина — MOSI, MISO, SCK и SS. Второй принимает порт для каждого пина, выглядит это так:
SPI_Base_Software_Separate<Portb, 0, Portc, 1, Portb, 4, Portd, 2> device;
Чтобы не делать два одинаковых класса, я сделал SPI_Base_Software наследником от SPI_Base_Software_Separate. Вот так:
template _SPI_SOFT_TEMPLATE_ class SPI_Base_Software : public SPI_Base_Software_Separate<PORT,PIN_MOSI, PORT,PIN_MISO, PORT,PIN_SCK, PORT,PIN_SS> {};
И что получилось?
spi_software.h
#ifndef _LIB_SPI_SOFTWARE_H_ #define _LIB_SPI_SOFTWARE_H_ #include "spi_software.hpp" #include "spi_software.cpp" #endif
spi_software.hpp
#ifndef _LIB_SPI_SOFTWARE_HPP_ #define _LIB_SPI_SOFTWARE_HPP_ #include <avr/io.h> // macros for easy operating // ex: template _SPI_SOFT_SEP_TEMPLATE_ #define _SPI_SOFT_SEP_TEMPLATE_ <class PORT_MOSI, uint8_t PIN_MOSI, class PORT_MISO, uint8_t PIN_MISO,\ class PORT_SCK, uint8_t PIN_SCK, class PORT_SS, uint8_t PIN_SS> // class nameOfClass : _SPI_SOFT_SEP_PARENT_ #define _SPI_SOFT_SEP_PARENT_ public SPI_Base_Software_Separate<PORT_MOSI, PIN_MOSI, PORT_MISO, PIN_MISO, PORT_SCK, PIN_SCK, PORT_SS, PIN_SS> // nameOfClass _SPI_SOFT_SEP_REALIZATION_ func #define _SPI_SOFT_SEP_REALIZATION_ <PORT_MOSI,PIN_MOSI, PORT_MISO,PIN_MISO, PORT_SCK,PIN_SCK, PORT_SS,PIN_SS>:: // same for non-separate #define _SPI_SOFT_TEMPLATE_ <class PORT,uint8_t PIN_MOSI,uint8_t PIN_MISO,uint8_t PIN_SCK,uint8_t PIN_SS> #define _SPI_SOFT_PARENT_ public SPI_Base_Software<PORT, PIN_MOSI, PIN_MISO, PIN_SCK, PIN_SS> #define _SPI_SOFT_REALIZATION_ <PORT, PIN_MOSI, PIN_MISO, PIN_SCK, PIN_SS>:: template <class PORT_MOSI, uint8_t PIN_MOSI, class PORT_MISO, uint8_t PIN_MISO, class PORT_SCK, uint8_t PIN_SCK, class PORT_SS, uint8_t PIN_SS> class SPI_Base_Software_Separate { public: SPI_Base_Software_Separate() { init();} ~SPI_Base_Software_Separate(){;} void init(); void select(); void release(); uint8_t fast_shift(uint8_t dataout); void transmit_sync(uint8_t *dataout, uint8_t len); void transfer_sync(uint8_t *dataout, uint8_t *datain, uint8_t len); }; template _SPI_SOFT_TEMPLATE_ class SPI_Base_Software : public SPI_Base_Software_Separate<PORT, PIN_MOSI, PORT, PIN_MISO, PORT, PIN_SCK, PORT, PIN_SS> {}; #endif
spi_software.cpp
#include "spi_software.hpp" template _SPI_SOFT_SEP_TEMPLATE_ void SPI_Base_Software_Separate _SPI_SOFT_SEP_REALIZATION_ select(){ PORT_SS::Clear(1<<PIN_SS); } template _SPI_SOFT_SEP_TEMPLATE_ void SPI_Base_Software_Separate _SPI_SOFT_SEP_REALIZATION_ release(){ PORT_SS::Set(1<<PIN_SS); } template _SPI_SOFT_SEP_TEMPLATE_ void SPI_Base_Software_Separate _SPI_SOFT_SEP_REALIZATION_ init(){ PORT_SCK::Clear(1<<PIN_SCK); PORT_MOSI::Clear(1<<PIN_MOSI); PORT_MISO::Set(1<<PIN_MISO); PORT_SCK::DirSet(1<<PIN_SCK); PORT_MOSI::DirSet(1<<PIN_MOSI); PORT_SS::DirSet(1<<PIN_SS); PORT_MISO::DirClear(1<<PIN_MISO); release(); } template _SPI_SOFT_SEP_TEMPLATE_ uint8_t SPI_Base_Software_Separate _SPI_SOFT_SEP_REALIZATION_ fast_shift(uint8_t dataout){ uint8_t recv=0; for(signed char i=7; i>=0; i--){ (((dataout&(1<<i))>>i)==1)? PORT_MOSI::Set(1<<PIN_MOSI) : PORT_MOSI::Clear(1<<PIN_MOSI); PORT_SCK::Set(1<<PIN_SCK); if ((PORT_MISO::Read()&(1<<PIN_MISO))>>PIN_MISO==1) recv|=1<<i; PORT_SCK::Clear(1<<PIN_SCK); } return recv; } template _SPI_SOFT_SEP_TEMPLATE_ void SPI_Base_Software_Separate _SPI_SOFT_SEP_REALIZATION_ transmit_sync(uint8_t *dataout, uint8_t len){ for (uint8_t i = 0; i < len; i++) { fast_shift(dataout[i]); } } template _SPI_SOFT_SEP_TEMPLATE_ void SPI_Base_Software_Separate _SPI_SOFT_SEP_REALIZATION_ transfer_sync(uint8_t *dataout, uint8_t *datain, uint8_t len){ for (uint8_t i = 0; i < len; i++) { datain[i]=fast_shift(dataout[i]); } }
Примеры
Способов использования — столько же сколько и у хардварного варианта.
Пример 5. Software SPI, драйвер.
#include <avr/io.h> #include "port.h" #include "spi_software.h" MAKE_PORT(PORTB, DDRB, PINB, Portb, 'B'); MAKE_PORT(PORTC, DDRC, PINC, Portc, 'C'); MAKE_PORT(PORTD, DDRD, PIND, Portd, 'D'); int main(){ SPI_Base_Software<Portb, 0, 1, 2, 4> driver1; // SPI уже настроен - MOSI на PB0, MISO на PB1, SCK - PB2 и SS - PB4 driver1.fast_shift(0x30); // теперь Separate-версия SPI_Base_Software_Separate<Portc, 0, Portb, 5, Portd, 3, Portc, 2> driver2; driver2.fast_shift(0x12); while (1){} return 0; }
Пример 6. Создание класса-наследника
#include <avr/io.h> #include "port.h" #include "spi_software.h" MAKE_PORT(PORTB, DDRB, PINB, Portb, 'B'); class Device : public SPI_Base_Software<Portb, 0,1,2,3>{ public: void init(){ fast_shift(0x31); } }; int main(){ // создаем объект Device Device monster; // SPI уже готов - вызван конструктор от SPI_Base_Software // вызываем метод нашего класса - "инициализация" monster.init(); while (1) {} return 0; }
Пример 7. Создание шаблона класса-наследника
#include <avr/io.h> #include "port.h" #include "spi_software.h" MAKE_PORT(PORTB, DDRB, PINB, Portb, 'B'); MAKE_PORT(PORTD, DDRD, PIND, Portd, 'D'); template _SPI_SOFT_TEMPLATE_ class Creature : _SPI_SOFT_PARENT_{ public: void init(){ this->fast_shift(0x31); } }; int main(){ Creature<Portb, 0, 1, 2, 3> monster1; // SPI уже готов - вызван конструктор от SPI_Base_Software // вызываем метод нашего класса - "инициализация" monster1.init(); // проделываем то же самое для монстра с SS на ноге PD5 Creature<Portd, 0, 1, 2, 5> monster2; monster2.init(); while (1) {} return 0; }
Пример 8. Создание шаблона класса, содержащим внутри себя драйвер
#include <avr/io.h> #include "port.h" #include "spi_software.h" MAKE_PORT(PORTB, DDRB, PINB, Portb, 'B'); MAKE_PORT(PORTD, DDRD, PIND, Portd, 'D'); template _SPI_SOFT_TEMPLATE_ class Creature { private: SPI_Base_Software _SPI_SOFT_REALIZATION_ spi; public: void init(){ spi.fast_shift(0x31); } }; int main(){ // создаем объект Device Creature<Portb, 0, 1, 2, 3> monster1; // SPI уже готов // вызываем метод нашего класса - "инициализация" monster1.init(); // проделываем то же самое для монстра с SS на ноге PD5 Creature<Portd, 0, 1, 2, 5> monster2; monster2.init(); while (1) {} return 0; }
Критика принимается. Только пожалуйста, бейте не очень сильно… )
Ссылки из статьи:
• Библиотека port.h
• Описание работы шаблонов
• spi_hardware_defs.h
• Обе версии библиотеки с примерами
P.S: Все ссылки сжаты с помощью моего ресурса urler.tk. Данные не собираются, только статистика (количество) визитов, честно! И это никоим образом не реклама… 🙂