led_dev.c
ws2812_set_color_buffer_clear() 与 ws2812_set_on_off_color_light()4.5 时间与调度4.6 关键全局变量的读写5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
hmi/led/led_dev.c所属模块:hmi/led(人机界面 - WS2812 灯带编码/驱动层) 代码行数:197
1. 文件职责
本文件是 WS2812 灯带编码/驱动层的实现,负责把"第几颗灯珠、什么颜色"翻译成定时器比较值序列(bit 流),并启动一次 DMA 发送。它不直接写寄存器之外的业务逻辑:定时器 PWM 初始化在 hardware/timer/timer4.c、DMA 通道初始化在 hardware/dma/dma.c,本文件只做数据编码 + 发送触发 + 互斥标志维护。
数据流位置:上游由 hmi/led/app_led.c:156-207 的 led_build_full_buffer() 逐颗调用 ws2812_set_color_hex() 填缓冲、最后调用 ws2812_update() 启动发送;下游是 DMA0 通道 0(源地址 g_u16_ws2812_data、目标 TIMER_CH3CV(TIMER4))与 DMA0_Channel0_IRQHandler(hardware/dma/dma.c:266-283)。
被谁驱动:ws2812_init() 由 system/board_init/board_init.c:242 在上电初始化阶段调用一次;ws2812_update() 与各写函数只可能由 led_display_task() 的调用链驱动(当前该链未接线,详见第 7 节)。
编码缓冲 g_u16_ws2812_data 被 __attribute__((at(SDRAM_WS2812_DATA_ADDR))) 固定到 SDRAM,因此本文件隐含依赖"SDRAM_Init() 必须先于任何写操作"的初始化顺序。
2. 关键数据结构与宏
| 名称 | 类型(结构体/枚举/宏/联合体) | 说明 |
|---|---|---|
WS2812_FLAG_U | 联合体(定义于 hmi/led/led_dev.h:52-67) | 读写互斥标志,本文件使用 read_flag(自身读写)与 write_flag(DMA 忙,只读) |
g_u_ws2812_data | 全局数组(u32[LED_BUFFER_SIZE + 3],hmi/led/led_dev.c:61) | DMA 编码缓冲,LED_BUFFER_SIZE = 121×24 = 2904,加 3 个帧尾表项共 2907 项(11628 B),固定地址 SDRAM_WS2812_DATA_ADDR |
g_u_ws2812_flag | 全局变量(volatile WS2812_FLAG_U,hmi/led/led_dev.c:62) | 任务/中断共用的握手标志 |
LED_BITS | 宏(= 24,led_dev.h:33) | 每颗灯珠的 bit 数,同时是每个灯珠在缓冲中占用的表项数 |
RKP_LED_NUM | 宏(= 121,led_dev.h:32) | 灯珠总数 |
T0_PULSE / T1_PULSE | 宏(54 / 134,led_dev.h:38-39) | 数据"0"/"1"对应的定时器比较值 |
SDRAM_WS2812_DATA_ADDR | 宏(hardware/sdram/sdram.h:64) | 编码缓冲的固定存放地址(预留 16 KB) |
TRUE / FALSE | 宏(includes.h) | 用于 write_flag/read_flag 赋值与比较 |
3. 函数清单
| 函数名 | 作用域(static/全局) | 说明(一句话) |
|---|---|---|
ws2812_init | 全局(hmi/led/led_dev.c:72) | 初始化 TIMER4 的 FL3512 位时序与 DMA0 通道 0,清零编码缓冲并打印一条调试信息 |
ws2812_update | 全局(hmi/led/led_dev.c:86) | 等待写缓冲空闲 → 置 write_flag → 把 PA3 切回复用功能 → 使能 DMA 与 TIMER4,启动一帧发送 |
ws2812_set_color_rgb | 全局(hmi/led/led_dev.c:101) | 以 R/G/B 参数写入第 led_idx 颗的 24 bit(不转 GRB,按 RGB 顺序出码,全工程无调用点) |
ws2812_set_color_hex | 全局(hmi/led/led_dev.c:118) | 以 0xRRGGBB 写入第 led_idx 颗,内部转成 GRB 后逐 bit 生成比较值 |
ws2812_set_color_buffer_clear | 全局(hmi/led/led_dev.c:153) | 循环 RKP_LED_NUM 次调用 ws2812_set_color_hex(i, 0) 清空整条灯带 |
ws2812_set_on_off_color_light | 全局(hmi/led/led_dev.c:170) | 先清屏,再按 u8_flag 把全部灯珠置成同一颜色或全灭;未在 led_dev.h 中声明,全工程无调用点 |
本文件共 6 个函数(1 个未在头文件声明),已全部列出。文件内无 static 函数。
4. 核心逻辑解析
4.1 帧结构与位编码
WS2812(灯珠型号 FL3512SRGBICK-8MA-3T)的时序规范在文件头注释中给出(hmi/led/led_dev.c:9-41):每位固定周期 1.20 µs(实际实现 1.25 µs),位"0"高 0.32 µs、位"1"高 0.67 µs,帧尾 >200 µs 低电平作为 Reset。
本文件的编码策略是"每位一个定时器比较值":
u32 *pdata = &g_u16_ws2812_data[led_idx * LED_BITS]; // 每颗灯珠占 24 个 u32
for (int i = 0; i < LED_BITS; i++)
*pdata++ = (grb_color & (0x800000 >> i)) ? T1_PULSE : T0_PULSE;
- MSB 先出(
0x800000 >> i从 bit23 走到 bit0)。 - 缓冲区元素是送给
TIMER_CH3CV(TIMER4)的比较寄存器值,不是电平宽度本身。DMA 在 TIMER4 的更新事件(TIMER_DMA_UPD)上逐个搬运,配合hardware/timer/timer4.c:49的period = 209(210 计数 @168 MHz = 1.25 µs)产生 800 kHz 的位流。 - 一个灯珠 24 表项 = 30 µs,121 颗 = 2904 表项 ≈ 3.63 ms;再加数组末尾 3 个表项(
ws2812_init()中memset为 0)。 hardware/dma/dma.c:55用sizeof(g_u16_ws2812_data) / 4作为传输个数(= 2907),因此数组的+3会被一并发送,形成帧尾的额外低电平时间;真正 >200 µs 的 Reset 由 DMA 完成中断把 PA3 拉低并保持实现(hardware/dma/dma.c:278-280)。
4.2 颜色字节序转换
ws2812_set_color_hex()(hmi/led/led_dev.c:118-145)内部有 #if 1(GRB)与 #else(RGB)两套:
u8 r = (u32_color >> 16) & 0xFF;
u8 g = (u32_color >> 8) & 0xFF;
u8 b = u32_color & 0xFF;
u32 grb_color = (g << 16) | (r << 8) | b; // 生效:GRB
WS2812 系列灯珠的 24 bit 顺序为 G7..G0/R7..R0/B7..B0,所以对外统一使用 0xRRGGBB 的调用者(hmi/led/app_led_cfg.c)无需关心字节序。ws2812_set_color_rgb()(hmi/led/led_dev.c:106)则不做转换,直接 rgb = r<<16 | g<<8 | b,两者行为不一致(该函数当前无调用点)。
4.3 发送流程与互斥标志
ws2812_update()(hmi/led/led_dev.c:86-98):
while (g_u_ws2812_flag.bit.read_flag == TRUE) // 1) 等待"没人在改缓冲"
OSTimeDlyHMSM(0, 0, 0, 1); // 1 ms 粒度让出 CPU
g_u_ws2812_flag.bit.write_flag = TRUE; // 2) 宣告"DMA 占用中"
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_3); // 3) PA3 复用为 TIMER4_CH3
delay_us(1);
dma_channel_enable(DMA0, DMA_CH0); // 4) 使能 DMA 通道
timer_enable(TIMER4); // 5) 启动定时器 → 产生位流
ws2812_init()(hmi/led/led_dev.c:72-78):
timer4_fl3512_init(); // TIMER4 CH3 PWM,800 kHz,极性 LOW
led_timer4_pwm_dma0_channel0_init(); // DMA0 CH0:内存 g_u16_ws2812_data → TIMER_CH3CV(TIMER4),32 bit
memset(g_u16_ws2812_data, 0, sizeof(g_u16_ws2812_data));
printf("ws2812初始化成功\n"); // USMART 调试串口输出
DMA 完成中断(hardware/dma/dma.c:266-283)负责收尾:清 FTF 标志 → timer_disable(TIMER4) → dma_channel_disable(DMA0, DMA_CH0) → 把 PA3 改成下拉输出并拉低(提供 Reset 低电平)→ g_u_ws2812_flag.bit.write_flag = FALSE。
因此一次完整的"改缓冲 → 发送 → 允许再改"闭环为:
| 阶段 | read_flag | write_flag | 谁在动 |
|---|---|---|---|
| 空闲 | 0 | 0 | — |
| 写某颗灯珠 | 1 → 0 | 0 | 任务(led_dev.c:109/114、131/144) |
| 发送中 | 0 | 1 | DMA + TIMER4 硬件 |
| 发送结束 | 0 | 0 | DMA0_Channel0_IRQHandler(dma.c:280) |
所有写函数开头都有 if (g_u_ws2812_flag.bit.write_flag == TRUE) return;(led_dev.c:103、121、155、174),即DMA 发送期间写入被静默丢弃(不返回错误码)。
4.4 ws2812_set_color_buffer_clear() 与 ws2812_set_on_off_color_light()
void ws2812_set_color_buffer_clear(void) // led_dev.c:153
{
if (g_u_ws2812_flag.bit.write_flag == TRUE) return;
for (int i = 0; i < RKP_LED_NUM; i++)
ws2812_set_color_hex(i, 0); // 内部又各自检查一次 write_flag
}
- 双层检查:外层一次 + 内层 121 次;每次
ws2812_set_color_hex还会独立地置位/清零read_flag121 次(led_dev.c:131/144),使每次清屏产生 242 次对同一字节的读-改-写,是效率与竞态面的双重放大。 ws2812_set_on_off_color_light(u8 u8_flag, u32 u32_color)(led_dev.c:170-192):先清屏,u8_flag == FALSE时用黑色 0,否则用u32_color,然后把 121 颗全部设成同一颜色。语义是"整条灯带单色点亮/熄灭",未在led_dev.h声明(编译器会告警"无前导原型"),且全工程无调用点。
4.5 时间与调度
- 本文件只有一处阻塞调用:
ws2812_update()中的OSTimeDlyHMSM(0, 0, 0, 1)(led_dev.c:90)。由于OS_TICKS_PER_SEC = 1000u(UCOSII/uCOS-CONFIG/os_cfg.h:51),即每次让出 1 个 tick。若调用时调度器未启动或该任务优先级最高且无其他任务就绪,则可能长期空转但不死锁(有延时调用)。 - 一帧发送约 3.63 ms,是纯硬件(DMA + TIMER)行为,不占用 CPU 时间;调用者在
ws2812_update()返回后即可继续执行,不能立即改写缓冲(会被write_flag拦住)。 - 与 uCOS-II 的其它配合:无信号量、无消息邮箱;唯一的跨上下文变量是
g_u_ws2812_flag。
4.6 关键全局变量的读写
| 变量 | 本文件的动作 | 外部读写方 |
|---|---|---|
g_u16_ws2812_data | 写(编码)、读(无) | hardware/dma/dma.c:53(设 DMA 源地址)、:55(传输个数) |
g_u_ws2812_flag | 读写 read_flag、置 write_flag | hardware/dma/dma.c:280(中断清 write_flag) |
5. 对外接口
本文件导出
| 名称 | 类型 | 使用方 |
|---|---|---|
ws2812_init | 全局函数(led_dev.h:79) | system/board_init/board_init.c:242 |
ws2812_update | 全局函数(led_dev.h:80) | hmi/led/app_led.c:207 |
ws2812_set_color_hex | 全局函数(led_dev.h:82) | hmi/led/app_led.c:191/201(生效分支)、led_dev.c:160/190 |
ws2812_set_color_buffer_clear | 全局函数(led_dev.h:83) | hmi/led/app_led.c:160、led_dev.c:177 |
ws2812_set_color_rgb | 全局函数(led_dev.h:81) | 无调用点 |
ws2812_set_on_off_color_light | 全局函数(头文件未声明) | 无调用点 |
g_u16_ws2812_data | 全局数组(led_dev.h:72) | hardware/dma/dma.c |
g_u_ws2812_flag | 全局变量(led_dev.h:73) | hardware/dma/dma.c:280 |
本文件调用的外部函数/变量
| 名称 | 来源模块 |
|---|---|
timer4_fl3512_init | hardware/timer/timer4.c(声明 timer4.h:19) |
led_timer4_pwm_dma0_channel0_init、dma_channel_enable、DMA0/DMA_CH0 | hardware/dma/dma.c(声明 dma.h:47) |
gpio_mode_set、gpio_bit_reset、GPIOA/GPIO_PIN_3、GPIO_MODE_AF | 芯片库(chip.h,经 led_dev.h:15) |
timer_enable | 芯片库 |
delay_us | hardware/delay(delay.h) |
memset、printf | C 标准库 / USMART 调试串口 |
OSTimeDlyHMSM | uCOS-II |
SDRAM_WS2812_DATA_ADDR | hardware/sdram/sdram.h:64 |
RKP_LED_NUM、LED_BITS、T0_PULSE、T1_PULSE | hmi/led/led_dev.h |
6. 依赖关系
依赖(本文件 include):led_dev.h(45)、includes.h(46)、timer4.h(47)、timer2.h(48)、dma.h(49)、delay.h(50)、app_led.h(51)、sdram.h(52)。
timer2.h、app_led.h在本文件正文中没有任何符号被使用(app_led.h反向依赖led_dev.h,构成循环 include),属多余依赖。
被依赖:
hmi/led/app_led.c(唯一业务调用方)hardware/dma/dma.c(通过dma.h:17→led_dev.h访问缓冲与标志)system/board_init/board_init.c(ws2812_init())
7. 备注与风险点
- 缓冲越界无保护:
ws2812_set_color_rgb()/ws2812_set_color_hex()对led_idx不做任何范围检查(hmi/led/led_dev.c:107、130)。led_idx = 120时写入 2880~2903 项,尚在 2907 项缓冲内;但led_idx ≥ 122会越界写 SDRAM,破坏紧随其后的SDRAM_BAT_INFO_ADDR数据区(hardware/sdram/sdram.h:65),且不会立即崩溃,属静默数据损坏。get_bms_soc()的返回值只被用作循环上限(hmi/led/app_led.c:188),所以目前不会触发。 g_u_ws2812_flag的位操作竞态:volatile不等于原子。任务侧bit.read_flag = TRUE/FALSE(led_dev.c:109/114/131/144)与中断侧bit.write_flag = FALSE(hardware/dma/dma.c:280)都是对同一字节u8_all的读-改-写;若中断落在任务侧 RMW 之间,write_flag的清零会被覆盖 → 后续所有写函数永久return,灯带冻结在最后一帧。建议改为独立的volatile u8变量或使用 OS 临界区。ws2812_update()只等read_flag,不等write_flag(hmi/led/led_dev.c:88):若上一帧尚未发完就再次调用,函数会重复置位write_flag、重新配置 GPIO、再次dma_channel_enable+timer_enable,可能造成 DMA 重装计数/半个帧中断,出现灯带乱码。当前调用链(1 s 周期)不会触发,但不属于安全设计。- 写入被静默丢弃:DMA 期间的写操作直接
return,没有返回值也没有重试机制(led_dev.c:103/121/155/174)。ws2812_set_color_buffer_clear()若恰逢发送中被丢弃,随后led_build_full_buffer()的逐颗设置也会被丢弃,但末尾的ws2812_update()仍会重发旧内容,表现为"这一秒灯带没变",属难排查的偶发现象。 ws2812_set_on_off_color_light()未在头文件声明(hmi/led/led_dev.c:170),既无法被其他模块正常调用,也会在 Keil 中产生 "no previous prototype" 告警。ws2812_set_color_rgb()与ws2812_set_color_hex()字节序不一致:前者 RGB、后者 GRB。若将来启用ws2812_set_color_rgb(),红蓝会互换。printf("ws2812初始化成功\n")(led_dev.c:77):初始化阶段经由串口输出中文,依赖 USMART/串口已初始化且printf已重定向;若board_init中串口初始化顺序变化会导致无输出或阻塞(usart0_init在board_init.c:225先于board_init.c:242的ws2812_init,目前安全)。g_u16_ws2812_data命名与类型不符(u32[]却叫u16_),且hardware/dma/dma.c:55用sizeof(...) / 4硬编码元素宽度,两处一旦不同步就会静默改变传输长度与越界范围。#if 0分支清理不彻底:hmi/led/led_dev.c:123/136-143保留了 RGB 版本代码;hmi/led/app_led.c:161-181保留了另一套 SOC 点亮规则,均靠编译开关切换。soc_color_flag/net_color_flag/sys_run_color_flag三个位(led_dev.h:60-62)在本文件及全工程均未使用,是设计未落地的痕迹。- 文件头注释(
hmi/led/led_dev.c:2)写的是File Name: app_led_cfg.c,与真实文件名不符。