基于51单片机和WS2812B的彩色流水灯(SPI+DMA)
彩色流水灯,51单片机,WS2812B,SPI+DMA,普中开发板。
·
系列文章目录
前言
本代码使用的是普中A2开发板。
【单片机】STC32G12K128
【频率】35.0000MHz
【外设】WS2812B彩灯
效果查看/操作演示:B站搜索“甘腾胜”或“gantengsheng”查看。
源代码下载:B站对应视频的简介有工程文件下载链接。
一、效果展示


二、各模块代码
无
三、主函数
main.c
/*by甘腾胜@20250701
【效果查看/操作演示】B站搜索“甘腾胜”或“gantengsheng”查看
【单片机】STC32G12K128
【频率】35.0000MHz(本案例实测烧录频率从22.1184MHz~35.0000Mhz均能驱动WS2812B)
【驱动】利用SPI+DMA驱动WS2812B
【接线】WS2812B:DIN接P13
【说明】
(1)DMA传输,不占用CPU时间。每个灯有24bit数据,需要24字节的占空比来控制。
(2)每个灯3个字节,分别对应绿、红、蓝,高位先发。
(3)800KHz码率,数据0(1/4占空比): H=0.3125us,L=0.9375us;数据1(3/4占空比): H=0.9375us,L=0.3125us,RESET>=50us。
(4)高电平时间要精确控制在要求的范围内,低电平时间不需要精确控制,大于要求的最小值并小于RES的50us即可。
(5)参考了国芯技术交流网站(https://www.stcaimcu.com/)梁工的代码。
(6)WS2812S的标准时序如下:
TH+TL = 1.25us±150ns,RES>50us
T0H = 0.25us±150ns = 0.10us ~ 0.40us
T0L = 1.00us±150ns = 0.85us ~ 1.15us
T1H = 1.00us±150ns = 0.85us ~ 1.15us
T1L = 0.25us±150ns = 0.10us ~ 0.40us
两个位数据之间的间隔要小于RES的50us
*/
//双引号表示先在工程目录寻找头文件,再在Keil安装目录寻找
//尖括号表示先在Keil安装目录寻找头文件,再在工程目录寻找
#include "STC32G.H" //51单片机头文件
//定义主时钟
#define MAIN_Fosc 35000000UL
//流水灯移动的时间间隔,单位:ms
#define Duration 100
//LED灯珠个数
//此款单片机的xdata为8K,可驱动8*1024/15=546颗灯珠
//如果数组WS2812B_Buffer的变量不存储在xdata,则最多可驱动8*1024/12=682颗灯珠
#define LED_Quantity 32
//LED灯对应SPI字节数
#define SPI_Quantity LED_Quantity*12
/** WS2812B显存数组
*
* 用LED_Quantity*3个字节作为WS2812B的显示缓存
* 共有LED_Quantity个灯珠,每个灯珠需要写入24Bit(3个字节)控制显示的颜色
* WS2812B芯片要求按G、R、B的顺序发送数据,并且每个字节要高位先发
* 缓存数组中每三个字节为一组,每一组分别对应一个灯珠的G(绿)、R(红)、B(蓝)三原色
* 想改变屏幕显示,先对此显存数组进行修改
* 随后调用WS2812B_Update函数
* 才会将显存数组的数据写入每个灯珠的WS2812B芯片内
*/
unsigned char xdata WS2812B_Buffer[LED_Quantity][3];
/** SPI缓存数组
*
* 使用SPI-MOSI输出直接驱动WS2812B彩屏,通过DMA传输
* 800KHz码率
* 数据0(1/4占空比): H=0.3125us,L=0.9375us
* 数据1(3/4占空比): H=0.9375us,L=0.3125us
* RESET>50us
*
* WS2812S的标准时序如下:
* TH+TL=1.25us±150ns,RES>50us
* T0H=0.25us±150ns=0.10us~0.40us
* T0L=1.00us±150ns=0.85us~1.15us
* T1H=1.00us±150ns=0.85us~1.15us
* T1L=0.25us±150ns=0.10us~0.40us
* 两个位数据之间的间隔要小于RESET的50us
*
* 用SPI传输,MSB先发,每个字节高4位和低4位分别对应WS2812的一个位数据
* SPI数据位 B7 B6 B5 B4 B3 B2 B1 B0
* 1 0 0 0 1 1 1 0
* WS2812数据0 WS2812数据1
*
* 参考了国芯技术交流网站(https://www.stcaimcu.com/)梁工的代码
*/
unsigned char xdata SPI_Buffer[SPI_Quantity]; //SPI缓存
void Delay(unsigned int xms);
void UpdateSPI(void);
void SPI_Init(void);
void DMA2SPI_Start(void);
void WS2812B_Clear(void);
void WS2812B_Update(void);
void WS2812B_Init(void);
void WS2812B_SetBuffer(unsigned int Number,unsigned char R,unsigned char G,unsigned char B);
/**
* 函 数:主函数(有且仅有一个)
* 参 数:无
* 返 回 值:无
* 说 明:主函数是程序执行的起点,负责执行整个程序的主要逻辑
*/
void main()
{
int i;
EAXFR=1; //使能访问XFR(扩展RAM区域特殊功能寄存器)
CKCON=0; //设置外部数据总线速度为最快
WTST=0; //设置程序代码等待参数,赋值为0可将CPU执行程序的速度设置为最快
//全部IO口设置为准双向口
P0M1=0;P0M0=0;
P1M1=0;P1M0=0;
P2M1=0;P2M0=0;
P3M1=0;P3M0=0;
P4M1=0;P4M0=0;
P5M1=0;P5M0=0;
P6M1=0;P6M0=0;
P7M1=0;P7M0=0;
WS2812B_Init();
while(1)
{
for(i=0;i<LED_Quantity;i++)
{
WS2812B_Clear();
WS2812B_SetBuffer((i+5)%LED_Quantity,63,0,0); //红色
WS2812B_SetBuffer((i+4)%LED_Quantity,0,63,0); //绿色
WS2812B_SetBuffer((i+3)%LED_Quantity,0,0,63); //蓝色
WS2812B_SetBuffer((i+2)%LED_Quantity,31,31,0); //黄色
WS2812B_SetBuffer((i+1)%LED_Quantity,31,0,31); //紫色
WS2812B_SetBuffer(i,0,31,31); //青色
WS2812B_Update(); //更新显示
Delay(Duration);
}
}
}
/**
* 函 数:延时函数,延时约xms毫秒
* 参 数:xms 延时的时长,范围:0~65535
* 返 回 值:无
*/
void Delay(unsigned int xms)
{
unsigned long i;
while(xms--)
{
i=MAIN_Fosc/4000;
while(i)i--;
}
}
/**
* 函 数:更新SPI缓存数组的数据(根据WS2812B缓存数组的数据来更新)
* 参 数:无
* 返 回 值:无
*/
void UpdateSPI(void)
{
unsigned char xdata *px; //定义一个指向片外扩展RAM的指针
unsigned int i,j=0;
unsigned char k;
unsigned char Temp; //临时变量
for(i=0;i<SPI_Quantity;i++) //先清空SPI数组数据
{
SPI_Buffer[i]=0;
}
px = &WS2812B_Buffer[0][0]; //获取数组WS2812B_Buffer的首地址
for(i=0;i<LED_Quantity*3;i++)
{
Temp=*(px+i);
for(k=0;k<4;k++)
{
if( Temp & (0x80>>(2*k)) )
{
SPI_Buffer[j] = 0xE0; //数据1(高四位)
}
else
{
SPI_Buffer[j] = 0x80; //数据0(高四位)
}
if( Temp & (0x80>>(2*k+1)) )
{
SPI_Buffer[j] |= 0x0E; //数据1(低四位)
}
else
{
SPI_Buffer[j] |= 0x08; //数据0(低四位)
}
j++;
}
}
}
/**
* 函 数:配置SPI
* 参 数:无
* 返 回 值:无
*/
void SPI_Init(void)
{
HSCLKDIV=0; //高速时钟不分频
/*
【SPCTL】SPI控制寄存器,SPI速度控制
B7 B6 B5 B4 B3 B2 B1B0
SSIG SPEN DORD MSTR CPOL CPHA SPR[1:0]
SSIG:SS 引脚功能控制位
0:SS 引脚确定器件是主机还是从机
1:忽略 SS 引脚功能,使用 MSTR 确定器件是主机还是从机
SPEN:SPI 使能控制位
0:关闭 SPI 功能
1:使能 SPI 功能
DORD:SPI 数据位发送/接收的顺序
0:先发送/接收数据的高位(MSB)
1:先发送/接收数据的低位(LSB)
MSTR:器件主/从模式选择位
设置主机模式:
若 SSIG=0,则 SS 管脚必须为高电平且设置 MSTR 为 1
若 SSIG=1,则只需要设置 MSTR 为 1(忽略 SS 管脚的电平)
设置从机模式:
若 SSIG=0,则 SS 管脚必须为低电平(与 MSTR 位无关)
若 SSIG=1,则只需要设置 MSTR 为 0(忽略 SS 管脚的电平)
CPOL:SPI 时钟极性控制
0:SCLK 空闲时为低电平,SCLK 的前时钟沿为上升沿,后时钟沿为下降沿
1:SCLK 空闲时为高电平,SCLK 的前时钟沿为下降沿,后时钟沿为上升沿
CPHA:SPI 时钟相位控制
0:数据 SS 管脚为低电平驱动第一位数据并在 SCLK 的后时钟沿改变数据,前时钟沿采样数据(必须 SSIG=0)
1:数据在 SCLK 的前时钟沿驱动,后时钟沿采样
SPR[1:0]:SPI 时钟频率选择
SPR[1:0] SCLK 频率
00 SPI输入时钟/4
01 SPI输入时钟/8
10 SPI输入时钟/16
11 SPI输入时钟/2
*/
SPCTL=0xD5;
/*
SPI_S[1:0]:SPI 功能脚选择位
SPI_S[1:0] SS MOSI MISO SCLK
00 P1.2/P5.4[1] P1.3 P1.4 P1.5
01 P2.2 P2.3 P2.4 P2.5
10 P5.4 P4.0 P4.1 P4.3
11 P3.5 P3.4 P3.3 P3.2
有 P1.2 口的型号,此功能在 P1.2 口上,对于无 P1.2 口的型号,此功能在 P5.4 口上
*/
//下面四个只能选一个解除注释使用
SPI_S1=0;
SPI_S0=0; //对应SPI_S[1:0]为00的情况,即用P13作为MOSI口,用P14作为MISO口
P14=0; //MISO = 0, 目的是让MOSI输出完毕保持低电平
// SPI_S1=0;
// SPI_S0=1; //对应SPI_S[1:0]为01的情况,即用P23作为MOSI口,用P24作为MISO口
// P24=0; //MISO = 0, 目的是让MOSI输出完毕保持低电平
// SPI_S1=1;
// SPI_S0=0; //对应SPI_S[1:0]为10的情况,即用P40作为MOSI口,用P41作为MISO口
// P41=0; //MISO = 0, 目的是让MOSI输出完毕保持低电平
// SPI_S1=1;
// SPI_S0=1; //对应SPI_S[1:0]为11的情况,即用P34作为MOSI口,用P34作为MISO口
// P33=0; //MISO = 0, 目的是让MOSI输出完毕保持低电平
}
/**
* 函 数:开始传输数据
* 参 数:无
* 返 回 值:无
*/
void DMA2SPI_Start(void)
{
unsigned int Address; //用来存储地址
Address=(unsigned int)&SPI_Buffer[0]; //取首地址,强制转换为unsigned int型
DMA_SPI_TXAH=(unsigned char)(Address>>8); //发送地址寄存器高字节
DMA_SPI_TXAL=(unsigned char)Address; //发送地址寄存器低字节
DMA_SPI_AMTH=(unsigned char)((SPI_Quantity-1)/256); //设置传输总字节数 = n+1
DMA_SPI_AMT=(unsigned char)((SPI_Quantity-1)%256); //设置传输总字节数 = n+1
/*
【DMA_SPI_STA】SPI_DMA 状态寄存器
- - - - - B2 B1 B0
TXOVW RXLOSS SPIIF
SPIIF:SPI_DMA中断请求标志位。
当SPI_DMA数据交换完成后,硬件自动将SPIIF置1,若使能SPI_DMA中断则进入中断服务程序。标志位需软件清零。
RXLOSS:SPI_DMA 接收数据丢弃标志位。
SPI_DMA 操作过程中,当 XRAM 总线过于繁忙,来不及清空 SPI_DMA 的接收 FIFO 导致 SPI_DMA 接收的数据自动丢弃时,
硬件硬件自动将 RXLOSS 置 1。标志位需软件清零。
TXOVW:SPI_DMA 数据覆盖标志位。
SPI_DMA 正在数据传输过程中,主机模式的 SPI 写 SPDAT 寄存器再次触发 SPI 数据传输时,
会导致数据传输失败,此时硬件硬件自动将 TXOVW 置 1。标志位需软件清零。
*/
DMA_SPI_STA=0x00;
/*
【DMA_SPI_CFG】SPI_DMA 配置寄存器
B7 B6 B5 B4 B3B2 B1B0
SPIIE ACT_TX ACT_RX - SPIIP[1:0] SPIPTY[1:0]
SPIIE:SPI_DMA 中断使能控制位
0:禁止 SPI_DMA 中断
1:允许 SPI_DMA 中断
ACT_TX:SPI_DMA 发送数据控制位
0:禁止 SPI_DMA 发送数据。主机模式时,SPI 只发送时钟到 SCLK 端口,但不从 XRAM 读取数据,
也不向 MOSI 端口上发送数据;从机模式时,SPI 不从 XRAM 读取数据,也不向 MISO 端口上发送数据
1:允许 SPI_DMA 发送数据。主机模式时,SPI 发送时钟到 SCLK 端口,同时从 XRAM 读取数据,
并将数据发送到 MOSI 端口;从机模式时,SPI 从 XRAM 读取数据,并将数据发送到 MISO 端口
ACT_RX:SPI_DMA 接收数据控制位
0:禁止 SPI_DMA 接收数据。主机模式时,SPI 只发送时钟到 SCLK 端口,但不从 MISO 端口读取
数据,也向 XRAM 写数据;从机模式时,SPI 不从 MOSI 端口读取数据,也不向 XRAM 写数据。
1:允许 SPI_DMA 接收数据。主机模式时,SPI 发送时钟到 SCLK 端口,同时从 MISO 端口读取数
据,并将数据写入 XRAM;从机模式时,SPI 从 MOSI 端口读取数据,并写入 XRAM。
SPIIP[1:0]:SPI_DMA 中断优先级控制位
SPIIP[1:0] 中断优先级
00 最低级(0)
01 较低级(1)
10 较高级(2)
11 最高级(3)
SPIPTY[1:0]:SPI_DMA 数据总线访问优先级控制位
SPIPTY [1:0] 总线优先级
00 最低级(0)
01 较低级(1)
10 较高级(2)
11 最高级(3)
*/
DMA_SPI_CFG=0x40;
/*
【DMA_SPI_CFG2】SPI_DMA 配置寄存器 2
B7 B6 B5 B4 B3 B2 B1B0
- - - - - WRPSS SSS[1:0]
WRPSS:SPI_DMA 过程中使能 SS 脚控制位
0:SPI_DMA 传输过程中,不自动控制 SS 脚
1:SPI_DMA 传输过程中,自动拉低 SS 脚,传输完成后,自动恢复原始状态
SSS[1:0]:SPI_DMA 过程中,自动控制 SS 选择位
SSS[1:0] SS 脚
00 P1.2/P5.4
01 P2.2
10 P7.4
11 P3.5
有 P1.2 口的型号,此功能在 P1.2 口上,对于无 P1.2 口的型号,此功能在 P5.4 口上
*/
DMA_SPI_CFG2=0x00;
/*
【DMA_SPI_CR】SPI_DMA 控制寄存器
B7 B6 B5 B4 B3 B2 B1 B0
ENSPI TRIG_M TRIG_S - - - - CLRFIFO
ENSPI:SPI_DMA 功能使能控制位
0:禁止 SPI_DMA 功能
1:允许 SPI_DMA 功能
TRIG_M:SPI_DMA 主机模式触发控制位
0:写 0 无效
1:写 1 开始 SPI_DMA 主机模式操作
TRIG_S:SPI_DMA 从机模式触发控制位
0:写 0 无效
1:写 1 开始 SPI_DMA 从机模式操作
CLRFIFO:清除 SPI_DMA 接收 FIFO 控制位
0:写 0 无效
1:开始 SPI_DMA 操作前,先清空 SPI_DMA 内置的 FIFO
*/
DMA_SPI_CR=0xC1;
}
/**
* 函 数:WS2812B彩屏清空显存数组
* 参 数:无
* 返 回 值:无
* 说 明:需要调用WS2812B_Update函数才能更新到屏幕显示
*/
void WS2812B_Clear(void)
{
unsigned int i;
for(i=0;i<LED_Quantity;i++)
{
WS2812B_Buffer[i][0]=0;
WS2812B_Buffer[i][1]=0;
WS2812B_Buffer[i][2]=0;
}
}
/**
* 函 数:WS2812B更新显示
* 参 数:无
* 返 回 值:无
* 说 明:每个灯珠要按G(绿)、R(红)、B(蓝)的顺序发送数据,且高位先发
*/
void WS2812B_Update(void)
{
while( (DMA_SPI_STA & 0x01) == 0 ); //等待SPI_DMA中断请求标志位置1(即等待传输完成)
DMA_SPI_STA &= ~0x01; //软件清零标志位
UpdateSPI(); //更新SPI缓存数组
DMA2SPI_Start(); //DMA开始传输数据
}
/**
* 函 数:WS2812B彩屏初始化
* 参 数:无
* 返 回 值:无
*/
void WS2812B_Init(void)
{
DMA_SPI_STA |= 0x01; //SPI_DMA中断请求标志位置1,这一步不能少,没有的话则无法开始DMA传输
SPI_Init();
WS2812B_Clear();
WS2812B_Update();
}
/**
* 函 数:WS2812B设置一个灯珠的缓存
* 参 数:Number 灯珠的顺序号,范围:0~LED_Quantity-1
* 参 数:R(Red)红,范围:0~255
* 参 数:G(Green)绿,范围:0~255
* 参 数:B(Blue)蓝,范围:0~255
* 返 回 值:无
* 说 明:需要调用WS2812B_Update函数才能更新屏幕显示
*/
void WS2812B_SetBuffer(unsigned int Number,unsigned char R,unsigned char G,unsigned char B)
{
WS2812B_Buffer[Number][0]=G; //绿
WS2812B_Buffer[Number][1]=R; //红
WS2812B_Buffer[Number][2]=B; //蓝
}
总结
直接IO模拟WS2812B芯片的时序也行,不过这会增加CPU的负担,用SPI+DMA的方式驱动可以自动批量传输数据,解放CPU。
需要参照手册进行寄存器的配置,这一步是比较难的,没什么经验的话需要参考别人的代码,我就是参考了国芯论坛上梁工的代码。
更多推荐



所有评论(0)