文章目录
- 一、WS2812手册分析
- 1.1 WS2812灯源特性及概述
- 1.2 手册重点内容分析
- 1.2.1 产品概述
- 1.2.2 码型及24bit数据设计
- 二、系统设计
- 2.1 模块设计
- 2.2 模块分析
- 2.2.1 驱动模块
- 2.2.1 数据控制模块
- 三、IP核设置及项目源码
- 3.1 MIF文件设计
- 3.2 ROM IP核调用
- 3.3 FIFO IP核调用
- 3.4 项目各模块源码
- 四、最终显示效果
- 五、总结
一、WS2812手册分析
1.1 WS2812灯源特性及概述
1.2 手册重点内容分析
1.2.1 产品概述
由产品概述可以得到的重要信息有:
- 该灯源数据协议采用单线归零码的通讯方式
- 一个像素点需要24bit数据才能正常工作(该灯板共有8×8 64个像素点)
- 复位时间需要至少280us
- 传输数据每经过一个像素点便会被锁存24bit数据,因此数据会逐级减少
- 数据发送速度最高为800Kbps
1.2.2 码型及24bit数据设计
由上述手册截图可以看出,该光源0码和1码占空比并不相同,同时一个码元持续时间也可以不同,因此在设计码型时,一个码元持续时间需要同时满足0码和1码。
数据传输方法与前文概述所说一样,没经过一个像素点便会被锁存24bit数据,然后继续逐级传输,同时每一次24bit数据传输结束需要经过经过至少280us的复位才能继续传输下一个24bit数据。
同时该光源所需24bit数据结构为GRB顺序,在设计时需要将RGB数据进行重新拼接,该拼接过程可以在传入数据时,也可以在传出数据时。
二、系统设计
2.1 模块设计
设计本项目时,建议将模块划分为:驱动模块,控制模块,以及顶层模块。
- 在控制模块中设计不同数据的输入
- 在驱动模块中设计码型以及输入数据的拼接及输出
- 顶层模块仅进行逻辑连线,不建议在顶层模块进行任何逻辑编写
2.2 模块分析
2.2.1 驱动模块
在驱动模块中,首先我们主要考虑的是64个24bit数据的传输。
-
由上述分析可知,每24bit数据传输间隔中我们需要至少280us的复位,复位结束才能传输下一个24bit数据,因此我们可以设计一个状态机实现数据传输与复位的状态切换。
-
同时我们可以设计一个ready使能信号,将该信号传输给控制模块,该信号拉高后代表驱动模块处于空闲状态,可以进入复位状态。
-
同时我们可以在控制模块设计一个data_vld信号,将该信号输入驱动模块,该信号拉高驱动模块应该立即进入复位状态,并在复位结束后即刻准备接受数据。
-
再由手册的介绍可知,在该模块我们至少需要四个计数器,分别为:
-
- 24bit数据计数器,计数从控制模块传来的数据量。
-
- 64个像素点计数器,该灯板共有8×8 64个像素点,每个像素点均需要24bit数据。
-
- 一个码元持续时间计数器,由于该光源1码和0码要求时间可以不同,因此我们需要取一个能同时满足0码和1码的值作为一个码元的传输时间。博主的一个码元所需传输时间设置为1200ns(既能满足0码(220ns-380ns的高电平时间以及580ns-1us的低电平时间)也能满足1码高低电平时间(均为580ns-1us))。
-
- 复位时间计数器,至少为280us,博主设计为400us。
-
同时,由于该光源数据格式为GRB,因此我们还需要在驱动模块对传入的RGB数据重新进行拼接。
-
此外,由于时钟数据采用频率为50MHz,而该光源发送速率只有800Kbps,传入数据的速度远大于传出数据的速度,因此博主调用了一个FIFO核,用来临时存储传入驱动模块的数据,避免造成数据丢失。
该模块状态机设计如下:
其中,end_cnt_rst为复位计数器结束信号,end_cnt_pix为64个像素点计数器结束信号。
2.2.1 数据控制模块
在该模块我们所需要的考虑仅为所需数据的存储以及如何传入驱动模块。
- 为了存储我们设计好的数据,博主在该模块调用了一个ROM IP核。
- 同时博主在该模块设计了X,Y两个计数器,计数最大值均为8。将X,Y作为光源像素点的坐标从而确定ROM中所要取得的数据。
- 为了方便设计并存储数据,在调用ROM时需要设计MIF文件初始化ROM,在后文博主会将自己设计MIF文件的方式和工具分享给大家。
三、IP核设置及项目源码
3.1 MIF文件设计
打开系统自带的画图工具,点击左上角文件,选择图像属性
在图像属性中设置图像为 32×8(大家可以根据自己所需自由设计宽度(如果自定义宽度,就需要在ROM核设置的时候进行更改,同时需要在博主的数据控制模块进行一个小细节的更改,博主会在后文标明),但高度固定因为动态显示相当于滚动移动宽度,无法移动高度)
然后在画图中设计自己想要显示的图案即可
设计完成后点击右上角文件另存为24位宽BMP文件,注意必须为24位宽
然后利用格式转换工具,将该BMP文件转化为mif文件
然后将生成的文件存放在自己创建的quartus prj文件夹下
3.2 ROM IP核调用
在quartus IP Catalog中搜索ROM并选择单端口ROM
按照下图进行设置:
- 输出数据设置为24bit宽
- 深度设置为256,若BMP图像高度不是博主设置的32,各位需要自行更改所需深度,将其改为:你所设置的宽度×8
点击next,按下图进行如下设置:
- 将输出q寄存一拍(勾选则在后续读使能需要打两拍,博主个人习惯,可以不选,后续只需打一拍)
- 勾选异步复位信号
- 勾选读使能信号
点击next按照下图进行如下设置:
- 点击Browse选择前文所设置的MIF文件,对ROM进行初始化
一路next至总结界面:
- 勾选例化模板
最终设置如下:
点击finish完成ROM IP核配置
3.3 FIFO IP核调用
在IP Catalog中搜索FIFO,进入配置选项卡
- 数据宽度同样为24bit,深度同ROM
- 由于本项目为跨时钟域,因此FIFO读写选择单时钟即可
点击next,按下图进行设置:
- 空、满信号可用于确定读写请求
- usew可在仿真时看到FIFO中的数据个数(此处不勾选,不仿真用不到)
- 勾选异步复位信号
点击next,按下图进行配置:
- 勾选前显模式
一路next至总结界面:
- 勾选例化模板
FIFO总体配置如下:
3.4 项目各模块源码
驱动模块:
module ws2812_driver (input wire clk ,input wire rst_n ,input wire [23:0] pix_data ,//输入数据input wire pix_data_vld,//输入数据有效信号output wire ready ,output reg ws2812_io //输出码元信号
);//内部参数定义//状态参数定义parameter IDLE = 3'b001 ,RST = 3'b010 ,DATA = 3'b100 ;//码元电平参数定义parameter T0H = 300/20 ,T0L = 900/20 ,T1H = 600/20 ,T1L = 600/20 ;//复位码元计数参数parameter RST_MAX = 14'd15_000;//内部信号定义//现态、次态寄存器reg [2:0] cstate ;reg [2:0] nstate ;//跳转条件定义wire idle2rst ;wire rst2data ;wire data2idle ;//一个码元时间计数器reg [5:0] cnt_time ;wire add_cnt_time ;wire end_cnt_time ;//24bit计数器reg [4:0] cnt_bit ;wire add_cnt_bit ;wire end_cnt_bit ;//64个数据计数器reg [5:0] cnt_pix ;wire add_cnt_pix ;wire end_cnt_pix ;//复位码元计数器reg [13:0] cnt_reset ;wire add_cnt_reset ;wire end_cnt_reset ;//FIFO信号定义wire [23:0] fifo_wr_data ;wire fifo_rd_req ;wire fifo_wr_req ;wire fifo_empty ;wire fifo_full ;wire [23:0] fifo_rd_data ;wire [7:0] fifo_usedw ;//FIFO例化fifo fifo_inst (.aclr ( ~rst_n ),.clock ( clk ),.data ( fifo_wr_data ),.rdreq ( fifo_rd_req ),.wrreq ( fifo_wr_req ),.empty ( fifo_empty ),.full ( fifo_full ),.q ( fifo_rd_data ),.usedw ( fifo_usedw ));assign fifo_wr_data = {pix_data[15:8],pix_data[23:16],pix_data[7:0]};//将传入的24bit RGB数据转为GRB顺序//24'hff0000;assign fifo_wr_req = pix_data_vld && ~fifo_full;//1'b1 && ~fifo_full;//assign fifo_rd_req = end_cnt_bit && ~fifo_empty;//三段式状态机//第一段always @(posedge clk or negedge rst_n) beginif(!rst_n)begincstate <= IDLE;endelse begincstate <= nstate;endend//第二段组合逻辑always@(*)begincase(cstate)IDLE : beginif(idle2rst)beginnstate = RST;endelse beginnstate = cstate;endendRST : beginif(rst2data)beginnstate = DATA;endelse beginnstate = cstate;endendDATA : beginif(data2idle)beginnstate = IDLE;endelse beginnstate = cstate;endenddefault : nstate <= cstate;endcaseendassign idle2rst = cstate == IDLE && pix_data_vld ;//1'b1;//assign rst2data = cstate == RST && end_cnt_reset ;assign data2idle = cstate == DATA && end_cnt_pix ;//码元最低持续时间计数器always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt_time <= 'd0;end else if(add_cnt_time)begin if(end_cnt_time)begin cnt_time <= 'd0;endelse begin cnt_time <= cnt_time + 1'b1;end endend assign add_cnt_time = cstate == DATA;assign end_cnt_time = add_cnt_time && cnt_time == 6'd59;//一位数据的24bit数计数器always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt_bit <= 'd0;end else if(add_cnt_bit)begin if(end_cnt_bit)begin cnt_bit <= 'd0;endelse begin cnt_bit <= cnt_bit + 1'b1;end endend assign add_cnt_bit = end_cnt_time;assign end_cnt_bit = add_cnt_bit && cnt_bit == 5'd23;//64个数据计数器always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt_pix <= 'd0;end else if(add_cnt_pix)begin if(end_cnt_pix)begin cnt_pix <= 'd0;endelse begin cnt_pix <= cnt_pix + 1'b1;end endend assign add_cnt_pix = end_cnt_bit;assign end_cnt_pix = add_cnt_pix && cnt_pix == 6'd63;//复位计数器always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt_reset <= 'd0;end else if(add_cnt_reset)begin if(end_cnt_reset)begin cnt_reset <= 'd0;endelse begin cnt_reset <= cnt_reset + 1'b1;end endend assign add_cnt_reset = cstate == RST;assign end_cnt_reset = add_cnt_reset && cnt_reset == RST_MAX - 1'b1;//第三段 状态机输出always@(*)begincase(cstate)IDLE : ws2812_io = 1'b0;RST : ws2812_io = 1'b0;DATA : beginif(fifo_rd_data[23 - cnt_bit])beginif(cnt_time < T1H)beginws2812_io = 1'b1;endelse beginws2812_io = 1'b0;endendelse beginif(cnt_time < T0H)beginws2812_io = 1'b1;endelse beginws2812_io = 1'b0;endendenddefault : ws2812_io = 1'b0;endcaseend //ready使能信号赋值
assign ready = cstate == IDLE;endmodule
数据控制模块:
/**************************************************************
@File : ws2812_control.v
@Time : 2023/08/11 15:09:52
@Author : majiko
@EditTool: VS Code
@Font : UTF-8
@Function: 提供64个RGB像素数据,给到ws2812接口模块,用于验证接口模块
**************************************************************/
module ws2812_ctrl2(input clk ,input rst_n ,input ready ,//可以接收图像数据了output [23:0] pix_data ,output pix_data_vld );parameter IDLE = 0,DATA = 1,DELAY = 2;parameter DELAY_TIME = 25'd25_000_000;wire rom_rd_req ;reg rom_rd_req1 ;reg rom_rd_req2 ;wire rom_rd_data ;reg [2:0] state ;reg [5:0] cnt_x ;wire add_x_cnt;wire end_x_cnt; reg [4:0] cnt_y ;wire add_y_cnt;wire end_y_cnt;reg [4:0] cnt_offset ;wire add_cnt_offset ;wire end_cnt_offset ;reg [24:0] cnt_delay ;wire add_cnt_delay ;wire end_cnt_delay ; // localparam RED = 24'hFF0000, //红色// ORANGE = 24'hFF8000, //橙色// YELLOW = 24'hFFFF00, //黄色// GREEN = 24'h00FF00, //绿色// CYAN = 24'h00FFFF, //青色// BLUE = 24'h0000FF, //蓝色// PURPPLE = 24'h8000FF, //紫色// BLACK = 24'h000000, //黑色// WHITE = 24'hFFFFFF, //白色// GRAY = 24'hC0C0C0; //灰色/**************************************************************状态机
**************************************************************/always@(posedge clk or negedge rst_n)if(!rst_n)state <= IDLE;else case(state)IDLE : if(ready)state <=DATA;DATA : if(end_y_cnt)state <=DELAY;DELAY : if(end_cnt_delay)state <=IDLE;default : state <= IDLE;endcase//帧间隔计数器always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt_delay <= 'd0;end else if(add_cnt_delay)begin if(end_cnt_delay)begin cnt_delay <= 'd0;endelse begin cnt_delay <= cnt_delay + 1'b1;end endend assign add_cnt_delay = state == DELAY;assign end_cnt_delay = add_cnt_delay && cnt_delay == DELAY_TIME - 1;/**************************************************************图像数据个数计数器
**************************************************************/ always@(posedge clk or negedge rst_n) if(!rst_n) cnt_x <= 'd0; else if(add_x_cnt) begin if(end_x_cnt) cnt_x <= 'd0; else cnt_x <= cnt_x + 1'b1; end assign add_x_cnt = state == DATA;assign end_x_cnt = add_x_cnt && cnt_x == 8 - 1;always@(posedge clk or negedge rst_n) if(!rst_n) cnt_y <= 'd0; else if(add_y_cnt) begin if(end_y_cnt) cnt_y <= 'd0; else cnt_y <= cnt_y + 1'b1; end assign add_y_cnt = end_x_cnt;assign end_y_cnt = add_y_cnt && cnt_y == 8 - 1;//偏移计数器always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt_offset <= 'd0;end else if(add_cnt_offset)begin if(end_cnt_offset)begin cnt_offset <= 'd0;endelse begin cnt_offset <= cnt_offset + 1'b1;end endend assign add_cnt_offset = end_cnt_delay;assign end_cnt_offset = add_cnt_offset && cnt_offset == 5'd23;// assign pix_data_vld = add_x_cnt;// always@(*)// case(cnt_y)// 0 : pix_data = RED ;// 1 : pix_data = ORANGE ;// 2 : pix_data = YELLOW ;// 3 : pix_data = GREEN ;// 4 : pix_data = CYAN ;// 5 : pix_data = BLUE ;// 6 : pix_data = PURPPLE ;// 7 : pix_data = GRAY ;// default : pix_data = RED ;// endcasewire [4:0] real_row ;assign real_row = cnt_x + cnt_offset;rom rom_inst (.aclr ( ~rst_n ),.address ( cnt_y*32 + real_row),.clock ( clk ),.rden ( rom_rd_req ),.q ( pix_data ));//ROM读请求打两拍always@(posedge clk or negedge rst_n)beginif(!rst_n)beginrom_rd_req1 <= 1'b0;rom_rd_req2 <= 1'b0;endelse beginrom_rd_req1 <= rom_rd_req;rom_rd_req2 <= rom_rd_req1;endendassign rom_rd_req = state == DATA;assign rom_rd_data = rom_rd_req2 ;assign pix_data_vld = rom_rd_data ;endmodule
顶层模块:
module ws2812_top(input wire clk ,input wire rst_n ,output wire ws2812_io
);wire [23:0] pix_data ;
wire ready ;
wire pix_data_vld;//模块例化
//显示图片
// ws2812_ctrl u_ws2812_ctrl(// .clk (clk ),// .rst_n (rst_n ),// .pix_data (pix_data ),// .pix_data_vld (pix_data_vld ),// .ready (ready )
// );//动态显示图片
ws2812_ctrl2 u_ws2812_ctrl2(.clk (clk ),.rst_n (rst_n ),.pix_data (pix_data ),.pix_data_vld (pix_data_vld ),.ready (ready )
);interface u_interface(.clk (clk ),.rst_n (rst_n ),.pix_data (pix_data ),.pix_data_vld (pix_data_vld),.ready (ready ),.ws2812_io (ws2812_io )
);endmodule
四、最终显示效果
引脚绑定如下:
实现效果:
五、总结
博主本人为FPGA初学者,因此才会写下此篇博客作为自己对WS2812的一个总结,如果有和博主一样的初学者,希望此博文能够帮助到你。
这也是博主第一次利用FPGA驱动外设,第一次设计一个较为简单的单总线通讯协议,尽管很大程度博主都是依靠着博主老师的代码才完成的。
如果此篇博文有任何错误,还请各位提出!