STM32F103标准库DMA+PWM驱动WS2812b

思路

前言

由于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灯省了

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值