第一次接触FPGA计数器设计时,我盯着开发板上闪烁的LED发愣——为什么简单的计数逻辑需要这么多行代码?直到亲手完成从Verilog编码到数码管显示的全流程,才真正理解数字电路设计的精妙之处。本文将带你完整实现一个24进制计数器系统,重点解决三个核心问题:如何用Verilog构建可扩展的计数逻辑、如何通过仿真验证设计正确性,以及如何将二进制输出转换为直观的数码管显示。
在开始编码前,我们需要搭建完整的FPGA开发环境。推荐使用Quartus Prime Lite 21.1(免费版本)搭配Cyclone IV EP4CE6开发板,这套组合性价比高且社区支持完善。新建工程时需特别注意器件选择:Tools -> IP Catalog中搜索"PLL"配置50MHz主时钟,这是后续数码管动态扫描的基准频率。
项目目录建议采用模块化结构:
code复制/24bit_counter
├── /rtl // Verilog源码
│ ├── counter24.v
│ └── seg7_driver.v
├── /sim // 仿真文件
│ └── tb_counter24.v
└── /constraints // 引脚约束
└── DE10-Lite.sdc
关键器件参数对比:
| 组件 | 型号 | 关键特性 |
|---|---|---|
| FPGA芯片 | Cyclone IV EP4CE6 | 6272逻辑单元,2个PLL |
| 数码管 | 4位共阳 | 动态扫描驱动 |
| 晶振 | 50MHz | ±50ppm精度 |
注意:不同开发板的引脚定义差异较大,务必在约束文件中正确定位时钟和数码管引脚
24进制计数器的本质是状态机设计,我们采用同步计数方案避免毛刺问题。下面这段代码展示了带使能控制的计数器实现:
verilog复制module counter24(
input clk, // 50MHz主时钟
input rst_n, // 低电平复位
output [4:0] cnt // 5位输出(0-23)
);
reg [4:0] cnt_reg;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt_reg <= 5'd0;
else begin
if(cnt_reg == 5'd23)
cnt_reg <= 5'd0;
else
cnt_reg <= cnt_reg + 1'b1;
end
end
assign cnt = cnt_reg;
endmodule
代码中的几个设计要点:
create_clock -period 20 [get_ports clk]仿真测试用例应该覆盖边界条件:
verilog复制initial begin
// 复位测试
rst_n = 0;
#100 rst_n = 1;
// 完整计数周期验证
repeat(50) @(posedge clk);
// 强制复位测试
#20 rst_n = 0;
#50 rst_n = 1;
// 长时间运行测试
repeat(1000) @(posedge clk);
$stop;
end
7段数码管显示需要解决两个关键问题:段码译码和动态扫描。下面这个驱动模块实现了将5位二进制数转换为两位十进制显示:
verilog复制module seg7_driver(
input clk,
input rst_n,
input [4:0] bin_in,
output reg [6:0] seg,
output reg [1:0] dig
);
// 分频产生1kHz扫描时钟
reg [15:0] div_cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
div_cnt <= 0;
else
div_cnt <= div_cnt + 1'b1;
end
wire scan_clk = div_cnt[15]; // 50MHz/65536≈763Hz
// BCD转换
wire [3:0] units = bin_in % 10;
wire [3:0] tens = bin_in / 10;
// 动态扫描
always @(posedge scan_clk or negedge rst_n) begin
if(!rst_n) begin
dig <= 2'b11;
seg <= 7'b1111111;
end else begin
case(dig)
2'b10: begin // 显示十位
dig <= 2'b01;
case(tens)
0: seg <= 7'b1000000; // 0
1: seg <= 7'b1111001; // 1
2: seg <= 7'b0100100; // 2
default: seg <= 7'b1111111;
endcase
end
default: begin // 显示个位
dig <= 2'b10;
case(units)
0: seg <= 7'b1000000; // 0
// ... 1-9段码省略
9: seg <= 7'b0010000; // 9
default: seg <= 7'b1111111;
endcase
end
endcase
end
end
endmodule
常见问题排查指南:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 显示闪烁 | 扫描频率过低 | 提高div_cnt位宽 |
| 数字重叠 | 位选信号冲突 | 检查dig信号时序 |
| 段码不全 | 共阳/共阴配置错误 | 反转seg信号极性 |
顶层模块需要协调计数器与显示驱动的工作时序,这里给出一个优化后的实现方案:
verilog复制module top(
input clk,
input rst_n,
output [6:0] seg,
output [1:0] dig
);
wire [4:0] counter_out;
counter24 u_counter(
.clk(clk),
.rst_n(rst_n),
.cnt(counter_out)
);
seg7_driver u_display(
.clk(clk),
.rst_n(rst_n),
.bin_in(counter_out),
.seg(seg),
.dig(dig)
);
endmodule
硬件调试时推荐使用SignalTap II逻辑分析仪,配置建议:
资源占用统计示例(Cyclone IV EP4CE6):
| 资源类型 | 使用量 | 总量 | 利用率 |
|---|---|---|---|
| 逻辑单元 | 43 | 6272 | <1% |
| 寄存器 | 28 | 6272 | <1% |
| 引脚 | 11 | 179 | 6% |
在项目验收阶段,建议增加以下测试用例:
基础功能实现后,可以考虑以下性能提升方案:
时钟域优化
verilog复制// 添加全局时钟缓冲
wire gclk;
altclkctrl u_clock_buf (
.inclk(clk),
.outclk(gclk)
);
低功耗设计
verilog复制// 时钟门控技术
reg [23:0] idle_cnt;
always @(posedge gclk) begin
if(counter_out == 0)
idle_cnt <= idle_cnt + 1;
else
idle_cnt <= 0;
end
wire clk_en = (idle_cnt < 24'hFFFFFF);
显示效果增强方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 动态扫描 | 省IO口 | 亮度较低 | 多位数码管 |
| 静态驱动 | 亮度高 | 占用资源多 | 单/双数码管 |
| PWM调光 | 亮度可调 | 增加代码复杂度 | 环境光变化大 |
最后分享一个实用技巧:在Quartus中使用Chip Planner工具手动调整IO布局,可以显著减少信号偏移(skew)。曾经有个项目因为数码管段信号走线过长导致显示残影,通过将相关引脚约束到同一IO Bank解决了问题。