STM32f407使用cubemx控制ws2812流水灯带(spi+dma方案)

前言

因为工作需要,要使用f407控制ws2812的流水灯带,发现网上的教程都乱七八糟的,有很多方案,于是决定自己写一个

简介

WS2812 的核心特点

  1. 单线控制(DI/DO接口)
    • 只需一个数据引脚即可串联控制多个 LED,通过单线协议(如NeoPixel)逐级传输数据,简化布线。
    • 无需额外的驱动芯片,每个 LED 会将信号转发给下一个,适合长灯带(通常建议不超过 300 颗)。
  2. 全彩独立寻址
    • 每个 LED 均可独立设置 RGB 颜色值(24位真彩色,1600万色) 和亮度。
    • 支持动态光效(如渐变、呼吸、追逐等),适合复杂动画设计。
  3. 内置信号整形电路
    • 内部集成廉价的整形电路(非专业芯片级整形),可在一定距离内减少信号失真,但在长距离或复杂电磁环境下可能需要额外缓冲。
  4. 兼容宽电压与多平台
    • 工作电压:5V DC(典型值,部分型号支持 3.3V-5V)。
    • 可与 Arduino、树莓派、ESP32 等常用开发板直接连接。
  5. 低成本与易扩展
    • 每颗 LED 单价低(约 $0.2–0.5),支持级联扩展,适合 DIY 项目。

WS2812 的主要应用场景

  1. 装饰照明与氛围灯
    • 智能家居:如电视背光灯、房间氛围灯带。
    • 节日装饰:圣诞树灯饰、节日彩灯。
  2. 艺术装置与互动设计
    • 光影艺术:通过传感器(声音、温度、运动)触发动态光效。
    • 示例:音乐频谱可视化,根据音频频率实时映射颜色变化。
  3. 穿戴式电子与道具
    • LED 服装:如舞台表演服饰、COSPLAY 道具。
    • 安全警示:骑行服或背包上的动态指示灯。
  4. 广告与商业展示
    • 动态招牌文字滚动,吸引眼球。
    • 零售店铺的商品展示灯箱。
  5. 教育与学生项目
    • 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点灯程序

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值