跳到主要内容

实验 12:WS2812 RGB LED 驱动

🛒 开发板: EK301 GW5A FPGA 开发板 芯片: GW5A-LV25UG324C1/I0 (Arora V 系列)


一、实验目的

  1. 理解 WS2812 单线通信协议——通过单根数据线传输 24-bit RGB 颜色数据。
  2. 掌握精密时序生成技术:在 50MHz 时钟下产生纳秒级精度的脉冲序列。
  3. 学习用状态机 + 移位寄存器实现串行数据发送。
  4. 掌握亮度调节的简单实现——右移截断法(bit-shift dimming)。

二、实验过程

2.1 硬件连接

信号方向FPGA 引脚Bank说明
CLK输入T9Bank 450MHz 系统时钟
KEY1输入C10Bank 1切换颜色
KEY2输入A10Bank 1亮度 +
KEY3输入B9Bank 0亮度 -
RGB_DATA输出R8Bank 5WS2812 数据线

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 计数值说明
T0H0.4µs200 码高电平(400ns)
T0L0.85µs430 码低电平(850ns)
T1H0.8µs401 码高电平(800ns)
T1L0.45µs231 码低电平(450ns)
BIT_TOTAL1.25µs63每位总周期
RST>280µs15000复位低电平(300µs)

实际 WS2812 对时序容差较大(±150ns),以上参数在容差范围内。

2.3 7 种颜色预置

索引颜色GRB 编码(24-bit)说明
0🟢 绿24'h00FF00G=255, R=0, B=0
1🔴 红24'h0000FFG=0, R=255, B=0
2🔵 蓝24'hFF0000G=0, R=0, B=255
3🟡 黄24'hFFFF00G=255, R=255, B=0(红+绿)
4🩵 青24'hFF00FFG=255, R=0, B=255(绿+蓝)
5🟣 品红24'h00FFFFG=0, R=255, B=255(红+蓝)
6⚪ 白24'hFFFFFFG=255, R=255, B=255

2.4 亮度调节(Bit-shift Dimming)

通过右移截断实现 5 档亮度:

wire [7:0] gr = colors[color_idx][23:16] >> (4 - bright);
bright右移位数效果示例(255→)
04最暗(/16)255 → 15
13较暗(/8)255 → 31
22中等(/4)255 → 63
31较亮(/2)255 → 127
40最亮(/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

端口

端口方向位宽说明
clkinput150MHz 系统时钟
keyinput[2:0]KEY1/1/2
rgb_dataoutput1WS2812 数据线

三、代码逐段讲解

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

逐段解析

  1. !sending:空闲状态,加载新颜色数据到移位寄存器,启动发送。
  2. phase < BIT_TOTAL-1:当前 bit 还未完成,phase 继续自增。
  3. phase == BIT_TOTAL-1 + bit_idx < 24:当前 bit 完成,移动到下一位——bit_idx+1shift 左移 1 位,最高位(shift[23])成为新的当前 bit。
  4. 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/1T0H/T0L vs T1H/T1L
移位寄存器串行输出,每周期移出 1 bitshift <= {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