实验 12:WS2812 RGB LED 驱动
🛒 开发板: EK301 GW5A FPGA 开发板 芯片: GW5A-LV25UG324C1/I0 (Arora V 系列)
一、实验目的
- 理解 WS2812 单线通信协议——通过单根数据线传输 24-bit RGB 颜色数据。
- 掌握精密时序生成技术:在 50MHz 时钟下产生纳秒级精度的脉冲序列。
- 学习用状态机 + 移位寄存器实现串行数据发送。
- 掌握亮度调节的简单实现——右移截断法(bit-shift dimming)。
二、实验过程
2.1 硬件连接
| 信号 | 方向 | FPGA 引脚 | Bank | 说明 |
|---|---|---|---|---|
| CLK | 输入 | T9 | Bank 4 | 50MHz 系统时钟 |
| KEY1 | 输入 | C10 | Bank 1 | 切换颜色 |
| KEY2 | 输入 | A10 | Bank 1 | 亮度 + |
| KEY3 | 输入 | B9 | Bank 0 | 亮度 - |
| RGB_DATA | 输出 | R8 | Bank 5 | WS2812 数据线 |
EK301 板载 1 颗 WS2812-2020 RGB LED。数据线仅需 1 根 GPIO。
2.2 WS2812 协议解析
WS2812 使用单线归零码(NRZ) 协议,通过不同占空比的脉冲区分 bit 0 和 bit 1:
Bit 0: ┌───┐────────────────────────┐
│ │ │
└───┘────────────────────────┘
←0.4µs→←────── 0.85µs ──────→
高电平 低电平
Bit 1: ┌──────────┐───────────────┐
│ │ │
└──────────┘───────────────┘
← 0.8µs →←── 0.45µs ──────→
高电平 低电平
RESET: ────────────────────────
←──── > 280µs 低电平 ───→
数据传输格式:24-bit GRB 格式(绿→红→蓝,每色 8-bit),高位在前。
[G7 G6 ... G0] [R7 R6 ... R0] [B7 B6 ... B0]
←── 绿色 ──→ ←── 红色 ──→ ←── 蓝色 ──→
⚠️ WS2812 的颜色顺序是 GRB 而非 RGB!绿色在前,红色在中,蓝色在后。
50MHz 时钟下的时序参数(T_clk = 20ns):
| 参数 | 标准值 | 50MHz 计数值 | 说明 |
|---|---|---|---|
| T0H | 0.4µs | 20 | 0 码高电平(400ns) |
| T0L | 0.85µs | 43 | 0 码低电平(850ns) |
| T1H | 0.8µs | 40 | 1 码高电平(800ns) |
| T1L | 0.45µs | 23 | 1 码低电平(450ns) |
| BIT_TOTAL | 1.25µs | 63 | 每位总周期 |
| RST | >280µs | 15000 | 复位低电平(300µs) |
实际 WS2812 对时序容差较大(±150ns),以上参数在容差范围内。
2.3 7 种颜色预置
| 索引 | 颜色 | GRB 编码(24-bit) | 说明 |
|---|---|---|---|
| 0 | 🟢 绿 | 24'h00FF00 | G=255, R=0, B=0 |
| 1 | 🔴 红 | 24'h0000FF | G=0, R=255, B=0 |
| 2 | 🔵 蓝 | 24'hFF0000 | G=0, R=0, B=255 |
| 3 | 🟡 黄 | 24'hFFFF00 | G=255, R=255, B=0(红+绿) |
| 4 | 🩵 青 | 24'hFF00FF | G=255, R=0, B=255(绿+蓝) |
| 5 | 🟣 品红 | 24'h00FFFF | G=0, R=255, B=255(红+蓝) |
| 6 | ⚪ 白 | 24'hFFFFFF | G=255, R=255, B=255 |
2.4 亮度调节(Bit-shift Dimming)
通过右移截断实现 5 档亮度:
wire [7:0] gr = colors[color_idx][23:16] >> (4 - bright);
| bright | 右移位数 | 效果 | 示例(255→) |
|---|---|---|---|
| 0 | 4 | 最暗(/16) | 255 → 15 |
| 1 | 3 | 较暗(/8) | 255 → 31 |
| 2 | 2 | 中等(/4) | 255 → 63 |
| 3 | 1 | 较亮(/2) | 255 → 127 |
| 4 | 0 | 最亮(/1) | 255 → 255 |
- 上电默认
bright=2(中等亮度)。 - KEY2 增加亮度(bright+1),KEY3 降低亮度(bright-1)。
bright范围 [0, 4],边界检查防止溢出。
2.5 发送状态机
发送完24bit
┌──→ S_BIT ──→ S_RST ──→ 完成 → 重新加载颜色 → S_BIT
│ ↑ │
│ │ bit_idx < 24 │
│ └────────────────────┘
│
└── phase 0..62 → 产生 bit 波形
2.6 模块设计
模块名:ws2812_rgb
端口:
| 端口 | 方向 | 位宽 | 说明 |
|---|---|---|---|
clk | input | 1 | 50MHz 系统时钟 |
key | input | [2:0] | KEY1/1/2 |
rgb_data | output | 1 | WS2812 数据线 |
三、代码逐段讲解
3.1 颜色选择与亮度控制
reg [2:0] color_idx;
reg [2:0] bright;
initial begin
color_idx = 0;
bright = 2;
end
always @(posedge clk) begin
if (kp[0]) color_idx <= (color_idx == 6) ? 0 : color_idx + 1;
if (kp[1] && bright < 4) bright <= bright + 1;
if (kp[2] && bright > 0) bright <= bright - 1;
end
color_idx在 0~6 之间循环,KEY1 每次按下前进一种颜色。bright带边界保护:< 4和> 0防止溢出。- 上电默认绿色、中等亮度。
3.2 亮度衰减计算
wire [7:0] gr = colors[color_idx][23:16] >> (4 - bright);
wire [7:0] rr = colors[color_idx][15:8] >> (4 - bright);
wire [7:0] br = colors[color_idx][7:0] >> (4 - bright);
colors[color_idx]是 24-bit{G, R, B}。bright=4→>> 0→ 原始亮度。bright=0→>> 4→ 1/16 亮度。- 组合逻辑,颜色或亮度变化时立即更新。
3.3 发送状态机(核心)
always @(posedge clk) begin
if (!sending) begin
// 开始新一帧:加载24-bit颜色数据
shift <= {gr, rr, br};
bit_idx <= 0;
sending <= 1;
phase <= 0;
end else begin
if (phase < BIT_TOTAL - 1)
phase <= phase + 1;
else begin
phase <= 0;
if (bit_idx < 24) begin
bit_idx <= bit_idx + 1;
shift <= {shift[22:0], 1'b0}; // 左移,最低位补 0
end else begin
sending <= 0; // 24-bit 发送完毕,进入 RESET
rst_cnt <= 0;
end
end
end
end
逐段解析:
!sending:空闲状态,加载新颜色数据到移位寄存器,启动发送。phase < BIT_TOTAL-1:当前 bit 还未完成,phase继续自增。phase == BIT_TOTAL-1+bit_idx < 24:当前 bit 完成,移动到下一位——bit_idx+1,shift左移 1 位,最高位(shift[23])成为新的当前 bit。- 24 bit 全部发完:
sending=0,进入复位间隙(rst_cnt)。
3.4 bit 波形输出
if (sending && bit_idx < 24)
rgb_data <= (phase < (shift[23] ? T1H : T0H)) ? 1 : 0;
else
rgb_data <= 0;
shift[23]是当前 bit:1 → 用 T1H/T1L 时序,0 → 用 T0H/T0L 时序。phase < T1H(或T0H)→ 输出高电平;否则输出低电平。- 不在发送期间 → 输出 0(低电平 = RESET 条件)。
四、实验现象
| 操作 | 预期结果 |
|---|---|
| 上电 | WS2812 显示绿色,中等亮度 |
| 按 KEY1 一次 | 切换为红色 |
| 按 KEY1 多次 | 依次显示:绿→红→蓝→黄→青→品红→白→绿... |
| 按 KEY2 | 亮度增加一档(更亮) |
| 按 KEY3 | 亮度降低一档(更暗) |
| 连续按 KEY2 到最亮 | 颜色鲜艳明亮 |
| 连续按 KEY3 到最暗 | 颜色微弱但可见 |
验证要点:
- ✅ 7 种颜色都能正确显示,色彩纯正(无偏色)。
- ✅ 亮度 5 档有明显区分,最暗档仍可见。
- ✅ 颜色切换时无闪烁、无错色。
- ✅ WS2812 在无按键时保持当前颜色(持续刷新)。
- ✅ 如果 LED 显示异常(颜色错乱、不亮),检查 CST 中
rgb_data引脚是否为 R8。
五、掌握知识点
| 知识点 | 说明 | 在本实验中的体现 |
|---|---|---|
| 单线协议 | 1 根数据线传输数据和时钟 | WS2812 NRZ 编码 |
| 精密时序 | 纳秒级脉冲宽度控制 | 50MHz 下 20ns 精度 |
| NRZ 编码 | 不同占空比区分 0/1 | T0H/T0L vs T1H/T1L |
| 移位寄存器 | 串行输出,每周期移出 1 bit | shift <= {shift[22:0], 1'b0} |
| Bit-shift 调光 | 右移截断降低亮度 | >> (4-bright) |
| GRB 颜色序 | WS2812 特定颜色顺序 | {G,R,B} 而非 {R,G,B} |
附录:源码
top.v
// =============================================================================
// EK301 实验10: WS2812 RGB LED驱动
// KEY1:切换颜色 KEY2:亮度+ KEY3:亮度- 7色循环
// 每50ms自动刷新, 24bit GRB + 300µs复位码
// =============================================================================
module ws2812_rgb (input wire clk, input wire [2:0] key, output reg rgb_data);
// ---- 按键消抖 ----
wire [2:0] kf, ks, kp;
assign kp = kf & ~ks;
key_debounce #(.MS(20)) u0(.clk(clk),.reset_n(1'b1),.key_in(key[0]),.key_flag(kf[0]),.key_state(ks[0]));
key_debounce #(.MS(20)) u1(.clk(clk),.reset_n(1'b1),.key_in(key[1]),.key_flag(kf[1]),.key_state(ks[1]));
key_debounce #(.MS(20)) u2(.clk(clk),.reset_n(1'b1),.key_in(key[2]),.key_flag(kf[2]),.key_state(ks[2]));
// ---- WS2812时序 (50MHz, 20ns) ----
// 0码: 0.4µs高 + 0.85µs低 = 20cyc + 43cyc
// 1码: 0.8µs高 + 0.45µs低 = 40cyc + 23cyc
localparam T0H=20, T0L=43, T1H=40, T1L=23, RST=15000; // 300µs
localparam BIT_LEN = 63; // max(T0H+T0L, T1H+T1L)
// ---- 7色 {G,R,B} 顺序: 红绿蓝黄青品白 ----
reg [2:0] color_idx; reg [2:0] bright; // 0-4档
reg [23:0] color;
initial begin color_idx=0; bright=4; end // 默认红色, 最大亮度
always @(posedge clk) begin
if (kp[1] && bright<4) bright <= bright+1;
if (kp[2] && bright>0) bright <= bright-1;
if (kp[0]) color_idx <= (color_idx==6) ? 0 : color_idx+1;
end
always @(*) case(color_idx)
0: color = 24'h0000FF; // 红
1: color = 24'h00FF00; // 绿
2: color = 24'hFF0000; // 蓝
3: color = 24'h00FFFF; // 黄(R+G)
4: color = 24'hFFFF00; // 青(G+B)
5: color = 24'hFF00FF; // 品红(R+B)
6: color = 24'hFFFFFF; // 白
default: color = 24'h000000;
endcase
// 亮度调节 (每个分量独立移位, 避免分量间串位)
wire [7:0] gr = color[23:16] >> (4-bright);
wire [7:0] rr = color[15:8] >> (4-bright);
wire [7:0] br = color[7:0] >> (4-bright);
wire [23:0] c = {gr, rr, br};
// ---- 50ms定时刷新 ----
localparam REFRESH = 50_000_000 / 20 - 1; // 50ms
reg [21:0] refresh_cnt;
reg start_send;
always @(posedge clk) begin
if (refresh_cnt == REFRESH) begin
refresh_cnt <= 0;
start_send <= 1;
end else begin
refresh_cnt <= refresh_cnt + 1;
start_send <= 0;
end
end
// ---- 发送状态机 ----
reg [4:0] bit_cnt; // 0-23
reg [5:0] phase; // 0-62
reg [15:0] rst_cnt;
reg [23:0] shift;
reg busy;
always @(posedge clk) begin
if (start_send && !busy) begin
shift <= c;
bit_cnt <= 0;
phase <= 0;
busy <= 1;
end else if (busy) begin
if (bit_cnt < 24) begin
// 发送一个bit
if (phase < BIT_LEN-1)
phase <= phase + 1;
else begin
phase <= 0;
bit_cnt <= bit_cnt + 1;
shift <= {shift[22:0], 1'b0};
end
// 输出: 0码或1码
rgb_data <= (phase < (shift[23] ? T1H : T0H)) ? 1 : 0;
end else begin
// 发送复位码 (>280µs低电平)
if (rst_cnt < RST) begin
rst_cnt <= rst_cnt + 1;
rgb_data <= 0;
end else begin
rst_cnt <= 0;
busy <= 0;
rgb_data <= 0;
end
end
end else begin
rgb_data <= 0;
end
end
endmodule