一、项目需求
该项目为硬禾学堂第四届2024“寒假在家一起练”内容,期望实现目标为:使用基于Lattice MXO2的小脚丫FPGA核心板-Type C接口,通过小脚丫FPGA核心板上的2个数码管和4个轻触按键制作一个秒表,通过按键来控制秒表的功能,并在数码管上显示数值。
使用七段显示器作为输出设备,在小脚丫FPGA核心板上创建一个2位数秒表。 秒表应从 0.0 秒计数到 9.9秒,然后翻转,计数值每0.1秒精确更新一次。
秒表使用四个按钮输入:开始、停止、增量和清除(重置)。 开始输入使秒表开始以10Hz时钟速率递增(即每0.1秒计数一次); 停止输入使计数器停止递增,但使数码管显示当前计数器值; 每次按下按钮时,增量输入都会导致显示值增加一次,无论按住增量按钮多长时间; 复位/清除输入强制计数器值为零。
二、需求分析
期望实现上述功能,需要以下功能模块:
① 七位数码管的显示驱动:依靠七位数码管来显示数字及小数点。
② 按键功能引入:四个轻触按键分别对应四个功能,开始、停止、增量和清除。在停止状态下按开始按键开始计时,在计时状态下按停止暂停计时、在停止状态下按增量实现每按一下计数增加0.1、在任何状态下按清除按钮实现计数复位。
③ 时钟分频:将12MHz的时钟降低到项目所需的10Hz需要时钟分频模块实现。
④ 计数逻辑框架:计数从0.0至9.9后需要重置0.0继续开始计数,需要搭建正确的计数逻辑和按键逻辑保证程序的正确运行。
三、实现的方式
实现一个基于FPGA的2位数秒表,涉及到数字设计和FPGA编程。这个项目需要创建一个计数器、一个时钟分频器、一个数码管显示系统以及一套按钮控制逻辑。
1.确定FPGA平台及开发环境:
验证小脚丫FPGA核心板的详细规格和开发工具链。
2.时钟分频器设计:
小脚丫FPGA核心板提供的主时钟频率为12MHz,需要设计一个时钟分频器以产生10Hz的时钟信号,用于计数器。
3. 计数器设计:
按照需求设计一个可以计数0.0秒到9.9秒的计数器。
设计计数器的增量逻辑,使其每次接收到10Hz时钟信号时递增0.1秒。
4. 七段显示器逻辑设计:
设计七段显示器的驱动逻辑,用于将计数器的值转换成对应的显示编码。
考虑使用BCD(二进制编码的十进制数)作为中间表示形式简化转换逻辑。
5. 按钮输入逻辑设计:
实现一个防抖动逻辑,保证按钮状态的稳定读取。
为每个按钮(开始、停止、增量、复位)实现对应的控制逻辑。
6. 开始、停止逻辑实现:
设计逻辑用于控制开始按钮使秒表开始计数。
设计逻辑用于停止按钮暂停秒表,保持当前显示。
7. 增量和复位逻辑实现:
实现一个对增量按钮敏感的逻辑,以实现每次按钮按下即增加0.1秒。
实现复位逻辑,将计数器和七段显示器归零。
8. 测试与验证:
在FPGA开发环境中模拟整个设计,验证时序和功能。
将设计编译,然后下载到小脚丫FPGA核心板上进行现场测试,包括所有按钮功能和显示。
9. 项目文档:
编写设计文档,记录项目的设计思路、实现细节和测试结果。
10. 优化:
分析设计的资源使用情况并对性能进行调优。
实现功能后,可以尝试减少逻辑资源消耗和提高时计的精确度。
四、代码及说明
counter.v
// This is a stopwatch module, it can start, pause, increment and reset via buttons.
module counter
(
clk, //input, clock signal
start, //input, start button
stop, //input, pause button
increment, //input, increment button
rst, //input, reset button
seg_led_1, //output, seven segment display for units position
seg_led_2, //output, seven segment display for tens position
);
input clk, rst;
input start;
input stop;
input wire increment;
output [8:0] seg_led_1, seg_led_2;
wire clk10h; //10Hz clock generated with a clock divider
wire increment_pulse; //Increment pulse signal generated with edge detector
reg [6:0] seg [9:0]; //stores values to be displayed on the seven segment display, corresponding to numbers 0 to 9.
reg [3:0] cnt_ge = 4'b0; //units position counter
reg [3:0] cnt_shi = 4'b0; //tens position counter
reg running = 0; //running state
initial
begin
// Initialization of seg values, these are 7-bit values, each corresponding to the segments of a seven segment display
seg[0] = 7'h3f; // corresponds to number 0
seg[1] = 7'h06; // corresponds to number 1
seg[2] = 7'h5b; // corresponds to number 2
seg[3] = 7'h4f; // corresponds to number 3
seg[4] = 7'h66; // corresponds to number 4
seg[5] = 7'h6d; // corresponds to number 5
seg[6] = 7'h7d; // corresponds to number 6
seg[7] = 7'h07; // corresponds to number 7
seg[8] = 7'h7f; // corresponds to number 8
seg[9] = 7'h6f; // corresponds to number 9
end
//Clock divider module
divide U1 (
.clk(clk),
.rst_n(rst),
.clkout(clk10h)
);
// Edge detector module checks the increment button status and outputs a pulse when the button state changes from button not pressed to button pressed
edge_detector u1(.clk(clk10h), .btn(increment), .pulse(increment_pulse));
// Triggered on the rising edge of the 10Hz clock or the falling edge of rst signal
always @(posedge clk10h or negedge rst) begin
if (rst==0) begin // When reset signal is low
cnt_shi <= 4'b0; // Reset tens digit counter
cnt_ge <= 4'b0; // Reset units digit counter
running <= 0; // Set running to 0 after reset, this will pause the count
end
else begin
if (start==0 && running == 0) begin // When start is pressed and counters are not running
running <= 1; // Set running to 1 and start count
end
if (stop==0 && running == 1) begin // When stop is pressed and counters are running
running <= 0; // Set running to 0 and pause count
end
if (increment_pulse && running == 0) begin // When increment button is pressed and counters are not running
if (cnt_ge == 9 && cnt_shi==9) begin // If count is at 9.9, reset the counters
cnt_ge <= 4'b0;
cnt_shi <= 4'b0;
end else if(cnt_ge == 9 && cnt_shi!=9)begin // If units counter is at 9, increment tens counter and reset units counter
cnt_ge <= 4'b0;
cnt_shi <= cnt_shi+4'b1;end
else begin // Otherwise, simply increment the units counter
cnt_ge <= cnt_ge + 1'b1;
end
end
if (running==1) begin // The counting logic
if (cnt_ge == 9) begin // If units counter is at 9
cnt_ge <= 4'b0;
if (cnt_shi == 9) cnt_shi <= 4'b0; // If also tens counter is at 9, reset both counters
else cnt_shi <= cnt_shi + 1'b1; // Otherwise only increment the tens counter
end else begin // Otherwise only increment the units counter
cnt_ge <= cnt_ge + 1'b1;
end
end
end
end
assign seg_led_1[8:0] = {2'b00, seg[cnt_ge]}; // Display units position
assign seg_led_2[8:0] = {2'b01, seg[cnt_shi]}; // Display tens position
endmodule
这段代码是一个秒表模块的Verilog实现。它利用小脚丫FPGA核心板上的四个按键和两个7段数码显示器作为输入和输出。四个按键分别是开始、停止、加一和复位,按键信号是二进制输入,开始和停止按键还有一个运行状态信号进行参考。当开始键被触发且运行状态为0时,模块开始计数,运行状态变为1;当停止键被触发且运行状态为1时,模块暂停计数,运行状态变回0。
计数器包括两部分:个位计数(cnt_ge)和十位计数(cnt_shi),它们在开始计数后每0.1秒递增一次,这是通过边缘检测和调频模块实现的。另外,加一键触发时,计数器将增加一个单位,无论此时的运行状态。
在计数过程中,如果个位计数等于9,且十位计数不等于9,则个位计数归零,十位计数加一;若十位计数也等于9,则所有计数复位。若个位计数不等于9,则只将个位计数增加一位。
计数的数值显示在两个7段数码显示器上,分别表示个位和十位。仅当它们对应的计数增加时,才会刷新显示。
代码的主体封装在一个名为“counter”的模块中,它包含了调频模块、边缘检测模块和计数控制部分。最后,即使复位信号被触发,计数也会回到初始状态。
divid.v
module divide(
input wire clk, // Input, 12MHz clock
input wire rst_n, // Input, Reset signal
output wire clkout // Output, 10Hz clock
);
reg [23:0] counter = 0; // Define a 24-bit counter
reg out_flag = 0; // Define a flag bit
assign clkout = out_flag; // Output the value of the flag bit
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin // When the negative reset signal is 0, reset the counter and the flag bit
counter <= 0;
out_flag <= 0;
end
else begin
if(counter == 600000) begin // When the counter value equals 600000, toggle the flag bit state and reset the counter
out_flag <= ~out_flag;
counter <= 0;
end
else begin // In other cases, the counter counts normally
counter <= counter + 1;
end
end
end
endmodule
这是一个频率分频器模块的Verilog实现,它接收一个12MHz的时钟信号,并将其分频为10Hz。
这个模块有三个接口,输入时钟(clk)和复位信号(rst_n),以及输出时钟(clkout)。
在模块内部,定义了一个24位的计数器(counter)和一个标志位(out_flag)。输出时钟信号(clkout)等于这个标志位。
在始终上升沿或复位信号下降沿时,计数器和标志位将触发计数或复位。当复位信号(rst_n)为0时,计数器和标志位都会重置为0。否则,如果计数器的值达到600000,标志位将切换状态,然后计数器重置为0。如果计数器的值小于600000,那么计数器将正常计数。
通过这种方式,这个模块实现了将上游12MHz的时钟分频为10Hz的功能。这在一些
需要低频信号的应用场景中非常有用。例如,对于一些需要低速操作的硬件或设备,将高频信号分频为低频信号是必要的。
edge_detector.v
// This module is an edge detector. It generates a pulse at the instant each time a button goes from not being pressed to being pressed.
module edge_detector(
input wire clk, // Input, Clock signal
input wire btn, // Input, Button state: 1 denotes the button is pressed, 0 denotes it is not pressed
output reg pulse // Output, Pulse signal: When a change in the button state is detected (from not pressed to pressed), a pulse is generated
);
reg btn_last; // The state of the 'btn' in the last clock cycle
// Trigger on the rising edge of the clock
always @(posedge clk)
begin
// If the current state is "pressed" and 'btn' was not pressed in the last clock cycle, a pulse is generated
if(btn && !btn_last)
pulse <= 1'b1;
else
pulse <= 1'b0; // Otherwise, no pulse is generated
btn_last <= btn; // Update the button state of the last cycle to the current state
end
// Set the initial value of 'btn_last' at the start of the simulation
initial begin
btn_last = 1; // Set the initial value of 'btn_last' to 1.
end
endmodule // end of edge_detector
这是一个边缘检测器模块的Verilog代码。边缘检测器用于在按钮从未被按下状态变为被按下状态时生成一个脉冲信号。
模块有三个接口:输入时钟信号(clk)、输入按钮状态(btn)和输出脉冲信号(pulse)。其中,按钮状态(btn)是二进制输入信号,1表示按钮被按下,0表示未被按下。
在模块内部,定义了一个寄存器btn_last来保存上一个时钟周期的按钮状态。
在每个时钟周期的上升沿,会检查当前的按钮状态,如果当前按钮被按下(btn为1),且上一个时钟周期未被按下(btn_last为0),那么就会产生一个脉冲,即pulse会被置为1。否则,pulse会被置为0。
在模块启动的初始状态(即开始仿真时),会先设定btn_last寄存器的初始值为1。这是考虑到在初始状态下,我们无法知道按钮的实际状态,因此默认设置为未按下的状态。
可以看到,这个模块很好地实现了边缘检测器的功能,即只在按钮状态发生改变(从未按下变为按下)时才产生一个脉冲信号。
五、仿真波形图
图1 divide.v模块仿真结果图
通过仿真结果可以看出,该模块输入为clk和rst_n,输出为clockout,设置输入时钟为12MHz信号,经过分频成功得到了10Hz信号,该输出信号以0.1s为周期。
图2 edge_detctor.v模块仿真结果图
输入时钟周期为10Hz,边缘检测电路当按键btn当前为0且btn前一周期为1时输出一个高电平。
图3 分频系统
我们在仿真文件counter_tb.v中设置时钟为100MHz,修改divide.v中的分频系数为5,得到clk10h的信号输出10MHz,证明分频系统在模块调用中正常工作且计数正常。
图4 按下复位按键
按下复位按键后rst输入信号由高电平变化为低电平,同时系统将计数归零。可以看到此时计数标志running为零,七段数码管在当前阶段显示0.0并且未开始计数,因此rst信号按下后均无变化。
图5 按下开始按键
仿真显示按下开始按键后,start信号由高电平变化至低电平,在下一个clk10h的时钟周期上升沿个位数数码管开始计数增加,由0增加至9之后变化为零,将十位数码管计数加1,继续按照该逻辑进行计数。仿真结果显示计数逻辑和开始按键逻辑正确。
图6 按下暂停按键
仿真显示按下暂停按键后,stop信号由高电平变化为低电平,在下一个clk10h时钟上升沿到来后计数暂停,证明暂停按键逻辑正确,功能正常。
图7 加一按键按下
仿真结果显示当加一按键按下后,increment信号由高电平变化至低电平,此时计数运行标志位running为0,符合程序的加一条件。在下一个时钟上升沿到来时increment_pulse信号由低电平变为高电平用来标志加一,防止了长按加一按键导致的计数多次增加。加一结果如下图所示。
图8 加一结果
和图7进行比较可以发现cnt_ge由4‘h8变化至4’h9,成功实现了个位加一的效果。
图9 暂停状态下复位
在暂停状态下加一后,复位信号有效实现了cnt_ge和cnt_shi均复位为0的效果,且复位后程序暂停,等待start信号有效后继续计数,符合程序设计逻辑。
六、FPGA的资源利用说明
1. 注册器 (Registers):
总注册器位数 (Number of register bits) => 32 位中的 4635 位(0%)
2. 逻辑单元 (SLICEs):
总逻辑单元 (Number of SLICEs) => 36 个中的 2160 个(2%)
作为逻辑/ROM的逻辑单元 (SLICEs as Logic/ROM) => 36 个中的 2160 个(2%)
作为RAM的逻辑单元 (SLICEs as RAM) => 0 个中的 1620 个(0%)
作为Carry的逻辑单元 (SLICEs as Carry) => 11 个中的 2160 个(1%)
3. 查找表 (LUTs):
总LUT4 (Number of LUT4s) => 71 个中的 4320 个(2%)
用作逻辑LUTs的查找表 (Number used as logic LUTs) => 49 个
4. 引脚输出 (PIO):
PIN引脚总数 (Number of PIO sites used) => 23 + 4(JTAG)个中的 105 个(26%)
锁定的PIN引脚百分比 (Pin Constraint Summary) => 100% 已锁定
5. 内存 (RAM):
阻塞RAM (Number of block RAMs) => 0 个中的 10 个(0%)
6. 全球设定复位 (GSR):
全球设定复位模块 (Number of GSRs) => 1 个中的 1 个(100%)
7. 时钟 (Clock):
时钟网 (Number of Clocks) => 2 个
8. 钟启用 (Clock Enable Nets):
钟启用网 (Number of Clock Enables) => 3 个
9. 时钟报告 (Clock Report):
时钟正常 (Clock Nets) 正常负载,没有Timing错误。