FPGA学习——驱动WS2812光源并进行动态显示

文章目录

  • 一、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灯源特性及概述

FPGA学习——驱动WS2812光源并进行动态显示_第1张图片

1.2 手册重点内容分析

1.2.1 产品概述

FPGA学习——驱动WS2812光源并进行动态显示_第2张图片
由产品概述可以得到的重要信息有:

  • 该灯源数据协议采用单线归零码的通讯方式
  • 一个像素点需要24bit数据才能正常工作(该灯板共有8×8 64个像素点)
  • 复位时间需要至少280us
  • 传输数据每经过一个像素点便会被锁存24bit数据,因此数据会逐级减少
  • 数据发送速度最高为800Kbps

1.2.2 码型及24bit数据设计

FPGA学习——驱动WS2812光源并进行动态显示_第3张图片
FPGA学习——驱动WS2812光源并进行动态显示_第4张图片
FPGA学习——驱动WS2812光源并进行动态显示_第5张图片

由上述手册截图可以看出,该光源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核,用来临时存储传入驱动模块的数据,避免造成数据丢失。

该模块状态机设计如下:

FPGA学习——驱动WS2812光源并进行动态显示_第6张图片
其中,end_cnt_rst为复位计数器结束信号,end_cnt_pix为64个像素点计数器结束信号。

2.2.1 数据控制模块

在该模块我们所需要的考虑仅为所需数据的存储以及如何传入驱动模块。

  • 为了存储我们设计好的数据,博主在该模块调用了一个ROM IP核。
  • 同时博主在该模块设计了X,Y两个计数器,计数最大值均为8。将X,Y作为光源像素点的坐标从而确定ROM中所要取得的数据。
  • 另外,我们需要设计一个帧间隔计数器,让每一帧之间的切换能够被肉眼捕获,博主设置的间隔为500ms
  • 为了方便设计并存储数据,在调用ROM时需要设计MIF文件初始化ROM,在后文博主会将自己设计MIF文件的方式和工具分享给大家。
  • 由于mif文件是一行一行保存数据,因此在显示每一帧像素时需要参考该公式:cnt_x +cnt_y+cnt_offset,cnt_offset为每一帧的偏移量,计数从0-23。

三、IP核设置及项目源码

3.1 MIF文件设计

打开系统自带的画图工具,点击左上角文件,选择图像属性

FPGA学习——驱动WS2812光源并进行动态显示_第7张图片

在图像属性中设置图像为 32×8(大家可以根据自己所需自由设计宽度(如果自定义宽度,就需要在ROM核设置的时候进行更改,同时需要在博主的数据控制模块进行一个小细节的更改,博主会在后文标明),但高度固定因为动态显示相当于滚动移动宽度,无法移动高度)

FPGA学习——驱动WS2812光源并进行动态显示_第8张图片

然后在画图中设计自己想要显示的图案即可

FPGA学习——驱动WS2812光源并进行动态显示_第9张图片

设计完成后点击右上角文件另存为24位宽BMP文件注意必须为24位宽

FPGA学习——驱动WS2812光源并进行动态显示_第10张图片

然后利用格式转换工具,将该BMP文件转化为mif文件

FPGA学习——驱动WS2812光源并进行动态显示_第11张图片

然后将生成的文件存放在自己创建的quartus prj文件夹下

FPGA学习——驱动WS2812光源并进行动态显示_第12张图片

3.2 ROM IP核调用

在quartus IP Catalog中搜索ROM并选择单端口ROM
FPGA学习——驱动WS2812光源并进行动态显示_第13张图片

按照下图进行设置:

  • 输出数据设置为24bit宽
  • 深度设置为256,若BMP图像高度不是博主设置的32,各位需要自行更改所需深度,将其改为:你所设置的宽度×8

FPGA学习——驱动WS2812光源并进行动态显示_第14张图片

点击next,按下图进行如下设置:

FPGA学习——驱动WS2812光源并进行动态显示_第15张图片

  • 将输出q寄存一拍(勾选则在后续读使能需要打两拍,博主个人习惯,可以不选,后续只需打一拍)
  • 勾选异步复位信号
  • 勾选读使能信号

点击next按照下图进行如下设置:
FPGA学习——驱动WS2812光源并进行动态显示_第16张图片

  • 点击Browse选择前文所设置的MIF文件,对ROM进行初始化

一路next至总结界面:

FPGA学习——驱动WS2812光源并进行动态显示_第17张图片

  • 勾选例化模板

最终设置如下:

FPGA学习——驱动WS2812光源并进行动态显示_第18张图片

点击finish完成ROM IP核配置

3.3 FIFO IP核调用

在IP Catalog中搜索FIFO,进入配置选项卡

FPGA学习——驱动WS2812光源并进行动态显示_第19张图片

  • 数据宽度同样为24bit,深度同ROM
  • 由于本项目为跨时钟域,因此FIFO读写选择单时钟即可

点击next,按下图进行设置:

FPGA学习——驱动WS2812光源并进行动态显示_第20张图片

  • 空、满信号可用于确定读写请求
  • usew可在仿真时看到FIFO中的数据个数(此处不勾选,不仿真用不到)
  • 勾选异步复位信号

点击next,按下图进行配置:

FPGA学习——驱动WS2812光源并进行动态显示_第21张图片

  • 勾选前显模式

一路next至总结界面:

FPGA学习——驱动WS2812光源并进行动态显示_第22张图片

  • 勾选例化模板

FIFO总体配置如下:

FPGA学习——驱动WS2812光源并进行动态显示_第23张图片

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) begin
        if(!rst_n)begin
            cstate <= IDLE;
        end
        else begin
            cstate <= nstate;
        end
    end

    //第二段组合逻辑
    always@(*)begin
        case(cstate)
            IDLE    :   begin
                            if(idle2rst)begin
                                nstate = RST;
                            end
                            else begin
                                nstate = cstate;
                            end
                        end
            RST     :   begin
                            if(rst2data)begin
                                nstate = DATA;
                            end
                            else begin
                                nstate = cstate;
                            end
                        end
            DATA    :   begin
                            if(data2idle)begin
                                nstate = IDLE;
                            end
                            else begin
                                nstate = cstate;
                            end
                        end
            default : nstate <= cstate;
        endcase
    end

    assign 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)begin
            cnt_time <= 'd0;
        end 
        else if(add_cnt_time)begin 
            if(end_cnt_time)begin 
                cnt_time <= 'd0;
            end
            else begin 
                cnt_time <= cnt_time + 1'b1;
            end 
        end
    end 
    
    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)begin
            cnt_bit <= 'd0;
        end 
        else if(add_cnt_bit)begin 
            if(end_cnt_bit)begin 
                cnt_bit <= 'd0;
            end
            else begin 
                cnt_bit <= cnt_bit + 1'b1;
            end 
        end
    end 
    
    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)begin
            cnt_pix <= 'd0;
        end 
        else if(add_cnt_pix)begin 
            if(end_cnt_pix)begin 
                cnt_pix <= 'd0;
            end
            else begin 
                cnt_pix <= cnt_pix + 1'b1;
            end 
        end
    end 
    
    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)begin
            cnt_reset <= 'd0;
        end 
        else if(add_cnt_reset)begin 
            if(end_cnt_reset)begin 
                cnt_reset <= 'd0;
            end
            else begin 
                cnt_reset <= cnt_reset + 1'b1;
            end 
        end
    end 
    
    assign add_cnt_reset = cstate == RST;
    assign end_cnt_reset = add_cnt_reset && cnt_reset == RST_MAX - 1'b1;
    
//第三段 状态机输出
    always@(*)begin
        case(cstate)
        IDLE    :   ws2812_io = 1'b0;
        RST     :   ws2812_io = 1'b0;
        DATA    :   begin
                        if(fifo_rd_data[23 - cnt_bit])begin
                            if(cnt_time < T1H)begin
                                ws2812_io = 1'b1;
                            end
                            else begin
                                ws2812_io = 1'b0;
                            end
                        end
                        else begin
                            if(cnt_time < T0H)begin
                                ws2812_io = 1'b1;
                            end
                            else begin
                                ws2812_io = 1'b0;
                            end
                        end
                    end
        default :   ws2812_io = 1'b0;
        endcase
    end 

//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)begin
            cnt_delay <= 'd0;
        end 
        else if(add_cnt_delay)begin 
            if(end_cnt_delay)begin 
                cnt_delay <= 'd0;
            end
            else begin 
                cnt_delay <= cnt_delay + 1'b1;
            end 
        end
    end 
    
    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)begin
            cnt_offset <= 'd0;
        end 
        else if(add_cnt_offset)begin 
            if(end_cnt_offset)begin 
                cnt_offset <= 'd0;
            end
            else begin 
                cnt_offset <= cnt_offset + 1'b1;
            end 
        end
    end 
    
    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      ;
        // endcase

    wire [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)begin
        if(!rst_n)begin
            rom_rd_req1 <= 1'b0;
            rom_rd_req2 <= 1'b0;
        end
        else begin
            rom_rd_req1 <= rom_rd_req;
            rom_rd_req2 <= rom_rd_req1;
        end
    end

    assign 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         ) 
);


ws2812_driver u_ws_2812_driver(
    .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光源并进行动态显示_第24张图片

实现效果:

五、总结

博主本人为FPGA初学者,因此才会写下此篇博客作为自己对WS2812的一个总结,如果有和博主一样的初学者,希望此博文能够帮助到你。

这也是博主第一次利用FPGA驱动外设,第一次设计一个较为简单的单总线通讯协议,尽管很大程度博主都是依靠着博主老师的代码才完成的。

如果此篇博文有任何错误,还请各位提出!

你可能感兴趣的:(fpga开发,学习,网络)