CASE_02 基于FPGA的动态扫描数码管万年历设计与实现

📅 发布时间:2026/7/3 21:50:27 👁️ 浏览次数:
CASE_02 基于FPGA的动态扫描数码管万年历设计与实现
1. 从零开始为什么用FPGA做万年历如果你玩过单片机肯定做过数字钟。用51单片机或者STM32配合一个实时时钟芯片再驱动几个数码管一个简单的时钟就出来了。那为什么我们还要“大费周章”地用FPGA来做呢这就像你平时开家用轿车代步很舒服但如果你想体验极致的操控感和定制化就得去玩改装车或者赛车。FPGA就是电子设计里的“赛车”。我最早接触这个项目是想做一个放在工作室的、看起来有点“极客范儿”的桌面时钟。市面上的成品要么功能单一要么显示效果呆板。用单片机做当然可以但当我想要实现更复杂的显示效果——比如让8个数码管以某种特定的动态效果切换显示内容或者让时间调整的交互逻辑更流畅——时单片机的程序结构就开始显得有点“力不从心”了。特别是那个动态扫描单片机是用软件延时循环去做的一旦主程序里有其他任务显示就容易闪烁。而FPGA的并行处理能力可以让动态扫描由一个独立的硬件模块全权负责像有一个专门的“显示管家”无论主逻辑在干什么显示都稳如泰山。所以这个基于FPGA的万年历项目绝不只是为了显示时间。它是一个完整的硬件系统设计实战从最底层的按键消抖、时钟计数逻辑到中间的闰年判断、模式切换再到最上层的数码管驱动每一步都需要你用硬件描述语言比如Verilog去“搭建”出实际的数字电路。做完这个项目你会对“数字系统”如何运作有脱胎换骨的理解。它适合谁呢适合已经学过数字电路和Verilog基础想找一个综合项目练手把知识点串起来的朋友也适合那些不满足于单片机顺序执行想探索并行硬件设计魅力的硬件爱好者。2. 核心基石动态扫描数码管的驱动奥秘要让8个数码管同时亮起来最笨的方法是什么给每个数码管的7个段a-g和1个公共端都单独接一个FPGA管脚。8个数码管就需要8*864个管脚这简直是IO资源的灾难PCB布线也会变成一团乱麻。动态扫描就是为了解决这个问题的“天才”设计。你可以把它想象成早期电影院放的“胶片电影”。电影并不是一整幅画面在动而是由一连串静态图片快速连续播放利用人眼的“视觉暂留”效应让我们感觉画面是连续的。动态扫描数码管也是这个道理。我们把所有数码管相同的段比如所有a段都连在一起接到FPGA的一组管脚上seg_data。每个数码管的公共端com端则单独控制seg_en。FPGA的工作就是在极短的时间内比如2.5毫秒只让一个数码管的公共端有效比如数码管1并给段数据线送上想让这个数码管显示的数字编码。2.5毫秒后关闭数码管1打开数码管2的公共端并送上数码管2要显示的数据……如此循环。只要这个循环的速度足够快通常扫描整个8位数码管一轮的时间在20ms以内即频率高于50Hz人眼就完全察觉不到闪烁看到的就是8个稳定同时显示的数字。这样做我们只需要7根段数据线 8根位选线 15个IO口比64个节省了太多在代码里怎么实现呢关键在于两个计数器。一个计数器用来控制单个数码管点亮的时间比如数125000个时钟周期对应50MHz时钟下的2.5ms。另一个计数器用来记录当前点亮的是第几个数码管从0到7。每次“单管时间”计数器计满就触发“位选计数器”加1切换到下一个数码管。同时根据“位选计数器”的值从一个8选1的数据选择器中选出对应数码管要显示的数字比如第0位对应“小时十位”再通过一个译码器就是查表把这个4位二进制数转换成7段码输出。这样一个高效的动态扫描引擎就运转起来了。3. 大脑的逻辑万年历的计数与时间管理时间是怎么“走”的这听起来简单但在FPGA里设计一个万年历的计数逻辑却需要仔细权衡。原始文章里提到了两种方案这其实是一个经典的“存储优化”与“计算便利”之间的权衡我深有体会。方案一符合直觉但费资源用完整的二进制数来存储时间。比如“秒”用一个6位二进制数表示0-59“年”用一个更大的数表示2000-2099。这样判断进位59秒到00分和闰年年份能被400整除或能被4整除但不能被100整除非常直观就是做大小比较和求余。但问题来了显示的时候我们需要把这个完整的二进制数拆成十位和个位。比如“秒”35二进制100011要拆成“3”和“5”送给数码管。这个“拆”的过程需要做除法35/103...5。在FPGA里除法器是非常消耗逻辑资源的为了显示而做大量除法性价比极低。方案二显示友好但逻辑稍复杂我们直接分开存储时间的每一位比如“秒”的十位second_d和个位second_u各用一个4位寄存器0-9。“年”的千位、百位、十位、个位也分开存。这样一来显示模块直接拿这些“位”去译码就行完全不需要除法。代价是进位逻辑变复杂了。比如“秒”从59变00你需要先判断个位second_u是不是9如果是则个位归零并且十位second_d要加1接着还要判断十位second_d是不是5且个位已归零如果是则十位也归零并向“分”的个位产生一个进位脉冲。闰年判断也更麻烦需要把分开的4个“年”位拼成一个完整的数再做求余判断。实测下来我强烈推荐方案二。FPGA的逻辑资源本来就是用来实现复杂逻辑的我们把资源用在更“值”的地方——让显示直接、快速、稳定。而进位逻辑的复杂度通过仔细的状态机设计完全可以解决。这就是硬件设计的思维选择最适合硬件实现的模型。4. 与“人”交互按键处理与系统模式设计一个万年历不能只是自己走时还得让我们能设置它。这就涉及到人机交互核心是四个按键模式键Mode、移位键Move、加键Add、显示切换键Switch。处理它们首先要过按键消抖这一关。机械按键在按下和弹起的瞬间金属触点会因为弹性产生一连串的抖动电信号上就是一堆毛刺。如果直接把这个信号给逻辑按一次键可能会被误认为是按了十几次。消抖的硬件思路很简单以静制动。当检测到按键状态变化时启动一个计时器比如20ms在这段时间内持续监测按键电平。只有当这20ms内按键电平都保持稳定在新的状态才认为这是一次有效的按键动作。在Verilog里我们用状态机和计数器就能优雅地实现。我通常会把消抖模块做成参数化的这样消抖时间可以根据不同的时钟频率灵活调整。处理好干净的按键信号后就要设计系统的**状态模式**了。我的设计通常有四个模式用一个2位的mode寄存器表示2‘b00正常运行模式。显示当前时分秒或年月日由Switch键切换时间自动走时。2’b01时间调整模式。此时可以调整时、分、秒。按Move键在时/分/秒的各位之间移动光标对应数码管闪烁按Add键对当前位加1。2’b10日期调整模式。逻辑同上用于调整年、月、日。这里逻辑最复杂因为要联动大小月和闰年判断。比如你从1月31日加一天应该跳到2月1日而不是2月31日。2’b11闹钟设置模式。设置闹铃时间。模式切换由Mode键循环控制。每个模式最好配一个独立的LED指示灯这样一眼就能知道当前状态非常直观。模式模块的核心就是一个在Mode键上升沿触发的、在0-3之间循环的计数器。5. 优雅的调整位置选择与数值修改逻辑进入调整模式后下一个问题就是我现在到底在调哪一位是小时的十位还是个位这就是位置调整模块的职责。它根据当前的mode和move_key信号产生一个3位的move_site信号0-7对应8个数码管的位置。它的工作流程是这样的当mode不为0即非正常模式时每次按下Move键检测上升沿move_site计数器就加1在0到7之间循环。同时一旦mode值发生变化比如从调时间切换到调日期move_site会立刻清零光标回到最高位这符合我们的操作直觉。有了mode和move_site数值选择模块就知道该把什么数据送给数码管显示了。在正常模式送实时时钟或日历数据在调整模式则送正在被调整的“临时值”。更贴心的是它会让move_site指向的那一位数码管以1Hz频率闪烁通过控制该位数码管的使能信号周期性关闭清晰地提示用户“现在改的是我”。当用户按下Add键时时间/日期/闹钟调整模块就开始工作。它根据mode和move_site精准地找到需要修改的那个寄存器比如hour_d小时的十位并使其加1。这里必须包含完整的边界检查和自动进位逻辑。比如hour_d从2加到3是允许的表示20点-30点不这里需要判断但如果是24小时制小时的十位最大只能是2且当十位为2时个位不能超过3。这些复杂的约束条件都需要用组合逻辑和状态判断来实现确保调出来的时间永远是合法的。6. 智慧的体现闰年与大小月判断万年历的“万年”二字核心难点就在于日历的自动修正而闰年判断是其中的灵魂。规则大家都知道能被400整除或者能被4整除但不能被100整除。但在FPGA里实现“整除判断”即求余数为0就绕不开除法运算。正如前面所说除法器很耗资源。一个年份我们可能需要在时钟运行模块和时间调整模块中都进行闰年判断难道要实例化两个除法器吗这里可以用一个分时复用的技巧。我们设计一个独立的闰年判断模块它内部包含一套除法计算电路。然后通过一个选择信号让这个模块在不同的时间片轮流为“运行年份”和“调整年份”进行计算。因为年份变化是很慢的最快1年才变一次我们完全有充足的时间几个时钟周期来慢慢算。这样一套昂贵的除法器硬件就被两个逻辑部分共享了大大节省了资源。判断出闰年后大小月的处理就简单了。我们可以用一个查找表LUT来实现month_days (month 2) ? (is_leap ? 29 : 28) : ((month 4||6||9||11) ? 30 : 31)。在日期进位逻辑中当“日”的值加到超过当月最大天数时就自动归1并且“月”加1。当“月”加到超过12时归1并且“年”加1。这套逻辑严密运转一个真正的“万年”历就诞生了。7. 从图纸到实物硬件电路设计要点代码仿真都通过了接下来就要让它跑在真实的电路板上。硬件设计这部分很多初学者容易栽跟头我结合踩过的坑来说几个要点。首先是电源。FPGA芯片比如常用的Cyclone IV EP4CE6需要多路电源核心电压VCCINT可能是1.2VPLL模拟电压VCCA2.5VIO口电压VCCIO3.3V。电源设计一定要稳每个电源引脚附近都要紧挨着放置一个0.1uF的陶瓷去耦电容用于滤除高频噪声。大一点的储能电容如10uF也不能少。原理图上看起来就是一堆电容但每个都至关重要少了容易导致芯片工作不稳定甚至无法启动。其次是数码管驱动电路。这是动态扫描能否成功的关键FPGA的IO口驱动能力有限通常每个引脚输出电流在20-40mA。如果直接用FPGA管脚去驱动8个并联的数码管段当显示数字8时7段全亮总电流可能超过100mA轻则显示昏暗重则烧坏IO口。所以必须在位选信号即每个数码管的公共端上增加电流驱动。最常用的方法就是加一个NPN三极管如SS8050做开关放大。FPGA的位选信号控制三极管基极三极管的集电极接数码管公共阴极发射极接地。这样流过数码管的电流主要由外部电源提供FPGA只提供微弱的控制电流非常安全可靠。时钟电路推荐使用有源晶振它输出的是标准方波稳定性好接法也简单电源、地、输出脚。复位电路一般采用RC低电平复位确保上电后FPGA能从一个确定的状态开始工作。下载电路JTAG和配置芯片如EPCS的连线要严格按照芯片手册推荐特别是那些需要上拉电阻的引脚不能省略否则可能导致程序无法下载或加载。8. 最后的冲刺系统集成与实物调试当所有模块的代码都准备好硬件板也焊接完毕就进入最激动人心也最考验耐心的系统集成与调试阶段了。这一步绝不是简单地把代码拼起来。首先你需要编写一个顶层模块Top Module。这个模块就像项目的总接线图它实例化所有子模块按键消抖、模式控制、时钟运行、数码管驱动等并根据设计框图用线网wire把它们正确地连接起来。这时候清晰的信号命名规范就派上大用场了。我习惯用clk_50m、rst_n、key_mode_filtered、seg_data这样的名字一眼就能看懂。接着是管脚分配。你需要根据自己设计的PCB原理图在Quartus II的Pin Planner工具里将顶层模块的每个输入输出信号分配到FPGA芯片具体的物理引脚上。比如seg_data[0]对应芯片的A12脚seg_en[0]对应B10脚。这个过程必须极其仔细一个引脚配错可能整个功能都异常。分配好后重新全编译工程生成最终的.sofSRAM对象文件或.jic配置芯片文件下载文件。下载到板子上如果幸运数码管应该亮起并开始走时。但更常见的是遇到各种问题显示乱码、按键没反应、时间不走动。我的调试三板斧是1. 查电源用万用表量各点电压是否正常。2. 查时钟和复位用示波器看晶振是否起振复位信号是否正常。3. 分段测试如果整体不行就写一个最简单的测试程序比如只让数码管静态显示一个“8”或者只测试一个按键控制一个LED。先确保最底层硬件是好的再一点点往上叠加功能。当8个数码管清晰地显示出“2024-05-27”或“14:30:05”并且你能通过按键流畅地切换模式、调整时间、设置闹钟听到蜂鸣器准时响起时那种从零开始构建一个复杂数字系统的成就感是无可比拟的。这个基于FPGA的动态扫描数码管万年历项目就像一次完整的硬件开发之旅它教给你的不仅仅是几行Verilog代码更是一套从需求分析、模块设计、仿真验证到硬件实现、调试排错的完整工程思维。