前言
因为工作需要,要使用f407控制ws2812的流水灯带,发现网上的教程都乱七八糟的,有很多方案,于是决定自己写一个
简介
WS2812 的核心特点
- 单线控制(DI/DO接口)
- 只需一个数据引脚即可串联控制多个 LED,通过单线协议(如NeoPixel)逐级传输数据,简化布线。
- 无需额外的驱动芯片,每个 LED 会将信号转发给下一个,适合长灯带(通常建议不超过 300 颗)。
- 全彩独立寻址
- 每个 LED 均可独立设置 RGB 颜色值(24位真彩色,1600万色) 和亮度。
- 支持动态光效(如渐变、呼吸、追逐等),适合复杂动画设计。
- 内置信号整形电路
- 内部集成廉价的整形电路(非专业芯片级整形),可在一定距离内减少信号失真,但在长距离或复杂电磁环境下可能需要额外缓冲。
- 兼容宽电压与多平台
- 工作电压:5V DC(典型值,部分型号支持 3.3V-5V)。
- 可与 Arduino、树莓派、ESP32 等常用开发板直接连接。
- 低成本与易扩展
- 每颗 LED 单价低(约 $0.2–0.5),支持级联扩展,适合 DIY 项目。
WS2812 的主要应用场景
- 装饰照明与氛围灯
- 智能家居:如电视背光灯、房间氛围灯带。
- 节日装饰:圣诞树灯饰、节日彩灯。
- 艺术装置与互动设计
- 光影艺术:通过传感器(声音、温度、运动)触发动态光效。
- 示例:音乐频谱可视化,根据音频频率实时映射颜色变化。
- 穿戴式电子与道具
- LED 服装:如舞台表演服饰、COSPLAY 道具。
- 安全警示:骑行服或背包上的动态指示灯。
- 广告与商业展示
- 动态招牌文字滚动,吸引眼球。
- 零售店铺的商品展示灯箱。
- 教育与学生项目
- STEM 教具:通过编程直观学习电子和光效原理。
- 创客竞赛中的低成本原型开发(如机器人状态指示灯)。
应用电路
驱动原理
每一个灯泡的LED点亮都需要24bit的数据帧,这24bit的数据帧组成RGB数据,单片机数据从DIN进入第一个灯珠时,第一个灯珠会锁存第一个24位数据,然后剩下的数据会从灯珠DOUT引脚输出到下一个灯珠的输入端,依此类推,可达到控制多个灯珠的目的。
而ws2812采用单总线通信的方式,有点类似常用的DS18B20温度传感器,通过在一个周期内,高电平和低电平的比例,确定了数据帧为0、还是为1。
数据帧组成:
数据帧 | 组成 |
---|---|
1 | 高电平持续T1H,低电平持续T1L |
0 | 高电平持续T0H,低电平持续T0L |
复位帧 | 电平持续时间大于280us |
![[Pasted image 20250217180117.png]] | |
通俗将就是如果要发送1bit的数据0,那么要发送高电平并持续220ns420ns之间再发送低电平750ns1.6us之间。如果要发送1bit的数据1,那么要发送高电平并持续220ns420ns之间再发送低电平750ns1.6us之间,如果中间超过50us的间隔,就会复位,此时再发送数据就是新的一帧了。 |
SPI+DMA 驱动
通过设置SPI时钟速率,单个SPI比特时间差不多对应WS2812比特的子周期,W2812B一位数据需要(0.35us+0.9us) = 1.25us,而f407的主频168MKz,挂在在APB2上的的spi的频率是42MHz,8分频后是5.25MBit/s,也就是说时钟周期是1/5.25MHZ ≈ 190ns,发送以为数据的时间是190ns/8
驱动方式 | 优势 | 缺陷 |
---|---|---|
GPIO循环翻转 | 无需额外硬件模块 | CPU占用率>90%,中断敏感,代码不可移植 |
PWM+DMA | 高波形精度(PWM硬件生成) | 仅适用于短灯带,编码逻辑复杂 |
专用芯片驱动 | 完全硬件解析协议 | 成本高(如TLC5947需每个RGB通道独立控制) |
SPI+DMA | 硬件级时序精度,CPU占用近乎0% | 需内存预编码缓冲区 |
cubeMX配置
首先打开cubeMX,新建工程
配置时钟
配置时钟树,这里直接输入168,系统会帮我们配置好的
配置spi,这里我们选择配置spi2,
注意分频系数选择8分频,不然时序不对跑不起来,然后打开DMA Setting,配置DMA,点击add添加SPI2_TX,
配置系统,选择调试,再配置输出格式,就可以生成工程了
驱动编写
spi驱动
生成的spi驱动是这样
void MX_SPI2_Init(void)
{
/* USER CODE BEGIN SPI2_Init 0 */
/* USER CODE END SPI2_Init 0 */
/* USER CODE BEGIN SPI2_Init 1 */
/* USER CODE END SPI2_Init 1 */
hspi2.Instance = SPI2;
hspi2.Init.Mode = SPI_MODE_MASTER;
hspi2.Init.Direction = SPI_DIRECTION_2LINES;
hspi2.Init.DataSize = SPI_DATASIZE_8BIT;
hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;
hspi2.Init.NSS = SPI_NSS_SOFT;
hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_ENABLE;
hspi2.Init.CRCPolynomial = 10;
if (HAL_SPI_Init(&hspi2) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN SPI2_Init 2 */
/* USER CODE END SPI2_Init 2 */
}
void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(spiHandle->Instance==SPI2)
{
/* USER CODE BEGIN SPI2_MspInit 0 */
/* USER CODE END SPI2_MspInit 0 */
/* SPI2 clock enable */
__HAL_RCC_SPI2_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/**SPI2 GPIO Configuration
PB10 ------> SPI2_SCK
PB15 ------> SPI2_MOSI
*/
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI2;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI2;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* SPI2 DMA Init */
/* SPI2_TX Init */
hdma_spi2_tx.Instance = DMA1_Stream4;
hdma_spi2_tx.Init.Channel = DMA_CHANNEL_0;
hdma_spi2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi2_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi2_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi2_tx.Init.Mode = DMA_NORMAL;
hdma_spi2_tx.Init.Priority = DMA_PRIORITY_LOW;
hdma_spi2_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_spi2_tx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(spiHandle,hdmatx,hdma_spi2_tx);
/* USER CODE BEGIN SPI2_MspInit 1 */
/* USER CODE END SPI2_MspInit 1 */
}
}
spi驱动和我的一样,就成功了一半
ws2812驱动
全局变量
实现驱动之前需要先写一些全局变量
#define RAINBOW_COLOR_COUNT (sizeof(RAINBOW_COLORS)/sizeof(RAINBOW_COLORS[0]))
#define MAX_METEORS 5
#define WS2812_0 0x80
#define WS2812_1 0xF0
#define WS2812_RST 0x00
#define LED_NUMS 90
#define RGB_BIT 24
volatile uint8_t RGB_BIT_Buffer[RGB_BIT];
volatile uint8_t buffer[RGB_BIT*LED_NUMS];
volatile LEDType LED[LED_NUMS];
// 在头文件中定义彩虹色数组
typedef struct{
uint16_t brightness; // 当前亮度等级 (0-255循环)
int8_t fade_step; // 亮度变化步长 (正负表示方向)
uint32_t last_color; // 缓存当前基色
}BreathingCtrl_t;
typedef struct
{
uint8_t R;
uint8_t G;
uint8_t B;
}LEDType;
BreathingCtrl_t breathe_ctrl = {.brightness=255, .fade_step=0};
DMA传输函数
void WS2812_Update()
{
HAL_SPI_Transmit_DMA(&hspi2,buffer,RGB_BIT*LED_NUMS);
}
色彩装换函数
这个函数将输入的色彩装填到数组中,是驱动的核心函数,将每一位的数据按位存储在数组中
static void WS2812_CreatData(uint8_t R,uint8_t G,uint8_t B)
{
uint8_t temp[RGB_BIT] = {0};
for (uint8_t i=0;i<8;i++){
temp[7-i] = (G & 0x01) ? WS2812_1 : WS2812_0;
G = G >> 1;
}
for (uint8_t i=0;i<8;i++){
temp[15-i] = (R & 0x01) ? WS2812_1 : WS2812_0;
R = R >> 1;
}
for (uint8_t i=0;i<8;i++){
temp[23-i] = (B & 0x01) ? WS2812_1 : WS2812_0;
B = B >> 1;
}
memcpy(RGB_BIT_Buffer, temp, RGB_BIT);
}
关灯函数
数组全部填充低电平,实现不发光
void WS2812_TurnOff()
{
for(uint16_t i=0;i<LED_NUMS*24;i++)
{
buffer[i]=WS2812_0;
}
}
点亮单个灯
输入颜色和点位,将输入的颜色按位分割开,存储在数组中,然后启动DMA传输数据
void WS2812_Color_Point(uint32_t color,uint16_t Pos)
{
uint8_t R,G,B;
uint16_t i;
R=(color >> 16 ) & 0x00FF;
G=(color >> 8 ) & 0x0000FF;
B=(color ) & 0x0000FF;
WS2812_CreatData(R,G,B);
if(Pos < LED_NUMS && Pos >=0)
{
memcpy(buffer + RGB_BIT * Pos, RGB_BIT_Buffer,RGB_BIT);
}
else
{
WS2812_TurnOff();
}
}
单色单点流水灯
首先清空所有的颜色缓存,然后给led背景添加统一的颜色,保留一个点作为流动点,并记录位置
void WS2812_Show_Wheel(uint32_t base_color)
{
static uint16_t dark_pos = 0; // 熄灭点的当前位置
const uint16_t LED_POSITIONS = LED_NUMS;
// 清空缓存但不直接发送(防止闪烁)
WS2812_TurnOff();
// 填充全部 LED 为基础色
for(uint16_t pos = 0; pos < LED_POSITIONS; pos++)
{
// 判断当前 LED 是否为熄灭点
if(pos != dark_pos)
{
// 设置基础色
WS2812_Color_Point(base_color, pos);
}
// 其他位置保持熄灭(由TurnOff默认设置)
}
dark_pos = (dark_pos + 1) % LED_POSITIONS;
WS2812_Update();
}
单色双点流水灯
与上一个流水灯结构类似,不过是两个对称的熄灭点在移动
void WS2812_Show_Wheel_Dual(uint32_t base_color)
{
static uint16_t dark_pos = 0;
const uint16_t LED_POSITIONS = LED_NUMS;
// 清空缓存
WS2812_TurnOff();
for(uint16_t pos = 0; pos < LED_POSITIONS; pos++)
{
// 双熄灭点位置计算
uint16_t dark_pos2 = (dark_pos + LED_POSITIONS/2) % LED_POSITIONS;
if(pos != dark_pos && pos != dark_pos2)
{
WS2812_Color_Point(base_color, pos);
}
}
dark_pos = (dark_pos + 1) % LED_POSITIONS;
WS2812_Update();
}
单色渐变流水灯
在单色流水灯的基础上新增了渐暗的效果,越靠近熄灭点越暗,类似“水波纹逐渐消散”的视觉效果
//亮度调整函数,降低RGB值实现
uint32_t WS2812_AdjustBrightness(uint32_t color, uint8_t brightness)
{
uint8_t r = ((color >> 16) & 0xFF) * brightness / 255;
uint8_t g = ((color >> 8) & 0xFF) * brightness / 255;
uint8_t b = (color & 0xFF) * brightness / 255;
return (r << 16) | (g << 8) | b;
}
// 在熄灭点两侧添加渐变效果
void WS2812_Show_Wheel_Fade(uint32_t base_color)
{
static uint16_t dark_pos = 0;
const uint16_t fade_distance = 5; // 渐变范围
WS2812_TurnOff();
for(uint16_t pos = 0; pos < LED_NUMS; pos++)
{
// 计算与熄灭点的距离
int16_t dist = abs(pos - dark_pos);
dist = (dist > LED_NUMS/2) ? LED_NUMS - dist : dist;
// 在渐变范围内调整亮度
if(dist < fade_distance)
{
// 亮度随距离线性降低
uint8_t brightness = 255 - (255 * dist / fade_distance);
uint32_t faded_color = WS2812_AdjustBrightness(base_color, brightness);
WS2812_Color_Point(faded_color, pos);
}
else
{
WS2812_Color_Point(base_color, pos);
}
}
dark_pos = (dark_pos + 1) % LED_NUMS;
WS2812_Update();
}
呼吸灯
void WS2812_Breathe(uint32_t base_color, uint8_t enable)
{
// 模式切换时的初始化(Must Do)
static uint8_t last_enable = 0;
if(enable != last_enable){
breathe_ctrl.brightness = 255; // 重启时恢复最大亮度
breathe_ctrl.fade_step = (enable) ? -5 : 0; // 步长调整(负值为渐暗)
breathe_ctrl.last_color = base_color;
last_enable = enable;
}
// 呼吸模式计算亮度
if(enable){
// 动态调整步长实现非线性渐变(仿呼吸自然感)
if(breathe_ctrl.brightness <= 10) breathe_ctrl.fade_step = +5; // 暗区加速变亮
if(breathe_ctrl.brightness >= 230) breathe_ctrl.fade_step = -3; // 亮区减缓变暗
breathe_ctrl.brightness += breathe_ctrl.fade_step;
}else{
breathe_ctrl.brightness = 255; // 强制最大亮度
}
// 基于亮度衰减颜色
uint8_t R = ((base_color >> 16) & 0xFF) * breathe_ctrl.brightness / 255;
uint8_t G = ((base_color >> 8) & 0xFF) * breathe_ctrl.brightness / 255;
uint8_t B = (base_color & 0xFF) * breathe_ctrl.brightness / 255;
uint32_t dimmed_color = (R << 16) | (G << 8) | B;
// 全屏填充衰减后的颜色
WS2812_TurnOff();
for(uint16_t i=0; i<LED_NUMS; i++){
WS2812_Color_Point(dimmed_color, i);
}
WS2812_Update();
}
流星灯
可设置背景,流星颜色,流星个数的流星灯,计算流星的起始位置,然后先填充背景色,按照位置绘制拖尾
void WS2812_MeteorTrail(uint32_t meteor_color, uint32_t bg_color, uint8_t meteor_count)
{
static int16_t pos[MAX_METEORS]; // 每个流星的位置
static uint8_t last_meteor_num = 0; // 记录上一次的流星数量
// Step1: 当流星数变化时重新初始化位置(仅执行一次)
if(meteor_count != last_meteor_num)
{
for(uint8_t m=0; m<meteor_count; m++)
{
// 根据流星数量和LED总数自动分配起始偏移
pos[m] = -6 + (m * (LED_NUMS + 5) / meteor_count);
}
last_meteor_num = meteor_count;
}
// Step2: 填充背景色(高覆盖效率)
for(int i=0;i<LED_NUMS;i++)
{
WS2812_Color_Point(bg_color, i);
}
// Step3: 绘制所有流星拖尾(避免重叠覆盖)
for(uint8_t m=0; m<meteor_count; m++)
{
// 逆序绘制确保后面的流星优先显示(视觉在前)
for(int i=4; i>=0; i--) // 拖尾长度5,索引0最暗
{
int16_t led_pos = pos[m] - i;
if(led_pos >=0 && led_pos < LED_NUMS)
{
// 渐变算法优化(使用二次曲线更自然)
float ratio = 1.0 - (i/(4.0 + 0.2));
uint8_t brightness = (uint8_t)(ratio * 255);
WS2812_Color_Point(WS2812_AdjustBrightness(meteor_color, brightness), led_pos);
}
}
// 更新该流星位置(环形缓冲)
pos[m] = (pos[m] +1) % (LED_NUMS + 5);
}
// Step4: 统一刷新显示
WS2812_Update();
}
顺序亮灯
按照顺序,逐一亮灯,全部点亮为止,当输入参数1,重置全部灯光
void WS2812_SequentialLight(uint32_t color, uint8_t reset)
{
static uint16_t current_led = 0;
const uint16_t max_led = LED_NUMS - 1;
// 重置逻辑
if (reset)
{
current_led = 0;
WS2812_TurnOff(); // 可选项:清空灯带从新开始
WS2812_Update();
return;
}
// 边界保护
if (current_led > max_led) return;
// 高效批量写入:只需点亮当前新LED(之前的LED已保留状态)
WS2812_Color_Point(color, current_led); // 假设此函数不修改其他LED状态
// 双缓冲机制保护:确保上次DMA传输完成后再更新
if (hspi2.hdmatx->State == HAL_DMA_STATE_READY)
{
WS2812_Update();
current_led++;
}
}
完整代码
完整代码已经上传到gitee,仓库地址
stm32F407_ws2812: 使用stm32F407搭配cubeMX开发的ws2812点灯程序