Перейти к содержанию
  • записи
    3
  • комментариев
    9
  • просмотров
    2 158

WS2812 или поморгаем ещё разок


parovoZZ

3 348 просмотров

Всем привет!
В этом топике я покажу, как с помощью новейшей Attiny от Atmel/Microchip управлять светодиодами WS2812. Для начала давайте вспомним, а лучше подсмотрим в даташите на временные интервалы нуля и единицы.

5cd3326befe3b_2812(1).png.9a34f85450f563e559d75c5b3742375a.png

Где видим, что для передачи нуля нам необходим импульс длительностью от 200 и до 500 наносекунд, для передачи единицы - от 550 и до 850 наносекунд. Период же так и вообще может изменяться в широких пределах.
Управлять светодиодами мы будем с помощью SPI. Но не напрямую, а через CCL. Почему? Всё просто - SPI содержит 8-ми битный регистр и как его не крути, целочисленная передача не получится - исходный бит надо будет "разбавлять" нулями либо единицами. А что если поступить наоборот - длительность исходного бита будет задавать период сигнала, а разбивать бит будем тактовой частотой SCK того же интерфейса SPI. Так мы сформируем "1", которую поймет драйвер светодиода. Да, T1H и T1L по длительности будут равны, но в параметры даташита мы укладываемся. Как же быть с нулем? Для этого мы воспользуемся помощью зрительного зала таймера TCA0. Пусть он нам разобьет тактовую частоту SCK ещё раз напополам и мы получим длительность T0H в четыре раза меньшую, чем период. Но всё равно укладываемся в даташит)) В результате манипуляций мы получим такую картину:

2812.thumb.png.061b0e369ecb16e01472f2c54a57934f.png

Звиняюсь, но диаграммы отображают (не то, что там подписано....)

желтая диаграмма - это RGB код для светодиодов, зеленая диаграмма - тактовая частота SCK, синий - меандр с таймера TCA0, фиолетовый - подготовленные импульсы для подачи на WS2812.
Работать мы будем на тактовой частоте в 20 МГц, поэтому первое, что сделаем, отключим прескалер:

CCP = CCP_IOREG_gc;
CLKCTRL.MCLKCTRLB = 0;

Затем подготовим блок SPI:


    //... Инициализация 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 мкс - в даташит укладываемся.

Далее настроим таймер:

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, после чего самостоятельно обнуляется и цикл повторяется.
Значение прескалера таймера и его режим работы я определю в отдельной функции:

 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 - таймер выключен   
}

Уже в основной функции настрою регистры:

 TCA_Init();
    TCA0.SINGLE.PER = TCA_period;
    TCA0.SINGLE.CMP2 = TCA_cmp2;

Компаратор использую CMP2 - далее будет понятно почему. Выход компаратора устанавливается в "1" при сбросе счетчика, а сбрасывается при равенстве значений в счетном регистре и регистре компаратора. Для того, чтобы получить частоту 625 * 2 = 1250 кГц, я определю следующие значений регистров:

#define    TCA_period    15
#define    TCA_cmp2        8

Давайте подключать таблицу LUT CCL. Для этого открываем даташит и внимательно читаем, на какой вход что можно подключить. Напомню, что нам надо завести сигналы от:
-MOSI SPI
-SCK SPI
-WO2
Смотрим - на нулевой канал LUT мы можем подключить от SPI только сигнал SCK (скопирую из файла определений iotn817.h):

/* 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:

/* 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:

/* 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;

Пишем:

 CCL.LUT0CTRLB    = CCL_INSEL0_SPI0_gc    // Выбор канала SPI SCK
            | CCL_INSEL1_SPI0_gc;    // Выбор канала SPI MOSI

    CCL.LUT0CTRLC    = CCL_INSEL2_TCA0_gc;    // Выбор канала W02 таймера TCA

Ну и общие настройки, про которые уже писал:

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". У меня получилось следующее:

 CCL.TRUTH0 = 0xA8;                // Таблица истинности

Теперь запишем функцию трансляции байта на выход MOSI модуля SPI. Также не забываем, что нам необходимо останавливать таймер TCA0 по окончании пересылки байта и запускать его (со сбросом) в начале посылки, иначе он нам тут натикает совсем не то)))

void SPI_out8bit(uint8_t data)
{
     TCA0.SINGLE.CNT = 0;

     TCA0_Mode(start);
     SPI_WriteByte(data);
     TCA0_Mode(stop);
}

Всё, что нам осталось - это заслать байты в порядке очереди:

 SPI_out8bit(green[0]);
        SPI_out8bit(red[0]);
        SPI_out8bit(blue[0]);

Где:

 uint8_t blue[1];
    uint8_t red[1];
    uint8_t green[1];

Как видите - никакого ассемблера, куча времени для расчета эффектов. Если подключить буферы в SPI (а мы это обязательно сделаем), ну или высший пилотаж - работать на прерываниях, то формировать узоры можно параллельно с пересылкой байтов в линию Также ещё можно поработать и с внешними интерфейсами. Но это уже совсем другая история.

ENJOY!

8 Комментариев


Рекомендуемые комментарии

Имхо, ужас нерожденного!

Вот участок кода для управления светодиодами WS2812 по SPI в "обычных" контроллерах, в которых нет наворотов всяких...

/// Константа для формирования импульса "0" для WS2812
#define WS_BIT_0	0b11000000
/// Константа для формирования импульса "1" для WS2812
#define WS_BIT_1	0b11111100

/** Побитовая выдача 1 байта данных в WS2812
 *
 * @param byte выводимый байт
 */
__inline
static void send_byte(uint8_t byte){
	for(uint8_t mask=0x80; mask; mask >>=1){
		while(bit_is_clear(SPSR, SPIF));
		SPDR = byte & mask ? WS_BIT_1 : WS_BIT_0;
	}
}

/** Обновление содержимого цепочки светодиодов WS2812
 *
 */
static void ws2812_show(void){
	uint16_t b;
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE){
		for(uint8_t i=0; i<PIXEL_CNT; i++){
			// для чипа WS2812 порядок цветовых составляющих должен быть таким:
			send_byte(pixels[i].g);	// сначала ЗЕЛЕНЫЙ
			send_byte(pixels[i].r);	// затем КРАСНЫЙ
			send_byte(pixels[i].b);	// в конце - СИНИЙ
		}
	}
	while(bit_is_clear(SPSR, SPIF)); // ждем завершения передачи последнего бита
}

Инициализация модуля SPI в код не вошла, но там всего-навсего три записи в регистры... И не нужны таймеры, всякие странные режимы и т.п.

Имхо, и в новых МК наверняка можно делать так же просто. К чему усложнять?

Изменено пользователем ARV
Ссылка на комментарий

У меня за один рабочий цикл SPI через LUT выдаёт сразу байт цвета. Здесь же мы имеем всего один бит (нолик или единичка). Т.е мне на передачу цвета для одного светодиода необходимо обратиться к SPI всего три раза, здесь же - 24.

Ссылка на комментарий

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

Ссылка на комментарий

Не понял: откуда? Функции у вас вроде как блокирующие, а тот факт, что выключение и включение режима таймера делается программно, как я понимаю, требует запрещать прерывания на все время передачи... Откуда ж такты?

Ссылка на комментарий

В данном примере блокирующие, но ни что не мешает их такими не делать.) Про таймер не понял - с его прерываниями не работаем.

Ссылка на комментарий

Если прерывание произойдёт перед обращением к TCA_mode или после , то, как я понимаю, вся диаграмма сигналов может быть нарушена. Поэтому я и предполагаю, что прерывания надо запрещать...

Ссылка на комментарий

Присоединяйтесь к обсуждению

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

Гость
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Добавить комментарий...

×   Вставлено с форматированием.   Восстановить форматирование

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

Загрузка...
×
×
  • Создать...