1. 七段数码管控制器的核心设计思路
七段数码管是嵌入式系统和FPGA开发中最常用的显示设备之一,它通过控制7个LED段的亮灭来显示数字和部分字母。在实际项目中,我们经常需要控制多个数码管,如果每个数码管都独立控制,会占用大量的IO资源。动态扫描技术就是为了解决这个问题而生的。
动态扫描的基本原理是利用人眼的视觉暂留特性,通过快速轮流点亮多个数码管,让它们看起来像是同时亮起的。比如我们有8个数码管,可以在极短的时间内(比如每1ms)切换一个数码管,由于切换速度很快,人眼无法分辨这种快速切换,就会看到所有数码管都在同时显示。
在Verilog中实现动态扫描,我们需要设计一个状态机来控制数码管的位选信号(选择哪个数码管亮)和段选信号(显示什么内容)。位选信号通过一个循环计数器来生成,段选信号则根据当前选中的数码管和要显示的内容来确定。
BCD码(Binary-Coded Decimal)是一种用4位二进制数表示1位十进制数的编码方式。比如十进制数123,用BCD码表示就是0001_0010_0011。在数码管显示中,我们经常需要将二进制数转换为BCD码,然后再将BCD码转换为七段数码管的段选信号。
2. VIVADO环境搭建与工程创建
开始设计之前,我们需要先搭建好开发环境。VIVADO是Xilinx公司推出的FPGA开发软件,它提供了从设计输入到比特流生成的全套工具链。如果你还没有安装VIVADO,可以去Xilinx官网下载安装包,选择WebPACK版本就可以,这个版本是免费的,功能也足够我们使用。
安装完成后,打开VIVADO,点击"Create Project"开始创建新工程。给工程起个有意义的名字,比如"seven_segment_display",选择工程存放路径。在项目类型选择时,选择"RTL Project",这样我们可以从Verilog代码开始设计。
在选择器件时,需要根据你实际使用的FPGA开发板来选择。比如我使用的是Basys3开发板,就选择Artix-7系列的XC7A35T-1CPG236C。如果你不确定自己的开发板用什么芯片,可以查看开发板的文档或者原理图。
工程创建完成后,我们需要添加源文件。右键点击"Design Sources",选择"Add Sources",然后创建新的Verilog文件。我给主模块起名为"seven_segment",这个模块将包含我们所有的控制逻辑。
除了设计文件,我们还需要添加约束文件。约束文件的作用是将Verilog代码中的信号映射到FPGA的实际管脚上。不同的开发板管脚定义不同,所以一定要根据自己使用的开发板来编写约束文件。
3. Verilog代码实现详解
让我们深入看看Verilog代码的具体实现。首先定义模块的输入输出端口:
module seven_segment(
input clk, // 系统时钟,通常是100MHz
input reset_n, // 复位信号,低电平有效
input button, // 按钮输入,用于控制显示
input switch, // 开关输入,用于模式选择
output reg [7:0] segment, // 段选信号,控制显示内容
output reg [7:0] digit // 位选信号,选择哪个数码管
);
接下来我们需要生成5Hz的时钟信号用于计数。使用100MHz的系统时钟分频得到5Hz:
reg [31:0] div_cnt = 32'd0;
reg clk_5hz = 0;
always @(posedge clk) begin
if (div_cnt >= 32'd10000000) begin // 100MHz / 10,000,000 = 10Hz
div_cnt <= 32'd0;
clk_5hz <= ~clk_5hz; // 翻转得到5Hz
end else begin
div_cnt <= div_cnt + 32'd1;
end
end
这里有个实用技巧:在仿真时,我们可以把分频系数改小,比如改成200,这样仿真时就不用等待太长时间。但在实际下载到FPGA时,一定要改回正确的分频系数。
计数器的实现相对简单,但要注意计数范围是2到254:
reg [7:0] count = 8'd2;
always @(posedge clk) begin
if (!reset_n) begin
count <= 8'd2;
end else if (clk_5hz) begin
if (count >= 8'd254)
count <= 8'd2;
else
count <= count + 8'd1;
end
end
二进制到BCD码的转换是重点也是难点。我们可以使用移位加3算法来实现:
module binary_to_bcd (
input clk,
input [7:0] binary,
output reg [3:0] hundreds,
output reg [3:0] tens,
output reg [3:0] ones
);
reg [19:0] bcd_reg = 0;
integer i;
always @(posedge clk) begin
bcd_reg = {12'd0, binary};
for (i = 0; i < 8; i = i + 1) begin
// 对每一位进行加3调整
if (bcd_reg[19:16] >= 5) bcd_reg[19:16] = bcd_reg[19:16] + 3;
if (bcd_reg[15:12] >= 5) bcd_reg[15:12] = bcd_reg[15:12] + 3;
if (bcd_reg[11:8] >= 5) bcd_reg[11:8] = bcd_reg[11:8] + 3;
if (bcd_reg[7:4] >= 5) bcd_reg[7:4] = bcd_reg[7:4] + 3;
bcd_reg = bcd_reg << 1;
end
hundreds = bcd_reg[19:16];
tens = bcd_reg[15:12];
ones = bcd_reg[11:8];
end
endmodule
这个算法可能看起来有点复杂,但其核心思想是通过移位和条件调整来逐步将二进制数转换为BCD码。我在第一次实现时也花了不少时间才理解透彻。
4. 动态扫描与显示控制
动态扫描的实现需要两个计数器:一个用于控制扫描频率,一个用于选择当前显示的数码管。扫描频率通常设置在1kHz左右,这样既不会让数码管闪烁,又能够保证亮度。
reg [15:0] scan_cnt = 0;
reg [2:0] digit_sel = 0;
always @(posedge clk) begin
if (scan_cnt >= 16'd100000) begin // 100MHz/100000 = 1kHz
scan_cnt <= 0;
digit_sel <= digit_sel + 1;
if (digit_sel >= 3'd7)
digit_sel <= 0;
end else begin
scan_cnt <= scan_cnt + 1;
end
end
根据当前选中的数码管,我们需要生成相应的位选信号:
always @(*) begin
case (digit_sel)
3'd0: digit = 8'b11111110;
3'd1: digit = 8'b11111101;
3'd2: digit = 8'b11111011;
3'd3: digit = 8'b11110111;
3'd4: digit = 8'b11101111;
3'd5: digit = 8'b11011111;
3'd6: digit = 8'b10111111;
3'd7: digit = 8'b01111111;
default: digit = 8'b11111111;
endcase
end
段选信号的生成需要根据当前模式和要显示的内容来决定。我们有两种显示模式:显示相同数字和显示计数器值。
reg [3:0] display_data [0:7];
reg [3:0] current_digit;
always @(*) begin
if (switch == 1) begin // 显示相同数字模式
current_digit = uniform_data;
end else begin // 显示计数器模式
case (digit_sel)
3'd0: current_digit = hundreds;
3'd1: current_digit = tens;
3'd2: current_digit = ones;
default: current_digit = 4'd0;
endcase
end
end
最后将BCD数字转换为七段数码管的段选信号:
always @(*) begin
case (current_digit)
4'd0: segment = 7'b1000000; // 0
4'd1: segment = 7'b1111001; // 1
4'd2: segment = 7'b0100100; // 2
4'd3: segment = 7'b0110000; // 3
4'd4: segment = 7'b0011001; // 4
4'd5: segment = 7'b0010010; // 5
4'd6: segment = 7'b0000010; // 6
4'd7: segment = 7'b1111000; // 7
4'd8: segment = 7'b0000000; // 8
4'd9: segment = 7'b0010000; // 9
default: segment = 7'b1111111; // 全灭
endcase
end
在实际调试时,我发现段选信号的编码方式与数码管的共阴/共阳特性有关。我的开发板使用的是共阳数码管,所以0表示亮,1表示灭。如果你的开发板是共阴的,需要将段选信号取反。
5. 仿真测试与验证方法
仿真测试是FPGA开发中非常重要的一环,它可以帮助我们在下载到硬件之前发现和修复问题。在VIVADO中,我们可以编写Testbench来模拟实际的工作环境。
首先创建仿真文件,通常以_tb结尾,比如seven_segment_tb.v:
module seven_segment_tb;
reg clk;
reg reset_n;
reg button;
reg switch;
wire [7:0] segment;
wire [7:0] digit;
// 实例化被测试模块
seven_segment uut (
.clk(clk),
.reset_n(reset_n),
.button(button),
.switch(switch),
.segment(segment),
.digit(digit)
);
// 生成时钟信号
initial begin
clk = 0;
forever #5 clk = ~clk; // 100MHz时钟,周期10ns
end
// 测试过程
initial begin
// 初始化
reset_n = 0;
button = 1;
switch = 0;
// 释放复位
#100 reset_n = 1;
// 测试计数器模式
#2000000; // 等待一段时间观察计数
// 测试相同数字模式
switch = 1;
#1000000;
// 测试按钮功能
button = 0;
#100000;
button = 1;
#1000000;
$finish;
end
endmodule
在仿真时,为了减少仿真时间,我们可以修改分频系数。比如把5Hz的分频系数从20000000改为200,这样仿真时就能更快地观察到计数变化。
运行仿真后,我们需要查看波形图来验证设计是否正确。重点关注以下几个方面:
- 时钟分频:检查5Hz时钟是否正常生成
- 计数器:检查计数是否在2到254范围内循环
- BCD转换:检查二进制数是否正确转换为BCD码
- 动态扫描:检查位选信号是否按预期循环
- 段选信号:检查显示内容是否正确
如果发现任何问题,需要回到设计代码中进行调试。我建议使用VIVADO的ILA(Integrated Logic Analyzer)IP核进行在线调试,它可以在FPGA运行时捕获内部信号,非常实用。
6. 实际部署与调试技巧
代码通过仿真测试后,就可以部署到实际的FPGA开发板上了。首先需要编写约束文件,将Verilog中的信号映射到具体的管脚。
以Basys3开发板为例,约束文件可能长这样:
set_property PACKAGE_PIN W5 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property PACKAGE_PIN U18 [get_ports reset_n]
set_property IOSTANDARD LVCMOS33 [get_ports reset_n]
set_property PACKAGE_PIN T18 [get_ports button]
set_property IOSTANDARD LVCMOS33 [get_ports button]
set_property PACKAGE_PIN U17 [get_ports switch]
set_property IOSTANDARD LVCMOS33 [get_ports switch]
# 段选信号
set_property PACKAGE_PIN W7 [get_ports {segment[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {segment[0]}]
# ... 其他段选信号
# 位选信号
set_property PACKAGE_PIN U2 [get_ports {digit[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {digit[0]}]
# ... 其他位选信号
生成比特流文件后,通过USB连接开发板,点击"Program device"将设计下载到FPGA中。
在实际调试时,可能会遇到一些问题。比如数码管显示闪烁,这可能是扫描频率太低导致的。或者显示内容不正确,可能是段选信号的编码方式错了。我遇到过最棘手的问题是数码管显示有重影,这是因为位选信号的切换时机和段选信号的更新时机没有配合好。
解决重影问题的方法是在切换位选信号之前,先关闭所有段选信号,切换到位选后再更新段选信号:
always @(posedge clk) begin
if (scan_cnt == 16'd99900) begin
segment <= 8'b11111111; // 先关闭显示
end else if (scan_cnt == 16'd99950) begin
digit <= next_digit; // 切换位选
end else if (scan_cnt == 16'd0) begin
segment <= next_segment; // 更新段选
end
end
另一个常见问题是显示亮度不均匀。这通常是因为不同数码管的点亮时间不一致,或者驱动能力不足。可以通过调整扫描时序或增加驱动电路来解决。
我在实际项目中还发现,按键输入需要做防抖动处理,否则可能会检测到多次按下。简单的防抖动可以在Verilog中实现:
reg [19:0] debounce_cnt = 0;
reg button_stable = 1;
always @(posedge clk) begin
if (debounce_cnt >= 20'd1000000) begin // 10ms防抖动
button_stable <= button;
debounce_cnt <= 0;
end else begin
debounce_cnt <= debounce_cnt + 1;
end
end
这些调试经验都是在实际项目中一点点积累的,希望对你有所帮助。FPGA开发就是这样,理论知识和实践经验同样重要。多动手实践,遇到问题时耐心调试,你会发现自己的技能在不断提升。
276



被折叠的 条评论
为什么被折叠?



