ws2812 幻彩灯带(WS2812/WS2812B)
一、概述
ws2812 是 LuatOS 里驱动 WS2812 / WS2812B 单线归零码灯带的核心库。一条数据线串起 N 颗灯珠,每颗灯珠吃掉 24 bit(G、R、B 各 8 bit),按「高电平长短表示 0/1」的时序逐位推下去,所有灯珠级联刷新。
要点:
1、模块名为 ws2812,由固件编译开关 LUAT_USE_WS2812 控制。并不是所有模组、所有固件号都编入了这个库(具体见第五节),脚本里先自检,避免直接报 nil:
if ws2812 then
log.info("ws2812", "当前固件包含 ws2812 库")
else
log.info("ws2812", "当前固件没有 ws2812 库, 可改用 sensor.ws2812b")
end
2、工作方式是先写缓冲、再整条刷新:ws2812.set() 只改内存里的颜色数组,ws2812.send() 才真正把整条灯带推出去。所以正确姿势是「循环 set 全部灯珠 → 调用一次 send」,不要 set 一颗 send 一次(那样每颗灯都会重新级联一遍,帧率极低还会有撕裂感)。
3、三种可用传输方式:GPIO(软件翻转,任意引脚)、PWM、SPI。RMT 和 HW 只是预留的常量名——开源实现里它们是空壳,send 会打印「该模式的ws2812发送方式未支持」并返回 false。
4、GPIO 模式必须先调 ws2812.args() 设时序。源码里 t0h/t0l/t1h/t1l 直接取自 args 数组,没有内置默认值;不设就是全 0,波形发不出去,表现为「灯全不亮」。实测可用参数见 4.4。
5、灯珠内部按 GRB 顺序排,但 API 一律用 RGB 传,库内部会换位,写代码时不用自己倒序。
6、同厂还有一条更「一次性」的老接口 sensor.ws2812b(pin, buff, t0h, t0l, t1h, t1l)(见 sensor 核心库),它不建上下文、直接把一段 zbuff 或字符串打出去,适合「画好一帧就发」的场景;需要反复改单颗颜色、维护帧缓冲时用本库更顺手。
二、核心示例
2.1 最小可用:GPIO 模式点亮 8 颗灯
-- GPIO 模式:数据脚任意可用 GPIO,这里用 GPIO16
local leds = ws2812.create(ws2812.GPIO, 8, 16)
if not leds then
log.error("ws2812", "create 失败,检查固件是否含 ws2812 库 / 灯珠数量是否合法")
return
end
-- 必须先设时序(ns),这组是合宙 22×22 灯板实测稳定的值
ws2812.args(leds, 20, 30, 35, 20, 0)
-- 写缓冲:第 0 颗红、第 1 颗绿、第 2 颗蓝
ws2812.set(leds, 0, 0xFF, 0x00, 0x00)
ws2812.set(leds, 1, 0x00, 0xFF, 0x00)
ws2812.set(leds, 2, 0x0000FF) -- 也支持一次传 0xRRGGBB
-- 整条刷新
ws2812.send(leds)
2.2 帧缓冲 + 定时刷新:彩虹流水灯
local COUNT = 64
local leds = ws2812.create(ws2812.GPIO, COUNT, 16)
ws2812.args(leds, 20, 30, 35, 20, 0)
-- HSV 转 24bit RGB
local function hsv2rgb(h, s, v)
local c = v * s / 255 / 255
local x = c * (1 - math.abs((h / 60) % 2 - 1))
local m = v / 255 - c
local r, g, b
if h < 60 then r, g, b = c, x, 0
elseif h < 120 then r, g, b = x, c, 0
elseif h < 180 then r, g, b = 0, c, x
elseif h < 240 then r, g, b = 0, x, c
elseif h < 300 then r, g, b = x, 0, c
else r, g, b = c, 0, x end
return math.floor((r + m) * 255 + 0.5) * 65536
+ math.floor((g + m) * 255 + 0.5) * 256
+ math.floor((b + m) * 255 + 0.5)
end
sys.taskInit(function()
local offset = 0
while true do
for i = 0, COUNT - 1 do
-- 亮度压到 50 左右,64 颗全白满亮电流也不小
ws2812.set(leds, i, hsv2rgb((i * 6 + offset) % 360, 255, 50))
end
ws2812.send(leds) -- 一帧只发一次
offset = (offset + 8) % 360
sys.wait(50)
end
end)
2.3 开机先清零,消掉「第一颗鬼影」
上电瞬间 DIN 引脚上的电气杂波可能被第一颗灯误判成数据位并锁存,表现是开机时第一颗灯随机亮一个颜色。做法是创建句柄后先整条清 0,稍等电源稳定再发一次:
local leds = ws2812.create(ws2812.GPIO, COUNT, 16)
ws2812.args(leds, 20, 30, 35, 20, 0)
for i = 0, COUNT - 1 do
ws2812.set(leds, i, 0)
end
ws2812.send(leds)
sys.taskInit(function()
sys.wait(50) -- 等电源和 DIN 稳定
for i = 0, COUNT - 1 do
ws2812.set(leds, i, 0)
end
ws2812.send(leds)
end)
三、常量详解
ws2812 的五个常量是 create 的传输模式,取值 1~5,和头文件 LUAT_WS2812_MODE_* 一一对应:
| 常量 | 值 | 含义 | id 传什么 |
可用性 |
|---|---|---|---|---|
| ws2812.GPIO | 1 | 软件翻转 GPIO 输出波形 | 数据引脚号,如 16 | ✅ 可用,最通用 |
| ws2812.PWM | 2 | 借用一路 PWM 输出高低电平 | PWM 通道号 | ✅ 可用,占一路 PWM |
| ws2812.SPI | 3 | 借用 SPI 把 0/1 编码成字节发出去 | SPI 号 | ✅ 可用,占一路 SPI |
| ws2812.RMT | 4 | 预留:乐鑫 RMT 类硬件外设 | 对照芯片手册 | ❌ 开源实现未支持 |
| ws2812.HW | 5 | 预留:厂商硬件加速发送 | 对照芯片手册 | ❌ 开源实现未支持 |
-- 三种可用模式的写法
local a = ws2812.create(ws2812.GPIO, 64, 9) -- GPIO9
local b = ws2812.create(ws2812.SPI, 32, 1) -- SPI1
local c = ws2812.create(ws2812.PWM, 16, 4) -- PWM 通道 4
注意:
SPI模式内部会按固定参数(主机、8 位、5 MHz、CPOL=0/CPHA=0)自己调spi.setup,用完即spi.close,不要再对同一路 SPI 做自己的配置;PWM模式同理,会按 800 kHz 周期反复改占空比。这两种模式本质是「抢外设」,同一路外设上不能再挂别的器件。
四、函数详解
4.1 ws2812.create(mode, count, id)
功能
创建一条灯带的上下文(userdata),内部按 count 一次性分配颜色缓冲(每颗 3 字节,GRB 排列)。
参数
mode
参数含义:传输模式
数据类型:number
取值范围:1 ~ 5,即 ws2812.GPIO / PWM / SPI / RMT / HW
是否必选:是
注意事项:超出 1~5 会打印「未知的模式」并返回 nil。
count
参数含义:灯珠总个数
数据类型:number
取值范围:1 ~ 4095
是否必选:是
注意事项:源码判断是 count <= 0 或 count >= 4096 都算非法,直接返回 nil。
id
参数含义:该模式的主参数
数据类型:number
取值范围:GPIO 模式为引脚号;SPI 模式为 SPI 号;PWM 模式为通道号
是否必选:是
注意事项:RMT/HW 模式需要对照芯片手册,且当前开源实现并未支持。
返回值
| 返回值类型 | 解释 |
|---|---|
| userdata | 创建成功,后续 set/send/args 都用它 |
| nil | 模式非法、灯数非法或内存不足 |
示例
local leds = ws2812.create(ws2812.GPIO, 64, 16)
if not leds then
log.error("ws2812", "创建失败")
end
提示:一次
create就固定了灯珠数量,之后不能改。灯带长度变化要重新 create(旧的句柄交给 GC 回收即可)。
4.2 ws2812.set(leds, index, R, G, B)
功能
设置某一颗灯的颜色,只写缓冲,不发送。
参数
leds
参数含义:ws2812.create 返回的上下文
数据类型:userdata
取值范围:必须是有效句柄
是否必选:是
注意事项:传 nil 会直接抛错,建议在 create 之后判空再用。
index
参数含义:灯珠编号
数据类型:number
取值范围:0 ~ count-1
是否必选:是
注意事项:从 0 开始;越界会打印「灯序号越界了」并返回 nil,不会写坏内存。
R / G / B
参数含义:红、绿、蓝分量
数据类型:number
取值范围:0 ~ 255
是否必选:R/G/B 要么三个都给,要么都不给
注意事项:三个都给时按分量写入;只给一个整数时按 0xRRGGBB 拆分(R 取 bit23~16、G 取 bit15~8、B 取 bit7~0)。
返回值
| 返回值类型 | 解释 |
|---|---|
| boolean | 设置成功返回 true |
| nil | 序号越界或参数形式不合法 |
示例
ws2812.set(leds, 5, 0xFF, 0xAA, 0x11) -- 逐分量
ws2812.set(leds, 5, 0xFFAA11) -- 等价写法(24bit 打包)
ws2812.set(leds, 5, 0) -- 熄灭这一颗
注意:单参数形式是按位拆的,
0xFFAA11拆出来是 R=0xFF、G=0xAA、B=0x11;如果本意是「把某个整数当亮度」,结果会完全对不上颜色。
4.3 ws2812.send(leds, fast_mode)
功能
把缓冲里的颜色整条发送到灯带。
参数
leds
参数含义:ws2812.create 返回的上下文
数据类型:userdata
取值范围:必须是有效句柄
是否必选:是
注意事项:无。
fast_mode
参数含义:是否走 GPIO 快速发送
数据类型:boolean
取值范围:true / false
是否必选:否,默认 false(非布尔值一律按 false 处理)
注意事项:只对 GPIO 模式有效,且要求固件编译时定义了 LUAT_WS2812B_MAX_CNT;其他模式强制按普通模式走。只适合少量灯珠——灯数不能超过「一个 os tick 时间 ÷ 单颗灯发送时间」。
返回值
| 返回值类型 | 解释 |
|---|---|
| boolean | 发送成功 true,失败 false(RMT/HW 模式必为 false) |
| int | 快速模式下为数据帧发送总耗时,单位 CPU tick(不含起始帧);非快速模式下该值是随机值,不要使用 |
示例
ws2812.send(leds) -- 普通模式,不用管返回值
local ok, t = ws2812.send(leds, true) -- 快速模式
if ok and t > 理论时间 * 1.2 then
-- 明显超时说明发送过程被中断打断过,这一帧可能花屏,需要重发
ws2812.send(leds, true)
end
⚠️ 两个坑:① 普通 GPIO 模式发送全程关中断(
luat_os_entry_cri/exit_cri),一颗灯 24 bit、约 30 µs,64 颗约 2 ms、484 颗约 15 ms,这期间串口接收、网络回调都会被推迟,灯多时请把刷新间隔放大或改用 SPI/PWM 模式;② 非快速模式下第二个返回值没有意义,别拿它做耗时统计。
4.4 ws2812.args(leds, arg0, arg1, arg2, arg3, arg4)
功能
给当前上下文配置额外参数。目前只有 GPIO 模式用到,作用就是校准 0/1 的时序。
参数
| 位置 | GPIO 模式含义 | 说明 |
|---|---|---|
| arg0 | T0H | 发「0」时的高电平时间,单位 ns |
| arg1 | T0L | 发「0」时的低电平时间,单位 ns |
| arg2 | T1H | 发「1」时的高电平时间,单位 ns |
| arg3 | T1L | 发「1」时的低电平时间,单位 ns |
| arg4 | 复位帧数 / 保留 | 仅在定义了 LUAT_WS2812B_MAX_CNT 的固件里作为复位帧参数使用 |
返回值
无。源码里是 return 0,不返回任何值(早期 wiki 文档写成返回 boolean,是错的)。
示例
-- Air1780P/H 22×22 灯板实测稳定的一组:T0H=20 T0L=30 T1H=35 T1L=20,复位帧 0
ws2812.args(leds, 20, 30, 35, 20, 0)
提示:这组值来自官方 demo(
module/Air780EPM/demo/ws2812/ws2812_config.lua),适用于多数 WS2812B 灯带/灯板。换灯带后若出现颜色错乱、末端闪烁,优先微调这四个值;调之前一定要先设,不设就是全 0,一帧都发不出去。
五、产品支持与固件差异
ws2812 是否可用,取决于该固件的 LUAT_USE_WS2812 有没有被打开。以当前 luatos-soc-2024 源码为准:
| 平台 | 是否支持 | 具体固件号 |
|---|---|---|
| Air780EPM | 部分 | 4、5 号(及对应的 104、105 号)开启,其余未编入 |
| Air780EHM | 是 | ec718hm 全系开启,18、118 号除外(post 配置里 undef 掉了) |
| Air780EHV | 是 | 同上(与 EHM 共用 ec718hm 配置) |
| Air780EGH | 是 | 同上 |
| Air8000 | 是 | 同上(18、118 号除外) |
| Air8101 | 否 | 无该库 |
| Air1780P | 是 | 100、101、102 号 |
| Air1780H | 是 | 100、101 号 |
| Air700ECH | 是 | ec718hm 全系(18、118 号除外) |
三种传输模式的可用性也不一样:
| 模式 | 依赖 | 说明 |
|---|---|---|
| GPIO | LUAT_USE_GPIO |
通用;快速模式还要求 LUAT_WS2812B_MAX_CNT |
| SPI | LUAT_USE_SPI |
未开 SPI 的固件里该模式会被编译掉 |
| PWM | LUAT_USE_PWM |
未开 PWM 的固件里该模式会被编译掉 |
| RMT / HW | 无 | 开源实现注册的是 send_notok,调用只会打日志并返回 false |
⚠️ RMT 和 HW 是给厂商硬件加速留的位置,不是「乐鑫/某些芯片专属但能用」。当前开源固件里调用
ws2812.create(ws2812.RMT, ...)不会报错(create 只检查 1~5),但一send就失败,很容易误判成「灯坏了」。
六、常见问题
1. 灯带完全不亮
按顺序查三处:① ws2812.args() 有没有调用(GPIO 模式必须设时序,不设全 0);② 数据线接的是不是 create 里传的那个引脚;③ 灯带电源与模组是否共地——WS2812 是 5 V 器件,模组 3.3 V 输出高电平能否被识别为「1」取决于灯带型号,电平裕量不足时会出现前面几颗亮、后面不亮或整体偏色,可在数据线串 100~470 Ω 并加 5 V 上拉,或加电平转换。
2. 只有第一颗灯亮,后面全黑
说明第一颗之后没有数据出去:数据线方向接反(灯带有 DI/DO 方向,必须从 DI 进)、灯带中间断线,或者灯珠型号不是 WS2812(如 SK6812 的 RGBW 是 32 bit 一颗,用本库按 24 bit 发会整体错位)。
3. 颜色错乱、频闪、末端颜色漂移
时序不匹配。先把 args 换成官方 demo 的 20, 30, 35, 20, 0 试;仍不稳就说明线太长或干扰大,缩短数据线、加屏蔽/串阻,或改用 SPI/PWM 模式(硬件产生波形,比软件延时稳)。
4. send 返回 false,日志里有「该模式的ws2812发送方式未支持」
用了 ws2812.RMT 或 ws2812.HW。改用 GPIO / SPI / PWM。
5. create 一直返回 nil
三种可能:模式不在 1~5、灯数小于 1 或大于等于 4096、内存不足。灯珠很多时(如 484 颗)缓冲区约 1.5 KB,模组 Lua 内存紧张时会失败,先 rtos.meminfo("lua") 看看剩余空间。
6. 灯带全白时整机复位 / 电压跌落
不是软件问题,是电流。一颗 WS2812 全白约 50~60 mA,484 颗满亮峰值 25~30 A,必须独立 5 V 电源、足够粗的线、并在电源端加大电容。软件上把亮度压到 50 以下(demo 默认就是 50),并避免长时间全白。
7. 刷新时串口丢数据、网络卡顿
GPIO 普通模式发送期间是关中断的(见 4.3 的注意事项)。灯多时降低刷新频率,或改用 SPI/PWM 模式。