Attiny817 & WS2812 = ?

Тема в разделе "Микроконтроллеры AVR", создана пользователем parovoZZ, 2 май 2019.

  1. parovoZZ

    parovoZZ Гуру

    Всем привет!
    В этом топике я покажу, как с помощью новейшей Attiny от Atmel/Microchip управлять светодиодами WS2812. Для начала давайте вспомним, а лучше подсмотрим в даташите на временные интервалы нуля и единицы.
    2812.png
    Где видим, что для передачи нуля нам необходим импульс длительностью от 200 и до 500 наносекунд, для передачи единицы - от 550 и до 850 наносекунд. Период же так и вообще может изменяться в широких пределах.
    Управлять светодиодами мы будем с помощью SPI. Но не напрямую, а через CCL. Почему? Всё просто - SPI содержит 8-ми битный регистр и как его не крути, целочисленная передача не получится - исходный бит надо будет "разбавлять" нулями либо единицами. А что если поступить наоборот - длительность исходного бита будет задавать период сигнала, а разбивать бит будем тактовой частотой SCK того же интерфейса SPI. Так мы сформируем "1", которую поймет драйвер светодиода. Да, T1H и T1L по длительности будут равны, но в параметры даташита мы укладываемся. Как же быть с нулем? Для этого мы воспользуемся помощью зрительного зала таймера TCA0. Пусть он нам разобьет тактовую частоту SCK ещё раз напополам и мы получим длительность T0H в четыре раза меньшую, чем период. Но всё равно укладываемся в даташит)) В результате манипуляций мы получим такую картину:
    2812.png
    желтая диаграмма - это 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
     
    KindMan, Daniil, ИгорьК и ещё 1-му нравится это.
  2. parovoZZ

    parovoZZ Гуру

    Давайте подключать таблицу 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!
     
  3. parovoZZ

    parovoZZ Гуру

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