ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于NIOS II软核的FPGA电子钟从RTL到片上系统的完整实践

基于NIOS II软核的FPGA电子钟从RTL到片上系统的完整实践 简介一套FPGA综合实训资源基于NIOS II软核与Verilog协同实现电子钟设计适合学习Qsys系统集成、SOPC开发流程的进阶用户也可作为课程设计或竞赛参考。项目完整覆盖从硬件平台搭建到应用逻辑实现的路径Verilog驱动八位数码管动态扫描NiOS II完成计时、按键调时、闹钟触发与流水灯指示借助Qsys定时器提供毫秒级时基支持五种调时模式待调整位带闪烁提示按键状态以发光二极管反馈。资源包共602个文件压缩包约22.66MB包含Quartus工程文件、Qsys外设配置、Verilog/VHDL源码、HAL库与C语言应用代码以及合成后的编程文件与仿真报告目录清晰便于直接定位到硬件配置、驱动编写、中断处理等模块。目前已有2274人学习对希望深入理解软核处理器地址映射、中断优先级、动态扫描和状态机设计的开发者有较高参考价值可据此复现、调试或扩展为自己的完整项目。1. 为什么我用NIOS II软核做一个慢电子钟而不是纯Verilog1.1 电子钟看似简单臃肿状态机才是真麻烦电子钟这个项目几乎是每个FPGA学习者都会碰到的一道坎。如果你直接用Verilog写大概会这样拆模块秒计数器、分计数器、时计数器、按键消抖逻辑、BCD码转换、数码管动态扫描、整点报时标志……每一块都要手写状态机光是一个按键长按连续加和短按单次加的逻辑就能让不少新手折腾一整天。更麻烦的是如果想加一个闹钟功能状态机数量会瞬间翻倍组合逻辑和时序逻辑铺在一起排查问题的时候信号波形看得人头皮发麻。而换用NIOS II软核之后情况完全不同。NIOS II是一个运行在FPGA内部的32位软核处理器你可以把它想象成在FPGA里软出一颗CPU。计时逻辑从RTL代码变成了C语言代码秒、分、时的更新就是几个if判断和变量自增按键模式切换就是一个switch语句。整个项目的重心从怎么写状态机转移到了怎么连接外设、怎么管理中断、怎么让硬件和软件高效协作上这才是嵌入式系统设计的核心思路。1.2 这个项目的真实定位FPGA片上系统SoC的敲门砖用NIOS II做电子钟最大的价值不在于电子钟本身有多复杂而在于它完整覆盖了一条片上系统开发链路用QSYS/Platform Designer搭建CPU和外设总线用Verilog封装自定义逻辑用C语言完成应用层逻辑用JTAG在线调试最后把程序固化到配置芯片里。这条链路走通一遍你对嵌入式FPGA这个概念的理解会立刻具象化。这个项目尤其适合三类人一是正在做课程设计或毕业设计的学生NIOS II电子钟比纯Verilog版本更容易扩展功能论文能写的东西也多二是希望从单片机转FPGA的嵌入式工程师NIOS II的编程思维和STM32很像上手门槛低三是想搞明白软核处理器和硬核ARM到底差在哪的硬件爱好者。你不需要一开始就碰ZYNQ那种双核ARMFPGA的复杂架构先用NIOS II把硬软件协同的流程摸透后面看ZYNQ Linux驱动、FPGA图像处理这些方向都会轻松不少。2. 系统方案与硬件框架先把哪些活交给硬件哪些活交给CPU分清楚2.1 芯片选型与开发板参考我做这个项目用的是Altera现在是IntelCyclone IV系列的EP4CE10F17C8这也是市面上黑金、小梅哥等常见开发板的主流芯片。EP4CE10的逻辑单元只有约10320个但做一个NIOS II/e经济型内核加几个PIO外设资源消耗大概只占三成左右完全够用。如果没有这块板子EP4CE6也足够区别只是片上RAM小一些编译时要稍微精简一下BSP外设。时钟方面板载50MHz有源晶振直接接入FPGA的CLK引脚。这里有一个关键点电子钟的精度上限是由时钟源决定的而不是由NIOS II决定的。50MHz晶振经过PLL或直接分频后理论上一天误差可以控制在0.1秒级别但前提是计数基准要算准。后面调试精度部分我会详细说这里先记住不要贪方便用内部RC振荡器做时基温度漂移会让你一天的误差飙到十几秒。2.2 外设功能划分CPU管逻辑FPGA管时序整个电子钟我用到的外设不多但每个外设的角色划分很明确这也是NIOS II工程里最重要的一项设计决策。外设硬件实现方式在NIOS II系统中的角色说明6位数码管时分秒动态扫描逻辑可硬件也可软件我推荐硬件PIO输出CPU只负责刷新时间寄存器减轻CPU中断负担4个独立按键按键消抖软件消抖或硬件RC滤波PIO输入带中断和边沿捕获用于设置时间、切换模式蜂鸣器1Hz方波或PWM驱动PIO输出CPU在整点时翻转整点报时50MHz时钟直接连接硬核时钟输入时间基准JTAG调试系统内嵌JTAG_UART下载、调试、串口打印我的经验是凡是时序敏感、要求确定性强的功能尽量放到FPGA硬件侧凡是逻辑判断、状态切换、运算处理的功能放到NIOS II软件侧。电子钟里最典型的例子是数码管动态扫描。2.3 动态扫描放在硬件侧是这次最值的一个决定6位数码管如果全部由CPU翻转I/O去扫描会产生两个问题一是刷新频率不稳定主循环里一旦插入了按键处理、时间计算等代码某一位的显示时间就会变长肉眼直接看到闪烁二是每2ms就要进一次中断CPU大量时间浪费在扫描上后期想加闹钟、温度传感器都会捉襟见肘。我的做法是在FPGA逻辑里写了一个非常简单的扫描模块内部计数器每1ms切换一次位选信号同时从一组寄存器中取出当前位的段码数据送出。这组寄存器以PIO外设的形式挂在NIOS II总线上CPU更新哪一位只需要向对应地址写一个8位数据。硬件扫描稳定地一遍遍循环CPU完全不用管刷新节奏只在时间变化时更新那6个寄存器。当然如果课程设计要求必须用NIOS II的定时器中断驱动扫描也能做只是要把中断间隔控制在2ms左右且主循环里尽量避免长阻塞操作后面调试部分我会讲这个坑。3. Platform Designer搭建与配置从空工程到最小系统细节全记录3.1 最小系统组件清单我用的是Quartus II 13.0之后的版本从Quartus II 14.0开始Qsys改名成了Platform Designer但操作逻辑完全一样。新建一个空的原理图工程后打开Platform Designer按以下顺序添加组件clk_0时钟源频率填50MHz不用接PLL直接作为系统时钟。nios2_gen2_0选择Nios II/eeconomy这个内核面积最小、功耗最低电子钟这种应用完全够用。如果你的板子资源充裕也可以选Nios II/f性能更强但BSP生成时SRAM初始化可能更慢。onchip_memory2_0片上RAM作为程序运行内存。Cyclone IV的EP4CE10有414Kbit建议分配至少24KB否则编译NIOS II/e的BSP都可能撑爆。总容量够的话给32KB更稳妥。jtag_uart_0JTAG串口用于调试输出和Nios II SBT下载。不用额外占用引脚非常推荐保留。sysid_qsys_0系统ID避免下载到不同版本工程的程序导致外设地址错乱。这个组件设置好基地址就行代码里几乎不用管它。timer_0Interval Timer用于产生1ms系统节拍。pio_0 ~ pio_4按键输入、数码管段码输出、数码管位选输出、蜂鸣器输出、时间更新锁存信号等按需添加。连接方式不用手动连。在Platform Designer的Diagram页面选中所有组件右键点击System Assign Base Addresses让系统自动分配地址然后点System Create Global Reset Bridge和Create Clock Bridge把外部时钟和复位信号引出来。这一步完成后底部消息栏会提示多少个interconnect已生成。3.2 Interval Timer参数1ms中断到底怎么配Interval Timer是这次工程里最关键的组件直接决定电子钟走时准不准。我采用的配置如下Preset预设选择1 ms预设后 再手动核对计数周期。Counter Size32位。Counter Period50000个时钟周期50MHz × 1ms。Start/Stop寄存器、Continuos计数器勾选Continuos连续计数这样计数器计到0后自动重装并继续不需要代码反复启动。Fixed period vs Loadable period我选了Loadable可加载这样后期校准精度时可以直接在软件里修改period寄存器不用重新编译硬件工程。Complete counter period register勾选后Counter Size自动扩展为64位方便读取当前计数值但电子钟用不到不勾也行。中断逻辑我采用Period脉冲模式计数器从50000递减到0时产生一个时钟周期宽度的脉冲同时触发IRQ。NIOS II的BSP会自动把timer_0注册为系统时钟如果选择system clock然后提供alt_uptime()等接口。我个人习惯不依赖系统时钟而是自己注册一个中断服务函数保证行为可控后面代码部分会说明。3.3 PIO配置输入按键和输出显示的不同门道按键PIO和数码管PIO的配置方式完全不同这是新手特别容易踩坑的地方。按键输入PIO我用了4位输入、带中断、带边沿捕获的模式DirectionInput输入Edge Capture Register勾选Rising edge因为按键另一端接的是GND按下时引脚从高变低如果直接检测电平容易误触发我实际测试下来用下降沿作为按下事件更干净。这里要根据自己板子的按键接法调整有的按键按下是拉高那就要选Rising edge。Enable IRQ勾选中断类型选择Edge。数码管段码PIO和位选PIO则是输出模式宽度分别为8位和6位。需要注意段码输出引脚连接的是数码管驱动电路通常有PNP三极管或译码芯片不是直接把PIO引脚怼到数码管上否则电流不够显示会发暗或不亮。一个容易忽略的细节PIO输出在NIOS II复位期间是高阻态还是确定电平取决于Quartus引脚设置。建议在Quartus的Pin Planner中给数码管位选引脚配置Output并设置默认电平否则上电瞬间数码管可能全亮一下再被程序接管观感很差。3.4 生成HDL实例化代码并接入顶层所有组件配置完成后在Platform Designer界面点击Generate Generate HDL生成实例化模板。回到Quartus顶层原理图或Verilog文件把生成的pll_system_inst名字取决于你的系统模块名元件例化到顶层。需要手动连接的只有时钟输入、复位按键和所有PIO引脚的对应FPGA引脚。顶层连接时要注意时钟Platform Designer生成的系统模块默认带一个clk_in和一个global_reset_n输入。时钟直接接板上50MHz引脚复位脚如果板上没有专门复位按键可以接一个按键按下时低电平复位整个NIOS II系统。这个复位都是全局复位不要接成局部复位否则外设初始化可能不同步调试时会出现灵异现象。4. Nios II软件逻辑时间状态机、按键处理和中断服务程序4.1 软件整体框架主循环只做三件事NIOS II的软件我基于Nios II SBT for Eclipse编写BSP类型选择nios2-elfRTOS选None裸机。整体逻辑分为两部分中断服务程序ISR负责维护时间节拍和按键事件主循环负责响应按键、更新显示、处理整点报时。主循环的伪代码思路如下int main(void) { time_init(); // 初始化时间默认 12:00:00 display_init(); // 清空数码管寄存器 while(1) { if(key_event ! 0) // 有按键事件 { handle_key(key_event); // 处理按键切换模式、加减时间 key_event 0; } if(time_updated) // 秒值有变化 { update_display(); // 刷新数码管寄存器 time_updated 0; } if(alarm_flag) // 整点报时标志 { beep_pulse(); // 蜂鸣器响一声 alarm_flag 0; } } }这样的结构让主循环永远处于事件驱动状态CPU占用率极低。所有按键消抖都在ISR里完成一部分主循环只消费结果两者通过全局标志位通信避免在中断里做耗时操作。4.2 时间计数不是简单的要处理好进位边界我用一个结构体保存时间状态ISR里每秒更新一次秒计数typedef struct { uint8_t sec; uint8_t min; uint8_t hour; } RTC_TIME; volatile RTC_TIME current_time {0x00, 0x00, 0x12}; volatile uint8_t time_updated 0;秒计数更新时从0到59再到0同时min加1min到59再清零hour加1hour到23清零。这里有个编码选择问题数码管需要的是BCD码4位一个数字所以我在结构体里直接存BCD值而不是二进制值。更新时用if (current_time.sec 0x59) current_time.sec 0x00;这类方式进位判断比二进制转BCD更直观。如果你更喜欢存二进制也可以在显示前做一次二进制转BCD的函数调用但那样每次显示刷新都要额外计算没有直接存BCD省事。课程设计中这个选择自由但建议一开始就统一。4.3 定时器ISR1ms节拍累计避免浮点误差我在Interval Timer的ISR里维护一个tick计数器volatile uint32_t tick_counter_ms 0; void timer_isr(void *context) { // 读回寄存器以清除中断标志不同版本的Timer IP略有差异 IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER_0_BASE, 0); tick_counter_ms; if(tick_counter_ms 1000) { tick_counter_ms 0; time_tick(); // 秒进位逻辑 time_updated 1; } }用1ms作为基准而不是直接用1秒好处是可以后期做频率校准。比如你实测一天快3秒那就要把1000ms改成1000 误差修正值比如1035ms。如果用1秒定时器想微调就得重新改硬件里的period寄存器麻烦得多。ISR里也尽量不要做数码管刷新这种耗时操作我只更新标志位真正刷新放在主循环。这样即使有瞬时高优先级中断抢占了也不会导致显示撕裂。4.4 按键处理状态机实现设置-加-减-确认4个按键的分配是KEY1进入设置模式并切换设置位秒-分-时-退出KEY2加一KEY3减一KEY4退出/确认。在主循环的handle_key函数里我用一个enum状态变量区分正常运行状态和设置状态。typedef enum { MODE_RUN, MODE_SET_SEC, MODE_SET_MIN, MODE_SET_HOUR } SET_MODE; SET_MODE current_mode MODE_RUN;按键事件产生时ISR里已经做了一次消抖连续两次采样间隔10ms状态一致才确认主循环直接根据current_mode判断当前处于哪个状态再决定是切换状态还是加减时间。这个状态机用C语言写非常自然换成Verilog则要写几个always块加状态寄存器复杂度高一个量级。一个实用小技巧加减时间时如果按住按键超过1秒可以连续加。做法是在主循环里检测按键释放标志未出现且时间超过阈值就每秒再触发一次加操作。这个在Verilog里实现费劲但在C里就是一个if(key_hold_counter 500) key_hold_counter 0;的问题充分体现了软核方案在交互逻辑上的优势。5. 踩坑实录从不走时到稳定走时的完整排查链路5.1 现象一下载程序后数码管全不亮或只有一位亮先说结论这种情况大多不是软件逻辑错误而是PIO基地址或引脚绑定出问题。我在第一次跑的时候下载后数码管完全没有反应用JTAG_UART打印发现main函数跑到display_init()就卡住了之后看CPU的状态寄存器发现是数据总线异常。排查链路是这样的先确认Platform Designer的地址分配里PIO组件是否被分配了合法基地址。我碰到的情况是添加组件后没有点Assign Base Addresses结果两个PIO的地址重叠了printf能运行但IOWR到PIO地址时直接触发异常。重新分配地址并重新生成BSP后问题解决。另外检查顶层引脚绑定数码管段码是8位位选是6位Quartus的Pin Planner里如果绑错了位置比如把位选0绑到了段码g的引脚现象就是显示完全乱套。建议先用一个最简单的软件循环把所有PIO输出0xFF和0x3F来回翻转用示波器或肉眼确认每一个引脚都对应正确再开始写时间逻辑。5.2 现象二按键按下没反应偶尔又像抽风一样连跳好几个数这是典型的边沿捕获寄存器没清的问题。PIO的Edge Capture寄存器在检测到指定沿时会置1并且必须由软件写0来清除。我在自定义按键ISR里读取事件后忘了清状态位导致同一个按键事件被反复触发。查起来非常隐蔽因为单步调试时你看不到重复触发——每次触发后ISR都会进入清不清标志在单步模式下很难察觉只有全速运行才能发现连跳。正确的ISR写法void key_isr(void *context) { uint32_t edge IORD_ALTERA_AVALON_PIO_EDGE_CAP(KEY_PIO_BASE); IOWR_ALTERA_AVALON_PIO_EDGE_CAP(KEY_PIO_BASE, 0); // 立即清边沿捕获 if(edge 0x01) key_event KEY_INC; // 其他按键类似 }另外按键硬件上最好加一个简单的RC低通滤波1kΩ电阻串串联0.1uF电容对地能显著减少进入FPGA的毛刺软件消抖压力也会小很多。如果板卡上没有滤波电路一定要在代码里做至少10ms的防抖时间窗否则在电磁环境差的实验室里按键误触发率会高到让你怀疑人生。5.3 现象三数码管显示乱码数字8上多出一个小数点这个问题有意思排查到最后发现是段码表位置搞错。我用的数码管是共阴接法段码表的第0~6位分别对应a~g段。我一开始按网上某篇文章的段码表直接抄结果那个表是共阳的显示出来自然不对。后来我直接用万用表测了板卡原理图发现它用了两个74HC573锁存器做段码和位选驱动而且段码和位选的使能信号是独立的需要先送段码锁存段码再送位选锁存位选。如果软件刷新顺序反了或者锁存信号的时序没对上就会出现残影和乱码。解决方案是在底层驱动display_write(uint8_t digit, uint8_t segment_data)中严格按照段码-锁存段码-位选-锁存位选的顺序写。如果板卡驱动电路是直接三极管驱动的就简化很多只要段码和位选都是电平直接控制即可。5.4 现象四走时不准一天快/慢好几秒这个要分两步排查。第一步是确认硬件基准频率用示波器测量板载50MHz晶振的实际频率如果偏差超过100ppm百万分之百那考虑换一个温补晶振TCXO。第二步是软件校准这也是最常用的办法以我实测为例我用NIOS II的间隔定时器产生1ms节拍累计1000次得到1秒。通过和手机基准时间对比发现24小时快约2.3秒。换算成节拍就是每1000次tick少了约0.0265秒即每次tick应为1.0000265ms。解决方法是把tick_counter_ms 1000的判断改成#define TICK_PER_SEC 1000 // 校准值为 26 ppm即每1000次tick增加0.026次 if(tick_counter_ms TICK_PER_SEC) { tick_counter_ms 0; time_tick(); }但这只是粗调。更精确的做法是修改Interval Timer的period寄存器。比如timer的中断周期本来为50000个时钟周期校准后发现每天慢2秒说明每秒实际有2秒的偏差需要把period从50000改成50000 校准增量。这里需要注意的是校准增量会随时间积累而线性放大尤其要注意环境温度对晶振频率的影响。如果项目要求高精度建议直接用PLLGPS校时模块或NTP思路单纯靠软件校准只能逼近不能做到长期零误差。我最后把period调成49992左右实测一周误差控制在0.5秒内完全满足课堂演示和日常使用。5.5 现象五程序烧进去能跑但断电重启就“失忆”这是一个非常典型的NIOS II入门困惑。NIOS II程序默认通过JTAG下载到FPGA内部的RAM中运行断电就没了。如果你想让它像单片机一样上电自动运行必须把程序固化到板载的EPCS或SPI Flash中。固化流程分两步在Platform Designer中添加一个EPCS控制器epcs_controller并把NIOS II的reset vector指向该控制器地址在Nios II SBT中用Generate BSP时把程序运行区间设置到RAM但编译结果生成.elf文件后使用Quartus的Convert Programming Files工具把ELF转成.hex再烧到EPCS中。这一步是我见过课程设计最常翻车的地方很多人做完下载实验就交了结果答辩时一断电就露馅。建议从一开始就把reset vector配置好哪怕临时不固化也避免后面返工。6. 项目收尾经验与后续扩展做完这个项目我对FPGA软核处理器的认知发生了很大转变。以前总觉得FPGA就是拿来跑并行算法的比如图像处理、FIR滤波、卡尔曼滤波这些高吞吐任务CPU在里面反而是异物。但NIOS II电子钟让我看到FPGA的真正价值不是排斥CPU而是把CPU当作一个灵活的外设——当你需要跑一个复杂的交互协议、管理一个文件系统、处理一段网络报文时硬核逻辑可能极其痛苦而CPU几行代码就搞定了。后续扩展方向我列几个自己感兴趣的路子增加温度传感器DS18B20单总线驱动在Verilog里写非常麻烦但在NIOS II里可以照着时序图写一个位操作驱动的C函数配合PIO引脚就能读温度扩展显示到数码管。RS485校时电子钟挂在教室场景里通常需要从主机获取统一时间。用NIOS II的UART外设接收广播帧解析时间戳并校准本地时间比纯Verilog实现简单太多。升级成Nios II 自定义硬件加速把FIFO、DMA等硬件模块接入Avalon总线让软核把高频任务卸载给FPGA这就是SoC设计的基本雏形了。放到ZYNQ平台跑Linux动态加载和ZYNQ的ARM硬核相比NIOS II只是一颗理解软件思维的软核但理解了NIOS II再去看ZYNQ Linux动态加载FPGA、AXI总线通信会觉得整个体系都是相通的。最后给一个实操建议如果你用的是Quartus Prime新版记得把Platform Designer生成的BSP源码路径存放在不含中文和空格的目录里否则Nios II SBT编译时可能会出现莫名其妙的中断连接错误。这个坑虽然不影响最终结果但排查起来非常浪费时间。电子钟虽然小但它把FPGA开发的完整闭环走通了从RTL、总线、外设、中断、CPU、软件、调试、固化一条链路全流程体验一遍后面再做大项目心里就有底了。本文还有配套的精品资源点击获取
返回列表