PWM+DMA驱动WS2812B

最近项目上用到WS2812B的LED光源,主控芯片为STM32F103,经过一翻折腾,顺利点亮光源,现分享与大家,仅供参考!

程序没有用ST库,直接操作寄存器完成,如有什么问题,欢迎大家留言交流。

一、关于WS2812B

二、理论

从上图我们知道,要发送1bit的时间为1.25us左右,这么短的时间,程序通过延时来完成引脚电平的翻转,严格按时序来发送数据,感觉有那么点吃力,于是想到用定时器产生PWM波形来完成,数据0或1,调节PWM占空比就可以。我们知道,TH+TL=1.25us,也就是PWM周期为1.25us,当前芯片系统时钟我用的是48M,故STM32定时器的设置为:预分频为0,自动重载值为60-1。比较值设为19可满足0码的要求,比较值设为38时可满足1码的要求。

现在我们实现0、1要求,还剩下一个问题,那就是传输完1bit之后,怎么快速改变占空比来传输下1bit?如果利用定时器溢出产生中断,在中断函中改变比较值,那就要考虑进/出中断的时间及中断函数里的执行时间,这样就有可能造成整个数据传输时序不准,然后得到的结果不是我们想要的。这个时候我们就得把DMA模块利用起来,通过定时器溢出触发DMA,然后DMA把我们要的比较值传输给定时器,整个过程不需要程序来干预,这样就完美的解决时序问题。

现在我们来看下数据的传输,

从上图可以看出,如果只有一个灯,那直接发送24bit数据就可以了,如果有多个灯,就得发送多个24bit数据,第一 个灯截取第一个24bit数据,然后把剩下的数据转发给下一个灯,然后依次截取自己的数据。发送完数据后,想要再次发送数据,则要与上次发送数据的间隔时间至少要280us,电平为低电平。

三、程序

知道了WS2812B的控制原理之后,我们现在就要开始代码的实现了。

程序中我们定义两个一维数组,一个是用来存放LED灯RGB的值,存放顺序如下(这个数组可以定义成二维数码)

另外一个数组用来存放前面数组转换成定时器的PWM值,DMA就是直接读取这些数据发送给定时器。其中第一和最后一个数据固定为0,意义就是为了让发送数据开始前和结束后一直是低电平,在整个程序运行过程中,我的定时器一直是在工作的,没有关闭。

PWM输出引脚我用的是定时器3的通道4(TIM3_CH4),触发的DMA的通道3

代码如下:

#ifndef __WS281x_H__
#define __WS281x_H__

#define TIMER_PERIOD    (60)  //定时器的自动重载值,控制PWM波形的周期
#define WS281x_0        (19)  //0码对应的比较值
#define WS281x_1        (38)  //1码对应的比较值

#define LED_NUM         (4)   //LED灯的数量

#define LEN_LED_BIT     (LED_NUM * 24 + 2)//加2:第一个和最后一个数据为0,使其输出为低电平


void WS281xInit(void);
void WS281xSend(void);
void SetRGBData(uint16_t NumLed,uint8_t ValG,uint8_t ValR,uint8_t ValB);

#endif // __WS281x_H__
uint8_t  ValRGB[LED_NUM * 3];
uint16_t ValPwmBuff[LEN_LED_BIT];

void WS281xInit(void)
{

	//定时器初始化
	RCC->APB1ENR  |= RCC_APB1ENR_TIM3EN;
	TIM3->CR1      = (TIM_CR1_CEN + TIM_CR1_ARPE);
	TIM2->CR2      = 0;
	TIM3->SMCR     = 0;
	TIM3->CCMR1    = 0;
	TIM3->CCMR2    = (TIM_CCMR2_OC4M_2+TIM_CCMR2_OC4M_1+TIM_CCMR2_OC4PE);
	TIM3->CCER     = TIM_CCER_CC4E;
	TIM3->PSC      = 0;
	TIM3->ARR      = (TIMER_PERIOD-1);           //0码-19;1码-38
	TIM3->DIER     = (TIM_DIER_TDE+TIM_DIER_UDE);//允许更新触发DMA请求

	//DMA初始化
	RCC->AHBENR   |= RCC_AHBENR_DMA1EN;
	DMA1_Channel3->CCR  &= ~DMA_CCR3_EN;
	DMA1_Channel3->CNDTR = 0;//先设置为0,DMA触发了也不会发送数据
	DMA1_Channel3->CPAR  = (uint32_t)&TIM3->CCR4;
	DMA1_Channel3->CMAR  = (uint32_t)&ValPwmBuff[0];
	DMA1_Channel3->CCR   = 0x3591;
}

//设置对应LED的RGB值
void SetRGBData(uint16_t NumLed,uint8_t ValG,uint8_t ValR,uint8_t ValB)
{
	if ((NumLed == 0) || (NumLed > LED_NUM)) return;
	NumLed--;
	ValRGB[NumLed * 3 + 0] = ValG;
	ValRGB[NumLed * 3 + 1] = ValR;
	ValRGB[NumLed * 3 + 2] = ValB;
}

//调用此函数发送数据时,确保上次数据已经发送完成
void WS281xSend(void)
{
	uint8_t Data,i,j,*pLedDat;
	uint16_t *pLedPwmBuf;

	//数据转换
	pLedDat    = &ValRGB[0];
	pLedPwmBuf = &ValPwmBuff[1];//LED数据从第二字节开始存放
	for (i = 0;i < (LED_NUM * 3);i++)
	{
		Data = *pLedDat;
		for (j = 0;j < 8;j++)
		{
			*ppLedPwmBuf = (Data & 0x80) ? WS281x_1 : WS281x_0;
			pLedPwmBuf++;
			Data <<= 1;
		}
		pLedDat++;
	}

	//设置第一和最后数据为0
	ValPwmBuff[0] = 0;
	ValPwmBuff[LEN_LED_BIT - 1] = 0;

	//要修改数据传输量及内存、外设地址是,要先让通道不工作
	DMA1_Channel3->CCR  &= ~DMA_CCR3_EN;
	//设置传输的数据量
	DMA1_Channel3->CNDTR = LEN_LED_BIT;
	//设置外设地址
	DMA1_Channel3->CPAR  = (uint32_t)&TIM3->CCR4;
	//设置存储器地址
	DMA1_Channel3->CMAR  = (uint32_t)&LedRGB.LedBuff[0];
	//通道优先级最高,存储器/外设数据宽度16位,存储器地址自增,从存储器读数据,通道3开启
	DMA1_Channel3->CCR   = DMA_CCR3_PL + DMA_CCR3_MSIZE_0 + DMA_CCR3_PSIZE_0 +
		               DMA_CCR3_MINC + DMA_CCR3_DIR + DMA_CCR3_EN;
}

好了,程序代码就结束了。使用时先调用WS281xInit()函数初始相关模块,然后调用SetRGBData函数设置LED灯的值,最后调用WS281xSend()函数发送数据。

下图是我程序发送4个灯的波形: