简介:面向FPGA与嵌入式学习者,这套基于Altera DE2开发板的OC8051点灯实验资源,完整演示了如何在DE2硬件平台上集成可综合的8051软核处理器,并通过自带LED测试程序验证运行效果。整个压缩包共1042个文件、约19.07MB,以完整的Quartus工程为主体,包含大量cdb、hdb、tdf等工程数据库与设计文件,同时有v、vhd源码文件、mif/hex存储器初始化文件,以及asm汇编示例,能清晰对应FPGA开发的完整流程。配套说明文档详细讲解了修改OC8051 ROM的具体方法,并给出可初始化ROM的LED测试程序,下载到DE2后无需额外改动即可直接点灯;文档同时涵盖Quartus工程操作思路,便于对照练习。该资源包已有1026人学习,工程结构完整、步骤可跟做,适合课程设计、毕业设计或自学OC8051处理器核移植、ROM配置与DE2开发流程的初学者和进阶者,是研究软核CPU在FPGA上运行的实用参考。
1. 这块板子和这个软核,到底能折腾出什么
最近把柜子里那块老DE2板又翻出来了。DE2用的是Altera Cyclone II系列FPGA,具体型号EP2C35F672C6,板上最显眼的是一排拨码开关、一排红色LED、一排绿色LED,外加一个50MHz有源晶振和板载USB Blaster下载器。这块板子在很多高校的数字逻辑课上都出现过,但大多数时候只是跑跑现成的VGA、数码管演示Demo。我这次想让它干点"正经事":把OpenCores上的开源8051软核OC8051跑起来,在上面运行一个最经典的点灯程序。
OC8051是一套用Verilog写的、兼容Intel 8051指令集的软核源码。它的意义不在于性能——标准8051的一个机器周期是12个时钟周期,跑在50MHz下也就4MIPS出头——而在于全套RTL都是开放的,指令译码、ALU、取指单元、存储器接口这些部件清清楚楚摆在眼前。点灯程序虽然只有几行代码,但它是嵌入式世界里名副其实的"Hello World":CPU能正常复位、能从ROM里取到指令、能完成加法跳转、能把数据写到SFR寄存器、IO引脚能翻转。任何一环出了问题,灯都不会按预期闪,问题范围也被天然缩小了。
DE2这块板子对OC8051来说算是"大材小用"了。整个8051软核加上8KB程序ROM、256字节数据RAM,总共消耗的资源在Cyclone II上只是九牛一毛。但正因为DE2资源充裕,初学者可以完全不用操心资源优化,专心把CPU运行的链路搞明白。而且DE2板载的USB Blaster自带JTAG下载,不需要额外买下载器,插上USB线就能干活。唯一的麻烦是Cyclone II这个器件太老,新版本Quartus已经不收了,这个后面专门说。
做这个项目之前,建议你先明确目标:不是要做一个能用的单片机替代品,而是通过最小系统理解"一条C语句是怎么变成FPGA内部的一堆信号翻转"的。理解了这一层,后面再去玩Nios II、RISC-V软核,或者去调总线时序,都会顺很多。这个项目适合三类人:学过Verilog但没跑过软核的学生、想搞懂8051内部结构的嵌入式爱好者、以及手里正好有一块DE2吃灰想再利用的工程师。
2. 系统架构与工具链选型
2.1 一个最小的OC8051系统由什么构成
OC8051从OpenCores下载下来后,源码里其实已经带了一个可仿真的最小系统。从设计思路上看,它把CPU核心和存储器分开了:oc8051_top是核心,oc8051_rom_top是程序存储器,oc8051_ram_top是数据存储器加特殊功能寄存器。ROM和RAM都挂在核心的存储器总线上,通过片选信号区分访问对象。这个结构和真实8051的哈佛结构一致——程序从ROM里取,数据在RAM里读写,两者地址空间独立。
对于FPGA实现来说,这套结构最大的好处是ROM和RAM都可以直接映射到Cyclone II内部的M4K块存储器上。8051的架构决定了它不需要外部扩展总线,只要把核心顶层实例化,再接上这个板子上的LED就能跑。写到P1口的指令,最终会通过内部存储器总线送到oc8051_ram_top里的SFR模块,更新P1锁存器,然后把p1_out信号引到FPGA引脚上,LED就亮了。整个过程不需要额外的总线桥、不需要地址译码逻辑,清清爽爽。
2.2 工具链的坑:Quartus版本和SDCC选型
先说最影响心情的一个坑:Cyclone II器件在Quartus Prime 18.0之后就被彻底移除了,新装的正版软件里根本找不到EP2C35这个器件。老老实实用Quartus II 13.0sp1,这是支持Cyclone II的最后一个靠谱版本,网上到处都有下载,装完也不需要额外授权,Web Edition就够用。你要是手头还有更老版本的Quartus 9.1,也能用,但13.0sp1对USB Blaster驱动的适配更好,建议直接上13.0sp1。
固件编译工具我选了SDCC,它是开源的8051 C编译器,命令行方式工作,配合Makefile可以自动化编译。有人会问为什么不用Keil C51,原因很简单:Keil的免费评估版有2KB代码限制,虽然点灯程序不到2KB,但后面一旦加功能就麻烦,而且Keil在Linux和macOS下用起来费劲。SDCC对标准8051的支持很完整,P0到P3、各种SFR在头文件里都定义好了,编译点灯程序毫无压力。下载器直接用DE2板载的USB Blaster,Quartus Programmer里选JTAG模式就能识别。
2.3 程序放哪、数据放哪、SFR怎么处理
存储规划是整个设计的重点。程序存储器我做了8KB,16位地址线只用低13位,复位向量0x0000放第一条跳转指令,这样SDCC编译出来的固件可以直接加载。数据存储器就是标准8051的256字节内部RAM,地址0x00到0x7F是通用RAM,0x80到0xFF是SFR区,OC8051的oc8051_ram_top模块已经把这两个区域统一管理了,写P1口本质上就是往地址0x90写一个字节。
这里有个细节值得注意:SDCC默认会把全局变量放在内部RAM的data段,点灯程序里那几个循环变量、模式表数组,占用的空间很小,256字节绰绰有余。但如果程序里有比较长的字符串或者大数组,就要考虑用code关键字把它放到程序ROM里,或者启用外部XRAM。点灯这关还用不到XRAM,不过理解了这个存储模型,后面做串口打印、跑复杂算法时就不会一头雾水。
3. 完整实操:从固件到下载的全流程
3.1 第一步:写出点灯C程序并用SDCC编译
先写固件。最简单的方式是让P1口输出交替翻转,LED全亮全灭,间隔靠软件延时。代码如下:
#include <8051.h> void delay(void) { unsigned int i; for (i = 0; i < 50000; i++) ; } void main(void) { P1 = 0x00; while (1) { P1 = 0xFF; delay(); P1 = 0x00; delay(); } }编译命令很简单:
sdcc -mcs51 --code-size 8192 -o blink.hex blink.c编译完成后会生成blink.hex,这就是要加载到FPGA内部ROM里的固件。SDCC生成的hex是Intel HEX格式,Quartus的ROM初始化可以直接认。有一点要注意:SDCC会自动链接一段启动代码,在地址0x0000处放一条跳转指令,然后跳到C语言初始化代码,所以你不必自己写启动汇编。
为了效果更好看一点,我把固件改成流水灯。这里用了一个code数组,让它在查表时走程序存储器的MOVC指令,顺便验证ROM访问没有问题:
#include <8051.h> static unsigned char code pattern[8] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 }; void delay(unsigned int n) { unsigned int i; while (n--) { for (i = 0; i < 2000; i++) ; } } void main(void) { unsigned char idx = 0; while (1) { P1 = pattern[idx]; idx = (idx + 1) & 0x07; delay(30); } }提示:
code关键字在SDCC里表示把数据放到程序ROM空间。点灯程序虽然不用它也能跑,但加上之后能验证OC8051的MOVC取指路径是否正常,这个能力后面做字符点阵、查表运算时很关键。
3.2 第二步:搭Quartus工程、接顶层模块
打开Quartus II 13.0sp1,新建工程,器件选Cyclone II下的EP2C35F672C6。把OC8051源码里的oc8051_top.v、oc8051_fetch.v、oc8051_decoder.v、oc8051_exec.v、oc8051_alu.v、oc8051_ram_top.v加进工程。注意源码里可能有include全局宏定义文件,比如oc8051_timescale.v,一并加进来,否则编译会报一堆找不到宏的错。
顶层模块我写了这样一个de2_top.v,例化CPU核心、RAM,并用Quartus的ROM IP例化程序存储器:
module de2_top( input wire CLOCK_50, input wire [3:0] KEY, output wire [7:0] LEDR ); wire reset_n; wire [7:0] p1_out; wire [15:0] mem_addr; wire [7:0] mem_data_w; wire [7:0] mem_data_r; wire mem_wr, mem_rd; assign reset_n = KEY[0]; assign LEDR = p1_out; oc8051_top u_cpu ( .clk (CLOCK_50), .rst (~reset_n), .p1_out (p1_out), .p0_in (8'hFF), .p1_in (8'hFF), .mem_addr(mem_addr), .mem_data_w(mem_data_w), .mem_data_r(mem_data_r), .mem_wr (mem_wr), .mem_rd (mem_rd) ); oc8051_ram_top u_ram ( .clk (CLOCK_50), .addr (mem_addr[7:0]), .data_w (mem_data_w), .data_r (mem_data_r), .wr (mem_wr), .rd (mem_rd) ); endmodule不同的OC8051版本端口名可能略有差异,比如存储器总线上可能还有mem_cs、mem_oen之类的信号,遇到报错就对照源码顶层文件把端口名改过来即可。核心的几个信号就是地址、写数据、读数据、写使能、读使能,理解了这些,改端口不是难事。
3.3 第三步:ROM初始化文件处理
这是整个项目里最容易卡住的一步。OC8051参考设计里的oc8051_rom_top是一个用case语句穷举地址的组合逻辑ROM,直接拿它来用的话,得把hex转成Verilog case语句文件,程序一大编译会非常慢,而且调试时要反复重新生成,烦得很。
我的做法是:不使用参考设计里的rom_top,改用一个由MegaWizard生成的altsyncram只读存储器IP,容量设成8192字节,初始化文件直接指定第3.1步编译出来的blink.hex。这样Quartus会把hex内容直接烧进M4K块存储器的初始化数据里,干净利落。
关键配置只有一个:在MegaWizard里把"Read Output"的寄存器选项关掉,让ROM输出是组合逻辑直通。如果开了输出寄存,读数据会晚一个时钟周期,OC8051的取指流水节奏就对不上了,程序要么乱跑要么直接卡死。Cyclone II的M4K块存储器组合读出时间大约在7到11纳秒,50MHz时钟周期是20纳秒,时序上没问题,但为了保险起见,我在时序约束里把CLOCK_50约束为20ns,并且对存储器读数据路径做了set_max_delay检查,实测下来非常稳。
如果不用MegaWizard,也可以用Verilog直接写一个带$readmemh的ROM模块,但要注意$readmemh加载的是纯十六进制文本,不是Intel HEX格式,得先用脚本把blink.hex转成每行一个字节的mem文件。相比之下,altsyncram直接认Intel HEX,省一步转换,强烈推荐。
3.4 第四步:引脚约束、时序约束与编译下载
引脚约束我直接在Quartus的Assignments菜单里做的,也可以改qsf文件。DE2板卡手册里有全部引脚对照表,我这次用到的引脚分别是:
| 信号 | DE2引脚 | 说明 |
|---|---|---|
| CLOCK_50 | PIN_N2 | 板载50MHz晶振 |
| KEY[0] | PIN_G26 | 复位按键,按下为低 |
| LEDR[0] | PIN_AE23 | 红色LED 0 |
| LEDR[1] | PIN_AF23 | 红色LED 1 |
| LEDR[2] | PIN_AB21 | 红色LED 2 |
| LEDR[3] | PIN_AC22 | 红色LED 3 |
| LEDR[4] | PIN_AD22 | 红色LED 4 |
| LEDR[5] | PIN_AD23 | 红色LED 5 |
| LEDR[6] | PIN_AD24 | 红色LED 6 |
| LEDR[7] | PIN_AE25 | 红色LED 7 |
时序约束只用加一条时钟约束,新建一个Synopsys Design Constraints文件:
create_clock -period 20.000 -name clk [get_ports {CLOCK_50}]然后全编译。编译完成后,打开Programmer,选择USB Blaster,Mode选JTAG,加载生成的.sof文件,点击Start下载。DE2板载的USB Blaster驱动在Quartus II 13.0sp1里是自动识别的,插上USB线就能看到设备。
注意:下载完后FPGA先按复位键再松手,顺序不能反。我一开始下载完没按复位,灯完全不亮,还以为是ROM加载失败,排查了半天才发现是复位信号一直处于复位状态。KEY按下是低电平,顶层里rst接的是
~reset_n,所以按下按键等于拉高复位。
4. 调试实录:常见问题与排查方法
4.1 问题速查表
跑这个项目过程中,我自己踩过、也帮别人排查过不少问题,整理成一张速查表,建议截图存一份:
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 下载后灯完全不亮 | 复位没释放、固件没加载、引脚没约束 | 按一下KEY[0]复位;检查alignsncram的init文件路径;核对qsf引脚 |
| 灯常亮不闪烁 | 延时循环被优化掉或ROM里取的指令异常 | 检查SDCC优化等级,volatile变量;仿真看PC值 |
| 灯乱闪、没有规律 | ROM读时序不匹配 | 确认altsyncram没开输出寄存器;降低时钟频率到25MHz试 |
| 编译报找不到宏 | 缺oc8051_timescale等全局文件 | 把源码目录下所有.v文件都加进工程 |
| 编译报非法引脚 | 引脚名写错或qsf重复约束 | 对照DE2手册重新核对引脚名 |
| Quartus找不到器件 | Quartus版本太新 | 换Quartus II 13.0sp1 |
4.2 几个印象深刻的坑与解决思路
第一个坑是ROM输出寄存器问题。我第一次图省事,MegaWizard里保留了默认的输出寄存器,结果下载后LED是全灭的,用SignalTap抓信号才发现ROM读出来的数据永远慢一拍,8051的取指状态机彻底错乱。这事的教训是:使用任何一个现成的存储器IP,必须先搞清楚它的读延迟是几个周期,不能想当然。
第二个坑是SDCC的优化把延时函数里的循环变成了空操作。我最初用的是for(i = 0; i < 50000; i++);,开启--opt-code-speed之后SDCC居然把整个循环干掉了,灯变成常亮。解决办法是把延时参数改成volatile,或者在循环体里加一个空语句。点灯程序本身无所谓,但这个坑在以后写驱动时经常遇到,值得记住。
第三个坑是复位方向。DE2的KEY按下是低电平,这是板卡硬件决定的,但OC8051的复位是高有效,很多人第一次都栽在这。建议在顶层模块里把复位逻辑写得直白一点,加个注释说明按下按键等于复位,省得下次翻代码时还要重新想一遍。
4.3 一个能省掉一半调试时间的建议
上板之前,强烈建议先在ModelSim里把整个设计仿真一遍。OC8051源码自带testbench,你只需要把ROM的初始化数据换成自己编译的hex,然后跑波形,重点看三个信号:取指地址mem_addr、ROM读出的指令数据、以及P1端口的p1_out。如果仿真里p1_out能按预期翻转,那硬件部分大概率没问题,上板之后就算灯不亮也是引脚约束或者复位的问题,排查范围一下就缩小了。
ModelSim和Quartus II 13.0sp1的联动是现成的,在Quartus里可以直接启动RTL Simulation。仿真速度很快,点灯程序跑个几百微秒就够观察好几个翻转周期了。我自己的习惯是,仿真过了再上板,上板只看两个东西:灯闪不闪、闪的节奏对不对。这样做的效率最高。
5. 如果想让效果更直观,可以做的小扩展
点灯跑通了之后,别急着收工。DE2板上还有数码管、拨码开关、蜂鸣器,稍微扩展一下就能变成一个更完整的"软核外设实验平台"。最简单的扩展方式是把SW拨码开关接到OC8051的P0口,把LED输出改成根据拨码状态点亮对应位,这样就验证了输入回路的兼容性——8051的IO口是准双向口,读引脚之前要先写1,这个细节在OC8051里同样存在,值得亲手试一下。
再进一步,可以把P1接到DE2的7段数码管译码器上,让CPU在循环里刷新数字显示。这需要写一个查表译码器,把0到9的段码表用code数组放在ROM里,正好复习一遍MOVC查表操作。运行效果比LED流水灯醒目得多,在实验室里展示也更像样。
我个人在实际调试中的体会是:OC8051这类开源软核最大的价值不是"能跑",而是"能拆开看"。点灯程序跑到这里,硬核CPU里那些抽象的概念——取指、译码、写寄存器、查表取数——全都变成了你在QuestaSim里能逐周期追踪的波形。接下来不管你是想给它加串口、加定时器,还是干脆换成RISC-V软核再玩一遍,思路都是相通的:先把最小系统跑通,再一个模块一个模块往上加,每加一个都回到点灯这个基准测试确认没有把原来的功能弄坏。最后再分享一个小技巧:把工程里用到的所有文件、脚本、hex固件归档到一个目录,写个README记下Quartus版本和SDCC编译命令。这看起来是小事,但三个月后你回头再想跑这个工程时,这套记录能帮你省下整整一个下午。
本文还有配套的精品资源,点击获取