WS2812b彩灯DMA PWM技术解析
一.WS2812B简介
WS2812B是一种数字可编程的LED灯条,可以使用单个数据线进行通讯控制LED灯的颜色和亮度。每个WS2812B都有一个唯一的地址,可以通过单个数据线进行级联。
二.WS2812B参数简介
三. WS2812B数据通讯简介
1.级联电路
2.数据传输
该芯片是通过数据传输的时间来判断数据是0码还是1码的。
此时就会有PWM的频率计算:
3.级联的数据传输
第一个数据缓存
·第一个24位有第一个模块接收并缓存
·第二个24位会被第一个模块转发到第二个模块上并缓存
·第三个24位会被第一个和第二个转发到第三个模块上并缓存
·第四个24位……
·第N个24位……
复位信号
第二个数据缓存
·第一个24位有第一个模块接收并缓存
·第二个24位会被第一个模块转发到第二个模块上并缓存
·第三个24位会被第一个和第二个转发到第三个模块上并缓存
·第四个24位……
·第N个24位……
复位信号
…………
四.WS2812B驱动程序简介
江协科技的程序
这里我们使用PWM+DMA的方式驱动WS2812B。这里我们用的江协科技的程序。(但是本人测试的时候出现错乱的情况不知道怎么回事)
DMA驱动程序:
#include "stm32f10x.h"
void (*DMA1_Handler)(void); //也要开中断 数据转运+DMA 没用ADC
void DMA1_Init(uint32_t MemoryBaseAddr)//MemoryBaseAddr是WS2812B_Bit数组地址
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA1_Channel2);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&TIM2->CCR1);//起始地址
DMA_InitStructure.DMA_MemoryBaseAddr = MemoryBaseAddr;//存放地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;//传输方向:双向 存储点到外设
DMA_InitStructure.DMA_BufferSize = 0;//缓存区大小,传输计数器大于0,触发源有触发信号,DMA使能
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设是否自增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器是否自增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//是否自增 以半字的形式传输16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//不使用自动重装
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//硬件触发
DMA_Init(DMA1_Channel2, &DMA_InitStructure);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
DMA_ITConfig(DMA1_Channel2, DMA_IT_TC, ENABLE);//DMA中断
DMA_Cmd(DMA1_Channel2, ENABLE);
}
void DMA1_SetIRQHandler(void (*IRQHandler)(void))
{
DMA1_Handler=IRQHandler;
}
void DMA1_Start(uint16_t DataNumber) //调运函数,连续转运
{
DMA_Cmd(DMA1_Channel2,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel2,DataNumber);
DMA_Cmd(DMA1_Channel2,ENABLE);
}
void DMA1_Channel2_IRQHandler(void) //DMA开中断12
{
if(DMA_GetFlagStatus(DMA1_FLAG_TC2))
{
DMA1_Handler();
DMA_ClearFlag(DMA1_FLAG_TC2);
}
}
这里我们通过定时器中断驱动pwm
PWN TIM2驱动:
#include "stm32f10x.h" //哪个函数不认识看芯片手册
void TIM2_Init(void) //TIM2开PWM的发送到DMA PA0输出
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启定时器2时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启复用引脚时钟
GPIO_InitTypeDef GPIO_InitStructure; //GPIO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 定时器控制引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_DeInit(TIM2); //初始化时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 90-1; //ARR周期 32视频里定时中断讲过,搞懂 90公式试出来的,然后一算
TIM_TimeBaseStructure.TIM_Prescaler = 0; //PSC预分频器
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_OCInitTypeDef TIM_OCInitStructure; //初始化输出比较单元
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0; //CCR捕获比较器 运行中设置
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_CtrlPWMOutputs(TIM2, ENABLE);
TIM_DMAConfig(TIM2,TIM_DMABase_CCR1,TIM_DMABurstLength_1Transfer); //DMA请求传输
TIM_DMACmd(TIM2,TIM_DMA_Update,ENABLE); //寄存器位传输对象
TIM_Cmd(TIM2, DISABLE);
}
void TIM2_Cmd(FunctionalState NewState)
{
TIM_Cmd(TIM2, NewState);
}
void TIM2_SetCompare1(uint16_t Value) //主输出使能,TM2为高级定时器,所以这里加上
{
TIM_SetCompare1(TIM2,Value);
}
WS2812B驱动:
#include "stm32f10x.h"
#include "TIM2.h"
#include "DMA1.h"
//#define WS2812B_LED_QUANTITY 32 //灯珠数量
#define WS2812B_LED_QUANTITY 30 //灯珠数量
//定义数组 类型符 数组名[常量]
uint32_t WS2812B_Buf[WS2812B_LED_QUANTITY]; //0xGGRRBB 正常的数据口输入,然后配置就行,绿红蓝总共24位,只有32
uint16_t WS2812B_Bit[24*WS2812B_LED_QUANTITY+1];//灯珠数量*24bit+1 颜色位数+复位信号 根据数据手册可知还要加复位信号0(正常的0),因此需要写入 num24+reset(reset=?自己调) 位。
uint8_t WS2812B_Flag;
void WS2812B_IRQHandler(void);
void WS2812B_Init(void)
{
DMA1_SetIRQHandler(WS2812B_IRQHandler);//把WS2812B_IRQHandler赋给DMA1_Handler
DMA1_Init((uint32_t) (&WS2812B_Bit));//灯位量的地址存放到DMA 这时DMA就是灯珠的量 强制类型转换 不足的高位补0
TIM2_Init();//定时器
} //用TIM2开PWM 送给DMA
void WS2812B_ClearBuf(void)//把LED的RGB数组值清零,全部输出0码,变成黑色,即不发光
{
uint8_t i;
for(i=0;i<WS2812B_LED_QUANTITY;i++)
{
WS2812B_Buf[i]=0x000000;//24位的二进制数 6位的16进制数
}
}
void WS2812B_SetBuf(uint32_t Color) //设置显示相同的颜色
{
uint8_t i;
for(i=0;i<WS2812B_LED_QUANTITY;i++)
{
WS2812B_Buf[i]=Color;//BUF是颜色,设置成同一个颜色,初始统一设置成不发光
}
}
void WS2812B_UpdateBuf(void) //30和60对应不同的高电平时间也对应了时序的0码和1码 占空比控制亮灭 CCR和AAR+1共同决定的
{
uint8_t i,j;
for(j=0;j<WS2812B_LED_QUANTITY;j++) //遍历每个LED
{
for(i=0;i<24;i++) //遍历每个LED的颜色数据的每一位 10000000 00000000 00000000
{
if(WS2812B_Buf[j]&(0x800000>>i)){WS2812B_Bit[j*24+i+1]=60;}//0x800000为二进制的1加23个0,>>:右移
else{WS2812B_Bit[j*24+i+1]=30;} //每一位,取值范围是0到23,所以再加1
}
}
DMA1_Start(24*WS2812B_LED_QUANTITY+1);
TIM2_Cmd(ENABLE);
while(WS2812B_Flag==0);
WS2812B_Flag=0;
}
void WS2812B_IRQHandler(void) //定时器2 开PWM 直接就是根据时序占空比设置控制灯条
{
TIM2_SetCompare1(0);//pwm设置
TIM2_Cmd(DISABLE);//关闭定时器
WS2812B_Flag=1;
}
幻影的程序
定时器驱动
#include "timer.h"
#include "led.h"
//通用定时器中断初始化
//这里时钟选择为APB1的2倍,而APB1为36M
//arr:自动重装值。
//psc:时钟预分频数
//这里使用的是定时器3!
void TIM3_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到5000为500ms
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 10Khz的计数频率
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
TIM_ITConfig( //使能或者失能指定的TIM中断
TIM3, //TIM2
TIM_IT_Update ,
ENABLE //使能
);
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级0级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级3级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIMx外设
}
DMA驱动程序
#include "dma.h"
u16 SendBuff[24*16+220]={
10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, //1
10,10,10,10,10,10,10,10,3,3,10,3,10,3,10,10,3,3,3,3,3,3,3,3, //2
10,10,10,10,10,10,10,10,3,10,3,10,10,10,3,3,3,3,3,3,3,3,3,3, //3
10,10,10,10,10,10,10,10,10,3,3,3,3,10,10,3,3,3,3,3,3,3,3,3, //4
10,10,10,10,10,10,10,10,10,3,10,10,3,10,10,3,3,3,3,3,3,3,3,3, //5
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3,//6
10,10,10,10,10,3,3,10,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3, //7
10,10,3,3,10,3,3,10,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3, //8
10,3,3,10,10,10,10,3,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3, //9
3,10,10,3,10,10,10,3,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3, //10
3,10,3,3,3,10,3,3,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3, //11
3,3,3,10,3,3,10,10,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3, //12
3,3,3,3,3,3,3,3,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3, //13
10,10,10,10,10,10,10,10,3,3,3,10,3,3,10,10,3,3,3,3,3,3,3,3, //14
10,10,10,10,10,10,10,10,3,10,3,3,3,10,3,3,3,3,3,3,3,3,3,3, //15
10,10,10,10,10,10,10,10,3,10,10,3,10,10,10,3,3,3,3,3,3,3,3,3, //16
// 3,3,3,3,3,3,3,3,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3,
// 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,10,10,10,10,10,10,10,10,
// 10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
// 3,3,3,3,3,3,3,3,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3,
// 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,10,10,10,10,10,10,10,10,
// 10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
// 3,3,3,3,3,3,3,3,10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3,
// 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,10,10,10,10,10,10,10,10,
// 10,10,10,10,10,10,10,10,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
u16 DMA1_MEM_LEN = 24*16+220;// 保存DMA每次数据传送的长度
void DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
// 使能DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能DMA传输
// 初始化通道参数
DMA_DeInit(DMA1_Channel2); // 将DMA的通道2寄存器重设为缺省值
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&TIM1->CCR1); // DMA外设TIM1的CCR1基地址
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)SendBuff; // DMA内存基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 数据传输方向,从内存读取发送到外设
DMA_InitStructure.DMA_BufferSize = DMA1_MEM_LEN; //DMA通道的DMA缓存的大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //数据宽度为16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //数据宽度为16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环发送
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA1_Channel2, &DMA_InitStructure); //根据DMA_InitStruct中指定的参数初始化DMA的通道寄存器
// 使能TIME的DMA
TIM_DMACmd(TIM1,TIM_DMA_CC1,ENABLE);
// 开始DMA
DMA_SetCurrDataCounter(DMA1_Channel2,DMA1_MEM_LEN);//DMA通道的DMA缓存的大小
DMA_Cmd(DMA1_Channel2, ENABLE); //使能USART1 TX DMA1 所指示的通道
}
//让LED全灭
void WS2812B_ClearBuf(void)
{
uint16_t i;
for(i=0;i<384;i++)
{
SendBuff[i]=3;
}
}
#ifndef __DMA_H
#define __DMA_H
#include "sys.h"
void DMA_Config(void);
void WS2812B_ClearBuf(void);
#endif
PWM驱动程序
#include "pwm.h"
#include "led.h"
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK Mini STM32开发板
//PWM 驱动代码
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//修改日期:2010/12/03
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 正点原子 2009-2019
//All rights reserved
//
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void delay_ms(u16 nms);
void TIM1_PWM_Init(u16 arr,u16 psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);// 定时器1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE); //使能GPIO外设时钟使能
//设置该引脚为复用输出功能,输出TIM1 CH1的PWM脉冲波形
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //TIM_CH1
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 80K
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 不分频
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
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_OC1Init(TIM1, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMx
TIM_CtrlPWMOutputs(TIM1,ENABLE); //MOE 主输出使能
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); //CH1预装载使能
TIM_ARRPreloadConfig(TIM1, ENABLE); //使能TIMx在ARR上的预装载寄存器
TIM_Cmd(TIM1, ENABLE); //使能TIM1
}
#ifndef __PWM_H
#define __PWM_H
#include "sys.h"
void TIM1_PWM_Init(u16 arr,u16 psc);
#endif
要初始化io口不初始化会出现错误
#include "led.h"
//初始化PB5和PE5为输出口.并使能这两个口的时钟
//LED IO初始化
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能PA端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; // LED0-->PA.8 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // IO口速度为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); // 根据设定参数初始化GPIOA.8
GPIO_SetBits(GPIOA,GPIO_Pin_8); // PA.8 输出高
}
#ifndef __LED_H
#define __LED_H
#include "sys.h"
#define LED0 PAout(8) // PA8
#define LED1 PDout(2) // PD2
void LED_Init(void);//初始化
#endif
主程序
#include "led.h"
#include "delay.h"
#include "sys.h"
#include "timer.h"
#include "pwm.h"
#include "dma.h"
extern u16 SendBuff[20]; //
extern u16 DMA1_MEM_LEN; // 保存DMA每次数据传送的长度
void TIM3_IRQHandler(void) //TIM3中断
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除TIMx的中断待处理位:TIM 中断源
// 用户逻辑
}
}
int main(void)
{
// 延时函数初始化
delay_init();
// IO初始化
LED_Init();
// DMA控制PWM输出
TIM1_PWM_Init(13-1,7-1); // 72/7=10.285MHz 1.26us
DMA_Config();
// 定时器任务
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
TIM3_Int_Init(10-1,72-1); // 72分频,10us
while(1)
{
// TIM1->CCR1 = 3; // 0
// TIM1->CCR1 = 10; // 1
}
}
五.江协科技程序文件
程序代码 链接:https://pan.baidu.com/s/12t9pRkyJYpP5MXVNhYFELg 提取码:5gkc
原来视频 链接[WS2812B] 七彩流水灯/呼吸灯/三级调光照明 *附赠程序*_哔哩哔哩_bilibili
视频讲解 链接:2ws2812b外设配置_哔哩哔哩_bilibili
作者:俊昭喜喜里