STM Урок 119. WS2812B. Лента на умных светодиодах RGB. Часть 2

 

 

 

В предыдущей части нашего урока мы познакомились поближе с умными светодиодами RGB WS2712B, а также приготовили схему для практического закрепления материала.

 

Думаю, хватит нам на сегодняшний день теории, пора плавно переходить к проекту.

Проект мы создадим новый. Для этого запустим проектогенератов Cube MX и выберем наш контроллер

 

 

Подключим кварцевый резонатор и отладчик

 

 

Включим таймер в режиме PWM на 2 канал, так как первый у нас подключен к ножке PA0, которая всегда может пригодиться и лучше её не занимать

 

 

 

Как видим, у нас включилась ножка PA1

 

 

Вот её мы и подключим к сигнальному проводу ленты.

Переходим в Clock Configuration и настроим там всё следующим образом (нажмите на картинку для увеличения изображения)

 

 

Переходим в Configuration и настроим таймер

 

 

Почему именно 89? А это легко объяснимо. Во-первых к значению предделителя и периода мы всегда прибавляем 1. Поэтому предделитель у нас 1, то есть он не учитывается. А 89 + 1 = 90. Делим частоту тактирования шины таймера 72 на 90 и получаем ровно 0,8 МГц, что составляет 800 килогерц. Это то, что нам и надо!

Прерывания на таймер не включаем, они нам не потребуются. Переходим в закладку DMA и добавим канал DMA, настройки оставляем все по умолчанию (проверьте, что они именно такие). Переключаем только направление работы DMA — Memory To Peripheral

 

 

А прерывания на DMA включатся сами.

Вроде пока всё. Заполним настройки проекта

 

 

Сохраним настройки. сгенерируем проект для Keil, откроем его, настроим программатор на авторезет и настроим уровень оптимизации 1.

Первым делом давайте попробуем зажечь хотя бы один светодиод. Так как мы знаем, как работает PWM, нам это будет несложно сделать. Правда мы не знаем, как он работает с применением DMA, ну ничего, разберёмся.

Чтобы применить DMA в PWM, да и вообще в любых режимах таймера, мы должны будем при старте таймера передавать некие данные, причём с применением DMA мы можем передавать целые массивы.

В случае с PWM в массиве, указатель на который мы передадим функции старта, будут находиться данные со скважностью, а точнее с периодом, в течение которого ножка, управляемая таймером, будет находиться в высоком состоянии. В нашем случае у нас общий период 90 (в настройках 89). Если мы передадим таймеру число 44 (что составляет 45 — 1, а это ровно половина периода), то мы получим период нахождения ножки в высоком состоянии — 45, следовательно, остальную часть периода, что будет тоже 45, ножка будет в низком состоянии. Таким образом мы получим ровные импульсы со скважностью 50 процентов. Но нам нужны не такие импульсы. Поэтому рассчитаем сначала значение нашего периода для передачи бита 0 в нашу шину. Оно по технической документации составляет 0,35 микросекунды. Общий период бита у нас составляет 1,25 микросекунды. Поэтому мы получим пропорцию: при периоде 1,25 — 0,35 мкс, а при 90 — X. Отсюда X = 90 * 0.35 / 1.25, что составит примерно 25. Но мы всегда вычитаем единицу, поэтому в массив для передачи бита 0 мы будем в соответствующую ячейку заносить число 24.

Давайте сразу создадим библиотеку под наши умные светодиоды. Состоять она будет из пары файлов ws2812.c и ws2812.h следующего содержания

 

ws2812.c:

 

#include "ws2812.h"

//----------------------------------------------------------------------------

extern TIM_HandleTypeDef htim2;

//----------------------------------------------------------------------------

 

ws2812.h:

 

#ifndef WS2812_H_

#define WS2812_H_

//--------------------------------------------------

#include "stm32f1xx_hal.h"

#include <string.h>

#include <stdlib.h>

#include <stdint.h>

//--------------------------------------------------

//--------------------------------------------------

#endif /* WS2812_H_ */

 

Подключим к нашей информационной ножке логический анализатор, чтобы следить, что мы всё делаем правильно, ну или неправильно (нажмите на картинку для увеличения изображения)

 

 

Давайте сразу создадим буфер для хранения периодов PWM. Буфер потребуется немаленький. Сейчас посчитаем. Во-первых, лучше сразу переждать больше 50 микросекунд, чтобы лента поняла, что это уже новые данные. Это можно сделать с помощью серии нулей. Период таймера у нас 1,25 микросекунд, я думаю 48 таких периодов будет вполне предостаточно. Поэтому, прежде чем организовывать буфер, мы в файле ws2812.h это пропишем в качестве макроса

 

#include <stdint.h>

//--------------------------------------------------

#define DELAY_LEN 48

 

Также добавим макрос количества управляемых светодиодов. У нас их 144

 

#define DELAY_LEN 48

#define LED_COUNT 144

 

А теперь макрос длины нашего буфера, так как мы помним, что для каждого светодиода требуется по 24 бита, следовательно, в буфере должно быть 24 полуслова, так как одно полуслово — это настройки для передачи одного бита. А полуслово потому, что у нас 16-битный таймер

 

#define LED_COUNT 144

#define ARRAY_LEN DELAY_LEN + LED_COUNT*24

 

Перейдём в файл ws2812.c и добавим там глобальный массив для нашего буфера, который мы будем передавать в функцию запуска таймера

 

extern TIM_HandleTypeDef htim2;

//------------------------------------------------------------------

uint16_t BUF_DMA [ARRAY_LEN] = {0};

//------------------------------------------------------------------

 

Перейдём в файл main.c и подключим там нашу библиотеку

 

/* USER CODE BEGIN Includes */

#include "ws2812.h"

#include <string.h>

#include <stdlib.h>

#include <stdint.h>

/* USER CODE END Includes */

 

Также подключим здесь наш глобальный буфер

 

/* Private variables ---------------------------------------------------------*/

extern uint16_t BUF_DMA [ARRAY_LEN];

/* USER CODE END PV */

 

Буфер подготовлен.

Только весь он нам сейчас не потребуется. Мы пока хотим понять, как вообще работает PWM с применением DMA. Поэтому давайте пока передадим только один элемент буфера.

Для этого мы в самое начало буфера запишем какое-нибудь число. А давайте не какое-нибудь, а сразу число 26. Хотя мы для нуля рассчитали и 24, но правильнее работает именно 26.

В функции main() запишем это число в нулевой элемент буфера и вызовем функцию старта таймера, в которую в качестве входного параметра мы и добавим адрес нашего буфера, а также в качестве следующего параметра — количество элементов. Пока отправим один

 

/* USER CODE BEGIN 2 */

BUF_DMA[0] = 26;

HAL_TIM_PWM_Start_DMA(&htim2,TIM_CHANNEL_2,(uint32_t*)&BUF_DMA,1);

 

Соберём код, прошьём контроллер. На ленту питание пока не подаём во избежание случайного зажигания всех светодиодов полной яркостью. Посмотрим результат в программе логического анализа

 

 

Мы видим, что у нас всё идеально. Только также мы видим, что импульсы следуют бесконечно, нам такого не надо. Впрочем, оно так и должно быть, так как мы таймер запустили и его не останавливали.

Нам надо найти место, где его остановить. Таким местом будет обработчик прерывания от периферии DMA. Поэтому перейдём в файл stm32f1xx_it.c и подключим хендл нашего таймера

 

/* USER CODE BEGIN 0 */

extern TIM_HandleTypeDef htim2;

/* USER CODE END 0 */

 

А затем в соответствующем месте в обработчике прерывания от DMA остановим таймер

 

void DMA1_Channel7_IRQHandler(void)

{

  /* USER CODE BEGIN DMA1_Channel7_IRQn 0 */

  HAL_TIM_PWM_Stop_DMA(&htim2,TIM_CHANNEL_2);

 

Вернёмся в файл main.c и в функции main() изменим количество элементов на 2. С одним не сработает

 

HAL_TIM_PWM_Start_DMA(&htim2,TIM_CHANNEL_2,(uint32_t*)&BUF_DMA,2);

 

Прежде чем прошивать контроллер, настроим программу логического анализа. Установим триггер на восходящий фронт

 

 

Соберём код, прошьём контроллер, и увидим, что у нас теперь уходит на информационный контакт только один импульс

 

 

Удалим строку с инициализацией первого элемента буфера

 

BUF_DMA[0] = 26;

 

 

Теперь перейдём опять в заголовочный файл ws2812.h и напишем там макросы для настроек PWM для передачи нуля и единицы. Для единицы экспериментальным путём получено число 65

 

#define ARRAY_LEN DELAY_LEN + LED_COUNT*24

#define HIGH 65

#define LOW 26

 

Перейдём в файл ws2812.c и напишем функцию инициализации нашей ленты. Мы здесь просто забьём все байты, кроме отведённых под задержку числами для передачи нуля на наш сигнальный провод

 

//------------------------------------------------------------------

void ws2812_init(void)

{

  int i;

  for(i=DELAY_LEN;i<ARRAY_LEN;i++) BUF_DMA[i] = LOW;

}

//------------------------------------------------------------------

 

Напишем для данной функции прототип и вызовем её в файле main.c в функции main()

 

/* USER CODE BEGIN 2 */

ws2812_init();

 

В файле ws2812.h добавим макрос установки бита в каком-либо числе

 

#define LOW 26

//--------------------------------------------------

#define BitIsSet(reg, bit) ((reg & (1<<bit)) != 0)

//--------------------------------------------------

 

Вернёмся в файл ws2812.c и добавим функцию, которая будет заполнять буфер в позиции, соответствующей определённому светодиоду значениями его цветов. Соответственно, во входящих параметрах будут значения каждого цвета, а также номер светодиода в ленте

 

//------------------------------------------------------------------

void ws2812_pixel_rgb_to_buf_dma(uint8_t Rpixel , uint8_t Gpixel, uint8_t Bpixel, uint16_t posX)

{

  volatile uint16_t i;

  for(i=0;i<8;i++)

  {

    if (BitIsSet(Rpixel,(7-i)) == 1)

    {

      BUF_DMA[DELAY_LEN+posX*24+i+8] = HIGH;

    }else

    {

      BUF_DMA[DELAY_LEN+posX*24+i+8] = LOW;

    }

    if (BitIsSet(Gpixel,(7-i)) == 1)

    {

      BUF_DMA[DELAY_LEN+posX*24+i+0] = HIGH;

    }else

    {

      BUF_DMA[DELAY_LEN+posX*24+i+0] = LOW;

    }

    if (BitIsSet(Bpixel,(7-i)) == 1)

    {

      BUF_DMA[DELAY_LEN+posX*24+i+16] = HIGH;

    }else

    {

      BUF_DMA[DELAY_LEN+posX*24+i+16] = LOW;

    }

  }

}

//------------------------------------------------------------------

 

С данной функцией, думаю всё понятно. Мы распределяем значения элементов в зависимости от цветов и зная то, что сначала передаётся зелёный, потом красный, а потом голубой цвета.

Напишем для данной функции прототип и вызовем её в файле main.c в функции main(), тем самым попробовав окрасить в какой-нибудь цвет самый первый светодиод нашей ленты, например, в зелёный. В вызове функции старта таймера, мы можем уже передавать в качестве аргумента весь наш буфер, так как он забит уровнями нуля

 

ws2812_pixel_rgb_to_buf_dma(0, 128, 0, 0);

HAL_TIM_PWM_Start_DMA(&htim2,TIM_CHANNEL_2,(uint32_t*)&BUF_DMA,ARRAY_LEN);

 

Подключим питание ленты, соберём код, прошьём контроллер и посмотрим результат

 

 

Давайте зажжём данный светодиод другим цветом, например красным (опять используем только половину свечения — 128)

 

ws2812_pixel_rgb_to_buf_dma(128, 0, 0, 0);

 

Посмотрим результат

 

 

Теперь попробуем окрасить синим цветом, и причём не этот, а например четвёртый светодиод

 

ws2812_pixel_rgb_to_buf_dma(0, 0, 128, 3);

 

Посмотрим результат

 

 

Давайте зажжём теперь разными цветами первые 4 светодиода, не забывая о нашем золотом правиле — общая сумма единиц свечения всех цветов светодиода не должна превышать 128, что составит 1/6 от интенсивности свечения полным белым цветом

 

ws2812_pixel_rgb_to_buf_dma(128, 0, 0, 0);

ws2812_pixel_rgb_to_buf_dma(0, 128, 0, 1);

ws2812_pixel_rgb_to_buf_dma(0, 0, 128, 2);

ws2812_pixel_rgb_to_buf_dma(64, 64, 0, 3);

 

Посмотрим результат

 

 

Отлично! Значит мы движемся в правильном направлении.

Инициализировать каждый раз каждый светодиод не очень удобно. Поэтому давайте расширять функционал нашей библиотеки.

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

В файле ws2812.c добавим такой временный буфер, который будет  двумерным, первым его размером будет количество светодиодов, а вторым — количество цветов

 

uint16_t BUF_DMA [ARRAY_LEN] = {0};

uint8_t rgb_temp[12][3];

 

После функции ws2812_pixel_rgb_to_buf_dma напишем функцию подготовки временного буфера

 

//------------------------------------------------------------------

void ws2812_prepareValue (uint8_t r00, uint8_t g00, uint8_t b00,

    uint8_t r01, uint8_t g01, uint8_t b01,

    uint8_t r02, uint8_t g02, uint8_t b02,

    uint8_t r03, uint8_t g03, uint8_t b03,

    uint8_t r04, uint8_t g04, uint8_t b04,

    uint8_t r05, uint8_t g05, uint8_t b05,

    uint8_t r06, uint8_t g06, uint8_t b06,

    uint8_t r07, uint8_t g07, uint8_t b07,

    uint8_t r08, uint8_t g08, uint8_t b08,

    uint8_t r09, uint8_t g09, uint8_t b09,

    uint8_t r10, uint8_t g10, uint8_t b10,

    uint8_t r11, uint8_t g11, uint8_t b11)

  {

  rgb_temp[0][0]=r00; rgb_temp[0][1]=g00; rgb_temp[0][2]=b00;

  rgb_temp[1][0]=r01; rgb_temp[1][1]=g01; rgb_temp[1][2]=b01;

  rgb_temp[2][0]=r02; rgb_temp[2][1]=g02; rgb_temp[2][2]=b02;

  rgb_temp[3][0]=r03; rgb_temp[3][1]=g03; rgb_temp[3][2]=b03;

  rgb_temp[4][0]=r04; rgb_temp[4][1]=g04; rgb_temp[4][2]=b04;

  rgb_temp[5][0]=r05; rgb_temp[5][1]=g05; rgb_temp[5][2]=b05;

  rgb_temp[6][0]=r06; rgb_temp[6][1]=g06; rgb_temp[6][2]=b06;

  rgb_temp[7][0]=r07; rgb_temp[7][1]=g07; rgb_temp[7][2]=b07;

  rgb_temp[8][0]=r08; rgb_temp[8][1]=g08; rgb_temp[8][2]=b08;

  rgb_temp[9][0]=r09; rgb_temp[9][1]=g09; rgb_temp[9][2]=b09;

  rgb_temp[10][0]=r10;rgb_temp[10][1]=g10;rgb_temp[10][2]=b10;

  rgb_temp[11][0]=r11;rgb_temp[11][1]=g11;rgb_temp[11][2]=b11;

}

//------------------------------------------------------------------

 

Теперь следующая функция, которая из временного буфера переместит данные в наш постоянный, заодно и размножит участки

 

//------------------------------------------------------------------

void ws2812_setValue(void)

{

  uint8_t n=0;

  for(n=0;n<12;n++)

  {

    ws2812_pixel_rgb_to_buf_dma( rgb_temp[0][0], rgb_temp[0][1], rgb_temp[0][2], n*12);

    ws2812_pixel_rgb_to_buf_dma( rgb_temp[1][0], rgb_temp[1][1], rgb_temp[1][2], n*12+1);

    ws2812_pixel_rgb_to_buf_dma( rgb_temp[2][0], rgb_temp[2][1], rgb_temp[2][2], n*12+2);

    ws2812_pixel_rgb_to_buf_dma( rgb_temp[3][0], rgb_temp[3][1], rgb_temp[3][2], n*12+3);

    ws2812_pixel_rgb_to_buf_dma( rgb_temp[4][0], rgb_temp[4][1], rgb_temp[4][2], n*12+4);

    ws2812_pixel_rgb_to_buf_dma( rgb_temp[5][0], rgb_temp[5][1], rgb_temp[5][2], n*12+5);

    ws2812_pixel_rgb_to_buf_dma( rgb_temp[6][0], rgb_temp[6][1], rgb_temp[6][2], n*12+6);

    ws2812_pixel_rgb_to_buf_dma( rgb_temp[7][0], rgb_temp[7][1], rgb_temp[7][2], n*12+7);

    ws2812_pixel_rgb_to_buf_dma( rgb_temp[8][0], rgb_temp[8][1], rgb_temp[8][2], n*12+8);

    ws2812_pixel_rgb_to_buf_dma( rgb_temp[9][0], rgb_temp[9][1], rgb_temp[9][2], n*12+9);

    ws2812_pixel_rgb_to_buf_dma(rgb_temp[10][0],rgb_temp[10][1],rgb_temp[10][2],n*12+10);

    ws2812_pixel_rgb_to_buf_dma(rgb_temp[11][0],rgb_temp[11][1],rgb_temp[11][2],n*12+11);

  }

}

//------------------------------------------------------------------

 

А следующая функция вызовет функцию старта таймера. Смысл этой функции — простота, так как официальная слишком велика, а у нас всегда будут одни и те же аргументы

 

//------------------------------------------------------------------

void ws2812_light(void)

{

  HAL_TIM_PWM_Start_DMA(&htim2,TIM_CHANNEL_2,(uint32_t*)&BUF_DMA,ARRAY_LEN);

}

//------------------------------------------------------------------

 

Добавим на эти 3 функции прототипы и попробуем их применить в функции main() файла main.c.

Старый код, кроме инициализации, во второй пользовательской секции удалим и добавим новый, останется там теперь вот это

 

/* USER CODE BEGIN 2 */

ws2812_init();

ws2812_prepareValue(128,   0,   0,

                      0, 128,   0,

                      0,   0, 128,

                     6464,   0,

                      06464,

                     64,   064,

                     9632,   0,

                     96,   032,

                     3296,   0,

                      09632,

                      0,  3296,

                     32,   096);

ws2812_setValue();

ws2812_light();

/* USER CODE END 2 */

 

В принципе, логический анализатор мы можем отключить. У нас всё правильно работает.

Соберём код, прошьём контроллер и получим следующий результат (нажмите на картинку для увеличения изображения)

 

 

Всё отлично работает и все блоки правильно повторяются.

Только вот смотреть картинку — это всё же не то. Поэтому в конце урока есть ссылка на видеоурок на Youtube, там уже будет гораздо красивее.

Но, конечно, ещё красивее это будет выглядеть тогда, когда вы сами соберёте эту схему, прицепите ленточку и увидите всё это вживую.

 

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

Предыдущая часть Программирование МК STM32 Следующая часть

 

 

Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6

Программатор недорогой можно купить здесь ST-Link V2

Ленты светодиодные WS2812B разные можно приобрести здесь WS2812B

Логический анализатор 16 каналов можно приобрести здесь

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

STM Name

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*