我在前面使用STM32F103成功能的驱动了WS2812B,这篇将分享在STM32H755的M4内核中驱动WS2812B。
【工程创建】
创建一个基于NUCLEO-H755的stm32cubeMX工程,在TIM中,使能TIM2的通道4(因此从开发板原理图上得到PB11在CN10)中。
【PWM图形配置】
1、使能TIM2及其通道4,同时根据ws2812的通信速率,计算出需要产生的PWM通首速度为800K,再根据TIM1的总线速度为200MHz,因此计算出PSC为1,ARR为250。
2、同时需要将GPIO的通信速度修改为高速
3、添加DMA配置,其配置界面如下图所示:
点击Add添加一个DMA,选择TIM2_CH4,数据传输方向为内存到外设,中断级别为高,内存地址为自增,数据宽度为word。
4、确认dma中断开启:
【代码编写】
1、生成工程后,我们需要首先验证一下TIM2—CH4输出的速度是不是800K
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_4); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, 30);
下载到开发板后,使用示波器查看PB11是否输出的波形同期为800K的PWM波形。
2、编写ws2812.h内容如下:
/** ****************************************************************************** * @file ws2812b.h * @brief WS2812B LED驱动头文件 * @details 包含WS2812B LED灯珠的控制函数和配置参数,支持15x15点阵显示 ****************************************************************************** * @attention * * Copyright (c) 2025 Sundea. * All rights reserved. * ****************************************************************************** */ #ifndef __WS2812_H #define __WS2812_H #ifdef __cplusplus extern "C" { #endif /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "tim.h" /* Exported types ------------------------------------------------------------*/ /** * @brief WS2812B颜色结构体定义 * @details 按照GRB顺序存储颜色值 */ typedef struct { uint8_t g; /*!< 绿色分量 (0-255) */ uint8_t r; /*!< 红色分量 (0-255) */ uint8_t b; /*!< 蓝色分量 (0-255) */ } ws2812b_color_t; /* Exported constants --------------------------------------------------------*/ /** * @brief WS2812B配置参数 */ #define WS2812B_NUM_LEDS 8 /*!< LED灯珠总数 (15x15=225个灯珠) */ #define WS2812B_TIM TIM2 /*!< 使用的定时器 */ #define WS2812B_TIM_CHANNEL TIM_CHANNEL_4 /*!< 使用的定时器通道 */ /** * @brief 定时器配置 (96MHz/120=800kHz) */ #define WS2812B_TIM_PERIOD 250 /*!< 定时器重载值ARR */ /** * @brief WS2812B时序参数 (基于800kHz PWM) * @details 每个PWM周期 = 1/800kHz = 1.25us * 0码: 0.4us高电平 + 0.85us低电平 * 1码: 0.8us高电平 + 0.45us低电平 */ #define WS2812B_PWM_1_HIGH 160 /*!< 1码高电平计数值 (0.8us / 1.25us* 250 ≈ 160) */ #define WS2812B_PWM_0_HIGH 80 /*!< 0码高电平计数值 (0.4us / 1.25us * 250 ≈ 80) */ #define WS2812B_PWM_LOW (WS2812B_TIM_PERIOD - WS2812B_PWM_1_HIGH) /*!< 低电平部分 */ /** * @brief LED数据配置 */ #define WS2812B_BITS_PER_LED 24 /*!< 每个LED的位数 */ #define WS2812B_RESET_PULSES 100 /*!< 复位信号脉冲数 (50us复位信号) */ /* Exported functions --------------------------------------------------------*/ /** * @brief 初始化WS2812B驱动 * @param 无 * @retval 无 */ void ws2812b_init(void); /** * @brief 设置指定坐标位置LED的颜色 * @param x: X坐标 (0-14) * @param y: Y坐标 (0-14) * @param r: 红色分量 (0-255) * @param g: 绿色分量 (0-255) * @param b: 蓝色分量 (0-255) * @retval 无 */ void ws2812b_set_pos(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b); /** * @brief 更新显示内容到LED * @param 无 * @retval 无 */ void ws2812b_update(void); /** * @brief 检查DMA传输是否正在进行 * @param 无 * @retval uint8_t: 1-传输中, 0-空闲 */ uint8_t ws2812b_is_busy(void); /** * @brief WS2812B测试函数 * @param 无 * @retval 无 */ void ws2812b_test_pattern(void); #ifdef __cplusplus } #endif #endif /* __WS2812B_H */
3、编写ws2812.c,内容如下:
/** ****************************************************************************** * @file ws2812b.c * @brief WS2812B LED驱动源文件 * @details 实现WS2812B LED灯珠的控制函数,支持15x15点阵显示 ****************************************************************************** * @attention * * Copyright (c) 2025 Sundea. * All rights reserved. * ****************************************************************************** */ /* Includes ------------------------------------------------------------------*/ #include "ws2812.h" #include <string.h> /* Private variables ---------------------------------------------------------*/ /** * @brief PWM数据缓冲区 * @details 存储所有LED的PWM数据和复位信号 */ static uint32_t pwm_buffer[WS2812B_NUM_LEDS * WS2812B_BITS_PER_LED + WS2812B_RESET_PULSES]; /** * @brief LED颜色数据数组 * @details 存储每个LED的RGB颜色值 */ static ws2812b_color_t led_colors[WS2812B_NUM_LEDS]; /** * @brief DMA忙标志 * @details 标识DMA传输是否正在进行 */ static volatile uint8_t dma_busy = 0; /* Private function prototypes -----------------------------------------------*/ static uint16_t ws2812b_xy_to_index(uint16_t x, uint16_t y); static void ws2812b_color_to_pwm(uint32_t *buffer, ws2812b_color_t color); /** * @brief 将XY坐标转换为LED索引 * @param x: X坐标 (0-14) * @param y: Y坐标 (0-14) * @retval uint16_t: 对应的LED索引 */ static uint16_t ws2812b_xy_to_index(uint16_t x, uint16_t y) { // 直线布线方式:从左到右,从上到下 return y * 15 + x; } /** * @brief 将颜色数据转换为PWM信号 * @param buffer: PWM数据缓冲区指针 * @param color: 颜色数据 * @retval 无 */ static void ws2812b_color_to_pwm(uint32_t *buffer, ws2812b_color_t color) { // 将RGB颜色转换为GRB格式 uint32_t grb_data = ((uint32_t)color.g << 16) | ((uint32_t)color.r << 8) | color.b; // 从最高位开始发送 (MSB first) for (int i = 23; i >= 0; i--) { // 检查当前位是否为1 if (grb_data & (1 << i)) { // 发送1码 *buffer = WS2812B_PWM_1_HIGH; } else { // 发送0码 *buffer = WS2812B_PWM_0_HIGH; } // 指向下一个缓冲区位置 buffer++; } } /** * @brief 初始化WS2812B驱动 * @param 无 * @retval 无 */ void ws2812b_init(void) { // 清零PWM数据缓冲区 memset(pwm_buffer, 0, sizeof(pwm_buffer)); // 清零LED颜色数据数组 memset(led_colors, 0, sizeof(led_colors)); // 初始化DMA忙标志为0(空闲状态) dma_busy = 0; // // 配置定时器 // TIM2->ARR = WS2812B_TIM_PERIOD - 1; // TIM2->CCR1 = 0; } /** * @brief 设置指定坐标位置LED的颜色 * @param x: X坐标 (0-14) * @param y: Y坐标 (0-14) * @param r: 红色分量 (0-255) * @param g: 绿色分量 (0-255) * @param b: 蓝色分量 (0-255) * @retval 无 */ void ws2812b_set_pos(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) { // 检查坐标是否在有效范围内 if (x >= 15 || y >= 15) return; // 将XY坐标转换为LED索引 uint16_t index = ws2812b_xy_to_index(x, y); // 设置红色分量 led_colors[index].r = r; // 设置绿色分量 led_colors[index].g = g; // 设置蓝色分量 led_colors[index].b = b; } /** * @brief 更新显示内容到LED * @param 无 * @retval 无 */ void ws2812b_update(void) { // 检查DMA是否正在传输,如果忙则直接返回 if (dma_busy) return; // 定义PWM数据指针,指向缓冲区起始位置 uint32_t *pwm_ptr = pwm_buffer; // 填充所有LED的数据 for (int i = 0; i < WS2812B_NUM_LEDS; i++) { // 将第i个LED的颜色数据转换为PWM信号 ws2812b_color_to_pwm(pwm_ptr, led_colors[i]); // 移动指针到下一个LED数据位置(每个LED占24位) pwm_ptr += 24; // 每个LED24位 } // 添加复位信号 (低电平) for (int i = 0; i < WS2812B_RESET_PULSES; i++) { // 设置占空比为0,即输出低电平 *pwm_ptr++ = 0; // 占空比为0 = 低电平 } // 设置DMA忙标志为1(传输中) dma_busy = 1; // 启动DMA传输 HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_4, pwm_buffer, WS2812B_NUM_LEDS * WS2812B_BITS_PER_LED + WS2812B_RESET_PULSES); } /** * @brief 检查DMA传输是否正在进行 * @param 无 * @retval uint8_t: 1-传输中, 0-空闲 */ uint8_t ws2812b_is_busy(void) { // 返回DMA忙标志状态 return dma_busy; } /** * @brief DMA传输完成回调函数 * @param htim: TIM句柄指针 * @retval 无 */ void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { // 判断是否为TIM2的回调 if (htim->Instance == TIM2) { // 停止DMA传输 HAL_TIM_PWM_Stop_DMA(&htim2, TIM_CHANNEL_4); // 清除DMA忙标志 dma_busy = 0; } } /** * @brief WS2812B测试函数 * @param 无 * @retval 无 */ void ws2812b_test_pattern(void) { static int i; // 初始化WS2812B驱动 ws2812b_init(); // 测试1: 设置前几个灯珠为不同颜色 ws2812b_set_pos(i, 0, 255, 0, 0); // 第一个灯珠设为红色 // ws2812b_set_pos(1, 0, 0, 255, 0); // 第二个灯珠设为绿色 // ws2812b_set_pos(2, 0, 0, 0, 255); // 第三个灯珠设为蓝色 // ws2812b_set_pos(3, 0, 255, 255, 0); // 第四个灯珠设为黄色 // 更新显示 ws2812b_update(); // 等待传输完成 while (ws2812b_is_busy()) { // 延迟1ms HAL_Delay(1); } // 延迟2秒以便观察效果 HAL_Delay(500); i++; if(i==8) i= 0; }
4、在main的while循环中执行
ws2812b_test_pattern
【总结】
在ws2812的使用TIM的PWM+DMA驱动,只需要占用1个GPIO,相比SPI占用的时间少,通信的时序比较灵活。因此相比SPI有很大的优势。同时使用DMA传输,点用MCU的资源也比较小。
在编写其驱动时,需要注意配置定时器的ARR,让总线产生800K的PWM时序,同时根据ARR来计算出WS2813的0电平与1电平的计数值。
【实验效果】