基于WS2812的贪吃蛇游戏系统

本设计是一种基于C4MB板卡的贪吃蛇游戏系统,使用C4MB开发板和WS2812 LED灯带实现硬件游戏系统。该系统通过C4MB板卡上的FPGA芯片对LED灯带进行控制,模拟贪吃蛇游戏的画面,玩家可以通过操纵按键来控制贪吃蛇的移动。通过C4MB板卡的GPIO接口和WS2812 LED灯带的连接,可以实现对LED灯珠的控制。C4MB板卡具有足够的计算能力和存储空间,可以支持贪吃蛇游戏的逻辑处理和图像显示。

游戏系统的操作方式是,玩家通过按键控制C4MB板卡的输入引脚,触发相应的移动命令。C4MB板卡接收到命令后,根据游戏逻辑更新贪吃蛇的位置,产生新的食物,并控制LED灯带显示出游戏画面。同时,C4MB板卡还可以通过PWM控制LED灯的亮度和颜色,增加游戏的视觉效果。

该项目的优势在于C4MB板卡具有较强的性能和丰富的外设接口,可以支持更复杂的游戏逻辑和交互方式。同时,WS2812 LED灯带提供了良好的可编程性和灵活性,可以呈现出丰富多彩的游戏画面。这套基于C4MB的贪吃蛇游戏系统不仅可以作为一款娱乐设备,还可以用于教育和学习,帮助人们深入理解硬件和编程的知识。 本项目系统架构主要分为以下几个模块:

按键消抖模块:设计按键输入接口,用于玩家控制贪吃蛇的移动方向,对输入按键信号的消抖,通过C4MB板卡来读取按键输入状态。

蛇控制模块:实现贪吃蛇的移动、生长和碰撞检测逻辑,包括更新蛇身位置、生成食物、判断游戏结束等功能。

Ws2812b接口显示模块:  利用WS2812 LED灯带显示贪吃蛇游戏画面,包括蛇身、食物和背景等元素的显示效果。

目录

1   项目目的

1.1 需求阐述

1.2 实验目的

2  实验原理

2.1    按键消抖理论原理

2.2    ws2812b理论原理

3 设计思路

3.1    系统流程的整体架构图

3.2    按键消抖模块

3.2    游戏逻辑控制模块

3.2.1   snake_ctrl

3.2.2   start/win/low_data

3.2.3   game_ctrl

3.3    游戏界面显示驱动模块(ws2812b接口)

4 系统测试与结果分析

4.1    整体仿真图

4.2    分模块仿真分析

4.2.1   按键消抖模块仿真

4.2.2   次子模块游戏界面模块整体仿真

4.2.3   游戏界面显示驱动模块仿真

总结


使用C4MB板卡,设计一套基于WS2812的贪吃蛇游戏系统。具体描述如下:

    1. 在WS2812矩阵灯阵列上,取一个点或者两个点作为蛇的初始位置和长度,另取一个点作为食物的初始位置

    2. 游戏开始后,玩家可通过方向键来决定蛇的移动方向,若没有方向键按下,蛇依然能够朝着原有方向前进,前进速率自拟。移动过程中,如果蛇吃到食物,蛇的长度增加,并随机生成新的食物

    3. 整个游戏能够显示以下几个游戏界面:游戏开始画面、游戏玩耍界面以及游戏成功或失败界面

    4. 当蛇的长度超过设定值以后判定游戏成功(win),撞墙或者撞到自身判定游戏失败(lose)

方案框图如图1所示:

图1 贪吃蛇游戏系统方案框图

1.2 实验目的

1、熟悉FPGA系统开发:通过设计基于C4MB板卡和WS2812的贪吃蛇游戏系统,可以深入理解FPGA系统的硬件接口控制、逻辑编程和外设驱动等方面的知识。

2、掌握按键输入和逻辑处理:实现按键输入与贪吃蛇逻辑的结合,能够让学习者掌握如何通过硬件输入控制游戏逻辑,以及如何处理游戏中的碰撞检测、移动规则等问题。

3、了解LED灯带的控制:利用WS2812 LED灯带显示贪吃蛇游戏画面,可以让学习者学习LED灯带控制的相关知识,包括对颜色、亮度和灯珠控制等方面的了解。

4、综合应用硬件和软件开发技能:这个项目结合了硬件接口控制和逻辑编程,是一个很好的综合实践项目,能够让学习者综合运用硬件和软件开发技能,提高解决实际问题的能力。

2  实验原理

2.1    按键消抖理论原理

按键消抖是指在按下或释放按键时,由于机械接触的特性可能导致开关状态出现短暂的不稳定状态,从而产生多次开关信号。为了准确获取按键的状态并避免误操作,需要对按键进行消抖处理。

按键消抖的原理主要涉及到机械开关的特性和电气信号的处理:

机械开关特性:当按键被按下或释放时,金属片或触点会产生弹性变形和震动,导致在短时间内出现多次接通或断开的情况,这种瞬时的不稳定状态称为“跳动”或“弹跳”。

电气信号处理:通过电路连接的方式,将按键的状态转换为数字信号(0或1),以便微控制器或其他数字逻辑设备进行识别和处理。消抖处理的目标是确保在按下或释放按键时只产生一次稳定的状态转换信号。

图2-1-1  按键按下抖动示意图

基于以上原理,常见的按键消抖方法包括以下几种:

软件延时消抖:在检测到按键状态改变后,通过在软件中增加一个短暂的延时来等待按键稳定下来,然后再进行状态读取,从而排除瞬时的干扰信号。

硬件滤波消抖:使用电容、电阻等元件构成滤波电路,减少开关状态变化时的电压波动,以实现信号的稳定转换。

状态标记消抖:记录上一次按键状态,并与当前状态进行比较,仅当连续多次读取到相同状态时才认定为有效的按键状态转换。

这些方法可以单独应用,也可以结合使用,在本系统中,我采用的方法是状态机延时法,以确保按键信号的稳定性和可靠性。在设计嵌入式系统中的按键输入处理时,按键消抖是一个重要的环节,能够有效提高系统的稳定性和用户体验。

2.2    ws2812b理论原理

WS2812B是一种常见的RGB LED灯带,每个灯珠内部都有一个芯片控制,通过发送特定的时序数据来控制其亮灭。发送数据时,需要按照一定的时序发送24位RGB数据,其中,高位在前低位在后,格式为GRB。发送数据时,需要注意不仅仅是发送高电平或低电平,而是要发送占空比不同的PWM波,比如给予一定的高电平和低电平时间。重置码是发送一个持续280us的低电平信号。可以先发送一组24位的数据,然后接一个重置信号表示一组结束。每个像素点接收24bit数据,溢出的数据传输给下一个像素点, 它具有以下主要原理和特点:

内置控制电路:WS2812B每个灯珠内部集成了控制电路,包括数据输入、数据处理和驱动LED发光的功能。这使得每个灯珠都可以独立控制,实现了灵活的颜色和亮度变化。

串行数据传输:WS2812B采用串行方式传输数据,每个灯珠依次接收数据并将自己对应的颜色和亮度信息提取出来,然后将剩余的数据继续传递给下一个灯珠,这样就形成了级联的串行数据传输结构。数据传输方法如图2-2-1所示

图2-2-1数据传输方法

数据格式:WS2812B使用的数据格式是时间间隔调制,具体来说,它采用的是一种被称为“单总线双向数据传输”的方式,即通过时间间隔的长短来表示0和1的逻辑状态,从而传输颜色和亮度的信息。数据结构如下图:

图2-2-2 数据结构

控制协议:WS2812B采用的控制协议是基于1-wire总线的数据传输协议,其中每个灯珠需要接收24位的数据,分别表示红、绿、蓝三种颜色的亮度值。控制器发送的数据流按照GRB(绿、红、蓝)的顺序传输。

图2-2-3 时序图及连接方式

供电要求:WS2812B工作电压一般为5V,因为每个灯珠内部需要有稳压电路和控制电路,所以需要保证足够的电压和电流来正常工作。

总的来说,WS2812B通过内置的控制电路和串行数据传输方式,实现了灯珠的独立控制和颜色的变化,可以广泛应用于LED灯带、灯条、装饰灯等领域,为用户提供了丰富的灯光效果和控制选择。

3 设计思路

3.1    系统流程的整体架构图

图3-1    系统整体框图

整个系统总共分为3个大模块,首先由于用到了按键信号,所以先对输入的按键信号进行消抖,后传输至蛇逻辑模块进行控制。其次是蛇控制模块,该模块包括开始界面、结束游戏显示和游戏逻辑控制,下文将详细介绍;最后是ws2812b接口模块,将游戏界面需要显示的数据接收,显示在LED灯上。

3.2    按键消抖模块

图3-2-1 按键消抖模块框图

本模块主要实现对输入按键信号的消抖,按键消抖模块是一种用于解决物理按键在按下或释放过程中可能产生的抖动问题的电路或算法。当我们按下或释放一个物理按键时,由于按键的机械结构和接触特性,会导致按键信号在短时间内快速切换多次,这种现象称为按键抖动。按键消抖模块的作用是通过适当的延时和逻辑判断方法,对按键信号进行稳定化处理,确保只有有效的按键状态被识别和响应,而抖动信号不被误判

  • 接口设计

fsm_ctrl模块

端口

说明

clk

时钟信号

rst_n

复位信号

key_in

消抖前的按键信号输入

key_down

输出消抖后的按键信号(高电平有效)

  • 状态机设计

图3-2-1 按键消抖模块状态转移图

IDLE:空闲状态,当检测到按键按下的下降沿,跳转至FILETER_DOWN状态;

FILETER_DOWN:按键按下抖动状态,负责延时20ms,计数结束后跳转至HOLD_DOWN;

HOLD_DOWN:按键稳定按下状态,检测到按键检测到上升沿跳转至FILTER_UP;

FILTER_UP:按键释放抖动状态,延时20ms后回到IDLE空闲状态。

 

assign idle2filter_down     = state_c == IDLE         && n_edge;
    assign fiter_down2hold_down = state_c == FILETER_DOWN && end_cnt_20ms;
    assign hold_down2filter_up  = state_c == HOLD_DOWN    && p_edge;
assign filter_up2idle       = state_c == FILTER_UP    && end_cnt_20ms;

  • 按键消抖过程时序图

图3-2-3 按键消抖模块时序图

(四)重要代码片段解析

将输入的按键信号进行打拍,第一拍key_r0用于输入信号同步,第二拍和第三拍用于进行下降沿和上升沿的检测

 

always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            key_r0 <= {WIDTH{1'b1}};
            key_r1 <= {WIDTH{1'b1}};
            key_r2 <= {WIDTH{1'b1}};
        end 
        else begin 
            key_r0 <= key_in;
            key_r1 <= key_r0;
            key_r2 <= key_r1;
        end 
    end      
    assign n_edge = ~key_r1 & key_r2;
    assign p_edge = ~key_r2 & key_r1;  

3.2    游戏逻辑控制模块

3.2.1   snake_ctrl

图3-2-1-1    snake_ctrl模块框图

将该模块分为界面显示子模块和游戏逻辑子模块,一共四个子模块在次顶层中进行调用,并利用状态机控制整个流程。

  • 接口设计

snake_ctrl

端口

说明

clk

时钟信号

rst_n

复位信号

Key_in[3:0]  

输入按键信号

ready

输入使能

pix_data[23:0]

输出数据

data_vld

输出使能信号

(二)状态机设计

图3-2-1-2    snake_ctrl模块状态转移图

IDLE  :空闲状态

START  :开始状态,当前状态输出开始界面数据;

PLAY  :游戏状态,产生play信号驱动game_ctrl模块产生蛇身、果实,控制蛇头移动,输出play_data;

LOSE  :游戏失败状态,当game_ctrl模块产生游戏失败使能信号(low_en)时,跳到该状态,输出low_data

WIN   :游戏成功状态,当game_ctrl模块产生游戏成功使能信号(win_en)时,跳到该状态,输出win_data

   

     

以上三个图片从左到右分别是开始界面、游戏通关界面和游戏失败界面。

(三)时序设计

(四)重要代码设计

主要利用if——else、case——default语句根据当前状态及使能信号,对输出数据进行选择控制

 

always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            pix_data <= 'd0;
        end 
        else case(state_s) 
            START  : if (start_en) begin
                 pix_data <= start_data;
                 data_vld <= data_vld_2;
            end
            PLAY   : if( play )  begin
                pix_data <= play_data;
                data_vld <= data_vld_1;
            end
            LOSE   : if( low ) begin
                pix_data <= low_data;
                data_vld <= data_vld_4;
            end
            WIN    : if( win ) begin
                pix_data <= win_data;
                data_vld <= data_vld_3;
            end
            default :data_vld <= data_vld_1 ;
        endcase
    end
3.2.2   start/win/low_data

图3-2-2-1    start/win/low_data模块状态转移图

将该模块实现游戏开始、结束页面显示,将开始界面、输界面和赢界面的RGB888 24位图像数据转为mif文件存入ROM   IP,并根据状态机设计控制整个数据流程,根据状态读取ROM IP中的数据。

  • 状态机设计

IDLE  :空闲状态,ready信号来临时,进入DATA状态;

DATA   :数据状态,进行数据传输;

DELAY  :延时状态,每一帧数据传输结束后延时一段时间

  • 接口设计

start/win/low_data

端口

说明

clk

时钟信号

rst_n

复位信号

ready

输入使能

start/win/low[23:0]

输出数据

pix_data_vld

输出使能

  • 时序设计

  • IP参数

       有上文可知,本设计插入ROM  IP的图片为8*16的上下两张8*8的图片,所以IP的位宽和大小如图所示。

  • 重要代码设计

想要实现动态闪烁的效果,就要进行图像个数计数和偏移量计算,由于我画的是8*16的图,相当于两张8*8的图片竖着拼接到一起,那么偏移量就为2如下:

/**************************************************************
                        图像数据个数计数器
**************************************************************/       
    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;
//cnt_y
    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_delay_cnt;
    assign end_cnt_offset = add_cnt_offset && cnt_offset == 2 - 1;

所以我们读取ROM      IP的地址计算方式就是cnt_x + cnt_y*8 + cnt_offset*64

 

 rom_low rom_low_inst  (
    .aclr       ( ~rst_n        ),
    .address    ( cnt_x + cnt_y*8 + cnt_offset*64),
    .clock      ( clk            ),
    .rden       ( low_en         ),
    .q          ( low_data       )
    );
    assign low_en   = state == DATA  ; 

为了解决跨时钟域的信号传输的问题,我将输出的使能信号进行了打拍,防止ws2812b显示错位

 

 reg         low_en_r;
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            low_en_r <= 'd0;
        end 
        else begin
            low_en_r <= low_en;
        end 
    end
    assign      pix_data_vld =  low_en_r ;
3.2.3   game_ctrl

图3-2-3-1    game_ctrl模块状态转移图

game_ctrl模块主要是对游戏逻辑进行编写,用于实现蛇头产生、身体移动、果实产生/更新、以及输赢的判定,并输出游戏界面和输赢信号,在游戏结束次顶层snake_ctrl模块中决定输出输or赢界面。

  • 接口设计

game_ctrl

端口

说明

clk

时钟信号

rst_n

复位信号

play

输入使能

Key_in[3:0]

输入按键信号

play_data[23:0]

输出游戏界面数据

win_en

输出通关成功信号

low_en

输出通关失败信号

  • 状态机设计

IDLE  :空闲状态,play信号来临时,进入PLAY状态,驱动整个游戏界面的逻辑;

PLAY  :游戏数据状态,进行游戏数据传输;

DONE  :延时状态,每一帧数据传输结束后延时一段时间

  • 重点代码解析

首先是蛇头的产生,在这里定义蛇头的初始坐标(x,y)= (3,4)

用一个方向寄存器direction 记录按键按下控制蛇头的移动。

/

//方向寄存器
    reg     [1:0]   direction;
    `define         up     2'b00
    `define         down   2'b01
    `define         left   2'b10
    `define         right  2'b11

按键控制如下:key_in[0]——向上;key_in[1]——向下;

              key_in[2]——向右;key_in[3]——向左;

/****************************************************************
                    控制方向
****************************************************************/
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n) 
            direction <= `left;//初始为左
        else if(key_in[0] && direction != `down )  
            direction <= `up;
        else if(key_in[1] && direction != `up )  
            direction <= `down;
        else if(key_in[3] && direction != `right )  
            direction <= `left;  
        else if(key_in[2] && direction != `left )  
            direction <= `right; 
        else
            direction <= direction;   
    end

当前设置初始方向为左边

/****************************************************************
                        蛇头移动
****************************************************************/
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            snake_x0 <= 3;
            snake_y0 <= 4;
        end 
        else if( end_delay)begin //更新坐标
            case (direction) 
               `up     : begin snake_x0 <= snake_x0; snake_y0 <= snake_y0 - 1;end
               `down   : begin snake_x0 <= snake_x0; snake_y0 <= snake_y0 + 1;end 
               `left   : begin snake_x0 <= snake_x0 - 1; snake_y0 <= snake_y0;end 
               `right  : begin snake_x0 <= snake_x0 + 1; snake_y0 <= snake_y0;end 
                default: ;
            endcase
        end 
end

那么蛇身的坐标就是将蛇头的坐标寄存下来

/蛇身寄存
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            snake_x1 <= 4;snake_y1 <= 4;
            snake_x2 <= 0;snake_y2 <= 0;
            snake_x3 <= 0;snake_y3 <= 0;
            snake_x4 <= 0;snake_y4 <= 0;
            snake_x5 <= 0;snake_y5 <= 0;
            snake_x6 <= 0;snake_y6 <= 0;
            snake_x7 <= 0;snake_y7 <= 0;
        end 
        else if(end_delay)begin 
            snake_x1 <= snake_x0;snake_y1 <= snake_y0;
            snake_x2 <= snake_x1;snake_y2 <= snake_y1;
            snake_x3 <= snake_x2;snake_y3 <= snake_y2;
            snake_x4 <= snake_x3;snake_y4 <= snake_y3;
            snake_x5 <= snake_x4;snake_y5 <= snake_y4;
            snake_x6 <= snake_x5;snake_y6 <= snake_y5;
            snake_x7 <= snake_x6;snake_y7 <= snake_y6;
        end 
    end

接下来是随机产生的果实:我们利用伪随机数,设置一个位宽为32位的reg型寄存器lfsr_num,初值赋值为32'habfcd8d9,再将其不断进行位移计算,得到新的值

 

always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            lfsr_num <= 32'habfcd8d9;
        end 
        else begin 
            lfsr_num <= {lfsr_num[14:0],lfsr_num[31:15]};
        end 
    end

接下来取lfsr_num的任意3位分别赋值给食物坐标

always @(posedge clk or negedge rst_n)begin

        if(!rst_n)begin

            food_x <= 5;

            food_y <= 6;

        end

        else if(eat_flag)begin          //更新食物坐标

            food_x <= lfsr_num[31:29];

            food_y <= lfsr_num[16:14];

        end

        else if(food_snake)begin        //食物坐标不可与蛇身坐标重合

            food_x <= food_x + 1;

            food_y <= food_y + 1;

        end

    end

当然,这里必须要注意的是,新产生的食物坐标不能与蛇身坐标重合,所以单独设置了一个food_snake信号,表示随机坐标与蛇身重合,此时食物横纵坐标分别加一,表示再次更新

 

assign  food_snake = (snake_x0 == food_x && snake_y0 == food_y)||

                         (snake_x1 == food_x && snake_y1 == food_y)||

                         (snake_x2 == food_x && snake_y2 == food_y)||

                         (snake_x3 == food_x && snake_y3 == food_y)||

                         (snake_x4 == food_x && snake_y4 == food_y)||

                         (snake_x5 == food_x && snake_y5 == food_y)||

                         (snake_x6 == food_x && snake_y6 == food_y)||

                         (snake_x7 == food_x && snake_y7 == food_y);

而每次正常更新实物坐标的信号eat_flag也就是蛇头坐标与食物坐标重合的信号,每吃到一次果实,蛇身长度加一。

//eat_flag

    always @(posedge clk or negedge rst_n)begin

        if(!rst_n)begin

            eat_flag  <= 'd0;      

        end

        else if(snake_x0 == food_x && snake_y0 == food_y )begin

            eat_flag <= 1;  

        end

        else begin

            eat_flag <= 0;

        end

    end

接下来是失败信号low_en 有两个信号组成,一个是撞墙信号bump_wall

//撞墙

    always @(posedge clk or negedge rst_n)begin

        if(!rst_n)begin

            bump_wall <= 'd0;

        end

        else if(state == PLAY)begin

            case (direction)

                `up     : if(snake_y0 == 15) bump_wall <= 1;

                `down   : if(snake_y0 == 8 ) bump_wall <= 1;

                `left   : if(snake_x0 == 15) bump_wall <= 1;

                `right  : if(snake_x0 == 8 ) bump_wall <= 1;

                default : bump_wall <= 0;

            endcase

        end

    end

一个是蛇头碰到蛇身的信号,只有蛇身长度大于4时,蛇头才有可能吃到自己

//撞自己

    always @(posedge clk or negedge rst_n)begin

        if(!rst_n)begin

            bump_body <= 'd0;

        end

        else if((snake_len > 4) && snake_snake)begin

            bump_body <= 1;

        end

end

  assign  snake_snake = ((snake_x0 == snake_x3 && snake_y0 == snake_y3)||

                           (snake_x0 == snake_x4 && snake_y0 == snake_y4)||

                           (snake_x0 == snake_x5 && snake_y0 == snake_y5)||

                           (snake_x0 == snake_x6 && snake_y0 == snake_y6));

最后将两个信号组合,输出的就是输信号

    assign  low_en = bump_body || bump_wall; //碰撞到身体或墙面都视为游戏失败

而赢信号相对来说更加简单,可以随意设置蛇身长度snake_len = X?

//赢信号

    assign      win_en = (snake_len == 8)? 1:0;//当蛇身长度等于8时,表示游戏成功

3.3    游戏界面显示驱动模块(ws2812b接口)

图3-3-1 游戏界面显示模块框图

该模块用于接收贪吃蛇控制模块传来的 64 个 24bit 数据,转换成对应的 0 码和 1 码。

  • 接口设计

ws2812_drive

端口

说明

clk

时钟信号

rst_n

复位信号

data[23:0]  

输入数据

din_vld

输入使能

ready

输出使能

ws2812b_io

输出led显示

  • 状态机设计

assign  IDLE2RES  = state_c == IDLE && data_vld_r    ;

assign  RES2DATA  = state_c == RES  && end_cnt_280US ;

assign  DATA2IDLE = state_c == DATA && end_cnt_64    ;

将输入使能打一拍后作为一阵数据接收成功的信号,进入复位状态,复位时间为400us,每次接收完64个24bit的数据返回一次IDLE状态。

  • IP设置

为了解决跨时钟域数据传输的问题,我们使用一个fifo进行数据缓冲,IP配置如下:

  • 重要代码片段

fifo缓冲数据,由于我们传输过来的数据是RGB格式,而我们的ws2812b现实的数据格式为GRB格式,所以在这里我们要进行一次拼接转换:

/******************************************************************

                             fifo                  

******************************************************************/

fifo    fifo_inst (

    .aclr  ( ~rst_n       ),

    .clock ( clk          ),

    .wrreq ( fifo_wr_req  ),

    .data  ( fifo_wr_data ),

    .q     ( fifo_rd_data ),

    .rdreq ( fifo_rd_req  ),

    .empty ( fifo_empty   ),

    .full  ( fifo_full    ),

    .usedw (              )  

    );  

assign fifo_wr_data = {data[15:8],data[23:16],data[7:0]};  

assign fifo_wr_req = data_vld_r && ~fifo_full;  

assign fifo_rd_req = end_cnt_24B && ~fifo_empty;

最后,根据数据的传输方式,使用1—wire协议对数据进行输出

实现单总线协议

parameter TIME_280US = 14_000,//280us

          TIME_01M   = 65    ,//0、1码周期1300ns

          TIME_24B   = 24    ,//24bit data

          TIME_64    = 64    ;//64个24bit data

 

    always @(posedge clk or negedge rst_n)begin  

        if(!rst_n)begin

            ws2812_io <= 1 ;

        end  

        else begin

            case (state_c)

                  IDLE : ws2812_io <= 0 ;

                  RES  : ws2812_io <= 0 ;

                  DATA : if(fifo_rd_data[23 - cnt_24B]) begin

                             if(cnt_01M < 35)  

                                 ws2812_io <= 1 ;

                             else

                                 ws2812_io <= 0 ;

                           end

                         else begin

                             if(cnt_01M < 18)  

                                 ws2812_io <= 1 ;

                             else

                                 ws2812_io <= 0 ;

                           end

                default: ws2812_io <= 1 ;

            endcase

        end  

    end

4 系统测试与结果分析

4.1    整体仿真图

在顶层仿真中,模拟一次游戏过程,随机产生按键信号,也就是按键一直控制蛇身往一个方向,撞墙后结束,控制游戏结果为输:

initial                                                

begin    

clk   <= 0;                                            

rst_n <= 0;

key_in   <= 4'hf;

#500                      

rst_n <= 1;  

#50000

key_in   <= 4'he;    

#50000

key_in   <= 4'hf;  

#50000

key_in   <= 4'hd;  

#50000

key_in   <= 4'hf;                                                              

// $display("Running testbench");     //显示函数                  

end                                                    

always #10 clk <= ~clk;      

顶层波形图如下:

顶层仿真各模块波形图(示例)snake_ctrl模块

4.2    分模块仿真分析

4.2.1   按键消抖模块仿真

(一)仿真文件解析

为了模拟多个按键按下进行消抖,我们使用for循环进行多次赋值,重点如下:

//产生激励

    initial  begin

        tb_rst_n = 1'b1 ;

        key_in = 4'b1111;//赋初值为高

        #(CLOCK_CYCLE*2);

        tb_rst_n = 1'b0 ;

        #(CLOCK_CYCLE*2);

        tb_rst_n = 1'b1 ;

        #1;

       key_in[0] = 0;

        for (j=0;j<8;j=j+1) begin //模拟按下抖动产生高低电平

            i = {$random}%500;

            #i;

            key_in[0] = i;//key_in位宽位1,只会取i的最低位;

        end

        key_in[0] = 1'b0;//为了保证按键稳定按下

        wait(tb_fsm_key_filter.end_cnt_20ms);//延迟到括号里的判断条件为真时

        #1000;

        key_in[0] = 1'b1;

        #100;

        #(CLOCK_CYCLE*100);

       #1;//模拟按键随机按下

        key_in = 4'b1001;

        for (j=0;j<8;j=j+1) begin //模拟按下抖动产生高低电平

            i = {$random}%500;

            #i;

            key_in[2] = i;

            key_in[1] = i;//key_in位宽位1,只会取i的最低位;

        end

        key_in = 4'b1001;//为了保证按键稳定按下

        wait(tb_fsm_key_filter.end_cnt_20ms);//延迟到括号里的判断条件为真时

        #1000;

        key_in = 4'b1111;

        #100;

        #(CLOCK_CYCLE*20);

         $stop;

    end

(二)波形分析

4.2.2   次子模块游戏界面模块整体仿真

(一)仿真文件解析

       这里使用的模拟方式和顶层相似,同样是对按键进行模拟,不过要注意的是各个子模块的时间参数重定义

    parameter       CLOCK_CYCLE = 20;  

    defparam        snake_ctrl_inst.start_data_inst.DLEAT_MAX = 15;

    defparam        snake_ctrl_inst.low_data_inst.DLEAT_MAX = 15;

    defparam        snake_ctrl_inst.win_data_inst.DLEAT_MAX = 15;

    defparam        snake_ctrl_inst.game_ctrl_inst.DELAY_MAX  = 20;



//描述输入

    initial begin

        ready = 1;

        key_in   <= 4'h0;

        #500                        

        #50000

        key_in   <= 4'he;    

        #50000

        key_in   <= 4'h0;  

        #50000

        key_in   <= 4'hd;  

        #50000

        key_in   <= 4'hf;

        wait(snake_ctrl_inst.game_ctrl_inst.low_en);

        #500

        $stop;

  • 波形分析

首先是游戏整体流程波形反馈:

然后是输出界面正常:

观察从ROM中读取出来的数据是否存在:

帧间隔,数据传输是连续的,cnt_y计数结束后接着传输下一帧数据,每次数据传输时间间隔较小,所以人眼无法捕捉,LED显示的画面就是“静止的”,要让图片动起来,就要参考偏移量了。

对照mif文件的数据,输出为正确

最后看看fifo缓冲的数据是否有误:

放大:

4.2.3   游戏界面显示驱动模块仿真

(一)仿真文件解析

由于我们输出的数据每一帧都是64个24bit的数据,所以这里使用repeat语句进行循环输出随机数

//产生激励

    initial  begin

        rst_n = 1'b1;

        #(CLOCK_CYCLE*2);

        rst_n = 1'b0;

        #(CLOCK_CYCLE*20);

       rst_n = 1'b1;

    end



    initial begin

        data     = 0;

        data_vld = 0;

        #(CLOCK_CYCLE*6);

       

        data_vld = 1;

        repeat(64) begin

            data = {$random};#20;

            data_vld = 0;

            #200;

            data_vld = 1;

        end

        #10080;

        $stop;

    end

(二)波形分析

总结

问题 1、工程实现问题:

问题:最开始在实现工程时,我直接开始状态机的设计,将蛇的所有功能全部写在一个模块,因为涉及到果子随机生成,和按键控制设移动还有界面的显示,导致我做的状态机逻辑梳理不够清晰。

解决方法:重新调整思路,我将工程拆分,把游戏控制和开始/输/赢界面分成多个子模块,最终在一个模块中利用状态机输出不同的数据。

问题 2、图像错位的问题:

问题:在上板验证时,图像显示,但是显示的时候图像向右偏移了两列。

解决:根据之前做串口发送数据 ws2812 实时显示项目经验,此种问题是数据有效信号跨时钟域(虽然都是50M但是在数据传输过程要考虑时序是否能刚好对上)的问题,在各个模块中对有效信号进行打一拍,数据就对的上了。

  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于WS2812的贪吃蛇小游戏需要使用Verilog语言进行设计。首先,需要了解WS2812是一种RGB LED驱动芯片,可实现每个像素独立控制颜色和亮度的特性。 设计思路是将WS2812组成一个2D网格,每个像素代表蛇的一节身体或食物。首先,需要实现蛇的移动逻辑,可以使用一个包含蛇身坐标的链表数据结构来记录蛇的位置信息。当蛇吃到食物时,链表会增加一个新的节点,表示蛇的身体增长。 接下来,需要处理用户输入。可以通过外部IO接口,例如按键或开关来控制蛇的移动方向。根据用户输入,更新蛇头的位置和链表中的节点顺序。 同时,还需要处理蛇与食物之间的碰撞检测。如果蛇头与食物位置重合,表示蛇吃到了食物,将食物节点添加到链表中,并生成一个新的随机食物位置。 另外,还需要处理蛇与自身身体的碰撞检测。如果蛇头与蛇身碰撞,表示游戏结束,可以根据设计需求,输出游戏结束信号或清空链表数据。 最后,需要将链表中的节点信息映射到WS2812上,实现蛇和食物的显示效果。可以使用Verilog中的定时器和状态机来控制WS2812的RGB值和亮度,实现动画效果。 总结起来,基于WS2812的贪吃蛇小游戏的Verilog设计主要包括蛇的移动逻辑、用户输入处理、碰撞检测以及WS2812的控制和显示效果。通过合理的设计和编程,可以实现一个动态、有趣的贪吃蛇游戏

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值