Протокол WS2812B на STM32 без пустых циклов и прерываний. И как сделать правильную радугу



    На Хабре уже есть пара статей о работе с RGB светодиодами WS2112B, но почему-то они все используют довольно архаичный способ формирования битовой последовательности. Способ заключается в формировании точных интервалов времени с помощью пустых программных циклов. Возможно, это издержки использования Arduino, но мы, конечно, уже давно перешли на ARM Cortex-M4 в лице STM32 и можем себе позволить сделать красивее.

    Итак, напомню “протокол” WS2112B.



    Светодиодная полоса на WS2112B имеет всего один цифровой вход – DIN, подключенный к первому светодиоду на полосе. На него подается специальная импульсная последовательность, кодирующая биты, как изображено на рисунке. У каждого светодиода есть один цифровой выход – DOUT соединенный с входом DIN следующего светодиода на полосе. Каждому светодиоду нужно передать 24 бита (по 8 бит на каждый цвет: красный R, зеленый G и синий B). Таким образом, чтобы зажечь все светодиоды надо передавать 24*N бит, где N количество светодиодов на полосе.

    Приняв биты светодиоды загораются и статично горят пока не получат новую битовую последовательность. Каждая битовая последовательность начинается с установки DIN в лог. ноль на время не менее 50 мкс.

    Как видно биты кодируются достаточно короткими импульсами с жесткими допусками. От микроконтроллера, пытающегося сформировать их программными задержками требуется как минимум запретить все прерывания чтобы невзначай не сформировался сброс или сбойный бит. Ресурсы процессорного времени здесь также расходуются нерационально, чтобы зажечь 100 светодиодов процессору нужно отработать 3 мс. Если обновлять состояние светодиодов с частотой 100 Гц, то такой” протокол” заберет 30% процессорного времени.

    Слышатся предложения использовать интерфейс SPI для передачи битового потока на WS2112B. Но здесь препятствием может стать недостаточное соответствие тактовой частоты системной шины и сильные погрешности длительностей импульсов.

    А между тем в STM32 и вообще во всех чипах на Cortex-M есть отличный механизм прямого доступа к памяти (DMA). Биты можно формировать с помощью таймеров в режиме широтно-импульсной модуляции, а каждый следующий бит извлекать из ОЗУ с помощью DMA.

    На рисунке ниже показана схема взаимодействия DMA и таймера TIM4 в чипе STM32F407VET6. Отладка проводилась на моем промышленном контроллере именно с таким чипом, но с тем же успехом все можно повторить на любом чипе семейства STM32. В данном случае у меня именно вывод 8 GPIOB был свободен, чем я и воспользовался.



    Далее текст инициализации таймера и контроллера:
    #define     BIT(n) (1u << n)
    #define     LSHIFT(v,n) (((unsigned int)(v) << n))
    
    #define LEDS_NUM    80
    #define COLRS       3
    
    INT16U DMA_buf[LEDS_NUM+2][COLRS][8];
    
    /*------------------------------------------------------------------------------
      Timer4 генерирует импульсы на светодиодную полосу
      Тактирование таймера идет от PCLK1 72 MHz
    
      Канал 3 таймера используется в режиме Compare с загрузкой по DMA регистра CCR3 для формирования битовых сигналов
     ------------------------------------------------------------------------------*/
    void Timer4_init(void)
    {
      TIM_TypeDef *tim = TIM4;
      RCC_TypeDef *rcc = RCC;
    
      rcc->APB1RSTR |= BIT(2);    // Сброс таймера 4
      rcc->APB1RSTR &= ~BIT(2);   
      rcc->APB1ENR |= BIT(2);     // Разрешаем тактирование таймера 4
      tim->CR1 = BIT(7);          //  1: TIMx_ARR register is buffered.
      tim->CR2 = 0;               
      tim->PSC = 0;               // Предделитель генерирует частоту 72 МГц
      tim->ARR = 90 - 1;          // Перегрузка таймера каждые 1.25 мкс
      tim->CCMR2 = 0
                   + LSHIFT(6, 4) // OC3M: Output compare 3 mode | 110: PWM mode 1 - In upcounting, channel 1 is active as long as TIMx_CNT<TIMx_CCR1 else inactive.
                   + LSHIFT(1, 3) // OC3PE: Output compare 3 preload enable
                   + LSHIFT(0, 0) // CC3S: Capture/Compare 3 selection | 00: CC3 channel is configured as output
      ; 
      tim->CNT = 0;
      tim->CCR3 = 0;
      tim->DIER = BIT(11);        // Bit 11 CC3DE: Capture/Compare 3 DMA request enable. Разрешаем запросы DMA
      tim->CR1 |= BIT(0);         // Запускаем таймер
      tim->CCER = BIT(8);         // Разрешаем работы выхода, чтобы возникали сигналы для DMA
    }
    
    /*------------------------------------------------------------------------------
      Инициализация канала 2 DMA1 Stream 7
      Используется для пересылки шаблоной битов потока управления светодиодной лентой на WS2812B в таймер TMR4 работающий в режиме генерации PWM 
     ------------------------------------------------------------------------------*/
    void DMA1_Stream7_Mem_to_TMR4_init(void)
    {
      DMA_Stream_TypeDef *dma_ch = DMA1_Stream7;
      RCC_TypeDef *rcc = RCC;
    
      rcc->AHB1ENR |= BIT(21);               // Разрешаем DMA1
    
      dma_ch->CR = 0;    // Выключаем стрим
      dma_ch->PAR = (unsigned int)&(TIM4->CCR3) + 1;  // Назначаем адрес регистра данных ADC
      dma_ch->M0AR = (unsigned long)&DMA_buf;
      dma_ch->NDTR = (LEDS_NUM + 2) * COLRS * 8;
      dma_ch->CR =
                   LSHIFT(2, 25) + // CHSEL[2:0]: Channel selection |    010: channel 2 selected
                   LSHIFT(0, 23) + // MBURST: Memory burst transfer configuration | 00: single transfer
                   LSHIFT(0, 21) + // PBURST[1:0]: Peripheral burst transfer configuration | 00: single transfer
                   LSHIFT(0, 19) + // CT: Current target (only in double buffer mode) | 0: The current target memory is Memory 0 (addressed by the DMA_SxM0AR pointer)
                   LSHIFT(0, 18) + // DBM: Double buffer mode | 0: No buffer switching at the end of transfer
                   LSHIFT(3, 16) + // PL[1:0]: Priority level | 11: Very high.  PL[1:0]: Priority level
                   LSHIFT(0, 15) + // PINCOS: Peripheral increment offset size | 0: The offset size for the peripheral address calculation is linked to the PSIZE
                   LSHIFT(1, 13) + // MSIZE[1:0]: Memory data size | 00: 8-bit. Memory data size
                   LSHIFT(1, 11) + // PSIZE[1:0]: Peripheral data size | 00: 8-bit. Peripheral data size
                   LSHIFT(1, 10) + // MINC: Memory increment mode | 1: Memory address pointer is incremented after each data transfer (increment is done according to MSIZE)
                   LSHIFT(0, 9) +  // PINC: Peripheral increment mode | 0: Peripheral address pointer is fixed
                   LSHIFT(1, 8) +  // CIRC: Circular mode | 1: Circular mode enabled
                   LSHIFT(1, 6) +  // DIR[1:0]: Data transfer direction | 01: Memory-to-peripheral
                   LSHIFT(0, 5) +  // PFCTRL: Peripheral flow controller | 1: The peripheral is the flow controller
                   LSHIFT(1, 4) +  // TCIE: Transfer complete interrupt enable | 1: TC interrupt enabled
                   LSHIFT(0, 3) +  // HTIE: Half transfer interrupt enable | 0: HT interrupt disabled
                   LSHIFT(0, 2) +  // TEIE: Transfer error interrupt enable | 0 : TE interrupt disabled
                   LSHIFT(0, 1) +  // DMEIE: Direct mode error interrupt enable | 0: Direct mode error interrupt disabled
                   LSHIFT(0, 0) +  // EN: Stream enable | 1: Stream enabled
                   0;
    
      dma_ch->FCR =
                    LSHIFT(0, 7) + // FEIE: FIFO error interrupt enable
                    LSHIFT(1, 2) + // DMDIS: Direct mode disable | 1: Direct mode disabled. Разрешаем чтобы была возможность пересылки из байт в двухбайтовый регистр 
                    LSHIFT(1, 0) + // FTH[1:0]: FIFO threshold selection | 01: 1/2 full FIFO
                    0;
      dma_ch->CR |= BIT(0); //  1: Stream enabled
    }
    
    




    После этой инициализации начинается автоматическая пересылка битового потока из массива DMA_buf расположенного в ОЗУ на внешний вывод 8 GPIOB. Автоматически генерируется и 50-и микросекундная пауза сброса. Процессор в пересылке никак не участвует, не используются даже прерывания. Чтобы зажечь какой-либо светодиод, надо просто записать соответствующее слово в массив DMA_buf по соответствующему смещению. Это делается в проекте функцией LEDstrip_set_led_state.

    Нельзя сказать, что данный механизм вообще не влияет на процессор. Его работа несколько замедляется. Поскольку он разделяет вместе с DMA общий доступ к ОЗУ и системной шине. Но измерения показали, что это замедление в данном случае не превышает 0.2%

    Для написания проекта использовалась среда разработки MDK-ARM Professional Version: 4.72.1.0. Частота процессора – 144 МГц, частота PCLK1 – 72 МГц. Легко переносится на платы серии STM32 MCU Discovery Kits. Весь проект выложен здесь

    В проекте не были использованы библиотеки от ST или какие-либо другие сторонние библиотеки. Проект очень компактный, всё написано через прямые обращения к регистрам, это делает текст короче, яснее и позволяет его легче переносить в другие среды разработки.

    И о радуге


    Дело в том, что просто линейно инкрементируя байты в слове в RGB формате цвета (битовое представление — 00000000 RRRRRRRR GGGGGGGG BBBBBBBB) нельзя на светодиодной полосе с сотней светодиодов изобразить красивую радугу. Еще трудней у этой радуги регулировать яркость, делая простые манипуляции на 32-битном слове с RGB информацией. Для таких манипуляций используют HSV формат. Например, вся радуга будет представлена если просто линейно инкрементировать H-составляющую. Потом уже преобразовать HSV в RGB и вывести на светодиоды.
    В проекте есть два конвертера HSV в RGB, один целочисленный, а другой с использованием вычислений с плавающей точкой. Визуально я отличий не увидел. Да, к сожалению, здесь STM32 отличиться негде.
    Share post

    Similar posts

    Comments 13

      +2
      Интересный способ, но вот если использовать порядка 500 светодиодов, получается что нам требуется 24 килобайта ОЗУ. Но способ конечно интересный. Однозначно плюс.
        0
        Для STM32 такой объем памяти — нормальный. Например, в семействе STM32F4 самый мелкий размер RAM — 96KB, а самый большой — 384. И это еще без внешней памяти…
        Жалко, конечно, выкидывать столько памяти на хранение цвета, но иначе получится только еще более громоздко и не факт, что будет выигрыш в чем-либо.
          0
          я бы сделал через таймер. RAM можно занять и другим.
          0
          1) Ниже пишут про двукратную экономию памяти с SPI.
          2) Использовать два банка DMA небольшого размера, пока один выходит наружу — второй заполняем.
            0
            Вариант 2 по мне предпочтительнее.
            0
            C случае:
            (битовое представление — 00000000 RRRRRRRR GGGGGGGG BBBBBBBB)...

            получаем 16к на буфер.
            На деле, WS2812B имеет такой порядок бит: GGGGGGGG RRRRRRRR BBBBBBBB,
            где будет занято 12к на буфер.

            Другое дело — совсем не вижу смысла использовать буфер для таких примитивных целей, даже для СМУ, здесь можно реализовать любые последовательности используя 30-60 байт.

            Вот если буфер заполнять с компьютера (от простейшего управления последовательностью, до полноценного потока), тогда в нем действительно есть необходимость.

            Впрочем, идея хороша уже тем что это STM32, а по поводу временных задержек в реализациях которые уже были — все проще: у Attiny нет DMA.
              0
              С этими двойными буферами есть тонкий момент. При передаче данных на WS2812B нельзя делать вообще никаких пауз. Если для решаемой задачи памяти в МК дофига и на фреймбуфер хватает, то почему бы не сделать железобетонный вариант с целиковым буфером и не забивать голову синхронизацией? Это экономит время разработчика.

              А если делать через двойной буфер, то надо DMA настраивать на циклический режим и задействовать half transfer interrupt.
            +3
            А я на SPI делал. Чтобы светодиод прочитал ноль передаем биты 1000, чтобы единицу 1110. Итого по два бита WS2812B на один байт SPI. Но есть ряд нюансов:
            1) Данные в SPI надо обязательно засылать с помощью DMA. Иначе между байтами буду достаточно длинные перерывы, чтобы светодиод прочитал reset.
            2) Конечно надо выставить правильную частоту у SPI.
            3) Провод до первого диода должен быть коротким. У меня начиная с 15-20 сантиметров в линии возникали слишком большие помехи и связь нарушалась.
            4) Поговаривают, что попадаются WS2812B которым надо именно 5ти вольтовый сигнал и 3.3 вольта от stm32 они могут не воспринимать. Но те две ленты, что у меня есть работают.
              0
              Присоединяюсь…

              Слышатся предложения использовать интерфейс SPI для передачи битового потока на WS2112B. Но здесь препятствием может стать недостаточное соответствие тактовой частоты системной шины и сильные погрешности длительностей импульсов.


              На STM32F103C8T6 как раз столкнулся с проблемой несовпадения частот. Обошел небольшой подстройкой частоты МК с помощью STM32CubeMX и увеличением количества бит SPI на один бит протокола WS2811 (да, я кодировал для другого контроллера RGB LED). При этом я устанавливал в МК FreeRTOS, и все работает без особых проблем.
                0
                Применял конвертер уровня. Достаточно примитивный чип типа компаратора. Из 3.3 в 5 переводит на ура.
                0
                Собираюсь скоро одну штуку на этих диодах сделать и возник такой вопрос — насколько сильно они греются? Насколько плотно их можно расположить, сколько нужно меди каждому для охлаждения?
                  0
                  Спасибо за интересную статью!
                  Действительно, содержать аппаратно-выгребаемый framebuffer — это правильный и во многих случаях удобный способ.
                  Было бы замечательно увидеть в статье видео с демонстрацией плавных цветовых переходов!
                    0
                    Может кому будет интересно: stm32f10x-ws2812b-lib — готовая библиотека для STM32F10x, работает на том же принципе, но только с использованием half-transfer interrupt.

                    Only users with full accounts can post comments. Log in, please.