思路
前言
由于stm32f103单片机的频率有限,之前试过仅单纯地使用__nop();延时来做时序,但是尝试了很久,最多只能点亮灯,却无法调节颜色,说明时序还是存在问题。所以为了驱动ws2812b这玩意还是得用硬件的方式去驱动,靠软件的话实在不太稳定,起码我没整出来。
WS2812b
这个东西对时序的要求非常苛刻,对于主频仅有72MHz的STM32F103来说很难依靠软件来驱动它,它的原理就是通过DIN引脚上电平信号作为时序信号来传输数据。
一般需要给这个DIN引脚一个固定周期的PWM信号,在单个周期里当高电平维持时间大于某一个值时,这个周期的信号会被WS2812作为“1”码读入;在单个周期里当低电平维持时间大于某一个值时,这个周期的信号会被WS2812作为“0”码读入;详细这里就不展开讨论了,可以在网上去参见其它资料了解,本文主要的重点放在PWM+DMA的配合使用上。
注:参考资料时请注意你搜索的是WS2812
还是WS2812b
,这俩玩意在时间的要求上是不同的。
PWM+DMA
这方案广为使用,原理依靠STM32自己的PWM功能,一个周期一个周期地向WS2812b发送“0”码或者“1”码(取决于占空比)。
为了严格控制时序的正确发送,很自然地能够想到当一个周期结束后改变STM32定时器比较输出通道的比较值,但是由于PWM的周期很短,很可能导致程序因为还没来得及改变比较值,定时器的计数器就已经加到很大的值了,因而无法控制发送的bit,所以需要搭配STM32的硬件在周期结束时自动更改比较值,为了达到这个目的,DMA就是一个不可或缺的技术。
代码
直接上代码,烧录即可使用。后面对照着代码讲讲实现过程。
u16 LED_Buffer[2400];//定义发送bit的地方
u8 color_buff[3] = {0x00,0x00,0x00};//颜色
//下面两个宏定义主要是为了方便更改PWM的比较值用的
//发送1码时,我设置比较值为ONE(也即是60),实际上是高电平维持60个时钟周期
//发送0码时,我设置比较值为ZERO(也就是30),实际上时高电平维持30个时钟周期
#define ONE 60
#define ZERO 30
#define NumOfLED 6 // 灯珠数量
void Timer1_init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
//时钟初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//引脚初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//定时器初始化,使用定时器4
TIM_TimeBaseStructure.TIM_Period = 90-1; // 72Mhz / 90 = 800K Hz
TIM_TimeBaseStructure.TIM_Prescaler = 0;//不分频
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
//PWM初始化,PB9所指向的通道为通道4
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC4Init(TIM4, &TIM_OCInitStructure);
TIM_CtrlPWMOutputs(TIM4,ENABLE);
TIM_OC4PreloadConfig(TIM4,TIM_OCPreload_Enable);
//DMA舒适化,使用DMA1的通道7
DMA_DeInit(DMA1_Channel7);
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&(TIM4->CCR4);//外设地址
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)LED_Buffer; //存储器地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;//转运方向,把外设作为目标地址,即从存储器里赋值到外设
DMA_InitStructure.DMA_BufferSize=NumOfLED*24;//根据需要自定义转运量,我这里是6个灯,每个颜色24位,所以6*24就够了
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设指针不需要自增,目标就是定时器4通道4的比较值的地址
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //数据指针需要自增,因为需要发送的bit全部存储在存储器上
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//stm32f103里半字指的是16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//stm32f103里半字指的是16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//不需要软件触发
DMA_Init(DMA1_Channel7, &DMA_InitStructure);
TIM_DMACmd(TIM4, TIM_DMA_Update, ENABLE);//依靠定时器4的更新中断触发DMA转运
TIM_Cmd(TIM4, DISABLE);
}
void WS2812_send_color(u8 *color, u16 len)
{
u8 i;
u16 memaddr = 0;
u16 buffersize = len*24;
memset(LED_Buffer,0,2400);
while(len)
{
for(i=0; i<8; i++) // G
{
LED_Buffer[memaddr] = ((color[0]<<i) & 0x80) ? ONE:ZERO;
memaddr++;
}
for(i=0; i<8; i++) // R
{
LED_Buffer[memaddr] = ((color[1]<<i) & 0x80) ? ONE:ZERO;
memaddr++;
}
for(i=0; i<8; i++) // B
{
LED_Buffer[memaddr] = ((color[2]<<i) & 0x80) ? ONE:ZERO;
memaddr++;
}
len--;
}
DMA_SetCurrDataCounter(DMA1_Channel7, buffersize);
DMA_Cmd(DMA1_Channel7, ENABLE);
TIM_Cmd(TIM4, ENABLE);
while(!DMA_GetFlagStatus(DMA1_FLAG_TC7));
TIM_Cmd(TIM4, DISABLE);
DMA_Cmd(DMA1_Channel7, DISABLE);
DMA_ClearFlag(DMA1_FLAG_TC7);
}
int main(void)
{
Timer1_init();
while(1)
{
for(u8 j=0;j<3;j++)
{
for(u8 i=0;i<240;)
{
color_buff[j] = i;
i = i + 10;
WS2812_send_color(color_buff,NumOfLED);
Delay_ms(5);
}
}
for(u8 j=0;j<3;j++)
{
for(u8 i=250;i>0;)
{
color_buff[j] = i;
i = i - 10;
WS2812_send_color(color_buff,NumOfLED);
Delay_ms(5);
}
}
}
}
代码讲解
初始化的部分以及注释得很详细了,这里主要说一下代码的使用过程。
WS2812_send_color()
这个函数在使用的时候先把上一次发送的bit信息清空memset()
是C语言内置的函数,专门来干清空内容这件事。然后把颜色信息所对应的bit信息存入buffer数组,然后告诉DMA需要转运的数量,并使能DMA转运,然后再打开定时器开始发送数据。代码里的那个((color[0]<<i) & 0x80) ? ONE:ZERO
就是一个if判断语句而已,如果((color[0]<<i) & 0x80) 为真(也就是不为0),那么就返回1,否则返回0。
发送数据的时候,每当定时器溢出时,触发DMA转运更改比较值,不停循环下去。
当转运完成之后,立刻把定时器关掉以免PWM及发送多余的数据出去,然后关闭DMA转运,清空转运完成标志位。
效果展示
【STM32】WS2812b驱动,以后机箱的RGB灯省了