跳转至

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 模式。

搜索