简介:面向FPGA学习者与数字逻辑课程设计者,这份压缩包提供了基于Altera DE2开发板的OC8051软核处理器点灯实验完整方案,解决如何将开源8051核移植到DE2并驱动LED的问题。压缩包共1042个文件,以Quartus工程文件(.qpf、.qsf)、Verilog/VHDL源码(.v、.vhd)、TDF文件、MIF初始化文件及SOF/POF配置文件为主,同时包含大量仿真波形与工程备份文件,整体约19.07MB。已有1026人学习下载。包内资源包括详细说明文档与完整Quartus工程,文档专门介绍了OC8051内部ROM的修改方法,并附带一段LED灯测试程序,可通过该程序初始化ROM,使OC8051在DE2上直接运行点灯任务。读者不仅能快速复现实验,还能借此理解CPU软核的存储器初始化流程与DE2平台外设接线方式,适合作为FPGA入门或计算机组成原理课程实验的参考。
1. 为什么放着Nios II不用,非要在DE2里塞一颗8051
看到“DE2上使用OC8051运行点灯程序”这个标题,先别急着觉得复古。我最初在DE2上折腾OC8051,起因很朴素:手里那块Cyclone II开发板在吃灰,丢掉又可惜,真去跑Nios II,又得开SOPC Builder、配置Qsys,一堆自动生成的东西像个黑盒。点灯虽然简单,但我想看的,是“CPU取指、译码、写寄存器”这套流程真的在一块FPGA里跑起来。
OC8051是OpenCores社区里一个用Verilog实现的8051兼容软核。它没有Cache,没有分支预测,甚至不少指令要多个周期才能执行完,但它有一个很实在的优点:DE2上的Cyclone II EP2C35塞下它绰绰有余,综合完只占几千个LE。C代码里那个P1寄存器、0x90这个SFR地址,全都看得见摸得着。跑一遍“点灯”,等于是把计算机组成原理课上那些概念,亲手在硬件里验证了一遍。
1.1 软核不是“软件”,而是用逻辑拼出来的CPU
软核软核,很多人听到这个词就以为是一个跑在FPGA上的软件程序,实际不是这么回事。软核是一段硬件描述语言代码,综合之后会变成FPGA里的查找表、触发器和布线资源。你可以把FPGA看作一块毛坯房,软核就是你自己用砖头砌出来的一个小户型。而DE2板上那颗Cyclone II,就是这块毛坯房的地基。
OC8051这类8位软核,放在今天性能当然不够看,但它足够简单、足够透明。打开oc8051_decoder.v,你能看到一条MOV指令是怎么被译码的;打开oc8051_sfr.v,你能看到特殊功能寄存器是怎么被读写的。这种透明感,是Nios II给不了的。Nios II当然更强大,但它的生成流程会替你包一层“封装”,你想看内部逻辑,得先去翻那一堆自动生成的BSP代码。
1.2 为什么偏偏是OC8051,它到底能干什么
8051是很多人学习单片机的启蒙架构,教材多、参考资料多、C编译器成熟。OC8051的作用,是把这颗老芯片重新用Verilog“翻译”了一遍,于是你可以在任何一块足够大的FPGA里,随时重建一个8051环境。对DE2来说,最经典的入门验证就是点灯:让CPU启动,从ROM里取出第一条指令,执行对P1口的写操作,LED亮起来,证明整条链路是通的。
这个项目适合两类人。一类是学FPGA但一直没搞懂CPU内部结构的人,另一类是学嵌入式想看看“8051在硬件层面是什么样子”的人。知识点虽然不新,但它的价值在于动手:从RTL源码、C代码、编译链到上板验证,整个过程没有黑盒。
2. 开工前的三件事:Quartus版本、源码清点、编译链装齐
OC8051这个项目不太挑工具,但DE2这块板子很挑版本。Cyclone II是Altera很早的器件,Quartus新版本早就把它踢出支持列表了。我建议直接装Quartus II 13.0sp1,这是最后一个完整支持Cyclone II的版本,既能写Verilog,也能用SignalTap II调试,整个流程不用来回切换工具。版本选错,后面连器件型号都选不到,更别说综合下载了。
2.1 Quartus版本与USB-Blaster驱动
Quartus II 13.0sp1是2013年左右的版本,安装包比较大,但好在稳定。装完之后第一件事是确认USB-Blaster驱动是否识别。DE2板上的下载器就是USB-Blaster,如果驱动没装好,Programmer窗口里永远是空的。Windows系统下可以在设备管理器里手动指定驱动路径,路径一般在Quartus安装目录下的drivers文件夹里。这块不算难,但它是整个流程里最容易让人卡在第一步的地方。
2.2 OC8051源码的清点与版本差异
从OpenCores官网下载oc8051工程包,解压后不要急着往Quartus里拖,先把目录结构看清楚。常见会有doc、rtl、bench这样几个目录,核心Verilog文件一般在rtl/verilog下面,典型包括oc8051_top.v、oc8051_alu.v、oc8051_control.v、oc8051_decoder.v、oc8051_ram.v、oc8051_rom.v、oc8051_sfr.v这些。不同版本文件命名略有差异,但主体结构差不多。
我特别想提醒一件事:OC8051这个项目已经很多年没怎么维护了,代码风格偏老,有些仿真专用的文件在上板综合时根本用不到,比如原本用于测试的ROM模型、带$fopen/$display语句的仿真文件。建工程的时候只添加核心RTL文件,把bench目录整个跳过。否则仿真文件里的系统函数会在综合时报错或者被Quartus当成普通逻辑处理,白白增加工作量。
2.3 8051编译链:SDCC还是Keil C51
点灯程序很小,选哪个编译器都行。SDCC是完全免费的,Linux下sudo apt install sdcc一条命令装好,Windows下也有安装包。Keil C51是老牌商业工具,工程配置比较顺手,Keil生成HEX文件需要在Options for Target的Output选项卡里勾选Create HEX File。
我个人更推荐SDCC,因为它命令行干净,编译点灯程序这种小工程不需要图形界面,而且最后生成的Intel HEX格式和Quartus的初始化文件能很好配合。SDCC编译方式也简单,后面章节我会直接给出命令。
3. 程序怎么进FPGA:HEX与片上ROM的完整关系
很多第一次接触软核的人会卡在这里:C代码编译出来的HEX文件,到底怎么变成FPGA里的程序?这得从8051的存储器结构说起。8051是典型的哈佛结构,指令放在程序存储器ROM里,数据放在数据存储器RAM里,两者分开。OC8051取指时,会用16位的rom_addr信号去ROM对应地址读一个字节回来,交给译码器处理。
3.1 程序ROM的深度:别一上来就建64KB
点灯程序编译出来的HEX通常只有几百字节,但8051的地址总线是16位的,理论寻址空间是64KB。新手最容易犯的错,就是在Quartus里直接建一个64KB的ROM——然后资源直接爆炸。DE2上的Cyclone II EP2C35,内部块RAM总共也就一百个左右M4K,一个M4K是4Kb,换算下来,64KB的8位ROM大约需要一百多个M4K,根本放不下。
正确做法是先看HEX文件的最后一条有效记录地址。比如程序实际只到0x01FF,那就建深度2048的ROM,8位数据总线,总共只占4个M4K。点灯程序留出2KB空间已经非常宽裕,以后想加点复杂的逻辑也有余量。
3.2 两种加载方式:MIF文件与Verilog $readmemh
把HEX变成ROM初始化内容,有两条路。第一条是用Quartus的IP Catalog生成一个ROM核,选择单端口ROM,位宽8、深度2048,然后在初始化文件那里加载MIF文件。MIF文件是Quartus自己的存储器初始化格式,长这样:
WIDTH=8; DEPTH=2048; ADDRESS_RADIX=HEX; DATA_RADIX=HEX; CONTENT BEGIN 0000:02; 0001:01; 0002:02; END;第二种方式更省事,直接用Verilog内嵌一个二维reg数组,配合$readmemh把HEX读进去:
reg [7:0] rom [0:2047]; initial $readmemh("led.hex", rom);Quartus对这个写法支持得不错,综合时会自动把rom数组推断成块RAM。需要注意$readmemh的路径问题,最好把led.hex放在工程根目录,路径不对综合不会报错,但下载到板上程序是空的。
3.3 Intel HEX转MIF的一个小脚本思路
如果坚持用MIF方式,又不想手工敲每一行的数据,可以写个Python小脚本做转换。解析Intel HEX的核心逻辑并不复杂:每行以冒号开头,后面是长度、地址、类型、数据和校验和。只需要收集类型为00的数据记录,按地址填入字典,最后输出成MIF格式就行。重点留意地址偏移,HEX里的地址就是最终ROM地址,不需要再额外加偏移,但如果你用了带起始地址的链接脚本,一定要确认生成HEX的基地址是不是0x0000。
4. 把点灯程序接到DE2的LED:SFR映射、顶层模块与引脚约束
程序能进ROM了,接下来就是软核上板最关键的部分:CPU写P1口,FPGA怎么知道该把哪个LED点亮。
4.1 先写一个最精简的C点灯程序
用SDCC写点灯程序,头文件是8051.h,代码很短:
#include <8051.h> void delay(unsigned int t) { unsigned int i; while (t--) { for (i = 0; i < 500; i++); } } void main(void) { unsigned char v = 0x01; while (1) { P1 = v; delay(50); if (v == 0x80) v = 0x01; else v <<= 1; } }这段代码逻辑很简单:P1口循环输出0x01、0x02、0x04……到0x80,实现流水灯效果。编译命令:
sdcc led.c packihx led.ihx > led.hex生成led.hex后,可以文本打开看一眼,确认第一行不是全零,说明程序内容已经生成。注意8051.h里已经声明了P1这个sfr寄存器,所以编译期不会出问题,真正要花心思的是硬件部分怎么把这个P1写操作“接”出来。
4.2 方案一:从SFR总线里把P1口抠出来
OC8051内部访问SFR时,会有一组和SFR读写相关的控制信号,不同版本信号名有差异,常见的是sfr_wr、sfr_addr、sfr_data_out。P1口在8051里的SFR地址是0x90,你需要写一个小的地址译码模块,监听这组信号:
always @(posedge clk or negedge rst_n) begin if (!rst_n) led_r <= 8'h00; else if (sfr_wr && sfr_addr == 8'h90) led_r <= sfr_data_out; end不过有个现实问题:某些OC8051版本并没有把这组SFR访问信号完整引到oc8051_top顶层,而是封装在oc8051_sfr.v内部。这时候要么去改源码把信号引出来,要么换方案二。我最早折腾的时候,就是被这个“信号没引出来”卡了一晚上。如果你下载的版本同样没有sfr_wr端口,不用纠结,直接看4.3。
4.3 方案二:走XDATA外部总线,更通用也更省事
我后来更推荐的方式是让C程序访问外部XDATA地址,在硬件端监听CPU的MOVX写总线。8051的外部数据总线一般有mem_wr、mem_addr、mem_data_out这样一组信号,OC8051为了支持外扩RAM,基本都会保留这组接口。
C代码改成这样:
#define LED_PORT (*(volatile unsigned char __xdata *)0x8000) void main(void) { unsigned char v = 0x01; while (1) { LED_PORT = v; delay(50); if (v == 0x80) v = 0x01; else v <<= 1; } }硬件端则监听地址0x8000上的写操作:
always @(posedge clk or negedge rst_n) begin if (!rst_n) led_r <= 8'h00; else if (mem_wr && mem_addr == 16'h8000) led_r <= mem_data_out; end这个方法的好处是,只要OC8051的MOVX指令能跑通,外部总线就一定能用。8051访问XDATA天然就是走这套接口的,你不再需要关心SFR总线到底有没有引出来。无论是SDCC还是Keil,对XDATA地址访问都有成熟支持,移植性很好。
4.4 顶层模块的关键片段
顶层模块里除了CPU和地址译码,还需要处理时钟和复位。我习惯先把50MHz分频到6MHz左右,降低时序压力;复位则用一个计数器做上电复位,避免按键复位的毛刺影响:
reg [2:0] clk_div; wire clk = clk_div[2]; always @(posedge clk_50m or negedge key0_n) begin if (!key0_n) clk_div <= 3'd0; else clk_div <= clk_div + 1'b1; end reg [19:0] rst_cnt; reg rst_n; always @(posedge clk or negedge key0_n) begin if (!key0_n) begin rst_cnt <= 20'd0; rst_n <= 1'b0; end else if (&rst_cnt) begin rst_n <= 1'b1; end else begin rst_cnt <= rst_cnt + 1'b1; rst_n <= 1'b0; end end引脚约束方面,DE2的时钟是CLOCK_50,复位可以用KEY0,LED就用LEDR。这些引脚的具体编号不用自己记,Quartus里通过Assignments菜单导入DE2官方Demo工程里的引脚分配文件,或者用Pin Planner手动点一遍。最忌讳的是自己瞎猜引脚号,压错一个pin,现象就是灯怎么都不亮,但你又不知道问题出在程序还是引脚。
5. 上板实测:灯不亮的排查顺序和几个隐蔽的坑
前面流程全部走通,下载SOF文件后,理论上LEDR0开始逐个点亮。如果灯没亮,先别急着怀疑代码,按下面这个顺序排查。
5.1 先隔离问题:不接CPU,先把LED链路调通
我个人的习惯是,第一次上板先不接OC8051,直接在顶层里写一行:
assign led_r = 8'b10101010;烧进去,看一下LED是不是亮成间隔模式。如果这一步都不对,说明引脚分配、下载链路、LED电路本身有问题,跟软核一毛钱关系没有。这一步做完,再换成CPU+ROM的完整设计,能省掉一大半的排查时间。
5.2 灯不亮的典型故障链路
如果固定值能亮,接上OC8051之后不亮,问题基本就锁在几个点:
第一,ROM初始化文件没加载进去。$readmemh的路径不对,或者MIF文件的地址范围小于HEX中最大地址,都会让CPU从0x0000取到的指令是空数据。可以用Quartus里的RTL Viewer查看ROM模块,确认是否有初始化标志。更彻底的办法是先用ModelSim跑一次行为仿真,直接看PC寄存器有没有走动。
第二,复位极性搞反了。OC8051的复位信号在不同版本里可能是高有效也可能是低有效,接反了CPU永远停在复位状态。看源码中复位逻辑,比对着改顶层。
第三,XDATA地址判断差了一位。比如C代码写0x8000,但顶层判断0x8001。这类问题隐蔽性很强,因为逻辑看起来完全正确,但8051的XDATA总线地址在时序上可能延后一拍或者经过锁存后变化。用SignalTap II抓一下mem_addr,一眼就能看出来。
第四,分频时钟没跑起来。DE2的50MHz时钟直接给OC8051理论上也能跑,但老代码时序余量不大,我遇到过一跑就乱、一降频就稳的情况。分频到6.25MHz之后,整个系统稳定得多,点灯这种程序完全不需要高速。
5.3 一个值得养成的调试习惯
最后分享一个我自己的习惯:在ROM里固定写一条跳转指令,比如地址0x0000处放一条LJMP 0x0100,然后用SignalTap II抓PC信号。如果复位后PC能从0x0000跳到0x0100,说明取指、ROM、复位全部正常。这时候再跑点灯程序,写不过去只能说明是C语言编译或者外设映射的问题。这个分步验证的思路,比直接烧完整点灯程序再瞎猜要省时间得多。
软核移植这件事,难点往往不是Verilog语法,而是“信号明明存在,但你没找到它,或者找错了位置”。OC8051这种老工程更是这样。把ROM、复位、时钟、地址四条线理清楚,点灯程序跑起来其实也就是一个下午的事。后续你还可以给它加串口、加定时器,甚至用AXI总线把它包起来当个协处理器玩,起点都是这份点灯工程。
本文还有配套的精品资源,点击获取