一文看懂WS2812的呼吸灯实现
一文看懂WS2812呼吸灯实现
1. 相关资料
WS2812是一个集控制电路与发光电路于一体的智能外控LED光源,外形一般为5050封装,每个LED灯珠为一个像素点,支持RGB无极调色,同时每颗灯珠内部集成有智能数字接口数据锁存信号整形放大驱动电路,还包含有高精度的内部振荡器和可编程定电流控制部分,有效保证了像素点光的颜色高度一致。
数据协议采用单线归零码的通讯方式,像素点在上电复位以后,DIN端接受从控制器传输过来的数据,**首先送过来的24bit数据被第一个像素点提取后,送到像素点内部的数据锁存器,剩余的数据经过内部整形处理电路整形放大后通过DO端口开始转发输出给下一个级联的像素点,每经过一个像素点的传输,信号减少24bit。**像素点采用自动整形转发技术,使得该像素点的级联个数不受信号传送的限制,仅受限信号传输速度要求。
有关此款灯珠的其他详细资料,大家可以看下各自手上的灯珠的对应的数据手册,里面会有详细描述。
需要注意一点的是,为什么在这里强调大家一定要看自己手上的那款灯珠的数据手册呢?这是因为ws2812这款灯珠型号特别多,如果你随便去网上找的话会发现一个有意思的现象,手册基本上差不多,通讯时序也差不多,但是细看你会发现,不同手册灯珠通讯时序的具体时间要求会大不相同!!!所以大家最好是拿自己买的那颗灯珠的手册仔细阅读下
以下是我找到的一款ws2812灯珠的数据手册内的通讯时序要求,见下图:
WS2812是一颗数字LED灯珠,采用单总线通讯,每颗灯珠支持24bit的颜色控制,也即RGB888,信号线通过DIN输入,经过一颗灯珠之后,信号线上前24bit数据会被该灯珠锁存,之后将剩下的数据信号整形之后通过DOUT输出
2. 灯光控制
2.1 点亮一颗WS2812
根据上述数据手册我们可以知道,使用mcu或其他控制器,生成一段特殊的方波即可完成ws2812的驱动,下面先介绍一种最简单的驱动方法:配置IO为推挽输出,软件模拟控制IO产生控制时序
//注意:以下程序需要根据实际情况修改完善
void ws2812_sendbit_1(void)
{
拉高io;
延时800ns;
拉低io;
延时300ns;
拉高io;
}
void ws2812_sendbit_0(void)
{
拉高io;
延时300ns;
拉低io;
延时800ns;
拉高io;
}
void ws2812_reset(void)
{
拉低io;
延时1ms;
拉高io;
}
void ws2812_set_rgb_for_onelamp(uint8_t r, uint8_t g, uint8_t b)
{
ws2812_reset();
for(uint8_t i = 0; i < 8; i++) {
if(r >> (8-i))
ws2812_sendbit_1();
else
ws2812_sendbit_0();
}
}
对于上述程序产生的时序是否符合ws2812驱动要求,可以通过使用示波器对波形进行测量之后对程序进行调整,以实现一颗灯珠的点亮,当需要点亮多颗灯珠时,重复调用上述ws2812_set_rgb_for_onelamp()函数即可。
需要注意的是,当点亮多颗灯珠的时候往往容易出现第一颗灯珠或最后一颗灯珠颜色显示不正常,此时需要减少点亮的灯珠数,如点亮三颗灯珠,使用示波器抓取所有波形进行分析,此类故障往往是一些bug导致;此外如果最后一颗灯珠显示不正常,可以在发送完所有灯珠的rgb值之后再次调用一次ws2812_reset();看下问题是否解决
以上方案是一种最简单,最容易实现的控制方式,但是此实现方式过于简单,点亮灯珠期间占用cpu资源,程序执行效率极低,特别是当灯珠数量多的时候,执行时间会更长,严重浪费cpu,不建议使用!
在实际项目开发过程中,为了提升效率,往往采用PWM+DMA控制方式或SPI单总线控制方式,接下来本博文将介绍如何使用PWM+DMA方式驱动WS2812
2.2 点灯大师的高级玩法
接下来将向大家介绍如何使用PWM+DMA的方式实现WS12812的驱动。
根据ws2812 datasheet上描述可知,ws2812通讯所采用的0码和1码是周期一致,占空比不相同的PWM波
那么实现对输出PWM的每个波形的占空比控制,并实现对输出pwm个数的精准控制,即可实现ws2812多灯珠的驱动!
如果需要实现pwm的每个波形的占空比控制,那么肯定需要在每个pwm输出完成之后触发一个事件,通知到我们的程序切换下一个pwm输出的占空比,联想到能实现此功能也只有更新中断/事件和DMA了
如果采用更新中断,则需要在中断处理程序内去修改下一个pwm的占空比,也即改变比较寄存器的值,我们查看时序可以发现,无论是发送0码还是1码,周期都是非常短,在1us左右,
如果采用在中断处理,往往难以做到如此快速切换,同时周期1us的中断,也是十分消耗cpu资源的,而DMA则不同,dma相当于第二颗CPU,每当一个PWM输出完成之后即可触发搬运事件,不消耗cpu资源,同时速度极快
综上分析,因此我们可以得出以下方案来完成WS2812的驱动:
- 采用定时器+DMA的方式
- 定时器负责完成PWM的输出,通过改变每一个PWM的占空比模拟
1
和0
的发送时序 - 采用定时器的更新事件作为DMA的搬运触发事件,这样每发送完一个PWM波之后,即触发一次DMA搬运,将下个需要发送的1或0的数据进行更新
- 设计一个缓冲数组,这个里面存放RGB解码之后对应的定时器的CCR的值,也即占空比
- 定时器每发送完一个波,即一个更新事件触发之后,触发DMA搬运
- DMA负责把这个缓冲区的数据往定时器的比较寄存器一个个搬就可以了
采用PWM+DMA的方式驱动WS2812理论上来说是一种可行的方案。
以下为实际驱动代码(基于gd32f303 主频120Mhz):
tim.c
文件内容如下,主要完成timer
及与timer
有关的 dma
有关的初始化配置:
#include "./TIM/tim.h"
#include "./WS2812/ws2812.h"
#include "./TIM/tim_core.h"
static void dma_tim_rcu_config(void);
static void dma_tim_config(void);
static void dma_nvic_config(void);
/*!
brief configure the GPIO ports
param[in] none
param[out] none
retval none
*/
void timer_pwm_gpio_config(void)
{
rcu_periph_clock_enable(PWM_1_GPIO_CLK);
#ifdef USE_TIM_REMAP
rcu_periph_clock_enable(RCU_AF);
gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE);
gpio_pin_remap_config(TIM_REMAP_CLK, ENABLE);
#endif
gpio_init(PWM_1_GPIO_PORT,
GPIO_MODE_AF_PP,
GPIO_OSPEED_50MHZ,
PWM_1_GPIO_PIN);
}
/*!
brief configure the TIMER peripheral
param[in] none
param[out] none
retval none
*/
void timer_pwm_config(void)
{
timer_oc_parameter_struct timer_ocintpara;
timer_parameter_struct timer_initpara;
rcu_periph_clock_enable(PWM_CLK);
timer_deinit(TIM_PWM);
timer_initpara.prescaler = TIM_PWM_PSC;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = TIM_PWM_CCR;
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(TIM_PWM, &timer_initpara);
timer_ocintpara.outputstate = TIMER_CCX_ENABLE;
timer_ocintpara.outputnstate = TIMER_CCXN_DISABLE;
timer_ocintpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
timer_ocintpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
timer_ocintpara.ocidlestate = TIMER_OC_IDLE_STATE_HIGH;
timer_ocintpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
timer_channel_output_config(TIM_PWM, TIMER_CH_1, &timer_ocintpara);
timer_channel_output_pulse_value_config(TIM_PWM, TIMER_CH_1, 0);
timer_channel_output_mode_config(TIM_PWM, TIMER_CH_1, TIMER_OC_MODE_PWM0);
timer_channel_output_shadow_config(TIM_PWM, TIMER_CH_1, TIMER_OC_SHADOW_DISABLE);
/* 设置timer的更新事件作为dma的触发信号,很关键!!! */
timer_dma_enable(TIMER3,TIMER_DMA_UPD);
/* auto-reload preload enable */
timer_auto_reload_shadow_enable(TIM_PWM);
/* auto-reload preload enable */
timer_enable(TIM_PWM);
}
/**
* @brief dma inital, config dma of adc.
*/
void dma_tim_init(void)
{
/* DMA of ADC clocks configuration */
dma_tim_rcu_config();
/* DMA of ADC configuration */
dma_tim_config();
dma_nvic_config();
}
static void dma_tim_rcu_config(void)
{
/* enable DMA0 clock */
rcu_periph_clock_enable(RCU_DMA0);
}
static void dma_nvic_config(void)
{
nvic_irq_enable(DMA0_Channel6_IRQn, 0, 0);
}
/*!
brief configure the DMA peripheral
param[in] none
param[out] none
retval none
*/
static void dma_tim_config(void)
{
/* ADC_DMA_channel configuration */
dma_parameter_struct dma_data_parameter;
/* ADC DMA_channel configuration */
dma_deinit(DMA0, DMA_CH6);
/* initialize DMA single data mode */
dma_data_parameter.periph_addr = (uint32_t)(&TIMER_CH1CV(TIMER3));
dma_data_parameter.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_data_parameter.memory_addr = (uint32_t)(&ws2812_rgb_buf);
dma_data_parameter.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_data_parameter.periph_width = DMA_PERIPHERAL_WIDTH_16BIT;
dma_data_parameter.memory_width = DMA_MEMORY_WIDTH_16BIT;
dma_data_parameter.direction = DMA_MEMORY_TO_PERIPHERAL;
dma_data_parameter.number = (WS2812_NUM+20) * 24;
dma_data_parameter.priority = DMA_PRIORITY_ULTRA_HIGH;
dma_init(DMA0, DMA_CH6, &dma_data_parameter);
dma_circulation_disable(DMA0, DMA_CH6);
dma_interrupt_enable(DMA0, DMA_CH6, DMA_INT_HTF);
/* enable DMA channel */
dma_channel_enable(DMA0, DMA_CH6);
}
void DMA0_Channel6_IRQHandler(void)
{
if(dma_interrupt_flag_get(DMA0, DMA_CH6, DMA_INT_HTF)){
dma_interrupt_flag_clear(DMA0, DMA_CH6, DMA_INT_FLAG_G);
ws2812_dma_irq_handle();
}
}
tim_core.c
文件内容如下,此为中间层程序:
#include "./TIM/tim_core.h"
#include "./TIM/tim.h"
#include "./WS2812/ws2812.h"
#include "main.h"
void timer_config_init(void)
{
timer_pwm_gpio_config();
timer_pwm_config();
dma_tim_init();
}
void ws2812_dma_irq_handle(void)
{
rt_sem_release(&ws2812_sync_sem);
}
void ws2812_enable(void)
{
dma_channel_disable(DMA0, DMA_CH6);
dma_transfer_number_config(DMA0, DMA_CH6, (WS2812_NUM+20) * 24);
dma_channel_enable(DMA0, DMA_CH6);
}
以下为ws2812.c
程序,为ws2812的应用层实现:
#include "./WS2812/ws2812.h"
#include "./TIM/tim_core.h"
#include <rtthread.h>
#include <string.h>
#include "./LOG/log.h"
#include "main.h"
#define TAG "ws2812b.c"
#define BIT_1 102
#define BIT_0 48
/* +20:前十用于启动时Reset 后十用于停止时Reset */
uint16_t ws2812_rgb_buf[WS2812_NUM+20][24] = {0};
void set_ws2812_color(uint8_t red, uint8_t green, uint8_t blue)
{
int i = 0, j = 0;
memset(ws2812_rgb_buf, 0, sizeof(ws2812_rgb_buf));
rt_sem_take(&ws2812_sync_sem, RT_WAITING_FOREVER);
for (i = 0; i < WS2812_NUM; i++) {
for (j = 0; j < 8; j++)
ws2812_rgb_buf[i+10][j] = (green & (0x01 << (7 - j))) ? BIT_1 : BIT_0;
}
for (i = 0; i < WS2812_NUM; i++) {
for (j = 0; j < 8; j++)
ws2812_rgb_buf[i+10][j+8] = (red & (0x01 << (7 - j))) ? BIT_1 : BIT_0;
}
for (i = 0; i < WS2812_NUM; i++) {
for (j = 0; j < 8; j++)
ws2812_rgb_buf[i+10][j+16] = (blue & (0x01 << (7 - j))) ? BIT_1 : BIT_0;
}
ws2812_enable();
}
void set_ws2812_single_color(uint8_t index, uint8_t red, uint8_t green, uint8_t blue)
{
int i = 0, j = 0;
memset(ws2812_rgb_buf, 0, sizeof(ws2812_rgb_buf));
rt_sem_take(&ws2812_sync_sem, RT_WAITING_FOREVER);
for (i = 0; i < WS2812_NUM; i++) {
for (j = 0; j < 8; j++)
ws2812_rgb_buf[i+10][j] = (0x00 & (0x01 << (7 - j))) ? BIT_1 : BIT_0;
}
for (i = 0; i < WS2812_NUM; i++) {
for (j = 0; j < 8; j++)
ws2812_rgb_buf[i+10][j+8] = (0x00 & (0x01 << (7 - j))) ? BIT_1 : BIT_0;
}
for (i = 0; i < WS2812_NUM; i++) {
for (j = 0; j < 8; j++)
ws2812_rgb_buf[i+10][j+16] = (0x00 & (0x01 << (7 - j))) ? BIT_1 : BIT_0;
}
for (j = 0; j < 8; j++)
ws2812_rgb_buf[index+10][j] = (green & (0x01 << (7 - j))) ? BIT_1 : BIT_0;
for (j = 0; j < 8; j++)
ws2812_rgb_buf[index+10][j+8] = (red & (0x01 << (7 - j))) ? BIT_1 : BIT_0;
for (j = 0; j < 8; j++)
ws2812_rgb_buf[index+10][j+16] = (blue & (0x01 << (7 - j))) ? BIT_1 : BIT_0;
ws2812_enable();
}
void set_ws2812_breathing(uint8_t index)
{
int i = 0;
switch (index) {
case 0: /* red */
for (i = 0; i < 254; i+=2) {
set_ws2812_color(i, 0, 0);
rt_thread_delay(30);
}
for (i = 254; i > 0; i-=2) {
set_ws2812_color(i, 0, 0);
rt_thread_delay(30);
}
break;
case 1: /* green */
for (i = 0; i < 254; i+=2) {
set_ws2812_color(0, i, 0);
rt_thread_delay(30);
}
for (i = 254; i > 0; i-=2) {
set_ws2812_color(0, i, 0);
rt_thread_delay(30);
}
break;
case 2: /* green */
for (i = 0; i < 254; i+=2) {
set_ws2812_color(0, 0, i);
rt_thread_delay(30);
}
for (i = 254; i > 0; i-=2) {
set_ws2812_color(0, 0, i);
rt_thread_delay(30);
}
break;
}
}
void set_ws2812_running_water(uint8_t red, uint8_t green, uint8_t blue)
{
int i = 0;
for (i = 0; i < WS2812_NUM; i ++) {
set_ws2812_single_color(i, red, green, blue);
rt_thread_delay(30);
}
for (i = WS2812_NUM; i >= 0; i --) {
set_ws2812_single_color(i, red, green, blue);
rt_thread_delay(30);
}
}