项 目 摘 要
本设计是一种基于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灯带显示贪吃蛇游戏画面,包括蛇身、食物和背景等元素的显示效果。
目录
使用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但是在数据传输过程要考虑时序是否能刚好对上)的问题,在各个模块中对有效信号进行打一拍,数据就对的上了。