系列文章目录


前言

本代码使用的是普中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。

需要参照手册进行寄存器的配置,这一步是比较难的,没什么经验的话需要参考别人的代码,我就是参考了国芯论坛上梁工的代码。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐