Linux字符设备驱动案例:用SPI+DMA点亮WS2812,从时序拼凑到驱动八步实现
WS2812是一款单总线(单线串行)智能LED控制芯片,每颗灯珠内置控制电路,接收24 bit的RGB数据后,多余的位自动透传给下一颗,从而实现整条灯带的级联控制。它不依赖时钟线,仅用一根数据线,通过一段时间内高低电平的占空比来区分数据0和1——这一特性决定了它对时序精度要求极高,也是整个驱动设计的出发点。

在Linux系统中驱动WS2812,最直观的做法是在应用层用GPIO翻转电平,一位一位地把数据发出去。但这种方式存在两个硬伤:一是持续占用CPU,发送一帧数据期间CPU需频繁操作电平,无法执行其他任务;二是时序抖动,Linux并非实时系统,应用层翻转电平会被进程调度、中断响应和缓存命中等因素干扰,一旦电平宽度超出WS2812的容差范围,灯带即显示错乱。
本文采用「SPI模拟时序 + DMA搬运」的方案:由SPI控制器产生精确波形,DMA负责数据搬运,CPU仅承担RGB编码,最终通过Linux字符设备向用户层暴露/dev/ws2812_spiX节点,应用程序只需write()一帧RGB数据即可点灯。以下基于瑞芯微平台,记录从时序设计、实测定量验证到字符设备驱动八步实现的完整过程。
01
WS2812时序要求码元与复位信号
WS2812通过一个码元周期(约1.25 μs)内高低电平的占空比来区分0和1:

除上表外,还有两个关键参数:
码元周期 ≈ 1.25 μs: 高电平与低电平合计约1.25 μs。
复位(锁存)信号: 一帧数据全部发送完成后,必须保持低电平 ≥ 280 μs,WS2812以此作为"本帧结束"的信号来锁存颜色。本驱动在发送完成后调用 udelay(300) 留出余量。
02
SPI码元映射3.2 MHz时钟下的4-bit拼接
核心设计在于:不自己翻转电平,而是让SPI硬件"拼"出WS2812的码元波形。
SPI的MOSI线在时钟驱动下逐位输出数据,每一位的宽度 = 1 / SPI时钟频率,波形由硬件保证。将SPI时钟配置为 3.2 MHz,则1个SPI bit ≈ 312 ns,4个SPI bit拼接为1个WS2812码元 ≈ 1.25 μs,正好等于WS2812的码元周期。
码元映射表:
两者的高低电平均落在WS2812的时序容差范围内,灯珠可正确识别。
编码实现(1字节RGB → 4字节SPI码元流):
/* WS2812 bit → SPI 4-bit 码元 */staticconstu8 ws2812_bit_to_spi[2] = {0x8,0xE};/* 1 字节 RGB(8 个 WS2812 bit)→ 4 字节 SPI 码元流 */staticvoidws2812_encode_byte(u8in, u8 *out){ inti; for(i =0; i < 4; i++) { u8 high = ws2812_bit_to_spi[(in >> (7- i *2)) &0x1]; u8 low = ws2812_bit_to_spi[(in>> (6- i *2)) &0x1]; out[i] = (high << 4) | low; /* 高 4 位先发 */ }}
以0xA5(1010_0101b)为例:最高位1 →1110,次高位0 →1000,拼成第一个输出字节1110_1000 = 0xE8……8个WS2812 bit恰好编码为4个输出字节,实现1:4 的数据膨胀。

03
数据膨胀与DMA触发为什么灯珠多反而更省CPU
每颗WS2812灯珠需要RGB888 = 24 bit数据,编码后按1:4膨胀:
1颗灯珠 = 24 × 4 = 96 SPI bit = 12字节
对照瑞芯微SPI驱动默认FIFO长度64字节这一DMA触发阈值:

FIFO可理解为SPI控制器的"暂存缸":CPU把数据舀进缸里,硬件自行舀出发送;当一次发送的数据量 ≥ FIFO长度时,瑞芯微SPI控制器驱动会自动触发DMA搬运,直接由DMA从内存往FIFO里"倒",CPU可以转去执行其他任务。
这恰好契合WS2812的场景:短灯带数据量少,CPU推FIFO即可;长灯带自动切换DMA。 驱动代码本身无需关心传输细节,只需调用spi_sync(),底层自动选择最优路径。
04
实测定量验证逻辑分析仪抓取真实波形
使用24 MHz逻辑分析仪抓取实际波形,验证编码方案的可靠性。
实测结果(抓取波形为0码元,对应SPI码元1000b):

说明: 逻辑分析仪采样率为24 MHz,精度约±40 ns,实测数据仅供参考。0码元实测值与理论值基本吻合,均落在时序容差内;1码元(1110b,理论938 ns / 312 ns)同样完全落在T1H(580~1000 ns)/ T1L(220~420 ns)容差内。实际点灯显示稳定,也从侧面印证了编码的可靠性,证明SPI码元映射方案可行。

05
字符设备驱动八步实现
驱动以ws2812_spi.c为例,按"每步可编译"的方式逐步搭建——每一步结束后,当前代码都能独立通过编译并insmod验证,可随时停下来make && insmod确认无误再继续。
该驱动同时扮演两个角色:SPI客户机驱动(挂在SPI总线上,设备树匹配成功后由内核回调probe())与字符设备驱动(向用户层提供/dev/ws2812_spiX,实现open/read/write/ioctl)。
八步总览:
① 最小模块骨架 → ② probe硬件初始化 →
③ 字符设备注册四件套 → ④ open取回设备 →
⑤ write数据通路(核心) → ⑥ read回读 →
⑦ ioctl动态调参 → ⑧ remove逆序清理

5.1 write():核心数据通路
write()实现整条数据流水线:编码 → SPI发送 → 落锁保护。
staticssize_tws2812_write(structfile *file,constchar__user *buf, size_tcount,loff_t*ppos){ structws2812_dev*dev = file->private_data; size_texpected = (size_t)dev->num_leds *3; /* 每颗灯 3 字节 RGB */ u8 *rgb; ssize_tret; if(count != expected) /* 必须一次写满整帧,否则拒绝 */ return-EINVAL; rgb =kmalloc(expected, GFP_KERNEL); if(!rgb) return-ENOMEM; if(copy_from_user(rgb, buf, expected)) { /* 内核态 ← 用户态 */ kfree(rgb); return-EFAULT; } mutex_lock(&dev->lock); /* 防止 ioctl 并发修改 num_leds 导致缓冲区越界 */ if(count != (size_t)dev->num_leds *3) { /* 二次检查 */ mutex_unlock(&dev->lock); kfree(rgb); return-EINVAL; } memcpy(dev->rgb_buf, rgb, expected); /* 存一份供 read 回读 */ ws2812_encode_frame(dev, rgb); /* RGB → SPI 码元 */ ret =ws2812_send(dev); /* SPI 发送 */ mutex_unlock(&dev->lock); kfree(rgb); returnret < 0 ? ret : count;}
SPI发送函数(含复位锁存):
staticintws2812_send(structws2812_dev *dev){ structspi_transfer t = { .tx_buf = dev->tx_buf, .len = dev->tx_len, .bits_per_word =8, .speed_hz = dev->spi->max_speed_hz, }; structspi_message msg; spi_message_init(&msg); spi_message_add_tail(&t, &msg); spi_sync(dev->spi, &msg); /* len ≥ 64 时底层自动走 DMA */ /* WS2812 要求最后一个数据位后保持 ≥280 μs 低电平锁存 */ udelay(WS2812_RESET_US); /* 300 μs */ return0;}
设计要点:write()全程持有dev->lock,且在拿锁后二次检查长度——防止拷贝数据期间有别的进程通过ioctl改了LED数量,导致编码越界。SPI核心层用"消息(message)→ 传输(transfer)"两级结构描述一次通信,即使只有一段transfer,也必须走init → add_tail → sync的流程。
5.2 ioctl():动态调整灯珠数量
命令码通过_IOW/_IOR宏打包为32 bit,内核可在调用前校验方向与数据长度,减少越界风险:
#defineWS2812_IOC_MAGIC 'W'#defineWS2812_IOC_SET_COUNT _IOW(WS2812_IOC_MAGIC,1, u32) /* 设置灯珠数 */#defineWS2812_IOC_GET_COUNT _IOR(WS2812_IOC_MAGIC,2, u32) /* 读取灯珠数 */

SET_COUNT的实现体现了"先校验、再加锁、后变更"的 ioctl 写法,配套ws2812_realloc_bufs()重新分配发送缓冲与回读缓冲。
06
设备树配置与加载验证
驱动通过compatible属性与设备树节点匹配,在SPI控制器节点下添加灯带节点即可(以spi0、CS0为例):
&spi0 { ws2812@0{ compatible ="worldsemi,ws2812-spi"; reg = <0>; spi-max-frequency = <3200000>; led-count = <24>; };};
各属性说明:

注意:
led-count是必填项,驱动读不到直接返回-ENODEV;
spi-max-frequency不可随意更改:它直接决定码元宽度,修改后时序将不合规;
SPI core匹配时modalias会剥掉厂商前缀变为ws2812-spi,因此驱动里同时提供同名的spi_device_id表以支持模块自动加载。
板端验证:
# 点红灯(第一颗)printf'\xff\x00\x00'> /dev/ws2812_spi0# 回读当前灯珠状态hexdump -C /dev/ws2812_spi0# 查看内核日志dmesg | grep ws2812
写在最后
综上所述,这套方案的核心价值在于把"时序生成"和"数据搬运"两件事都交给了硬件:SPI控制器负责产生精确波形,其精度只取决于晶振而非CPU调度;DMA负责在数据超过FIFO阈值时自动搬运;CPU仅承担RGB编码这一件事,且在长灯带场景下编码完成后即可转去执行其他任务。
这种"用硬件外设替代软件bit-bang"的思路,在嵌入式驱动开发中具有普适性——无论是SK6812、DHT11,还是红外NEC协议,凡是依赖严格时序的单总线设备,都可以复用"硬件外设造时序 + DMA解放CPU"这一范式。工程师需要做的,只是找到合适的外设时钟频率与码元映射关系。
- Linux字符设备驱动案例:用SPI+DMA点亮WS2812,从时序拼凑到驱动八步实现
- 士兰微电子精彩亮相2026秋季亚洲充电展
- 电容触摸薄膜开关工作原理与关键工艺路线:从导电回路到成品测试
- 立讯精密顺利完成Ultra96 HDMI Cable官方认证
- 哈交通部:今年前8个月“中间走廊”运输量同比增长22%
- 甘肃省委:坚决拥护党中央对雷思维开除党籍、开除公职的决定
- 中国代表:政治外交解决才是伊朗核问题出路
- 偷拍未公开战机、撬锁擅闯军事禁区 国安部披露多个案件详情
- 第十五届中国知识产权年会在北京开幕 中外代表8000余人与会
- 美籍专家“灵魂质问”西方抹黑:都没来过中国,怎能随意评判?
- “沙德尔”残涡继续影响福建等地 北方降雨发展气温将下降
- 四川绵阳北川县发生4.8级地震 暂未收到伤损报告
- 愿同中国持续巩固永久全面战略伙伴关系——访哈萨克斯坦总统托卡耶夫
- 长光卫星建成全球规模最大立体测绘星群
- 特朗普签署行政令 启动筹建“美国太空学院”
- 镜观中国|思想之光领航 实干书写答卷