WS2812的驱动搞了好久才成功,有点惭愧。
WS2812B的驱动信息
从WS2812的数据手册中,可以看出其使用单总线级联的方式进行通信,一组数据包括所有灯珠的显示信息,每组数据之间通过RESET分隔。在同一组数据中,数据每过1个灯珠,减少24位,也就是说每个灯珠都读取收到的前24位数据,作为自己的显示信息,其它的数据接着往下传递,直到遇到RESET信号,然后重新读取24为的数据。
对于每位数据是0还是1,则是通过占空比进行判断的。在这里我选取1.25us为1位数据的周期,300ns的高电平作为T0H,625ns的高电平作为T0L 。
WS2812B的驱动思路
现在需要做的就是不断的改变定时器PWM的占空比,以实现数据的发送。每发送1位数据就要改变一次占空比的值,如果使用中断的方式,那么进入中断的频率就会很高,所以使用DMA的方式是比较合适的。
我这里选择的方式是,使用定时器的更新事件触发DMA的数据传输,DMA使用循环模式一直发送,这样编程比较简单,不需要关注何时发送数据,只需要关注如何修改内存中的数据就行。
对于RESET的发送,从图中可以看出RESET信号是持续280us以上的低电平,那么就可以用高电平占空比为0的PWM信号代替,也就是说224个以1.25us为周期占空比为0的PWM就可以作为RESET信号了。
对外设的设置
外设的设置包括3个部分:IO、DMA和定时器。
IO设置
我使用的是PB4,其AF1的复用功能为TIMER2_CH0 。
rcu_periph_clock_enable(RCU_GPIOB);
gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_4);
gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_4);
gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4);
DMA的设置
DMA和STM32F1系列不同,GD32L23x中的DMA的通道不再是和外设对应起来的,而是通过复用器去与外设连接,这样每一个通道都可以对应所有的外设,使用起来更加的灵活。
DMA的请求用的是TIMER2的更新事件DMA_REQUEST_TIMER2_UP
,注意这个事件需要和TIMER设置里的事件对应起来,否则无法触发DMA。
DMA设置为循环模式dma_circulation_enable(DMA_CH0);
。
void dma_config(void)
{
dma_parameter_struct dma_data_parameter;
rcu_periph_clock_enable(RCU_DMA);
dma_deinit(DMA_CH0);
dma_data_parameter.periph_addr = (uint32_t)TIMER2_CH0CV;
dma_data_parameter.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_data_parameter.memory_addr = (uint32_t)(&buffer);
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 = SIZE_OF_BUFFER;
dma_data_parameter.priority = DMA_PRIORITY_HIGH;
dma_data_parameter.request = DMA_REQUEST_TIMER2_UP;
dma_init(DMA_CH0, &dma_data_parameter);
dma_circulation_enable(DMA_CH0);
dma_channel_enable(DMA_CH0);
}
TIMER2的设置
定时器的设置包含三部分:
- 时基单元
- 输出模式
- DMA事件设置
时基单元配置为1.25us的周期。输出模式使用的是PWM0的模式,同时使能TIMER2的TIMER_DMA_UPD
DMA请求。
void timer_config(void)
{
timer_oc_parameter_struct timer_ocinitpara;
timer_parameter_struct timer_initpara;
timer_ic_parameter_struct timer_icinitpara;
rcu_periph_clock_enable(RCU_TIMER2);
timer_deinit(TIMER2);
timer_struct_para_init(&timer_initpara);
timer_initpara.prescaler = 0;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = 79;
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_init(TIMER2, &timer_initpara);
timer_channel_output_struct_para_init(&timer_ocinitpara);
timer_ocinitpara.outputstate = TIMER_CCX_ENABLE;
timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
timer_channel_output_config(TIMER2, TIMER_CH_0, &timer_ocinitpara);
timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_0, 0);
timer_channel_output_mode_config(TIMER2, TIMER_CH_0, TIMER_OC_MODE_PWM0);
timer_dma_enable(TIMER2, TIMER_DMA_UPD);
timer_auto_reload_shadow_enable(TIMER2);
}
驱动代码
驱动代码包括了两大部分:
- 使能定时器
- 将颜色数据放入内存中
使能定时器
注意不要直接使能定时器,而是要先产生1次更新事件,然后再使能定时器。因为这样才能将内存中的第一个数据放入到定时器的中。
timer_event_software_generate(TIMER2,TIMER_EVENT_SRC_UPG);
timer_enable(TIMER2);
将颜色数据放入内存中
需要做的工作有2个:
- 将颜色的数据转化成定时器比较值寄存器中的数据
- 转化数据格式:常用的颜色数据是RGB数据,而WS2812中颜色的存放顺序是GRB。
#define T1H_COUNT 40 //对于WS2812输出位1时,PWM的计数值
#define T0H_COUNT 15 //对于WS2812输出位0时,PWM的计数值
void change_color_to_bit(uint32_t rgb_color,uint16_t* bit)
{
uint32_t temp = rgb_color;
int i;
uint16_t temp_bit;
for(i=0;i<24;i++){
if(temp & 0x00000001){
bit[23-i] = T1H_COUNT;
}else{
bit[23-i] = T0H_COUNT ;
}
temp = temp>>1;
}
for(i=0;i<8;i++){
temp_bit = bit;
bit = bit[8+i];
bit[8+i] = temp_bit;
}
}
上面的代码将颜色数据转化成比较值寄存器中的值。
效果展示
直接拍摄的效果不太好,用一张白纸遮着的效果要好一些。
ws2812b.c
(4.44 KB, 下载次数: 106)
ws2812b.h
(121 Bytes, 下载次数: 70)