YAOTU INSIGHTS

FPGA基于NIOS II软核的电子钟设计:从系统搭建到误差校准

FPGA基于NIOS II软核的电子钟设计:从系统搭建到误差校准
简介面向FPGA学习者的NIOS II软核电子钟完整工程以Qsys定时器实现计时使用8个数码管显示时间通过3个按键完成时间与闹钟调节并具备闪烁位指示、LED反馈及蜂鸣器响闹时的流水灯效果。工程共602个文件压缩包约22.66MB涵盖Verilog/SystemVerilog源文件、C语言软件代码、Qsys与BSP配置、MIF初始化文件及SOF下载文件等目录结构清晰便于直接打开运行。已有2280人学习下载适合熟悉基础Verilog、希望进阶NIOS II软核开发的读者。通过完整源码可学习定时器中断、数码管动态扫描、按键状态机处理以及硬件驱动与C软件协同调试方法是FPGA课程设计或电子竞赛准备的高价值参考。1. 为什么电子钟值得用 NIOS II 软核来做很多刚接触 FPGA 的同学第一次点亮数码管之后都会冒出同一个念头做个能调时间、带闹钟的电子钟。这个题目看起来不大可真用纯 Verilog 去做麻烦会迅速超过预期。于是“FPGA基于NIOS II的电子钟设计”成了 SOPC 教学里出现频率最高的入门题目之一因为它把原来要写进状态机的交互逻辑变成了几十行容易被理解的 C 代码。NIOS II 是 FPGA 内部可定制的软核 CPU大致等于你在逻辑电路里“长出”一个处理器再围绕它挂上定时器、并口、调试串口等外设。电子钟恰恰是验证这套流程最合适的项目外设不多、交互逻辑适中、一周以内能跑通还能顺手把中断、总线映射和软硬件协同调试这些核心概念摸一遍。这篇文章适合正在做相关课程设计、第一次接触软核开发、或者想把手头 FPGA 板卡从“逻辑试验箱”升级成“小系统”的人。2. 选 NIOS II 还是纯 Verilog电子钟开发模式的取舍2.1 纯逻辑状态机写电子钟问题在哪从秒到分钟到小时在纯 RTL 里就是三个级联计数器这部分对熟悉 FPGA 的人来说不难。真正让人翻车的是“人机交互”按键消抖、设置模式、分时增减、闹钟开关每一个功能都是新状态。A同学把电子钟做成全状态机最后数了数差不多有十几个相互耦合的状态想加一个闹钟响铃改了一晚上还是没理顺。问题不在于能力而是这类“多输入 模式切换 显示刷新”的逻辑本质上是顺序思维硬塞进并行硬件描述会让状态爆炸。调试时更是如此想看一个按键按下去之后内部走到了哪个状态得在一堆 always 块和信号中间反复找效率极低。2.2 NIOS II/e 软核的资源账单软核就是把 CPU 的 RTL 代码放进 FPGA它不是什么黑匣子而是可配置的处理器模块。NIOS II 系列里最省的是 NIOS II/e 内核在常见的入门级 FPGA 上只占几百个逻辑单元。做电子钟再配上定时器、并口、调试串口和片上 RAM整体资源占用也远低于大多数开发板的容量。选择软核的核心收益是开发方式的变化。外设驱动由 HAL 库处理业务逻辑用 C 语言调试时直接看变量而不是分析波形改一个按键策略通常只动几行代码。下表是三种常见方案的对比方案开发方式调试手段改需求成本资源占用纯 RTLVerilog 状态机仿真 / 逻辑分析仪高很低NIOS II 软核C 语言 HAL变量观察 / 在线调试低中外挂单片机单片机 C 语言单片机调试器中低FPGA 资源极少但多一颗芯片2.3 三步判断这个项目该不该上软核我一般用三个问题判断一个题目要不要上软核。第一这个功能以后会不会加新需求第二有没有多个输入设备需要在同一个界面上切换第三调试时更想看到波形还是想直接看到某个变量的值电子钟三个答案都指向软核。具体判断步骤可以这样走先列需求清单再估算纯逻辑方案的状态数量然后划定软硬件边界。电子钟这条边界很清楚计数节拍和显示刷新由定时器硬件给出秒分时换算、按键响应、模式切换全部交给软件。边界划好之后项目难度就从“写状态机”变成了“写业务逻辑”复杂度明显下降。3. 在 Platform Designer 里搭最小 NIOS II 系统IP 清单、参数与连接3.1 最小系统需要哪些 IP为什么是这几个Quartus Prime 环境里搭建 NIOS II 系统用的是 Platform Designer老版本叫 Qsys。搭建电子钟最小系统只需要七个组件少了跑不起来多了只会浪费片上资源。组件用途为什么需要NIOS II 处理器运行 C 代码系统的核心On-Chip Memory (RAM)存放程序和数据调试阶段最简方案JTAG UART控制台打印程序运行状态输出Interval Timer产生毫秒时基电子钟计时的精度来源System ID软硬件版本校验防止 BSP 和硬件不匹配PIO (SEG)数码管段选8 位段码输出PIO (DIG)数码管位选6 位位选输出PIO (KEY)按键输入调时与闹钟输入片上 RAM 承担了程序存储和运行空间调试阶段最省事。JTAG UART 不是真实串口但它的调试输出在 NIOS II 里几乎零成本一眼就能看出程序跑到哪里。PLL 不是必选除非板载晶振频率不符合系统时钟需求用 PLL 倍频或分频后统一给 NIOS II 和总线提供时钟。3.2 各 IP 的关键参数与连接关系时钟频率决定了定时器的周期计算。如果板载晶振是 50MHz直接接系统时钟如果是其他频率加 PLL 调整到整数值避免后续算周期产生小数误差。NIOS II 内核选经济型JTAG 调试等级设为 Level 1保留断点能力又只占少量片上 RAM。参数推荐值说明Clock Frequency50 MHz以板载晶振为准必要时 PLL 调整NIOS II CoreNIOS II/e资源占用最省JTAG Debug LevelLevel 1可下断点、可单步On-Chip RAM64 KB低于 32 KB 编译会非常紧张Interval Timer 宽度32 bit避免计数溢出Timer Period1 ms兼顾显示刷新和按键消抖PIO (SEG)8 bit输出段码共阴共阳由电路决定PIO (DIG)6 bit输出位选通常经三极管驱动PIO (KEY)4 bit输入按键置为带上拉输入连接关系上所有组件挂在 Avalon-MM 总线上CPU 对定时器、PIO 的读写都映射为总线地址操作。Interval Timer 的中断输出接到 CPU 中断控制器建议把定时器放在中断优先级 0按键优先级 1JTAG UART 放最后。定时器周期设在 1ms 而不是 1s是因为数码管动态扫描需要 50Hz 以上的刷新率按键消抖也需要毫秒级采样1ms 时基可以同时服务这两件事。提示如果板载晶振不是 50MHz换用对应频率后要重算定时器周期值公式是 period f_clk / 1000 - 2不要照抄本文的数字。3.3 顶层实例化、管脚分配与编译流程系统生成后会在工程里多出一个软核封装模块需要顶层 Verilog 例化它。下面是一个典型例化写法module top( input clk_50m, input rst_n, output [7:0] seg, output [5:0] dig, input [3:0] key ); system u0( .clk_clk (clk_50m), .reset_reset_n (rst_n), .seg_pio_export (seg), .dig_pio_export (dig), .key_pio_export (key) ); endmodule逻辑说明system 是 Platform Designer 生成的模块名端口名字由你在图形界面里添加时钟和 PIO 时确定。clk_clk 接板上晶振reset_reset_n 低有效。seg 宽度 8 位对应八段数码管dig 宽度 6 位对应六个数码管位选。如果你的板子是四位或者八位数码管按实际宽度改顶层端口。参数说明PIO 的 export 名字在生成时是可改的顶层例化必须和生成后的名字一致。管脚分配在 Assignments 菜单的 Pin Planner 里完成seg、dig 分配到对应 FPGA 引脚key 根据原理图接法分配。分配完引脚必须重新全编译只编译而不跑全流程管脚约束不会生效。4. 用 C 语言写完电子钟逻辑定时中断、数码管扫描与按键消抖4.1 先把 BSP 和最小工程跑起来硬件工程编译成功并下载后下一步是建 NIOS II 软件工程。新建工程时选择对应 BSPBSP 会根据 hardware 里的 .sopcinfo 文件自动生成所有外设的基地址宏。先写一个最简程序确认链路通畅#include stdio.h #include system.h int main(void) { printf(NIOS II clock started.\r\n); while (1) { // 先空转确认系统稳定再往下加功能 } return 0; }逻辑说明printf 在 NIOS II 里默认输出到 JTAG UART不需要额外配置串口参数打开 Nios II 终端就能看到打印。system.h 由 BSP 生成里面包含所有外设的基地址和中断号宏定义后续代码都要依赖它。参数说明main 里的空循环不是浪费而是先确认复位向量、JTAG 连接、终端打印三个环节都正常。如果你跑不到这一步问题多半在上一章的复位向量配置而不是软件。4.2 定时器中断电子钟的“秒”从哪来时基是整个电子钟的地基。常见做法是让 Interval Timer 产生 1ms 中断软件在中断里做毫秒累加累到 1000 次产生一秒。这样显示扫描和按键采样也能共用同一套时基。#include altera_avalon_timer_regs.h #include sys/alt_irq.h #define TIMER_PERIOD 49999 volatile unsigned int tick_ms 0; volatile unsigned long seconds 0; static void timer_isr(void* context) { // 写 STATUS 寄存器 0 清除中断标志 IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER_0_BASE, 0); // 计数器到 0 后不会自动装载新周期必须重新写 PERIOD IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER_0_BASE, TIMER_PERIOD 0xFFFF); IOWR_ALTERA_AVALON_TIMER_PERIODH(TIMER_0_BASE, (TIMER_PERIOD 16) 0xFFFF); // 重新启动定时器让计数器立即装载 period IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_0_BASE, ALTERA_AVALON_TIMER_CONTROL_ITO_MSK | ALTERA_AVALON_TIMER_CONTROL_CONT_MSK | ALTERA_AVALON_TIMER_CONTROL_START_MSK); tick_ms; if (tick_ms 1000) { tick_ms 0; seconds; } } void timer_init(void) { IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER_0_BASE, TIMER_PERIOD 0xFFFF); IOWR_ALTERA_AVALON_TIMER_PERIODH(TIMER_0_BASE, (TIMER_PERIOD 16) 0xFFFF); IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_0_BASE, ALTERA_AVALON_TIMER_CONTROL_ITO_MSK | ALTERA_AVALON_TIMER_CONTROL_START_MSK); alt_ic_isr_register(TIMER_0_IRQ_INTERRUPT_CONTROLLER_ID, TIMER_0_IRQ, timer_isr, 0, 0); }逻辑说明Altera 的 Interval Timer 计数到 0 时只翻转不会自动把 PERIOD 寄存器的值重新装载到计数器。如果不重写 PERIOD第二个中断不会在 1ms 后到来而是要等计数器从 0 一直绕到 0xFFFFFFFF中断间隔会异常拉长。这也是定时器中断最容易踩的雷。参数说明TIMER_PERIOD 49999 是在 50MHz 时钟下的近似 1ms实际中断周期是 (49999 2) / 50000000约 1.00002ms。误差来自硬件装载新周期的两个额外时钟周期这个误差单靠系统本身消除不了留到最后一章做校准。tick_ms 和 seconds 必须声明为 volatile因为它们被中断和主循环同时访问不加 volatile 可能被编译器优化掉。4.3 数码管扫描与按键调时的实现数码管动态扫描的原理是同一时刻只点亮一位依靠视觉暂留让人感觉所有位同时显示。扫描周期控制在 1~2ms 比较合适太快驱动功率跟不上太慢会看到明显闪烁。#include altera_avalon_pio_regs.h // 共阳段码表0~9最后一位带小数点 const unsigned char seg_code[10] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90 }; volatile unsigned char hh 12, mm 30, ss 0; void disp_refresh(void) { static unsigned char pos 0; unsigned char dat; switch (pos) { case 0: dat seg_code[hh / 10]; break; case 1: dat seg_code[hh % 10] 0x7F; break; // 小时位加点 case 2: dat seg_code[mm / 10]; break; case 3: dat seg_code[mm % 10]; break; case 4: dat seg_code[ss / 10]; break; default: dat seg_code[ss % 10]; break; } IOWR_ALTERA_AVALON_PIO_DATA(SEG_PIO_BASE, dat); IOWR_ALTERA_AVALON_PIO_DATA(DIG_PIO_BASE, (1 pos)); pos; if (pos 6) pos 0; } void key_scan(void) { static unsigned char key_count[4] {0}; unsigned char now (~IORD_ALTERA_AVALON_PIO_DATA(KEY_PIO_BASE)) 0x0F; int i; for (i 0; i 4; i) { if (now (1 i)) { if (key_count[i] 10) key_count[i]; if (key_count[i] 10) { key_action(i); // 连续 10ms 采样到按下确认有效 } } else { key_count[i] 0; } } }逻辑说明位选口每次只输出一个 1通过三极管驱动对应位数码管段码口输出该位要显示的字形。按键消抖利用了 1ms 时基每毫秒扫描一次按键连续 10 次采样相同才判定为有效按下相当于 10ms 软件消抖不需要单独的延时函数。参数说明seg_code 是共阳接法的码值如果板子上是共阴数码管把表内每个值按位取反即可。key_scan 里对电平取反是为了适配按键低有效接法。如果按键按下时读到的是高电平去掉取反操作否则按键逻辑会完全反着来。key_action 是具体处理函数可以在这里做模式切换、分时加减、闹钟设置不影响主循环节奏。主循环里只需要不断调用两个函数int main(void) { timer_init(); while (1) { disp_refresh(); key_scan(); } return 0; }这里没有加任何 delay因为 1ms 时基由硬件中断维持显示和按键都跟着中断的节奏走。主循环里不要出现 printf 刷屏或者长延时它们会让数码管刷新周期不均匀肉眼表现为闪烁。5. NIOS II 电子钟最常见的 5 个坑现象、原因和补救5.1 下载以后一直复位JTAG 一连就断现象sof 文件下载成功但程序完全没有运行数码管不亮Nios II 终端也连不上GDB 一启动就报错退出。原因复位向量没有指向可用的存储器。如果 Platform Designer 里 Reset Vector 指向外部 Flash而 Flash 里没有烧录任何代码CPU 上电后去一个空地址取指整个系统就挂在那里反复复位。解决调试阶段把 Reset Vector 和 Exception Vector 都设为 On-Chip Memory。等程序功能稳定、需要上电自启动时再把 Reset Vector 改到 EPCS 一类的配置器件并生成镜像文件写入。5.2 System ID mismatch硬件和软件版本对不上现象Nios II 终端能打开但跑程序时控制台或者调试器报 System ID mismatch或者程序运行行为完全错乱。原因BSP 是基于旧的 .sopcinfo 文件生成的Platform Designer 里改过硬件比如给 PIO 挪了个位置、加了新外设软件工程还在用旧地址表。解决在软件工程的 BSP Editor 里重新导入最新生成的 .sopcinfo然后重新生成 BSP再重新编译整个软件工程。这个错误不是环境坏了是软硬件不同步重新生成 BSP 是唯一的正解。5.3 数码管亮度不均整个系统像在闪现象六个数码管亮度不一样有的明显暗有的亮刷新时还能看见残影和闪烁。原因显示刷新放在 main 循环里而 main 循环里如果有按键处理延时段、printf 输出或者其他阻塞操作刷新周期就会忽长忽短视觉上就是亮度和闪烁不稳定。另外共阳共阴和段码表不匹配也会表现为显示错乱甚至全灭。解决把数码管刷新放到定时器中断里或者用 volatile 标志让主循环每 1ms 只做一次刷新保证每位点亮时长恒定。位选停留时间不要低于 0.5ms否则数码管驱动电流还没建立起来亮度不够。先写一个逐位点亮的测试程序把段选、位选、极性一次验证完再接显示逻辑。5.4 连续跑一天时钟偏快或偏慢现象程序稳定运行数码管显示正常但和手机时间对比一天下来差了几秒甚至几十秒。原因板上晶振本身有 ppm 级偏差加上定时器周期计算中period 2带来的小数误差以及中断响应延迟累计一天后误差被放大到肉眼可见的程度。解决先把电子钟连续跑 24 小时记录和标准时间的误差。得到误差后按下一章的校准方法修正而不是反复怀疑代码逻辑。写一个测试宏记录启动时间每天同一时刻对比一次比盯着波形猜问题有效得多。5.5 编译报错on-chip memory 容量不足现象软件工程编译时报 memory overflow 或链接失败错误指向 BSP 和堆栈段占用。原因NIOS II 基础 BSP 本身要占用一部分内存加了 printf 后缓冲区、堆栈和标准库都会挤占空间。32KB 的片上 RAM 在带调试等级较高时会非常紧张。解决先把 JTAG Debug Level 从 Level 1 降到 Level 0再把代码优化级别设为 -O2 或 -Os减小占用的存储空间。如果还是不够考虑外接 SDRAM 或换用更大容量的 FPGA。注意优化级别提高后单步调试会不准确排错时临时降回 -O0。6. 电子钟误差校准一个 24 小时就能见效的补偿技巧6.1 校准思路误差测量与 tick 周期修正校准第一步是拿到真实误差。把电子钟跑满 24 小时记录和标准时间的差值 E单位用秒。E 为正表示电子钟偏快E 为负表示偏慢。下一步不是去改硬件时钟频率而是在软件里修正累加逻辑。假设 E 3说明每 86400 秒多计了 3 秒等价于每 1 现实秒电子钟走了 1.0000347 秒的计数量。软件的最小时间单位是 1ms不能直接改出小数周期那就每隔一定秒数跳过或补加一次 1ms。修正间隔 N 按公式 N 86400 / |E| 计算E 为正是快每 N 个现实秒跳过 1ms 的累计E 为负是慢每 N 秒在中断里额外补 1ms。24 小时误差 E修正策略修正间隔1 秒每 86400 秒跳 1ms86400 秒2 秒每 43200 秒跳 1ms43200 秒3 秒每 28800 秒跳 1ms28800 秒-3 秒每 28800 秒补 1ms28800 秒修正逻辑放在定时器 ISR 里跳 1ms 就是让 tick_ms 少加一次补 1ms 就是连续两次执行 tick_ms。第一次校准后继续跑 24 小时误差会缩小很多再按同样的方法微调一次通常两轮就能把日误差压到 1 秒以内。6.2 扩展方向把电子钟做成系统验证平台电子钟跑顺手之后加闹钟、加温度传感器、换 LCD 显示基本都能沿同一套流程走在 Platform Designer 里加 IP分配地址更新 BSP写软件。这套流程的价值比电子钟本身大得多它把一个陌生板卡变成了能反复验证新外设的平台。我有一个自己坚持的习惯每次拿到一块新的 FPGA 开发板先做一遍最小 NIOS II 电子钟作为系统的“冒烟测试”。板卡的时钟、复位、GPIO、定时器、调试链路是不是正常一个项目全查完了。之后再接新外设每次都先回到这个基准工程跑一跑确认总线没有被改坏再继续往下做。我第一次做 NIOS II 电子钟时也把定时器重载、System ID、复位向量这几个坑反反复复踩过。后来养成了动手前先在 Paperwork 里检查复位向量指向、动手后同步更新 BSP 的习惯省下了大量查错时间。希望帮到你。本文还有配套的精品资源点击获取