简介:一套基于NIOS II软核处理器与FPGA的电子钟设计完整工程,适合FPGA初学者、嵌入式爱好者和电子设计竞赛队伍学习参考。工程针对数字钟的常见功能需求,给出了从硬件驱动到软件控制的整体方案:底层使用Verilog编写数码管驱动,上层基于Qsys构建的SOPC系统完成计时控制,通过定时器产生基准时基,驱动八位数码管显示时分秒;三个按键分别实现模式切换、数值加一和减一,调节中的对应位会闪烁提示,操作LED同步点亮反馈;闹钟时间到后,蜂鸣器报警并联动流水灯指示。资源共602个文件,压缩包22.66MB,包含设计源码、C程序、Qsys系统配置、SDC约束以及可直接下载的SOF与ELF镜像,各类型文件用途清晰,目录结构利于按模块排查和二次修改。目前已有2274人学习下载,适合希望从头完整搭建NIOS II硬件系统、掌握软核处理器与FPGA逻辑协同开发的读者。 前阵子帮朋友做课程设计,选的题目就是“FPGA基于NIOS II的电子钟设计”,做完之后发现这个项目特别适合分享出来。它麻雀虽小五脏俱全,既要用Verilog把NIOS软核、定时器、GPIO这些组件在FPGA里搭起来,又要写C语言让处理器跑起来,还要处理数码管动态扫描、按键消抖这些硬件相关的细节,非常适合想入门SoC方向的朋友。市面上很多电子钟都是纯逻辑状态机做的,但用NIOS II软核做,思路完全不一样,更像是“在FPGA里嵌入一颗CPU来控制外围设备”,这个思维转换对后面学Zynq、学嵌入式都很有帮助。
顺便说一句,如果你手头是EGO1或者其他Altera/Intel系列的板子,这套流程直接照着走就行。如果用的是Xilinx的板子,NIOS换成MicroBlaze,思路完全一样。这篇文章不以具体板卡型号为限制,重点把系统设计、硬件搭建、软件开发、上板调试这几个环节讲透。
1. 电子钟软硬件方案的整体拆解
1.1 纯逻辑方案与NIOS软核方案的取舍
先说说为什么选NIOS II。传统电子钟用纯Verilog写状态机,把计数、进位、译码、扫描全部用硬件逻辑实现,好处是延时确定、不依赖代码执行流程,坏处也很明显:改时间格式、加闹钟、调整显示模式,每一处改动都要动RTL代码,改完还要重新综合布线,调试周期很长。
NIOS II方案的本质是“在FPGA内部生成一颗32位处理器软核”,外围接上定时器、GPIO、UART这些标准外设,然后像写单片机程序一样用C语言控制整个系统。这样做有三个实际好处:第一,计时逻辑、按键扫描、显示刷新全部变成软件逻辑,改起来无非是改C代码重新编译,几分钟就能迭代一版;第二,NIOS II的开发流程和单片机很接近,对于学过STM32的同学,学习曲线非常平缓;第三,NIOS II外设都提供HAL库和寄存器级驱动,不用自己造轮子,省下大量时间。
我也必须说两句实在话。NIOS II方案在资源占用上比纯逻辑大不少,还要占用不少内部存储做程序和数据空间,但对于EGO1这种Cyclone IV级别板子,资源完全够用。另外,纯逻辑方案的实时性天然更好,而NIOS II依赖中断和主循环调度,只要程序设计得当,做电子钟的计时精度毫无问题。
1.2 系统整体架构的划分思路
整个电子钟系统的架构可以分成两大部分:FPGA硬件逻辑部分和NIOS II软件部分。硬件部分通过Platform Designer(老版本叫QSys)搭建一个完整的最小系统,包括NIOS II处理器软核、片上RAM、定时器IP、PIO控制器。软件部分跑在NIOS II内部,通过外设寄存器地址访问硬件。
我把架构画出来你感受下:
- CPU:NIOS II/e经济型内核,适合做控制类任务
- 时钟:由FPGA外部50MHz晶振统一供给
- 存储:片上RAM,存放程序指令和数据
- 定时器:Interval Timer IP,产生1ms周期中断,作为计时基准
- PIO:若干组GPIO,分别控制数码管段选、位选和独立按键
- UART(可选):调试打印信息用
这个架构说白了就是一个微型计算机。硬件上的连线,相当于把CPU、内存、定时器、GPIO用总线连接起来;软件上的逻辑,相当于在CPU上跑一个裸机程序。这个模型一旦建立,后面做任何基于NIOS II的项目,基本都是在往这个骨架上添砖加瓦。
2. Platform Designer下的硬件平台搭建
2.1 在Platform Designer里搭一个最小NIOS系统
打开Quartus,新建工程后,在Tools菜单找到Platform Designer并打开。这一步是在图形化环境下把各个IP拖出来连线,操作起来像搭积木,但每根线的含义都要清楚。
第一步添加NIOS II处理器。双击添加NIOS II Processor,选择Nios II/e经济型。这里有个关键选择:JTAG调试模块要不要加?我建议选上JTAG调试模块,因为上板调试的时候,很多问题要依赖它查看内部变量。如果空间紧张,可以在最后阶段把调试模块去掉以省资源。
第二步添加片上存储。选择On-Chip Memory,设置总容量。我用的是64KB,对电子钟这种程序量极小的项目,32KB其实也够,但64KB更从容,Quartus综合的时候也不会因为容量不足报错。存储类型选RAM,不要选ROM。
第三步添加串口调试,选择JTAG UART。这个IP很有用,可以把printf的输出映射到Quartus的调试终端,我调试时习惯在代码里加一串打印信息,观察状态机变化,没有串口会很不方便。
第四步添加Interval Timer定时器,也就是核心的计时器IP。这里有个比较重要的配置项,后面单独讲。
第五步添加PIO控制器,根据外设引脚需求创建。我用了两组PIO:一组输出给数码管段选和位选,一组输入读按键。
所有组件添加完后,最关键的步骤来了:在System Contents里双击各个IP的时钟和复位连接,确保它们都在System 50MHz这个时钟总线上。然后点击Assign Base Address,让系统自动分配基地址。最后点击Generate,生成整个硬件系统。生成的.v文件会在工程目录下自动关联到顶层模块。
2.2 定时器与PIO外设的参数规划
定时器的参数规划是这个项目硬件设计的重头戏。我使用的是Full-featured定时器模式,计数器宽度设为32位。为什么是32位而不是16位?因为系统时钟是50MHz,也就是一个时钟周期20ns,如果我要实现1ms定时,需要的计数次数是50000,这个数字16位能放下,但考虑到以后可能要扩展成更长的计时周期,32位更保险,省得以后返工。
定时器的工作方式选择往下计数模式(Count down),当计数值从初始值减到0,会输出一个脉冲并触发中断。初始值设置在软件里通过寄存器写入,这里硬件上只需要把周期值预留出来。
PIO参数的规划更加琐碎,但不能错。我把段选引脚配置为8位输出,位选配置为6位输出,按键配置为4位输入。PIO的方向设置是个经典坑:在Platform Designer里,Output Only和Input Only都会自动配置方向,但如果你选的是Bidirectional,一定要在代码里写方向寄存器,否则输入输出方向是乱的。我的习惯是用Input Only和Output Only,把方向控制交给硬件,软件少操心。
另外,每个PIO都要单独设置中断条件。按键PIO我选择了“上升沿触发中断”和“下降沿触发中断”都打开,这样按键按下和松开都能感知到。定时器PIO不需要中断,PIO对应的是中断控制器里的中断号,这个中断号要记下来,后面写软件ISR要用。
2.3 硬件生成后的顶层集成方法
Platform Designer生成的是一个带固定接口名称的模块,名称通常是my_system之类的名字,加上你生成时填写的实例名。回到Quartus工程,需要新建一个顶层Verilog文件,把这个模块例化进去。
顶层模块要解决两件事:一是把Platform Designer生成的NIOS系统各个接口,连接到FPGA的真实引脚上;二是把没有被NIOS系统占用的引脚,比如数码管的公共端,根据位选信号进行分配。
这里有个非常实用的经验:先把Platform Designer生成的模块源码添加进工程,然后在顶层文件里例化,用wire定义好各接口信号,再把与引脚相关的信号通过Quartus的Pin Planner绑定到具体引脚。绑定引脚时,务必在Pin Planner里确认每个信号的IO标准,我用的板子大多是3.3V,所以IO Standard选3.3V-LVTTL,选错会导致信号不稳定甚至损坏引脚。
3. Nios II软件的编写与逻辑实现
3.1 BSP工程创建与代码结构约定
硬件平台生成后,进入Nios II SBT for Eclipse(或更新版本的Nios II Command Line Shell),创建BSP包和软件工程。BSP包相当于板级支持包,会自动根据sopcinfo文件生成对每个外设的寄存器定义和驱动函数。我的做法是先用File > New > Nios II Board Support Package,选中生成的.sopcinfo,使用默认设置直接生成,再把软件工程和它关联起来。
这里有个重要选择:BSP的设置中,关于stdout设备的选项。默认会指向jtag_uart,这个不用改,方便printf调试。另外,在BSP Editor里确认timer的驱动支持是打开的,如果没有生成对应的timer驱动,代码里访问寄存器会报错。
工程建好后,代码建议分成三个文件:main.c放主循环和初始化逻辑,timer.c放定时器中断服务程序,display.c放数码管扫描和按键响应。这样结构清晰,调试也方便。如果你之后要往工程里加闹钟、秒表功能,也是在这三个文件上继续扩展。
3.2 定时器中断:电子钟的核心计时逻辑
计时逻辑是整个软件的核心,我把它全部写在定时器中断服务程序里。思路是把1ms作为最小时间粒度,中断每触发一次,就对ms计数加1,累加到1000说明过了1秒,然后秒计数加1。秒满60进分钟,分钟满60进小时,小时满24自动归零。
中断服务程序里有个细节:硬件定时器触发中断后,需要清除中断标志位,否则中断会反复进入。清除方式是在状态寄存器里写0。代码上我习惯用IOWR_ALTERA_AVALON_TIMER_STATUS宏,把寄存器值为0写入状态寄存器,这条语句不能漏。很多人做NIOS的时候,定时器一直狂触发中断,原因多半就是没清标志位。
另外,在ISR中尽量不要做耗时操作。我早期版本在ISR里直接做数码管刷新,结果计时明显变慢,主循环和ISR抢资源。后来把ISR只负责计数和置标志位,主循环里检查标志位再做刷新,问题立刻解决。这下你明白为什么ISR要短小精悍了吧。
3.3 数码管动态扫描与按键输入的工程实现
数码管的动态扫描原理是一个共阴或共阳数码管靠位选信号选通某一位,然后用段选信号决定这一位显示什么数字。因为人眼有视觉暂留效应,只要循环扫描每一位点亮的频率足够高(通常1ms切换一次),看起来就像所有位同时亮着。
实现上,我在主循环里用一个scan_index变量,从0到5循环,每次循环做两件事:先把前一位的位选信号拉低,避免显示重叠;再往段选寄存器写入当前位要显示的字符对应的段码;最后把当前位的位选信号拉高。整个过程本质上是一个小型状态机,状态切换靠主循环轮询,不阻塞。
段码表的编写要特别留意共阳共阴的区别。共阳数码管,公共端接高电平,某个段要点亮,对应段信号必须为低电平。比如要显示数字0,共阳段码是0xC0。共阴极正好相反,对应位为高电平。我用的是六位共阳数码管,所以段码表做了注释,以免换板子时搞混。
按键输入方面,我用PIO中断加软件消抖的方式来处理。按键按下时,PIO会产生一次中断,我在ISR里只记录当前按键状态并置一个按键事件标志,真正执行功能调整放在主循环。消抖采用延时法,检测到按键状态改变后,延时20ms再读取一次,如果状态一致就确认是有效按键。这样做的好处是简单可靠,缺点是需要主循环及时处理,但对电子钟这种低频应用完全够用。
4. 上板调试的完整流程与排查技巧
4.1 从烧写硬件配置到运行软件程序的步骤
上板调试的第一步是综合编译Quartus硬件工程,生成.sof文件。打开Programmer,选择JTAG模式,添加.sof文件,勾选Program/Configure,点击Start。这一步会把FPGA的硬件配置烧到板子上的FPGA配置RAM中,掉电就丢失,适合调试阶段使用。
注意,如果NIOS II核里配置了JTAG调试模块,烧写完.sof之后,需要把软件工程也编译好,生成.elf文件,然后在Eclipse里右键工程名,选择Run As > Nios II Hardware。这一步会自动连接JTAG,先下载.sof(也可以选择跳过,如果你已经烧过.sof),再下载.elf,最后复位并运行程序。我建议分开操作,先用Programmer烧.sof,再用Eclipse运行.elf,这样能明确区分到底是硬件问题还是软件问题。
一个常见的连锁现象是:只烧了.sof忘记运行.elf,板子上数码管不亮;或者只运行了.elf没有烧.sof,JTAG找不到设备。所以每次上电后第一件事:先烧.sof,再确认Nios II的JTAG调试模块能被识别,最后才运行软件。
4.2 通过调试器和串口定位异常的实战思路
调试NIOS II的时候,最大的优势是可以看到变量,设置断点。我用的是Eclipse自带的Debug配置,断点打在ISR里或主循环中,观察sec、min、hour变量是否正确累加。如果发现秒数不正常,优先检查定时器初始值是否算错,以及标志位有没有清除。
另一个调试法宝是JTAG UART打印。我在初始化完成后打印一行System Ready,然后在按键响应里打印按键编号。有次按键功能失效,我打印后发现按键中断一直在触发,原因是PIO中断使能后在ISR里没有清除PIO的中断捕获标志。这个通过打印很快就能定位。
数码管显示异常的排查思路稍微复杂一些。如果显示乱码,优先怀疑段码表错误或共阳共阴搞反;如果只有个别位不亮,优先检查位选驱动;如果整体闪烁,优先检查扫描延时是否过大。我用示波器量了一下位选波形,发现扫描周期大约6ms刷新一遍,视觉上正好稳定。这里也分享一个技巧:如果不方便量频率,可以用肉眼观察闪烁程度来估算,一般1到2ms刷新一位,整体刷新周期不要超过8ms。
4.3 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 编译通过但上板无反应 | 未烧.sof或.elf | 按顺序烧写.sof并运行.elf |
| 定时中断不停触发 | ISR未清除定时器状态标志 | 在ISR首行写状态寄存器清0 |
| 按键无响应 | PIO中断捕获标志未清除 | 读取并清除PIO捕获寄存器 |
| 数码管乱码 | 共阳共阴段码表弄反 | 核对段码表极性 |
| 个别位不亮 | 位选信号方向或电平不对 | 检查PIO方向和位选驱动极性 |
| 计时明显偏慢 | ISR耗时太长 | 把耗时操作移到主循环 |
| 程序跑飞或硬件报错 | 片上RAM容量不足 | 增大On-Chip Memory容量 |
5. 从基础功能到扩展玩法的优化路线
5.1 显示刷新算法和计时精度的进一步优化
如果觉得动态扫描的主循环占CPU太多,可以做一个简单的优化:把扫描放到定时器中断里,用1ms中断切换一位。这样主循环就解放出来了,可以做更复杂的事情。我当时实测,把扫描放进中断后,主循环CPU占用几乎为零,按键响应也更灵敏。
计时精度的优化要关注晶振频率。板载晶振标称50MHz,但实际可能有些偏差,长期运行会累积误差。NIOS II提供了altera_avalon_timer驱动,可以在软件里做一个简单的校正:用秒表看一小时差多少秒,然后在Cnt子寄存器里补偿Tick数。这个思路和机械表调快慢是一个道理。
5.2 闹钟、整点报时和扩展外设的加入思路
有了NIOS II软核,加功能主要就是软件层面的工作。加闹钟只需要定义一个闹钟变量,在秒进位后比较当前时间和闹钟时间,一致则触发蜂鸣器;整点报时同理,在分进位时判断当前分钟数是否为0即可。
如果要用到蜂鸣器或继电器,只需要在Platform Designer里再加一组PIO,然后在软件里控制高低电平。这个工作量比纯逻辑方案小太多,也是软核方案最大的价值所在。不过要注意,新增外设后,硬件工程要重新综合生成.sof,软件的BSP也要重新生成,否则地址映射会不一致,导致访问到错误的外设地址。
我自己做完电子钟后,又把显示从六位数码管换成了LCD1602,加了温湿度读取,只是多加了一个I2C的IP和相应驱动,一周不到就弄完了。可见架构搭好之后,扩展性是相当大的。
5.3 给新手学习者的实践建议
如果你第一次接触NIOS II,建议按这个顺序推进:第一步,什么都不改,只跑通Platform Designer自带的hello_world工程,确认JTAG下载、软件运行、printf打印都正常;第二步,在这个基础上加一个LED闪烁程序,理解PIO输出;第三步,对照本文加定时器、数码管显示、按键中断;最后再整合成完整电子钟。每步都生成一个独立版本的工程,方便回溯。
代码维护上,建议用Git管理工程,每次能稳定运行就打个tag,因为FPGA工程文件多,随便改个底层可能就编译不通过,没有版本管理容易心态崩溃。这个建议不仅适用于本项目,做任何FPGA大项目都值得遵循。
我实际操作中最深刻的体会是:NIOS II电子钟这个项目,真正的难点不在代码量,而在于理解“硬件生成软件、软件控制硬件”的边界。硬件上少一个IP、少连接一条总线,软件层面就多一次崩溃;软件上少清一次标志位,硬件上就多一次异常。所以遇到问题不要慌,先从硬件配置检查起,再从软件驱动程序查起,一步步缩小范围,最终一定能解决。以后你往Zynq这类ARM+FPGA异构架构切换时,这个项目的经验会给你很扎实的底气。
本文还有配套的精品资源,点击获取