1. 项目需求
在当今快速发展的智能设备领域,对于高效、灵活的输入设备的需求不断攀升。第四届2024年“寒假在家一起练”活动为我们提供了一个绝佳的平台,该活动推出了10款不同难度的硬件平台,旨在鼓励大一新生及不同水平的学生参与。本次活动涵盖了多种MCU和FPGA平台,并支持不同的编程语言,以适应不同的应用领域。
本项目的核心宗旨是通过实践结合数字电路书本知识,深刻理解数字逻辑的功能实现及设计流程。同时,项目也旨在培养工程化设计理念、规范化的设计流程,以及解决未知问题的能力。本次活动我选择了STEP小脚丫FPGA开发板STEP-MXO2-LPC,该板卡基于基于Lattice MXO2 硬件平台,MachXO2内置硬件加速使您能够减轻处理器工作负载并提升系统性能,MachXO2提供3.3/2.5伏和1.2伏版本,待机功耗低至22微瓦。
由于RGB控制任务的仿真没调通,时间来不急,所以又重新选择了:秒表功能任务
任务实现目标功能
- 秒表:使用小脚丫FPGA核心板上的两个数码管和轻触按键制作一个秒表。秒表将通过按键控制其功能,并在数码管上显示数值。秒表将从0.0秒计数到9.9秒,然后翻转,计数值每0.1秒精确更新一次。
- 按键操作:秒表的操作将通过四个按钮实现:开始、停止、增量和清除(重置)。
2.需求分析
- 功能需求
- 开始按钮:按下开始按钮后,秒表应从0.0秒开始计时。
- 停止按钮:在秒表运行时,按下停止按钮应暂停计时,且数码管显示当前时间。
- 增量按钮:按下增量按钮应使秒表的计数值增加0.1秒,可连续按压以实现连续增加。
- 清除按钮:按下清除按钮应将秒表重置至0.0秒,并停止计时。
- 翻转功能:当秒表计数值达到9.9秒时,应自动翻转回0.0秒,继续计时。
- 性能需求
- 计时精度:秒表的计时精度需达到0.1秒。
- 响应时间:按键操作后,数码管上的显示应在100毫秒内更新以反映当前状态。
- 稳定性:秒表在运行过程中应保持稳定,无明显延迟或卡顿现象。
- 用户界面需求
- 数码管显示:使用清晰可见的数码管显示秒表读数,确保用户在不同光线条件下均能准确读取。
- 按键布局:四个操作按钮应布局合理,易于用户识别和操作
3.硬件设备介绍
STEP-MXO2-LPC板卡:
STEP-MXO2-LPC板卡在易用性方面进行了大幅升级,采用USB Type C接口供电和配置FPGA,并新增UART通信功能。此外,支持U盘模式下载,使得任何操作系统的电脑都能在无需安装驱动程序的情况下完成编程。为了配合这款FPGA的使用,Web IDE系统也进行了升级,用户可以在任何电脑上通过浏览器进行FPGA的编程和编译,无需下载安装Diamond软件,使得操作更加直观和便捷。
- 使用了USB Type C接口提供板上+5V供电、FPGA的配置,并新增了UART通信的功能,因此无需再通过其它端口与PC进行数据通信;
- 支持U盘模式(连接到上位机的USB端口,上位机自动弹出StepFPGA的U盘盘符)的下载,任何操作系统的电脑 - Windows、Mac OS以及Linux(包括树莓派)都可以在不安装任何驱动程序的情况下,直接将生成的jed配置文件发送到StepFPGA盘中即可完成编程;
- 为配合这款小脚丫FPGA的使用,我们特别升级了Web IDE系统,用户不必再下载安装Diamond软件,即可在任何一款电脑上通过浏览器进行FPGA的编程和编译。图形化的界面使得操作非常直观、便捷。
硬件特性:
- 核心器件:Lattice LCMXO2-4000HC-4MG132 132脚BGA封装,引脚间距0.5mm,芯片尺寸8mm x 8mm; 上电瞬时启动,启动时间<1ms; 4320个LUT资源, 96Kbit 用户闪存,92Kbit RAM; 2+2路PLL+DLL; 嵌入式功能块(硬核):一路SPI、一路定时器、2路I2C 支持DDR/DDR2/LPDDR存储器; 104个可热插拔I/O; 内核电压2.5-3.3V;
- 板载资源: 两位7段数码管; 两个RGB三色LED; 8路用户LED; 4路拨码开关; 4路按键;
- 36个用户可扩展I/O(其中包括一路SPI硬核接口和一路I2C硬核接口)
- 支持的开发工具思德普开发的Web IDE以及Lattice官方提供的Diamond
- 支持MICO32/8软核处理器以及RISC-V软核
- 板上集成FPGA编程器,采用U盘的模式
- 一路USB Type C接口,可用于给核心板供电、给FPGA下载JED文件以及同上位机通过UART通信 板卡尺寸52mm x 18mm
引脚定义 如下图所示:
4.实现与功能流程图
实现:
整个程序流程开始于系统上电或复位时,此时所有寄存器和计数器被重置到初始状态。随后,消抖模块开始处理原始的按钮输入,确保在检测到稳定的按键动作时才输出脉冲信号。主控制逻辑不断监测这些脉冲,并根据当前的按键状态更新系统状态。按键的不同状态触发不同的操作:启动或停止计数器、增加或重置计数值。计数器本身在每个100毫秒的时钟脉冲上升沿递增,并且在达到最大值时回滚重新从0.0开始计数。计数值的十位和个位数字用于更新两个七段显示器的显示。此外,LED的状态也根据计数器和其他控制信号进行相应的更新。整个流程由两个始终活跃的逻辑块组成,一个负责按键消抖,另一个负责计数器逻辑和显示更新,确保系统能够响应用户的输入并正确显示计数值。程序流程如下图所示:
程序流程图:
管脚配置如下图所示,按蓝色标识所示,依次为:开始,停止,加1和复位清除
5.代码及功能说明
5.1 开发环境准备:
STEP-MXO2-LPC的编程芯片已经集成到小脚丫开发板上,因此只需要一根USB Type C线和电脑相连,就可以完成供电和编程的功能,无需安装驱动。
开发过程可以使用思德普的Web IDE或Lattice的Diamond软件,将该板卡与PC连接后,将被识别成“大容量存储设备”,盘符为StepFPGA,你只需要将程序生成的.JED文件复制进入板卡之中,即可完成下载。
采用思得普开发的网页版编译工具,参见页面:快速上手小脚丫线上设计工具,在小脚丫网站www.stepfpga.com注册账号后就可以体验使用线上设计工具,基于浏览器端的开发环境,无需下载FPGA设计工具到本地电脑。使用方便简单。
5.2 分频器功能实现:
CLKDIV模块是一个分频器,它的功能是将输入时钟信号clk分频,以产生一个周期更长的输出信号divout。这个输出信号用于生成一个定时的脉冲,CLKDIV模块通过计数输入时钟周期,并在达到预设的计数值时产生一个脉冲。这个脉冲的周期可以通过改变CNT2MS和CNT_1MS的值来调整。在本例中,模块被配置为每100毫秒产生一个高电平脉冲,这个脉冲可以用于驱动其他电路或触发定时事件:
module CLK_DIV(
input clk,
input rst_n,
output div_out
);
parameter CNT_2MS = 'd12_00000 - 1; // 2ms, system clock 12000000 d60_000 - 1
parameter CNT_1MS = CNT_2MS >> 1;
reg [23:0] div_counter = 0; //
reg div_clk=1'b0;
reg clkdiv;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) div_counter <= 1'b0;
else if (div_counter >= CNT_2MS)
div_counter <= 1'b0;
else div_counter <= div_counter + 1'b1;
clkdiv <= (div_counter > CNT_1MS)? 1'b1 : 1'b0;
end
assign div_out = (div_counter > CNT_1MS)? 1'b1 : 1'b0;
endmodule
5.3 分频器功能实现:
LEDDOT模块的功能是驱动一个七段数码管显示,它接收一个4位的输入segdata_1,该输入代表一个0到9之间的数字或者特定的字母(如A到F),并将其转换为七段数码管上的显示。模块内部使用一个名为mem的9位寄存器数组来存储预定义的数码管编码,这些编码对应于不同的数字和字母。每个编码代表数码管上的七个段(从A到G),以及一个可选的点(DP)。
简而言之,LED_DOT模块通过查找表(mem数组)的方式,将输入的数字或字母转换为七段数码管的显示编码,并根据需要控制点的显示,从而实现数字和字母在数码管上的可视化。
module LED_DOT(seg_data_1,seg_led_1);
input [3:0] seg_data_1;
output [8:0] seg_led_1;
parameter DOT_EN = 0;
reg[8:0] mem [15:0];
initial
begin
mem[0] = 9'h3f; // 0
mem[1] = 9'h06; // 1
mem[2] = 9'h5b; // 2
mem[3] = 9'h4f; // 3
mem[4] = 9'h66; // 4
mem[5] = 9'h6d; // 5
mem[6] = 9'h7d; // 6
mem[7] = 9'h07; // 7
mem[8] = 9'h7f; // 8
mem[9] = 9'h6f; // 9
mem[10]= 9'h77; // A
mem[11]= 9'h40; // b
mem[12]= 9'h39; // C
mem[13]= 9'h5e; // d
mem[14]= 9'h79; // E
mem[15]= 9'h71; // F
if (DOT_EN == 1) begin
mem[0][7] = 1'b1;
mem[1][7] = 1'b1;
mem[2][7] = 1'b1;
mem[3][7] = 1'b1;
mem[4][7] = 1'b1;
mem[5][7] = 1'b1;
mem[6][7] = 1'b1;
mem[7][7] = 1'b1;
mem[8][7] = 1'b1;
mem[9][7] = 1'b1;
mem[10][7] = 1'b1;
mem[11][7] = 1'b1;
mem[12][7] = 1'b1;
mem[13][7] = 1'b1;
mem[14][7] = 1'b1;
mem[15][7] = 1'b1;
end
end
assign seg_led_1 = mem[seg_data_1];
endmodule
5.4 按键防抖功能:
这个debounce模块的目的是消除按键操作时产生的抖动,确保按键动作能够被稳定地识别。
定义一组寄存器型变量存储上一个触发时的按键值和当前时刻触发的按键值,检测到按键由高到低变化是产生一个高脉冲。延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效。
// Module Function:按键消抖
module debounce (clk,rst,key,key_pulse);
parameter N = 1; //要消除的按键的数量
input clk;
input rst;
input [N-1:0] key; //输入的按键
output [N-1:0] key_pulse; //按键动作产生的脉冲
reg [N-1:0] key_rst_pre; //定义一个寄存器型变量存储上一个触发时的按键值
reg [N-1:0] key_rst; //定义一个寄存器变量储存储当前时刻触发的按键值
wire [N-1:0] key_edge; //检测到按键由高到低变化是产生一个高脉冲
//利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
always @(posedge clk or negedge rst)
begin
if (!rst) begin
key_rst <= {N{1'b1}}; //初始化时给key_rst赋值全为1,{}中表示N个1
key_rst_pre <= {N{1'b1}};
end
else begin
key_rst <= key; //第一个时钟上升沿触发之后key的值赋给key_rst,同时key_rst的值赋给key_rst_pre
key_rst_pre <= key_rst; //非阻塞赋值。相当于经过两个时钟触发,key_rst存储的是当前时刻key的值,key_rst_pre存储的是前一个时钟的key的值
end
end
assign key_edge = key_rst_pre & (~key_rst);//脉冲边沿检测。当key检测到下降沿时,key_edge产生一个时钟周期的高电平
reg [17:0] cnt; //产生延时所用的计数器,系统时钟12MHz,要延时20ms左右时间,至少需要18位计数器
//产生20ms延时,当检测到key_edge有效是计数器清零开始计数
always @(posedge clk or negedge rst)
begin
if(!rst)
cnt <= 18'h0;
else if(key_edge)
cnt <= 18'h0;
else
cnt <= cnt + 1'h1;
end
reg [N-1:0] key_sec_pre; //延时后检测电平寄存器变量
reg [N-1:0] key_sec;
//延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec <= {N{1'b1}};
else if (cnt==18'h3ffff)
key_sec <= key;
end
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec_pre <= {N{1'b1}};
else
key_sec_pre <= key_sec;
end
assign key_pulse = key_sec_pre & (~key_sec);
endmodule
5.5 顶层文件主要功能实现
- 按键状态功能实现:用于处理和识别来自不同按键的信号,并将其状态存储在key_state寄存器中。这里的逻辑是为了实现一个按键状态机,它可以根据按键的不同脉冲信号来更新状态。
always @(posedge clk_in or negedge rst_in) begin
if (!rst_in) begin
key_state = 4'd0;
end
else if (key_pulses[0]) begin
key_state <= 4'd1;
end
else if (key_pulses[1]) begin
key_state <= 4'd2;
end
else if (key_pulses[2]) begin
key_state <= 4'd3;
end
else if (press_key_process_done) begin
key_state <= 4'd0;
end
end
- 按键动作功能代码实现: 它在led2的100ms上升沿(由外部时钟或其他逻辑产生)或rstin的下降沿触发。主要功能是响应按键状态(由keystate表示),并根据按键动作来控制一个计数器(totalcounter)的启动、停止、重置和计数逻辑,同时更新两个七段数码管(segledshowdata1和segledshow_data2)的显示内容。
always @(posedge led2 or negedge rst_in) begin
if (!rst_in) begin
bit_num1 <= 4'd0;
bit_num2 <= bit_num1;
seg_led_show_data1 <= 4'd0;
seg_led_show_data2 <= 4'd0;
counter_enable <= 1'b0;
total_counter <= 0;
press_key_process_done = 1'b0;
end
else begin
case (key_state)
4'd1:begin
counter_enable <= 1'b1;
press_key_process_done <= 1'b1;
end
4'd2:begin
counter_enable <= 1'b0;
press_key_process_done <= 1'b1;
end
4'd3:begin
total_counter <= total_counter + 1;
press_key_process_done <= 1'b1;
end
4'd0:begin
press_key_process_done <= 1'b0;
end
endcase
if (counter_enable) begin
if (total_counter > 99) total_counter <= 00;
else total_counter <= total_counter + 1;
end
bit_num1 <= total_counter / 10;
bit_num2 <= total_counter - (bit_num1*10);
end
end
5.6 项目算法实现和问题处理方法
此外,项目实现和调试过程中,大量使用到Chat-GPT功能,特别是相关问题解决与处理,和verilog相关语法问题的解决,例如:传统程序设计向FPGA并行设计思路切换时会遇到多重驱动问题,因各个always模型是并行运行的,因此,不同能两个always语句块中驱动修同一变量等,使用示例如下图所示:
6.仿真波形图
由于本次设计所需功能均为动态功能,不太好以图片形式进行展示,然而FPGA的仿真测试可以直观地展示功能实现情况和数据逻辑功能变化,因此,本部分展示逻辑仿真实验结果:
- 计数器开始启动功能:
从仿真图片可见,模似按下按键后,从黄线开始total_counter 寄存器变量开始自增:
- 计数器停止计数功能:
从仿真图片可见,模似按下按键后,从黄线开始total_counter 寄存器变量开始保持88不变:
- 计数器加一计数功能: 从仿真图片可见,模似按下按键后,从黄线开始total_counter 寄存器变量从88增加到89后不变:
- 计数器重置计数功能: 从仿真图片可见,模似按下按键后,从黄线开始total_counter 寄存器变量由89重置为0:
7.FPGA的资源利用说明
8.活动总结与未来计划建议
首先, 感谢硬禾学堂联合Lattice公司发起第四届2024年“寒假在家一起练”活动,为我们提供了一个绝佳的平台,让我有机会参与FPGA的开发与实践,通过参与本项目,参与者不仅能够提升自己的数字电路设计和FPGA应用开发能力,还能够在实践中深入理解数字逻辑设计思想。
通过实践结合数字电路书本知识,深刻理解数字逻辑的功能实现及设计流程。同时,项目也旨在培养工程化设计理念、规范化的设计流程,以及解决未知问题的能力。通过本项目,参与者将有机会探索使用行业新工具在项目研发中可能遇到的问题,并寻找有效的解决方法。
希望硬禾学堂和DigiKey越办越好。