Очень немалый интерес у посетителей ресурса вызвал урок 142 по подключению светодиодных лент количеством 150 и 300 светодиодов к контроллеру STM32. Поэтому хотелось бы немного продолжить и развить данную тему.
Также на данный урок меня подтолкнула статья посетителя ресурса по применению цветовой модели HSV, за что ему огромное спасибо, думаю, не только от меня но и от многих посетителей данного ресурса.
Мне также понравилась данная модель, так как для наших конкретных целей она подходит больше, чем модель RGB, которую мы также в своём проекте без внимания не оставим.
Также мне в его проекте понравилось не только это, а также настройка DMA на 8-битный режим, что позволило в значительной мере сэкономить пространство в оперативной памяти.
Также мы постараемся сделать полную отвязку от количества светодиодов в ленте, это будет задаваться в макросе и после этого нам не потребуется перелопачивать весь код для того, чтобы переходить к другому количеству светодиодов.
Немного расскажу о цветовом пространстве HSV.
В отличии от RGB, в котором мы имеем три настройки, каждая из которых управляет интенсивностью свечения трёх цветов — красного, зелёного и синего, цветовая модель HSV имеет также три настройки, только немного других.
Первая — это Hue — цветовой тон.
Вторая настройка — это Saturation или насыщенность.
Третья — Value или яркость.
Модель HSV делится также на несколько модификаций, но нас это сегодня не интересует. Нас интересует возможность удобного использования данной модели.
Цветовой тон измеряется в градусах и тем самым обеспечивается плавный переход между основными цветами и их смешивание.
0o — красный, 120o — зеленый, 240o — синий.
Посмотрим визуализацию цветовой модели HSV в виде цилиндра
Также посмотрим горизонтальный срез данного цилиндра и увидим, как распределились цвета в градусах
Мы будем тон назначать также в градусах, от 0 до 359, а другие два параметра, не как принято от 0 до 1, а как нам привычнее — от 0 до 255, чтобы уместиться в байт.
Ну что ж, после небольшого вступления давайте приступим к проекту.
Проект сделаем из проекта урока 142 с именем WS2812_300 и назовём его, например WS2812_HSV.
А тренироваться будем на ленточке маленькой с количеством светодиодов в ней 144, не снимать же ленту с фронтона. Потом отнесём прошитый контроллер на чердак и подключим его к основным («парадным») лентам.
Откроем наш проект в проектогенераторе Cube MX и в настройках DMA таймера 2 колонке с памятью изменим тип данных на Byte
Сгенерируем проект, откроем его в Keil, подключим файл ws2812.c, настроим программатор на автоперезагрузку и попробуем собрать проект.
Если всё нормально, то идём в файл ws2812.h и первым делом там пока настроим проект на количество светодиодов 144
#define LED_COUNT 144
Добавим ещё один макрос, который будет хранить яркость свечения светодиодов по умолчанию, пока установим ему небольшое значение, когда будем прошивать большие уличные ленты, то прибавим
1 2 |
#define DELAY_LEN 48 #define BRIGHT 64 |
Добавим также две структуры для хранения параметров различных цветовых моделей
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#define BitIsSet(reg, bit) ((reg & (1<<bit)) != 0) //-------------------------------------------------- typedef struct { uint16_t H; uint8_t S; uint8_t V; } HSV_t; //-------------------------------------------------- typedef struct { uint8_t R; uint8_t G; uint8_t B; } RGB_t; //-------------------------------------------------- |
В файле main.c из функции main() удалим вот этот код
ws2812_prepareValue(255, 0, 0,
0, 255, 0,
0, 0, 255,
128, 128, 0,
0, 128, 128,
128, 0, 128,
192, 64, 0,
192, 0, 64,
64, 192, 0,
0, 192, 64,
0, 64, 192,
64, 0, 192,
192, 32, 32,
32, 32, 192,
32, 192, 32
);
ws2812_setValue();
ws2812_light();
Также удалим весь пользовательский код из бесконечного цикла.
Идём теперь в файл ws2812.c и изменим тип переменных в массиве основного буфера для DMA
uint8_t BUF_DMA [ARRAY_LEN] = {0};
добавим ещё один буфер для хранения массива полных настроек RGB, то есть полностью для всех светодиодов ленты
1 2 |
uint8_t rgb_temp[15][3]; RGB_t rgb_temp2[LED_COUNT]; |
Аналогичный буфер добавим только в виде массива переменных типа структуры для HSV, а также ещё одну глобальную переменную того же типа
1 2 3 |
RGB_t rgb_temp2[LED_COUNT]; HSV_t back_buf [LED_COUNT] = {0}; HSV_t back_buf_temp; |
После функции ws2812_pixel_rgb_to_buf_dma добавим функцию, которая будет забирать значения из массива HSV, конвертировать в модель RGB, более понятную ленте. Пока в нашей функции мы только объявим некоторые переменные и массив
1 2 3 4 5 6 7 8 |
//------------------------------------------------------------------ void ws2812_hsv_to_rgb(void) { uint8_t color[3]; volatile uint16_t j; uint32_t base_V; } //------------------------------------------------------------------ |
Начнём писать тело данной функции.
Создадим цикл на количество итераций, равное количеству наших светодиодов в ленте
1 2 3 4 |
uint32_t base_V; for(j=0;j<LED_COUNT;j++) { } |
Начнём писать тело данной функции.
В цикле начнём конвертацию.
Если параметр насыщенности будет 0, то все цвета пространства RGB примут значение яркости
1 2 3 4 5 6 7 8 |
for(j=0;j<LED_COUNT;j++) { if (back_buf[j].S == 0) { color[0] = back_buf[j].V; color[1] = back_buf[j].V; color[2] = back_buf[j].V; } |
В противном случае уже будет другой расчёт. Для начала посчитаем смещение
1 2 3 4 5 6 |
color[2] = back_buf[j].V; } else { base_V = ((255 - back_buf[j].S) * back_buf[j].V) >> 8; } |
Смещение потребуется для вычисления величины соседнего цвета. Смысл такой: Если цвет строго красный, полнонасыщенный, то будет R = яркость, G = 0 и B = 0. Но если цвет будет уходить в сторону зелёного, то R убавляться будет не сразу, а только ровно после достижению середины между этими цветами на цветовом круге, то есть 60 градусов. Если будет H = 60 градусов, то и R и G будут равны яркости. А уж затем по мере увеличения H к 120 градусам G останется на отметке 255, а R будет плавно убывать. Аналогично с остальными основными цветами. То есть base_V или смещение нужно будет для определения яркости соседнего цвета. Только равно оно ему не будет. base_V — это минимальное значение яркости. Если значение тона находится в секторе между красным и зелёным, то это значение основного цвета, не входящего в данный сектор, то есть синего. В случае максимального значения насыщенности данное значение будет равно, соответственно, 0. По мере уменьшения насыщенности у нас будут добавляться постепенно значения цветов всех цветов, отличных от основного, то есть тог, к которому ближе заданный тон, поэтому в формуле определения смещения мы учитываем также и величину насыщенности. Не совсем понятен, может быть, битовый сдвиг влево на 8 бит. Это сделано для того, чтобы более быстро разделить на 256.
Затем разобьём наш круг на секторы по 60 градусов с помощью оператора вариантов, то есть разделим значение цветового тона на 60, тем самым определим, в каком из этих шести секторов находится наш тон
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
base_V = ((255 - back_buf[j].S) * back_buf[j].V) >> 8; switch (back_buf[j].H / 60) { case 0: break; case 1: break; case 2: break; case 3: break; case 4: break; case 5: break; } |
Так как в результате деления мы получаем целое число без остатка, как раз на секторы всё и разделится. В первый кейс (вариант) попадут значения от 0 до 59, во второй — от 60 до 119, в третий — от 120 до 179 и т.д.
Осталось лишь посчитать теперь каждый из трёх цветов в каждом варианте. Начнём с первого
1 2 3 4 5 |
case 0: color[0] = back_buf[j].V; color[1] = (((back_buf[j].V - base_V) * back_buf[j].H) / 60) + base_V; color[2] = base_V; break; |
В данном секторе, как я и объяснял до этого, красный цвет будет равен значению основной яркости, зелёный рассчитывается по формуле, учитывающей смещение, а синий ему равен.
Далее следующий сектор
1 2 3 4 5 |
case 1: color[0] = (((back_buf[j].V - base_V) * (60 - (back_buf[j].H % 60))) / 60) + base_V; color[1] = back_buf[j].V; color[2] = base_V; break; |
Аналогично вычислим значение трёх составляющих остальных четырёх секторов
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
case 2: color[0] = base_V; color[1] = back_buf[j].V; color[2] = (((back_buf[j].V - base_V) * (back_buf[j].H % 60)) / 60) + base_V; break; case 3: color[0] = base_V; color[1] = (((back_buf[j].V - base_V) * (60 - (back_buf[j].H % 60))) / 60) + base_V; color[2] = back_buf[j].V; break; case 4: color[0] = (((back_buf[j].V - base_V) * (back_buf[j].H % 60)) / 60) + base_V; color[1] = base_V; color[2] = back_buf[j].V; break; case 5: color[0] = back_buf[j].V; color[1] = base_V; color[2] = (((back_buf[j].V - base_V) * (60 - (back_buf[j].H % 60))) / 60) + base_V; break; |
В принципе, у нас уже всё сконвертировалось, Выйдем из оператора вариантов, а также из тела условия и сохраним значения цветов в массив значений RGB
1 2 3 4 5 6 |
break; } } rgb_temp2[j].R = color[0]; rgb_temp2[j].G = color[1]; rgb_temp2[j].B = color[2]; |
Ниже добавим ещё одну функцию, в которой значения параметров всех светодиодов, находящиеся в массиве RGB, будут заполнять буфер DMA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
//------------------------------------------------------------------ void ws2812_rgb_copy_buf_to_dma(void) { volatile uint16_t j; uint8_t i; for(j=0;j<LED_COUNT;j++) { for(i=0;i<8;i++) { if (BitIsSet(rgb_temp2[j].R,(7-i)) == 1) { BUF_DMA[DELAY_LEN+j*24+i+8] = HIGH; }else { BUF_DMA[DELAY_LEN+j*24+i+8] = LOW; } if (BitIsSet(rgb_temp2[j].G,(7-i)) == 1) { BUF_DMA[DELAY_LEN+j*24+i+0] = HIGH; }else { BUF_DMA[DELAY_LEN+j*24+i+0] = LOW; } if (BitIsSet(rgb_temp2[j].B,(7-i)) == 1) { BUF_DMA[DELAY_LEN+j*24+i+16] = HIGH; }else { BUF_DMA[DELAY_LEN+j*24+i+16] = LOW; } } } } //------------------------------------------------------------------ |
Функцию ws2812_prepareValue удалим вместе с телом, а также удалим её прототип из заголовочного файла.
Функции тестов типа ws2812_test0x также все удалим вместе с телами и прототипами, иначе у нас код не будет собираться. Тесты мы быстро напишем.
Начнём с первого.
Так как цвета в нашем круге расположены в том же порядке, как и в радуге, то это слово в нашем уроке будет использоваться часто.
Первый тест будет выводить радугу в количестве стольких штук или повторений, сколько будет задано в первом параметре функции теста, а во втором параметре мы зададим количество полных прогонов этой радуге по кругу, то есть количество кругов, так как ленты свои мы располагаем замкнуто, чтобы присутствовал эффект непрерывности.
Первым делом в нашем тесте мы заполним буфер в формате HSV, так как при использовании данного цветового пространства нам стало делать это легко, мы справимся с этим быстро.
В самом низу файла начнём добавлять наши тесты
1 2 3 4 5 6 7 8 9 10 11 |
//------------------------------------------------------------------ void ws2812_test01(uint8_t n, uint8_t nm) { uint16_t i; uint16_t j; for(i=0;i<LED_COUNT;i++) { back_buf[i].H = i * 360 / LED_COUNT* n % 360; back_buf[i].S = 255, back_buf[i].V = BRIGHT; } } //------------------------------------------------------------------ |
Выйдем из цикла и добавим теперь цикл, в котором с некоторой задержкой наш буфер будет продвигаться по «кругу» и в каждой итерации мы будем копировать наш буфер в буфер DMA и затем зажигать нашу ленту нужными цветами
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
back_buf[i].H = i * 360 / LED_COUNT* n % 360; back_buf[i].S = 255; back_buf[i].V = BRIGHT; } for(j=0;j<LED_COUNT*nm;j++) { memcpy((void*)&back_buf_temp,(void*)&back_buf[0],4); for(i=0;i<(LED_COUNT-1);i++) { memcpy((void*)&back_buf[i],(void*)&back_buf[i+1],4); } memcpy((void*)&back_buf[LED_COUNT-1],(void*)&back_buf_temp,4); ws2812_hsv_to_rgb(); ws2812_rgb_copy_buf_to_dma(); ws2812_light(); HAL_Delay(20); } |
Создадим на данную функцию прототип в заголовочном файле и вызовем её в бесконечном цикле функции main() файла main.c
1 2 |
/* USER CODE BEGIN 3 */ ws2812_test01(1,2); |
Тем самым мы выводим две радуги и вращаем их по кругу дважды.
Соберём код, прошьём контроллер и посмотрим результат выполнения теста
Очень даже плавные переходы. Представьте себе, если бы нам пришлось всё это писать с использованием цветового пространства RGB.
В следующей части урока мы напишем несколько тестов и проверим их на практике.
Предыдущий урок Программирование МК STM32 Следующая часть
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Ленты светодиодные WS2812B разные можно приобрести здесь WS2812B
Импульсный источник питания 5 В в 40A 200 Вт можно приобрести здесь 5 В в 40A 200 Вт
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Сори что суда (не нашёл почту), А не планируется уроки по STM32+norFlash? вроде я понимаю что это FMC но так не понял как работает, зарание спасибо
По идее разницы никакой нет в специфике программирования NOR и NAND, у меня просто нет такой памяти
не могли бы вы объяснить небольшую проблему с дма.
суть в следующем, первые 2 старта DMA, откуда то появляется лишняя информация в виде появления на первом месте непонятных мне данных, поэтому при отправке буфера на, скажем, 7 байтов в таймер, надо писать 8, а после 2-х стартов все в норму приходит. (у вас вроде была такая проблема, приходилось вместо 1, два писать)
Простите, может и была, но уже не помню.
Здравствуйте.
На 407VE в CubeMX в настройка DMA нет возможности раздельно изменять Data Width. Только если вместе с Peripheral. Подскажите как быть в такой ситуации?
Здравствуйте!
Измените в коде.
Добрый вечер, установил длинну в кубе байт каки в уроке но когда меняю массив uint16_t BUF_DMA на uint8_t начинается хардкор))
весь код только с uint16_t BUF_DMA работает, единственное что не меняются все светодиоды — пришлось делать
HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_2, (uint32_t*) &BUF_DMA,ARRAY_LEN*2+DELAY_LEN);
все 10 раз проверил — хоть убей