【STM32】WS2812B灯珠的PWM+DMA控制(库函数)


一、WS2812B资料

  很多博客都有中文的DATASHEET,自行搜索,本文为英文DATASHEET截图。

1.硬件参数

绝对最大额定值与电气特性
在这里插入图片描述
  根据参数,使用5V电压供电即可;输入的电流1uA,IO口可以直接驱动,但若想驱动多个灯珠,最好还是独立供电。高电平最小值2.7V,低电平最大值0.7V,使用IO口直接做控制输入可以满足。

2.硬件连接

下图为DataSheet中的应用电路
在这里插入图片描述
  请将“DIN”信号连接到单片机上有定时器通道的引脚。前一个WS2812B的“DOUT”连接到下一个WS2812B的“DIN”。

二、驱动方式选择

1.数据传输与组成

首先看一眼数据传输方式,D1、D2、D3、D4代表按顺序排列的WS2812B灯珠。
可以看到,第三个WS2812B灯珠接收到了第三个24bit的数据,
故推断第一个WS2812B灯珠先收到第一个24bit的数据后,做出响应(发光);
再收到第二个24bit的数据后,直接转发给第二个WS2812B灯珠,由第二个WS2812B灯珠做出响应;
依次类推。
做不同颜色的灯光显示时,注意数据传输的先后。
在这里插入图片描述
  那么这24bit的数据内容是什么呢?下图为该24bit数据的组成
在这里插入图片描述
  根据描述,数据发送的顺序是GRB,高位先发。
  每8bit代表了WS2812B灯珠对此种颜色光发光的强度,故耀眼的绿、红、蓝纯色光对应十六进制的编码分别为:0xff0000(绿)、0x00ff00(红)、0x0000ff(蓝)。

2.数据编码

  有了编码信息,再看看WS2812B灯珠对0/1码的接收方式是什么。下图为WS2812B的编码时序图
在这里插入图片描述
  其中的数据传输时间如下图所示。
在这里插入图片描述
  可以看到,WS2812B编码所需的时间精度为ns级别,定时器中断不合适了;stm32的时钟周期在72mhz的系统时钟下为13.89ns,使用systemtick做delay也可以实现,但时间不会很精确,且需要考虑不同的指令周期对编码的影响,粗糙又繁琐。
  WS2812B的0/1编码很像一个周期内不同占空比的波形,可以考虑PWM控制,但此时又出现一个问题:
  如何在不改变频率的情况下在几个周期内连续改变占空比?
  通常我们是用库函数中TIM_SetCompare Y(TIM_TypeDef* TIM X, uint16_t Compare Z)来改变,但由于该函数执行时间很长,影响了编码时序,故不能采用此方法。
  另一种方式是直接改变定时器CCRx寄存器的值,此时又出现一个问题:
  如何在不到1us的时间内改变CCRx寄存器值?
  知识点来了:STM32中,有影子寄存器的预装载寄存器有三个,分别是:分频寄存器PSC,自动重装载ARR,自动捕获CCRx,我们读写访问的是这几个寄存器的值。CCRx寄存器的值会放到其影子寄存器中,然后定时器按照此比较值运行。
参考手册对于CCRx寄存器说明参考手册对于OCxPE位说明

当我们使能了预装载寄存器后(TIMx_CCMRy寄存器的OCzPE位为0 ),当PWM执行完一个周期后,产生更新事件(更新事件UEV:在定时器的计数值溢出时,会产生一次事),将CCRx预装载寄存器中下一半字(uint16_t)的值赋给影子寄存器。参考:STM32xx参考手册的"捕获/比较模式寄存器 1(TIMx_CCMR1)"章节。
  知道了这点后,我们可以在确定24bit的颜色编码后,得到对应位的0/1编码比较值,用数组传输给CCRx。然后启动PWM,即可得到占空比不断变化的PWM波。
  注意到STM32在定时器部分有DMA功能后,便可以尝试将数据传输部分交给DMA了。

三、软件驱动编写

  我测试使用的MCU为STM32f103c8t6,定时器使用是TIM2_CH2,移植时需要注意函数及实参使用相对应的。附图通道对应的DMA,作为移植参考。
在这里插入图片描述
  其他废话不多说,直接上代码,注释我写的很清楚。

头文件:HDL_WS2812B.h

/*******************************************************************************
  * @file  HDL_WS2812B.h
  * @author  huiyang
  * @version  V1.0
  * @date  01/21/2022
  ******************************************************************************/ 
#ifndef _HDL_WS2812B_H
#define _HDL_WS2812B_H

#include "stm32f10x.h"

#define WS2812B_PIN    GPIO_Pin_1
#define WS2812B_PORT   GPIOA
#define WS2812B_HIGH   GPIO_SetBits(WS2812B_PORT,WS2812B_PIN)
#define WS2812B_LOW	   GPIO_ResetBits(WS2812B_PORT,WS2812B_PIN)
/*	根据DataSheet定义0/1编码对应的高电平比较值,因为高电平在前,定义高电平的即可
**	高低电平的比例约为2:1
*/
#define WS2812B_ARR 90		//TIM2的自动重装载值
#define T0H 30						//0编码高电平时间占1/3
#define T1H 60						//1编码高电平时间占2/3

/* 灯珠亮的个数 */
#define LED_NUM  100		//底层驱动未用,为应用方便,先加上
#define DATA_SIZE  24 	    //WS2812B一个编码的bit数,3*8

void PWM_WS2812B_Init(uint16_t arr);
void WS2812B_Reset(void);
void PWM_WS2812B_Write_24Bits(uint16_t num,uint32_t GRB_Data);
void PWM_WS2812B_Show(uint16_t num);
void PWM_WS2812B_Red(uint16_t num);
void PWM_WS2812B_Green(uint16_t num);
void PWM_WS2812B_Blue(uint16_t num);

#endif

c文件:HDL_WS2812B.c

/*******************************************************************************
  * @file  HDL_WS2812B.c
  * @author  huiyang
  * @version  V1.0
  * @date  01/21/2022
  ******************************************************************************/ 
#include "HDL_WS2812B.h"
#include "delay.h"	//延时函数加自己对应的头文件,在复位处使用

/* 单个灯珠的编码对应的比较值数组 */
uint16_t Single_LED_Buffer[DATA_SIZE*LED_NUM];

/*
*作用:通用定时器2 TIM2_CH2 PWM输出+DMA初始化 
*参数:arr:自动重装值
*说明:定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
*      Ft=定时器工作频率,单位:Mhz
*/
void PWM_WS2812B_Init(uint16_t arr)
{
	//结构体变量
	GPIO_InitTypeDef  GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	TIM_OCInitTypeDef  TIM_OCInitStructure;
	DMA_InitTypeDef DMA_InitStructure;
	
	//使能RCC时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);		//使能PORTA时钟	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);			//使能TIM2时钟    
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);				//使能DMA1时钟    
	
	//初始化GPIO口
	GPIO_InitStructure.GPIO_Pin = WS2812B_PIN;					//GPIO口
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//速度50MHz
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;			//复用推挽输出                               
	GPIO_Init(WS2812B_PORT,&GPIO_InitStructure);				//根据指定的参数初始化GPIO口
	
	//初始化TIM2
	TIM_TimeBaseStructure.TIM_Prescaler=0;											//定时器分频:(0+1)=1,不分频
	TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;		//向上计数模式
	TIM_TimeBaseStructure.TIM_Period=arr;												//自动重装载值
	TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;				//时钟分割
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);							//根据指定的参数初始化TIM2
	
	//初始化TIM2 Channel2 PWM模式
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; 						//选择定时器模式:TIM脉冲宽度调制模式1
 	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
	//TIM_OCInitStructure.TIM_Pulse = 0;													//待装入捕获比较寄存器的脉冲值(此程序不用加初值)
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; 		//输出极性:TIM输出比较极性高
	TIM_OC2Init(TIM2, &TIM_OCInitStructure);  										//根据指定的参数初始化外设TIM2 Channel2
	/* 此处为知识点所描述处的对应代码,一定要有。移植时注意0C2代表定时器通道2*/
	TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable);  						//使能TIM2在CCR2上的预装载寄存器
	
	TIM_Cmd(TIM2, DISABLE);  									//失能TIM2,防止第一个脉冲异常
	
	TIM_DMACmd(TIM2, TIM_DMA_CC2, ENABLE);		//使能TIM2_CH2的DMA功能(CC2对应通道2)
	
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(TIM2->CCR2); 				//设置DMA目的地址
  	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Single_LED_Buffer;					//设置DMA源地址
 	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;													//方向:从内存SendBuff到内存ReceiveBuff  
 	DMA_InitStructure.DMA_BufferSize = DATA_SIZE; 															//一次传输大小DMA_BufferSize=SendBuffSize   
  	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; 						//ReceiveBuff地址不增
  	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;											//SendBuff地址自增
  	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;	//ReceiveBuff数据单位,16bit
  	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;					//SENDBUFF_SIZE数据单位,16bit
  	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;																//DMA模式:正常模式(传输一次)
  	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;												//优先级:中 
  	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; 																//内存到内存的传输
  	DMA_Init(DMA1_Channel7, &DMA_InitStructure);																//配置DMA1的7通道(不同定时器的通道不一样)
    
  	DMA_Cmd(DMA1_Channel7, DISABLE);					//失能DMA1的7通道,因为一旦使能就开始传输
}
//复位灯带
void WS2812B_Reset(void)
{
	TIM_Cmd(TIM2, DISABLE);
	WS2812B_LOW;
	delay_ms(1);	//此处延时时间最小值大于50us即可
}
//写数据编码
void PWM_WS2812B_Write_24Bits(uint16_t num,uint32_t GRB_Data)
{
  uint8_t i,j;
  for(j = 0; j < num; j++)
  {
  	for(i = 0; i < DATA_SIZE; i++)
	{
		/*因为数据发送的顺序是GRB,高位先发,所以从高位开始判断,判断后比较值先放入缓存数组*/
		Single_LED_Buffer[i] = ((GRB_Data << i) & 0x800000) ? T1H : T0H;
	}
  }
}

//点亮灯珠
void PWM_WS2812B_Show(uint16_t num)
{
	/* 移植时此处对应的通道和中断标志都需要更改 */
  	DMA_SetCurrDataCounter(DMA1_Channel7, num*DATA_SIZE);
  	DMA_Cmd(DMA1_Channel7, ENABLE);
  	TIM_Cmd(TIM2, ENABLE);
  	while(DMA_GetFlagStatus(DMA1_FLAG_TC7) != SET);
  	DMA_Cmd(DMA1_Channel7, DISABLE);
  	DMA_ClearFlag(DMA1_FLAG_TC7);
  	TIM_Cmd(TIM2, DISABLE);
}

//N个灯珠发红光
void PWM_WS2812B_Red(uint16_t num)
{
	PWM_WS2812B_Write_24Bits(num,0x00ff00);
	PWM_WS2812B_Show(num);
}
//N个灯珠发绿光
void PWM_WS2812B_Green(uint16_t num)
{
	PWM_WS2812B_Write_24Bits(num,0xff0000);
	PWM_WS2812B_Show(num);
}
//N个灯珠发蓝光
void PWM_WS2812B_Blue(uint16_t num)
{
	PWM_WS2812B_Write_24Bits(num,0x0000ff);
	PWM_WS2812B_Show(num);
}

测试主函数

#include "stm32f10x.h"
#include "delay.h"
#include "HDL_WS2812B.h"
int main(void)
{
	SystemInit();
	delay_init(72);
	PWM_WS2812B_Init(WS2812B_ARR);
	while(1)
	{
		WS2812B_Reset();
		PWM_WS2812B_Red(1);		//灯珠个数可修改
		PWM_WS2812B_Green(1);
		PWM_WS2812B_Blue(1);
	}
}

四、测试效果

在这里插入图片描述

总结

  至此,WS2812B灯珠的简单驱动就编写完成了,其他颜色以及亮灯效果可以在此基础上任意发挥。

  • 93
    点赞
  • 427
    收藏
    觉得还不错? 一键收藏
  • 65
    评论
下面是一个使用STM32F103标准库函数的串口+DMA发送不定长数据的示例代码: ```c #include "stm32f10x.h" // 定义串口DMA发送缓存区大小 #define TX_BUFFER_SIZE 256 // 定义串口DMA发送缓存区 uint8_t tx_buffer[TX_BUFFER_SIZE]; // 初始化函数 void init(void) { // 使能DMA1时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能串口1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 配置USART1的GPIO引脚 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置USART1 USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 9600; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE); // 配置DMA1通道4 DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(USART1->DR); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = 0; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel4, &DMA_InitStructure); // 使能DMA1通道4传输完成中断 DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE); // 使能DMA1通道4 DMA_Cmd(DMA1_Channel4, ENABLE); } // 串口DMA发送函数 void uart_send_dma(uint8_t *data, uint16_t len) { // 等待DMA1通道4传输完成 while (DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET); // 清除DMA1通道4传输完成标志位 DMA_ClearFlag(DMA1_FLAG_TC4); // 复位DMA1通道4 DMA_Cmd(DMA1_Channel4, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel4, len); DMA_Cmd(DMA1_Channel4, ENABLE); // 将数据写入发送缓存区 for (int i = 0; i < len; i++) { tx_buffer[i] = data[i]; } } int main(void) { init(); // 发送数据 uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; uart_send_dma(data, sizeof(data)); while (1); return 0; } ``` 需要注意的是,本示例使用了USART1的TX引脚,并且只实现了发送功能。如果需要实现接收功能,需要使用USART1的RX引脚,并且需要在USART_InitStructure中设置USART_Mode为USART_Mode_Rx | USART_Mode_Tx。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 65
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值