1. 项目概述与设计思路

大家好,今天我想和大家分享一个非常实用的FPGA项目——基于FPGA的矩阵键盘计算器实现。这个项目特别适合刚开始接触FPGA和数字逻辑设计的同学,不仅能让你理解硬件描述语言Verilog的实际应用,还能掌握从设计到仿真的完整开发流程。

我在实际项目中发现,用FPGA来实现计算器功能是一个非常棒的学习方式。它不像软件编程那样抽象,你能实实在在地看到每个逻辑门如何协作,数据如何在寄存器间流动。这个计算器设计采用了4×4矩阵键盘作为输入设备,6位数码管作为显示输出,核心处理器是FPGA芯片,使用Verilog进行编程。

整个系统的设计思路很清晰:通过矩阵键盘扫描获取用户输入,经过消抖处理确保按键准确识别,然后用状态机来管理计算流程,最后将运算结果通过数码管动态显示出来。这种架构在实际的嵌入式系统中非常常见,掌握了这个项目,对你以后做更复杂的FPGA设计会有很大帮助。

2. 硬件架构设计详解

2.1 系统整体架构

我们的计算器系统采用了模块化设计,这样不仅便于调试,也让代码结构更清晰。整个系统包含以下几个核心模块:

首先是时钟管理模块,负责将板载的50MHz系统时钟分频到适合键盘扫描和数码管显示的频率。我一般会把键盘扫描频率设置在500Hz左右,这个频率既能保证扫描速度,又不会消耗太多资源。数码管动态扫描则需要更高一些的频率,通常在1KHz到2KHz之间,这样人眼才不会感觉到闪烁。

矩阵键盘处理模块是输入的关键,4×4的键盘布局给了我们16个按键资源。我通常这样分配:0-9是数字键,A-D作为加减乘除运算符,E键作为清零,F键作为等号。当然你也可以根据自己的习惯重新定义。

状态控制模块是整个系统的大脑,我用有限状态机来实现计算流程的控制。通常包含以下几个状态:等待输入第一个操作数、等待选择运算符、等待输入第二个操作数、执行计算、显示结果。状态之间的转换由按键事件触发,这样逻辑清晰且易于维护。

2.2 矩阵键盘接口设计

矩阵键盘的接口设计很有讲究,4根行线和4根列线的扫描方式需要精心设计。我常用的方法是逐列扫描:依次将每一列置为低电平,然后读取行线的状态。当某一行某一列的按键被按下时,对应的行线就会变成低电平。

在实际实现中,我会设置一个扫描计数器,循环产生0-3的计数值,对应4列。然后将这个计数值解码为独热码输出到列线。比如计数器为0时,只有第0列为低,其他为高;计数器为1时,只有第1列为低,以此类推。

行线的读取需要同步处理,我会用寄存器缓存行线状态,然后根据当前扫描的列号和行线状态来确定是哪个按键被按下。这里要注意的是,键盘扫描频率不能太高也不能太低,太高会浪费资源,太低会导致响应迟钝。经过多次实验,我发现500Hz是个不错的折中选择。

3. Verilog核心代码实现

3.1 键盘扫描与消抖模块

键盘扫描模块的Verilog实现需要特别注意时序问题。下面是我常用的键盘扫描代码框架:

module key_scan(
    input clk,        // 500Hz扫描时钟
    input [3:0] row_i,// 行输入
    output reg [3:0] col_o, // 列输出
    output reg [3:0] key_value // 按键值
);

reg [1:0] scan_counter;
reg [3:0] row_sync;
reg [15:0] debounce_cnt;
reg key_pressed;

// 列扫描信号生成
always @(posedge clk) begin
    scan_counter <= scan_counter + 1;
    case(scan_counter)
        2'b00: col_o <= 4'b1110;
        2'b01: col_o <= 4'b1101;
        2'b10: col_o <= 4'b1011;
        2'b11: col_o <= 4'b0111;
    endcase
end

// 行信号同步与消抖
always @(posedge clk) begin
    row_sync <= row_i;
    if (row_sync != 4'b1111) begin
        if (debounce_cnt == 16'hFFFF) begin
            key_pressed <= 1'b1;
            // 根据当前扫描列和行状态确定按键值
            case({scan_counter, row_sync})
                6'b00_1110: key_value <= 4'd0;
                6'b00_1101: key_value <= 4'd1;
                // ... 其他按键编码
            endcase
        end else begin
            debounce_cnt <= debounce_cnt + 1;
        end
    end else begin
        debounce_cnt <= 0;
        key_pressed <= 1'b0;
    end
end
endmodule

消抖处理是这个模块的关键,机械按键在按下和释放时会产生抖动,通常持续10-20ms。我采用计数器的方式来消抖,当检测到按键状态变化时开始计数,直到计满一定时间(比如20ms)才认为按键状态稳定。这种方法简单有效,在实际项目中表现很稳定。

3.2 状态机控制模块

状态机模块是计算器的控制核心,我通常用三段式状态机来实现,这样逻辑清晰且易于维护:

module state_ctrl(
    input clk,
    input rst,
    input [3:0] key_value,
    input key_valid,
    output reg [2:0] current_state,
    output reg [1:0] operation
);

// 状态定义
parameter S_IDLE = 3'd0;
parameter S_OP1  = 3'd1;
parameter S_OPER = 3'd2;
parameter S_OP2  = 3'd3;
parameter S_CALC = 3'd4;
parameter S_DISP = 3'd5;

// 状态寄存器
always @(posedge clk or posedge rst) begin
    if (rst) current_state <= S_IDLE;
    else case(current_state)
        S_IDLE: if (key_valid && key_value < 10) 
                   current_state <= S_OP1;
        S_OP1:  if (key_valid && key_value > 9) 
                   current_state <= S_OPER;
        S_OPER: if (key_valid && key_value < 10) 
                   current_state <= S_OP2;
        S_OP2:  if (key_valid && key_value == 15) // 等号键
                   current_state <= S_CALC;
        S_CALC: current_state <= S_DISP;
        S_DISP: if (key_valid && key_value == 14) // 清零键
                   current_state <= S_IDLE;
    endcase
end

// 输出逻辑
always @(posedge clk) begin
    if (key_valid) begin
        case(current_state)
            S_OPER: operation <= key_value[1:0]; // 存储操作符
            S_CALC: begin
                // 根据operation执行相应计算
                case(operation)
                    2'b00: result <= op1 + op2;
                    2'b01: result <= op1 - op2;
                    2'b10: result <= op1 * op2;
                    2'b11: result <= op1 / op2;
                endcase
            end
        endcase
    end
end
endmodule

这种状态机设计让计算流程非常清晰,每个状态都有明确的职责和转换条件。在实际调试时,我建议添加状态输出显示,这样在仿真和调试时能直观地看到状态转换过程。

4. 数码管动态显示技术

4.1 显示驱动原理

数码管动态显示是FPGA项目中常用的显示技术,它能用较少的IO口驱动多个数码管。原理其实很简单:快速循环点亮每个数码管,利用人眼的视觉暂留效应形成连续显示的视觉效果。

我通常将显示模块分成两个部分:段选信号生成和位选扫描。段选信号决定显示什么数字或字符,位选信号决定哪个数码管被点亮。6位数码管需要6个位选信号,通常用3-8译码器来产生,这样可以节省IO资源。

显示刷新率很重要,太低了会闪烁,太高了会增加功耗。我一般设置在1KHz左右,每个数码管点亮时间约160μs(1ms/6)。这个频率下显示稳定且不会有过多的功耗。

4.2 Verilog显示代码实现

下面是数码管显示模块的典型实现:

module display(
    input clk, // 1KHz显示时钟
    input [15:0] disp_data, // 显示数据
    output reg [2:0] digit_sel, // 位选信号
    output reg [7:0] segment // 段选信号
);

reg [15:0] bcd_data [5:0]; // 存储6位BCD码
reg [3:0] current_digit; // 当前显示的数字

// 二进制转BCD码
always @(posedge clk) begin
    // 二进制转BCD代码
    // 将16位二进制数转换为6位BCD码
    // 存储到bcd_data数组中
end

// 位选扫描
always @(posedge clk) begin
    digit_sel <= digit_sel + 1;
    if (digit_sel == 3'd5) digit_sel <= 3'd0;
    
    current_digit <= bcd_data[digit_sel];
end

// 段选译码
always @(*) begin
    case(current_digit)
        4'h0: segment = 8'b11000000; // 0
        4'h1: segment = 8'b11111001; // 1
        4'h2: segment = 8'b10100100; // 2
        4'h3: segment = 8'b10110000; // 3
        4'h4: segment = 8'b10011001; // 4
        4'h5: segment = 8'b10010010; // 5
        4'h6: segment = 8'b10000010; // 6
        4'h7: segment = 8'b11111000; // 7
        4'h8: segment = 8'b10000000; // 8
        4'h9: segment = 8'b10010000; // 9
        default: segment = 8'b11111111; // 灭
    endcase
end
endmodule

在实际项目中,二进制转BCD码是个值得注意的地方。我常用的方法是双倍位法,通过左移和条件加3来实现转换。这种方法虽然需要多个时钟周期,但资源消耗小,适合FPGA实现。

5. Quartus工程创建与配置

5.1 工程设置与文件管理

创建Quartus工程是项目开发的第一步,好的工程结构能大大提高开发效率。我通常这样组织工程文件:

首先创建项目目录,建议按功能模块分文件夹管理。比如src文件夹放所有Verilog源文件,sim文件夹放仿真文件,doc文件夹放设计文档,output文件夹放编译输出文件。

在Quartus中创建新工程时,我建议选择正确的器件型号。如果你是学习用途,选择Cyclone IV EP4CE6系列就很合适,这个器件资源适中,学习板价格便宜。设置顶层实体文件时,要确保与你的顶层模块名一致,这是常见的错误来源。

工程设置中需要特别注意优化选项。对于初学者,我建议先选择平衡优化(Balanced),这样能在性能和资源间取得平衡。等项目完成后,如果需要优化性能或面积,再调整相关设置。

5.2 管脚分配与约束

管脚分配是硬件设计的关键步骤,错误的管脚分配会导致功能异常甚至损坏器件。我通常这样进行管脚分配:

首先根据开发板的原理图确定每个信号对应的管脚号。矩阵键盘的行线接GPIO输入,列线接GPIO输出。数码管的段选信号接GPIO输出,位选信号也接GPIO输出。

在Quartus的Pin Planner中分配管脚时,要注意bank的电压配置。同一个bank的所有IO口电压必须一致,如果电压不匹配,需要调整硬件设计或使用电平转换芯片。

除了管脚分配,时序约束也很重要。虽然这个项目的时钟频率不高,但良好的时序约束习惯很重要。我一般会创建.sdc文件,设置时钟频率、输入输出延迟等约束。即使对于50MHz的设计,添加适当的时序约束也能提高系统的稳定性。

6. 功能仿真与调试技巧

6.1 Testbench编写方法

仿真验证是FPGA设计的重要环节,好的Testbench能大大提高调试效率。我通常这样编写计算器的Testbench:

首先创建时钟和复位信号,然后模拟矩阵键盘的输入行为。在Testbench中,我会定义一些任务(task)来模拟按键操作,比如press_key任务模拟按下某个按键并包含消抖时间。

module calculator_tb;

reg clk;
reg rst;
reg [3:0] row_i;
wire [3:0] col_o;
wire [2:0] digit_sel;
wire [7:0] segment;

// 实例化被测设计
calculator uut(
    .clk(clk),
    .rst(rst),
    .row_i(row_i),
    .col_o(col_o),
    .digit_sel(digit_sel),
    .segment(segment)
);

// 时钟生成
always #10 clk = ~clk;

// 测试用例
initial begin
    // 初始化
    clk = 0;
    rst = 1;
    row_i = 4'b1111;
    
    // 复位
    #100 rst = 0;
    
    // 模拟按键输入:1 + 2 = 3
    press_key(4'd1); // 按下1
    press_key(4'd10); // 按下加号
    press_key(4'd2); // 按下2
    press_key(4'd15); // 按下等号
    
    #1000 $finish;
end

// 模拟按键任务
task press_key;
    input [3:0] key;
    begin
        // 根据按键值设置行线状态
        // 包含消抖时间模拟
        #20000; // 等待20ms模拟按键持续时间
        row_i = 4'b1111; // 释放按键
        #20000; // 等待20ms
    end
endtask

endmodule

在Testbench中,我还会添加一些自动检查机制,比如检查计算结果是否正确。如果结果不符合预期,就输出错误信息。这种自动验证能大大提高测试效率。

6.2 仿真波形分析技巧

仿真波形分析是调试的重要手段,我通常这样组织仿真波形:

首先添加关键信号到波形窗口:时钟、复位、状态机状态、按键输入、显示输出等。对于状态机,我建议用模拟(Analog)方式显示,这样能直观看到状态转换。

在分析波形时,要特别注意时序关系。比如按键输入后,状态机是否正确转换状态;计算完成后,显示输出是否正确。我经常使用标记(Marker)来测量关键路径的延迟时间。

如果发现功能异常,可以采用二分法排查:先确定问题发生的大致范围,然后逐步缩小范围。比如先检查输入模块是否正确识别按键,再检查状态机转换是否正确,最后检查计算模块是否正确。

7. 常见问题与解决方案

7.1 硬件连接问题

在实际硬件调试中,我遇到过不少硬件连接问题。最常见的是管脚分配错误,症状是功能完全不正常或者部分功能异常。解决方法是仔细检查原理图和管脚分配文件,确保每个信号都分配到正确的管脚。

另一个常见问题是上拉/下拉电阻配置。矩阵键盘通常需要上拉电阻,确保空闲时行线为高电平。如果开发板没有内置上拉电阻,需要在Verilog中启用内部上拉,或者外接上拉电阻。

电源噪声也会导致系统不稳定,特别是数码管动态显示时电流变化较大。我建议在电源入口处加滤波电容,数字地和模拟地之间用磁珠隔离。如果显示有闪烁,可以尝试增加滤波电容容值。

7.2 软件设计问题

在软件设计方面,我最常遇到的是时序问题。比如键盘扫描频率过高导致CPU占用率高,或者过低导致响应迟钝。解决方法是通过实验找到合适的扫描频率,我推荐500Hz用于键盘扫描,1KHz用于数码管显示。

状态机设计也是容易出问题的地方。常见的错误是状态转换条件不完整,导致状态机卡死。解决方法是在状态机中添加超时机制和看门狗,确保即使出现异常也能自动恢复。

资源优化是另一个需要考虑的问题。如果FPGA资源紧张,可以优化编码方式,比如用二进制编码代替独热码;合并功能相似的模块;使用资源共享等技术。在Quartus的编译报告中可以查看资源使用情况,根据报告进行针对性优化。

8. 性能优化与扩展功能

8.1 计算性能优化

虽然这个计算器项目对性能要求不高,但优化实践对学习FPGA设计很有帮助。我常用的性能优化方法包括:

流水线设计是提高计算速度的有效方法。对于乘法运算,可以拆分成多个阶段,每个时钟周期完成一部分计算。虽然会增加延迟,但能提高吞吐量。

资源复用也能提高性能,比如加法器可以在不同时间用于不同目的。通过多路选择器控制,一个加法器既能用于地址计算,也能用于数据处理。

对于显示模块,我可以使用双缓冲技术:一个缓冲区用于显示,另一个缓冲区用于准备下一帧数据。这样能避免显示撕裂,提高显示质量。

8.2 功能扩展建议

这个基础计算器有很多扩展方向,我实现过的一些扩展功能包括:

支持浮点数运算是个有趣的扩展。我建议先实现定点数运算,相对简单一些。浮点数运算需要处理指数和对齐,复杂度较高,但更实用。

添加计算历史记录功能也很有用。可以用FPGA内部的RAM存储历史记录,通过按键浏览之前的计算结果。这个功能需要管理存储空间和显示界面。

支持更多数学函数,如三角函数、指数函数、对数函数等。这些函数可以用CORDIC算法实现,适合FPGA的硬件特性。

我最近还尝试添加了语音提示功能,使用PWM驱动蜂鸣器发出按键音和结果提示音。虽然音质一般,但增加了项目的趣味性。

另一个实用的扩展是增加串口通信功能,可以将计算过程和结果发送到电脑,便于记录和分析。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐