Мне всегда нравилась идея объектно-ориентированного программирования. Это очень удобно и легко, особенно, когда программа раздувается до больших размеров, или есть несколько очень похожих элементов, но с разными настройками. И меня всегда интересовали нестандартные, красивые решения и новинки языка — шаблоны, лямбда-функции, тернарные операторы… К сожалению, я все никак не мог к ним подобраться — то времени не было, то мозг был не готов. В общих чертах знал, что это, но сам никогда не пробовал. Но вдруг в одной из программ для 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. Данные не собираются, только статистика (количество) визитов, честно! И это никоим образом не реклама… 🙂
