简介:一份基于SOPC的数字电子时钟课设完整工程资料,以DE2-115开发板为验证平台,使用Quartus II与NIOS II软件环境,硬件语言采用Verilog,面向电子信息类本科生FPGA课程设计与SOPC项目实践。系统实现了数码管、LCD、VGA大屏三种显示方式,支持时钟、倒计时、闹钟功能,计时精度达到1秒,可在软件中直接运行。资料内含课设报告与完整可运行工程,方便对照学习和验证。资源共4175个文件,压缩包约50.29MB,核心文件包括Verilog源文件(.v)、Quartus工程配置文件(.qpf/.qsf)、SOPC系统描述文件、NIOS II软件C/C++代码、FPGA配置文件(.sof)、存储器初始化文件(.mif)以及课设报告(.docx),同时保留了编译中间文件,便于还原完整工程结构。目前已有1793人学习/下载。借助该工程可系统学习SOPC系统集成、Verilog逻辑设计、NIOS II软核处理器编程以及数码管、LCD、VGA显示驱动的整体实现思路,涵盖从系统搭建、软核配置到显示驱动调试的完整流程,对完成同类课设和入门FPGA开发有直接参考价值。 看到“基于SOPC的数字电子时钟设计.rar”这个标题,我第一反应就是:又有人在为FPGA课设和电子设计竞赛挠头了。这个项目在高校的EDA/FPGA课程里算是常青树,网上资源一大把,但真正把SOPC方案讲透的并不多。SOPC全称System On Programmable Chip,通俗讲就是把CPU、存储器、定时器、I/O接口这些原本摆在电路板上的东西,一股脑塞进一颗FPGA芯片里,然后用C语言写应用逻辑,而不是拿Verilog/VHDL去堆状态机。本篇就以Nios II软核处理器加Qsys工具的方案,把这个数字电子时钟从需求拆解、硬件搭建、软件实现到调试踩坑完整走一遍。
1. 数字电子时钟的需求拆解与SOPC方案选型
1.1 课设项目到底在考什么
数字电子时钟听着简单,实际拆开来看,它至少包含了三个相互独立又需要协同的功能模块:
- 计时功能:时、分、秒的实时递增,满60进1、满24归零。
- 显示功能:至少6位数字管(时/分/秒各两位),需要解决动态扫描刷新问题。
- 人机交互:按键校时,至少要有“模式切换”和“加一”两个按键。
如果只用纯逻辑去实现,这三件事都得写进RTL代码里。时间计数用分频器和计数器没问题,但“校时”这种交互逻辑用状态机写起来会比较啰嗦,尤其是“长按连续加”这类人性化功能,纯状态机实现会相当痛苦。SOPC方案把“控制逻辑”交给Nios II软核处理器,用C语言写状态机、写按键消抖、写扫描刷新,开发效率完全不在一个量级。
1.2 为什么选Nios II软核而不是纯Verilog
我见过不少同学纠结要不要上软核,觉得多此一举。这里直接说结论:题目明确写了SOPC,那就绕不开软核方案。从学习角度讲,SOPC设计要掌握的技能更多,包括Qsys组件配置、总线互联、外设地址映射、中断服务程序编写、软件驱动开发,这些恰好是“嵌入式+FPGA”两条线的交叉点。
从实际工作量讲,纯HDL方案的时间计数和消抖逻辑都不难,难的是后续如果还要加闹钟、加秒表、加温度显示,纯逻辑代码量会像滚雪球一样膨胀。而基于Nios II的软件架构下,新增一个功能往往就是新增一个C函数的事。使用软核还能用上串口打印调试,这对找bug的价值极大——用SignalTap去抠按键抖动问题,比对着一行printf找问题慢多了。
1.3 系统总体框架
整个系统的数据流大概是这样的:
FPGA内部的Nios II软核通过Avalon总线挂接若干外设。一个Interval Timer定时器作为系统心跳,产生固定周期中断,驱动时间计数与显示刷新;一组PIO输出端口控制数码管的段码和位选;一组PIO输入端口接三个按键,用于模式切换和数值调整。外部时钟源由开发板上的50MHz有源晶振提供,经PLL倍频或直接作为系统时钟。时间信息保存在软件全局变量中,由中断服务程序更新,主循环负责响应按键并刷新显示。
2. 硬件环境准备与Qsys组件配置要点
2.1 开发板选型与工程建立
SOPC实验普遍使用Altera/Intel系列的Cyclone IV芯片,比如EP4CE6F17C8这种经典型号。很多学校的口袋实验板、DE2-115开发板都是这类芯片。初期设计无所谓具体板子型号,因为Nios II软核本身就是“可裁剪”的,板子上缺什么外设,我就在软核里加什么IP,硬件接口反而不像单片机选型那样锁死。
工程建议直接使用Quartus II(13.0以上版本就够用)建一个空的顶层文件,再打开Qsys(老版本叫SOPC Builder)搭建处理器子系统。顶层模块例化一个名为cpu_system的Qsys生成模块即可。这里有个新手经常踩的坑:Quartus的顶层文件名必须与工程名一致,否则综合会报错。
2.2 Qsys组件清单与参数配置
下面是我在Qsys里添加的组件及配置参数,直接抄即可:
| 组件名 | 实例名 | 关键参数 |
|---|---|---|
| Nios II Processor | cpu | Nios II/e经济型,HW乘法器可选,JTAG调试级别 |
| On-Chip Memory | ram | 64KB,类型RAM,初始化文件可留空 |
| Interval Timer | timer_0 | 全功能模式,计数器位宽32位,初始周期500000 |
| PIO(输出) | seg_out | 位宽8位,连接数码管段码 |
| PIO(输出) | sel_out | 位宽6位,连接数码管位选 |
| PIO(输入) | key_in | 位宽3位,输入捕捉,生成下降沿中断 |
| System ID | sysid | 默认即可 |
| UART(可选) | uart_debug | 波特率115200,用于printf调试 |
时钟方面,直接使用50MHz外部时钟接入clock桥,不加PLL也完全够用。Nios II/e软核在50MHz下跑这个任务毫无压力。定时器周期500000这个数字是有讲究的:50MHz时钟下计满500000个周期正好是1秒。但更合理的做法是让定时器产生10ms中断,用软件计数器累计100次得到1秒,好处是显示扫描和计时共用同一个中断源,逻辑更统一。我实际采用的是10ms方案,定时器周期值设为500000是1ms方案,若用10ms方案则设为5000000,注意32位计数器完全承载得了。
2.3 引脚分配与硬件连接
Qsys配置完成后要在Quartus里做引脚分配。数码管的段码一般是8位(含小数点),接开发板的SEG0到SEG7。位选只有6位,接数码管位选引脚。三个按键分别接入KEY0、KEY1、KEY2。
引脚分配有个容易出问题的点:很多开发板上的数码管位选经过了三极管或译码芯片驱动,这意味着软件输出的逻辑电平与实际“选中哪一位”之间可能不是简单对应关系。比如位选为高电平时,经反相驱动后实际点亮的是低电平的那一位。这种硬件极性差异直接看原理图最稳妥,否则显示顺序会乱掉。段码也有共阴共阳之分,共阴极数码管要输出高电平点亮段,共阳极则要输出低电平点亮段。段码表需要在初始化阶段确认好,否则数字显示会“缺胳膊少腿”。
3. 软件架构与核心代码实现
3.1 工程创建与BSP设置
在Quartus里完成全编译后,用Qsys导出的SOPC信息文件打开Nios II SBT for Eclipse。新建Software Application工程时选Blank Project,BSP设置里指定好硬件配置文件即可。如果用UART调试,记得在BSP的Linker Script里确认内存地址映射正确。工程里只需要一个main.c文件,代码量控制在400行以内,结构清晰。
BSP里有一项启动模式设置需要注意:如果最终要烧进FPGA配置芯片里独立运行,需要在BSP Settings里将stdout/stderr指向uart,启动loader选择epcs_flash_controller;如果只是临时下载RAM运行,保持默认就行。很多同学的板子断电后程序丢失,就是因为用的是RAM运行模式,代码没固化到EPCS里面。
3.2 中断服务程序与时间计数逻辑
时间计数是整个时钟的心脏,我的常用做法是维护一个包含时、分、秒三个字段的结构体,再加上一个毫秒级的累计变量,全部定义成volatile全局变量,因为中断和主循环都会访问它们。
定时器中断每10ms触发一次。中断服务程序里做三件事:毫秒计数值加10;当计数值累加满1000毫秒时执行一次秒递增;每次中断置一个display_update标志位,通知主循环刷新数码管。伪代码如下:
static volatile unsigned int tick_10ms = 0; static volatile unsigned char time_stamp = 0; /* bit0秒位变化标志 */ void timer_0_isr() { IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER_0_BASE, 0); tick_10ms += 10; if (tick_10ms >= 1000) { tick_10ms = 0; update_clock(); /* 秒+1并处理分、时进位 */ time_stamp = 1; } display_update = 1; } void update_clock() { sec++; if (sec >= 60) { sec = 0; min++; if (min >= 60) { min = 0; hour++; if (hour >= 24) hour = 0; } } }这段逻辑的关键点是sec、min、hour三个变量在中断里被修改,主循环读取显示时会可能读到“半更新”的状态。单字节读操作在Nios II上是原子的,所以只要数据结构简单,不用关中断也能安全访问。但如果把时间封装成结构体,读取时就需要临时关闭中断再读,否则可能出现秒加完分还没加的中间态。
3.3 数码管动态扫描与段码表
主循环里读取display_update标志位,然后执行一次完整的扫描刷新。动态扫描的原理是利用人眼视觉暂留,同一时刻只点亮一位数码管,轮流点亮6位,刷新频率大于50Hz就看不到闪烁。
扫描时钟由主循环循环执行构成,每个位保持约1到2ms。为了保证刷新率稳定,我在扫描函数里也做了简单的软件延时。段码表可以预定义成数组:
const unsigned char SEG_CODE[10] = { 0x3F, /* 0 */ 0x06, /* 1 */ 0x5B, /* 2 */ 0x4F, /* 3 */ 0x66, /* 4 */ 0x6D, /* 5 */ 0x7D, /* 6 */ 0x07, /* 7 */ 0x7F, /* 8 */ 0x6F /* 9 */ };扫描输出时把当前时间的每一位数字拆分出来映射成段码,再选中对应位选。需要注意扫描函数如果放在主循环里做软件延时,按键检测的实时性会受影响。我的做法是按键检测也放在扫描循环里,每次扫描间隙顺便扫一遍按键状态,这样自然就实现了分时复用,不需要额外调度器。
3.4 按键校时与消抖处理
三个按键的规划:KEY0为模式切换(正常/秒校/分校/时校四个状态),KEY1为加一,KEY2为减一或复位。按键输入接的是PIO输入口,读取时需消抖。软件消抖很简单:检测到低电平后延时20ms再确认一次,确认有效才执行操作。
校时状态切换的关键是“不要在按键按下时反复切换”,需要检测按键的下降沿而不是电平值。代码如下:
void key_scan() { unsigned char key_raw = IORD_ALTERA_AVALON_PIO_DATA(KEY_IN_BASE); static unsigned char key_last = 0xFF; unsigned char key_pressed = key_raw & ~key_last; key_last = key_raw; if (key_pressed & 0x01) { mode = (mode + 1) % 4; } if (key_pressed & 0x02) { if (mode == 1) { sec = 0; } else if (mode == 2) { min++; if (min >= 60) min = 0; } else if (mode == 3) { hour++; if (hour >= 24) hour = 0; } } }这里用key_pressed = key_raw & ~key_last来捕捉下降沿,既是按下瞬间有效,避免了长按导致的连续触发。延时消抖则依赖主循环的扫描节奏,实测下来效果稳定。如果要求更严格,可以把按键PIO配置成中断模式,在中断里做消抖,但对本设计来说轮询足够。
4. 常见问题与调试技巧实录
4.1 数码管显示乱码、缺笔画、亮度不均的排查
这类问题的概率最高。乱码基本是段码表极性写反了:共阳极板子用了共阴极段码。缺笔画则是段码值不对,比如数字6的0x7D和数字9的0x6F在市面上存在两种定义,必须参考板子的原理图确认。亮度不均通常是因为扫描周期太长或太短,单个位点亮时间最好控制在1毫秒左右,小于0.5毫秒亮度会明显偏低,大于3毫秒则能看到闪烁。
还有个大坑是位选扫描顺序和PCB布线顺序不一致。明明写了低位先扫,结果显示顺序是反的,这不是代码问题,是位选PIO的接线顺序可能与数码管物理位置不对应。用单个位轮流点亮的测试程序跑一遍,把实际点亮顺序记录下来,改数组下标匹配即可。
4.2 计时不准、越走越快的纠正方法
数字电子时钟的时间基准完全取决于定时器中断的精确度,而定时器又依赖系统时钟频率。开发板上的50MHz晶振虽然有精度误差,但短时间观察不出来,真正让时钟变快的大多是定时器周期设置错误。比如系统时钟50MHz,想要10ms中断,定时器计数周期应该是50,000,000 × 0.01 = 500,000,很多同学在这里少算了一个0,导致中断频率变成100Hz而不是1000Hz,时钟自然狂奔。
另外中断服务程序里的处理时间也会影响实际周期。Nios II/e处理一条分支跳转指令大约需要几十纳秒,相比10ms的中断周期可以忽略不计,但如果中断里调用了printf或者复杂的除法运算,开销就会变大。测量方法很简单:让一个PIO引脚在中断里翻转,用示波器或逻辑分析仪看翻转频率,能直接算出来实际中断周期是否标准。
4.3 下载程序后正常运行,断电重启程序消失
这个问题几乎每届学生都会遇到。程序正常下载能跑,说明逻辑和软件都没问题;掉电没程序,是因为你把程序下载到了FPGA的RAM里,RAM上电即失忆。要解决需要在Qsys里添加EPCS Serial Flash Controller,编译后通过Nios II的Flash Programmer工具将软件代码烧写进EPCS芯片。硬件配置里还要把FPGA的配置模式设为Active Serial。这个步骤比较繁琐,但做一次就能看到掉电保存的效果,建议提前准备好。
4.4 调试经验分享
整套系统里最好用的调试工具其实是串口。早期搭建时不要急着写数码管扫描,先把UART调通,用printf往电脑上打印时间计数值,确认进位逻辑正确后再写显示。串口打印的时间和数码管显示时间不一致时,很容易判断出是显示模块的问题还是计时模块的问题。中断里不要加printf,主循环里打印就好。
SignalTap II逻辑分析仪也很有用。出现按键失灵这类诡异问题时,拉出按键PIO的波形看看,能直观地看到按键抖动波形,从而判断是硬件电路问题还是软件消抖不足。实测发现,很多开发板的按键接口已经加了RC滤波电路,这种情况下10ms的软件消抖就足够,没必要做20ms,省下的时间让按键手感更跟手。
4.5 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 显示乱码 | 段码极性不匹配 | 确认共阴/共阳,改段码表 |
| 数字循环点亮但顺序错乱 | 位选接线顺序与软件扫描顺序不一致 | 单独位选测试程序,修正扫描数组 |
| 秒走时明显偏快 | 定时器周期计算错误 | 核对系统频率,用示波器验证中断频率 |
| 按按键无反应 | PIO位宽配置错误或消抖延时不足 | 检查Qsys里的PIO位宽,加大消抖延时 |
| 下载后掉电程序丢失 | 未烧写至EPCS | 添加EPCS控制器,使用Flash Programmer烧写 |
| 数码管亮度不均匀 | 扫描周期不一致 | 在扫描函数中加入固定延时,确保每位停留时间一致 |
5. 项目复盘与可扩展方向
做完这个项目,个人最深的感受是:SOPC设计真正的难点不在“写代码”,而在“搭系统”。从裸机逻辑设计思维切换到“硬件为软件服务”的思维,需要一段适应期。刚开始我习惯性地想在硬件里把所有显示逻辑做掉,后来才意识到,有了Nios II软核,大部分外设逻辑都可以软件化,硬件设计只需提供足够数量的PIO口、定时器和内存即可。这也正是SOPC的核心思想——硬件平台化,功能软件化。
实际测试中,Nios II/e内核主频50MHz跑这个应用绰绰有余,CPU占用率目测不到20%。如果把定时器中断、按键扫描、数码管刷新全部做成独立模块,后续扩展非常灵活。比如现在想加一个闹钟功能,只需要在软件里增加一个时间比较逻辑,让蜂鸣器PIO在匹配时翻转;想加秒表功能,就在定时器中断里维护一个独立的毫秒计数值;想加温度显示,加一个DS18B20的一线式驱动,同样是软件层面的事。整个系统的扩展成本低得惊人。
最后再分享一个小技巧:如果你手头有一块带SD卡槽或SDRAM的开发板,可以考虑把On-Chip Memory换成SDRAM控制器,再把Nios II升级成Nios II/s标准型,运行速度提升明显,跑简单操作系统都不成问题。数字电子时钟只是SOPC的入门敲门砖,套路熟悉之后,用它做智能小车、图像采集、网络通信这类更复杂的项目,才能真正体会到这套工具链的价值。
本文还有配套的精品资源,点击获取