Всем привет! В этом топике я покажу, как с помощью новейшей Attiny от Atmel/Microchip управлять светодиодами WS2812. Для начала давайте вспомним, а лучше подсмотрим в даташите на временные интервалы нуля и единицы. Где видим, что для передачи нуля нам необходим импульс длительностью от 200 и до 500 наносекунд, для передачи единицы - от 550 и до 850 наносекунд. Период же так и вообще может изменяться в широких пределах. Управлять светодиодами мы будем с помощью SPI. Но не напрямую, а через CCL. Почему? Всё просто - SPI содержит 8-ми битный регистр и как его не крути, целочисленная передача не получится - исходный бит надо будет "разбавлять" нулями либо единицами. А что если поступить наоборот - длительность исходного бита будет задавать период сигнала, а разбивать бит будем тактовой частотой SCK того же интерфейса SPI. Так мы сформируем "1", которую поймет драйвер светодиода. Да, T1H и T1L по длительности будут равны, но в параметры даташита мы укладываемся. Как же быть с нулем? Для этого мы воспользуемся помощью зрительного зала таймера TCA0. Пусть он нам разобьет тактовую частоту SCK ещё раз напополам и мы получим длительность T0H в четыре раза меньшую, чем период. Но всё равно укладываемся в даташит)) В результате манипуляций мы получим такую картину: желтая диаграмма - это RGB код для светодиодов, зеленая диаграмма - тактовая частота SCK, синий - меандр с таймера TCA0, фиолетовый - подготовленные импульсы для подачи на WS2812. Работать мы будем на тактовой частоте в 20 МГц, поэтому первое, что сделаем, отключим прескалер: Код (C++): CCP = CCP_IOREG_gc; CLKCTRL.MCLKCTRLB = 0; Затем подготовим блок SPI: Код (C++): //... Инициализация SPI inline void SPI_Init(void) { SPI_PORT.DIR |= (1<<MOSI_PIN) | (1<<SCK_PIN) | (0<<MISO_PIN) | (1<< PIN4_bp); // Все выводы, кроме MISO, выходы SPI0.CTRLA = (0<<SPI_DORD_bp) // Старший бит вперёд | (1<<SPI_MASTER_bp) // SPI мастер | (1<<SPI_CLK2X_bp) // Удвоенная скорость тактирования | SPI_PRESC_DIV64_gc // Делитель = 64 | (1<<SPI_ENABLE_bp); // Разрешаем работу SPI SPI0.CTRLB = (0<<SPI_BUFEN_bp) | (0<<SPI_BUFWR_bp) | (1<<SPI_SSD_bp) | SPI_MODE_0_gc; SPI0.DATA = 0; } //... Передать байт данных inline void SPI_WriteByte(uint8_t data) { SPI0.DATA = data; while(!(SPI0.INTFLAGS & SPI_IF_bm)); } Выставляем прескалер на 64 такта и взводим бит SPI_CLK2X, что в итоге нам даст частоту тактирования 625 кГц. Период тактирования при такой частоте будет равен 1.6 мкс - в даташит укладываемся. Далее настроим таймер: Код (C++): void TCA_Init(void) { TCA0.SINGLE.CTRLB = 1<<TCA_SINGLE_ALUPD_bp | // Установим бит автоматической блокировки обновления регистров компаратора (0 << TCA_SINGLE_CMP0EN_bp) | (0 << TCA_SINGLE_CMP1EN_bp) | (0 << TCA_SINGLE_CMP2EN_bp) | // пины выводов компараторов не включаем TCA_SINGLE_WGMODE_SINGLESLOPE_gc; // Режим работы таймера - односкатный ШИМ с прерыванием OVF в вершине счета } Режим работы таймера следующий - счетчик таймера считает от нуля и до значения в регистре PER, после чего самостоятельно обнуляется и цикл повторяется. Значение прескалера таймера и его режим работы я определю в отдельной функции: Код (C++): inline void TCA0_Mode(uint8_t mode) { TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | // Определяем значение прескалера - 1 mode << TCA_SINGLE_ENABLE_bp; // mode = 1 - таймер включен, mode = 0 - таймер выключен } Уже в основной функции настрою регистры: Код (C++): TCA_Init(); TCA0.SINGLE.PER = TCA_period; TCA0.SINGLE.CMP2 = TCA_cmp2; Компаратор использую CMP2 - далее будет понятно почему. Выход компаратора устанавливается в "1" при сбросе счетчика, а сбрасывается при равенстве значений в счетном регистре и регистре компаратора. Для того, чтобы получить частоту 625 * 2 = 1250 кГц, я определю следующие значений регистров: Код (C++): #define TCA_period 15 #define TCA_cmp2 8
Давайте подключать таблицу LUT CCL. Для этого открываем даташит и внимательно читаем, на какой вход что можно подключить. Напомню, что нам надо завести сигналы от: -MOSI SPI -SCK SPI -WO2 Смотрим - на нулевой канал LUT мы можем подключить от SPI только сигнал SCK: Код (C++): /* LUT Input 0 Source Selection select */ typedef enum CCL_INSEL0_enum { CCL_INSEL0_MASK_gc = (0x00<<0), /* Masked input */ CCL_INSEL0_FEEDBACK_gc = (0x01<<0), /* Feedback input source */ CCL_INSEL0_LINK_gc = (0x02<<0), /* Linked LUT input source */ CCL_INSEL0_EVENT0_gc = (0x03<<0), /* Event input source 0 */ CCL_INSEL0_EVENT1_gc = (0x04<<0), /* Event input source 1 */ CCL_INSEL0_IO_gc = (0x05<<0), /* IO pin LUTn-IN0 input source */ CCL_INSEL0_AC0_gc = (0x06<<0), /* AC0 OUT input source */ CCL_INSEL0_TCB0_gc = (0x07<<0), /* TCB0 WO input source */ CCL_INSEL0_TCA0_gc = (0x08<<0), /* TCA0 WO0 input source */ CCL_INSEL0_TCD0_gc = (0x09<<0), /* TCD0 WOA input source */ CCL_INSEL0_USART0_gc = (0x0A<<0), /* USART0 XCK input source */ CCL_INSEL0_SPI0_gc = (0x0B<<0), /* SPI0 SCK source */ } CCL_INSEL0_t; На первый канал подключается только MOSI: Код (C++): /* LUT Input 1 Source Selection select */ typedef enum CCL_INSEL1_enum { CCL_INSEL1_MASK_gc = (0x00<<4), /* Masked input */ CCL_INSEL1_FEEDBACK_gc = (0x01<<4), /* Feedback input source */ CCL_INSEL1_LINK_gc = (0x02<<4), /* Linked LUT input source */ CCL_INSEL1_EVENT0_gc = (0x03<<4), /* Event input source 0 */ CCL_INSEL1_EVENT1_gc = (0x04<<4), /* Event input source 1 */ CCL_INSEL1_IO_gc = (0x05<<4), /* IO pin LUTn-N1 input source */ CCL_INSEL1_AC0_gc = (0x06<<4), /* AC0 OUT input source */ CCL_INSEL1_TCB0_gc = (0x07<<4), /* TCB0 WO input source */ CCL_INSEL1_TCA0_gc = (0x08<<4), /* TCA0 WO1 input source */ CCL_INSEL1_TCD0_gc = (0x09<<4), /* TCD0 WOB input source */ CCL_INSEL1_USART0_gc = (0x0A<<4), /* USART0 TXD input source */ CCL_INSEL1_SPI0_gc = (0x0B<<4), /* SPI0 MOSI input source */ } CCL_INSEL1_t; И на третьем канале от таймера TCA0 возможно подключить только WO2: Код (C++): /* LUT Input 2 Source Selection select */ typedef enum CCL_INSEL2_enum { CCL_INSEL2_MASK_gc = (0x00<<0), /* Masked input */ CCL_INSEL2_FEEDBACK_gc = (0x01<<0), /* Feedback input source */ CCL_INSEL2_LINK_gc = (0x02<<0), /* Linked LUT input source */ CCL_INSEL2_EVENT0_gc = (0x03<<0), /* Event input source 0 */ CCL_INSEL2_EVENT1_gc = (0x04<<0), /* Event input source 1 */ CCL_INSEL2_IO_gc = (0x05<<0), /* IO pin LUTn-IN2 input source */ CCL_INSEL2_AC0_gc = (0x06<<0), /* AC0 OUT input source */ CCL_INSEL2_TCB0_gc = (0x07<<0), /* TCB0 WO input source */ CCL_INSEL2_TCA0_gc = (0x08<<0), /* TCA0 WO2 input source */ CCL_INSEL2_TCD0_gc = (0x09<<0), /* TCD0 WOA input source */ CCL_INSEL2_SPI0_gc = (0x0B<<0), /* SPI0 MISO source */ } CCL_INSEL2_t; Пишем: Код (C++): CCL.LUT0CTRLB = CCL_INSEL0_SPI0_gc // Выбор канала SPI SCK | CCL_INSEL1_SPI0_gc; // Выбор канала SPI MOSI CCL.LUT0CTRLC = CCL_INSEL2_TCA0_gc; // Выбор канала W02 таймера TCA Ну и общие настройки, про которые уже писал: Код (C++): CCL.LUT0CTRLA = 0 << CCL_CLKSRC_bp // Блок CCL не тактируем | 1 << CCL_ENABLE_bp // Разрешение работы LUT0 | 1 << CCL_OUTEN_bp; // Разрешение выхода LUT0 CCL.CTRLA = 1 << CCL_ENABLE_bp // Разрешение работы CCL | 0 << CCL_RUNSTDBY_bp; // В стендбае не работаем Теперь необходимо запрограммировать таблицу истинности. Для этого смотрим на диаграмму выше и отмечаем входные состояния, при которых на выходе "1". В остальных случаях "0". У меня получилось следующее: Код (C++): CCL.TRUTH0 = 0xA8; // Таблица истинности Теперь запишем функцию трансляции байта на выход MOSI модуля SPI. Также не забываем, что нам необходимо останавливать таймер TCA0 по окончании пересылки байта и запускать его (со сбросом) в начале посылки, иначе он нам тут натикает совсем не то))) Код (C++): void SPI_out8bit(uint8_t data) { TCA0.SINGLE.CNT = 0; TCA0_Mode(start); SPI_WriteByte(data); TCA0_Mode(stop); } Всё, что нам осталось - это заслать байты в порядке очереди: Код (C++): SPI_out8bit(green[0]); SPI_out8bit(red[0]); SPI_out8bit(blue[0]); где: Код (C++): uint8_t blue[1]; uint8_t red[1]; uint8_t green[1]; ENJOY!
Как видите - никакого ассемблера, куча времени для расчета эффектов. Если подключить буферы в SPI (а мы это обязательно сделаем), ну или высший пилотаж - работать на прерываниях, то формировать узоры можно параллельно с пересылкой байтов в линию Также ещё можно поработать и с внешними интерфейсами. Но это уже совсем другая история.