简介:这份资源面向FPGA与嵌入式软核学习者,聚焦在Altera DE2开发板上运行OC8051单片机点灯程序这一实践场景。OC8051是开源的8051 IP核,资源中给出了修改后的OC8051源码,并编写了一段LED测试程序用于初始化OC8051的ROM,使下载到DE2后能直接运行点灯测试,文档中详细说明了ROM的修改方法,适合想打通软核移植与硬件验证链路的中级开发者。压缩包共1042个文件,约19.07MB,包含Quartus工程文件(qpf、qsf、sof、pof等)、Verilog与VHDL源码(v、vhd)、OC8051相关模块文件(asm、mif、hex)、综合与仿真报告(rpt、summary、vcd)以及大量中间缓存与备份文件,工程结构完整,便于直接打开复现。目前已有1027人学习下载,读者可借此掌握OC8051 ROM初始化、软核下载配置与LED验证的完整流程,并对照文档排查移植中的常见问题。
1. DE2 上使用 OC8051 运行点灯程序:从零跑通的第一手路径
很多人第一次拿到 DE2 开发板,第一反应是跑官方例程,但真要把一颗开源 8051 核(OC8051)塞进 FPGA 里点个灯,中间隔着的坑比想象中多。OC8051 是一颗用 Verilog 写的 8051 兼容软核,挂在 DE2 的 Cyclone II 上,本质上是你自己搭了一台“单片机”,只不过 CPU 是 FPGA 逻辑综合出来的。点灯程序本身只有几行汇编,但要让这颗核真正跑起来,你得搞定时钟、复位、存储器映射、程序加载四件事。这篇笔记面向已经会用 Quartus 建工程、但没把软核跑通过的人,从工程结构一路讲到下载验证,中间会给出可直接抄的顶层例化和 ROM 初始化脚本。跑通之后,你手里就有了一颗可改、可裁、可加外设的 8051,比买现成单片机更适合做体系结构实验。
2. OC8051 软核与 DE2 平台的对接逻辑
2.1 为什么选 OC8051 而不是现成 IP
在 FPGA 上跑 8051,常见做法有三种:买商业 IP、用厂商提供的软核、自己找开源核。商业 IP 授权贵且不透明,厂商软核往往绑定特定器件系列,而 OC8051 是纯 Verilog、结构清晰、指令集覆盖 8051 标准,适合拿来改。它的代码量不大,综合到 Cyclone II 上资源占用可控,DE2 的 EP2C35 足够放下核加片上 RAM。选它的另一个理由是存储器接口简单,程序和数据共用一条总线,点灯这种小实验不需要外挂 SDRAM,直接用片上 M4K 块搭 ROM 和 RAM 就行。如果你后续想加定时器、串口,OC8051 的源码里留了扩展点,改起来比黑盒 IP 舒服得多。
2.2 DE2 上必须接对的四组信号
OC8051 核对外暴露的信号可以归成四组:时钟与复位、程序存储器接口、数据存储器接口、I/O 端口。DE2 板载 50MHz 晶振,但 8051 核不一定能直接吃 50MHz,常见做法是先用 PLL 降到 12MHz 或 24MHz,再送给核。复位信号要同步化,直接拿按键做异步复位容易在释放时产生亚稳态,我一般加两级触发器同步。程序存储器接口接 ROM,数据存储器接 RAM,两者地址空间要分开译码,否则取指和读写数据会打架。I/O 端口里 P1 口拿来做点灯最方便,直接连到 DE2 的 LED 引脚,注意 LED 是低电平点亮还是高电平点亮,接反了程序跑对也看不见。
2.3 存储器映射:ROM 和 RAM 怎么分
8051 的存储器分程序空间和数据空间,OC8051 默认程序和数据分开编址。程序空间放你的点灯代码,数据空间放堆栈和变量。在 FPGA 里,这两块都用 M4K 块实现,但初始化方式不同:ROM 需要预加载机器码,RAM 上电后是随机的,靠程序自己初始化。常见映射是 ROM 从 0x0000 开始,大小 4KB;RAM 从 0x0000 开始,大小 1KB,两者地址重叠但总线分开,不会冲突。要注意 OC8051 取指时地址线是 16 位,而 M4K 深度有限,高位地址要参与片选译码,否则程序计数器跑飞会读到空区域。下面这张表是我在 DE2 上用的映射方案,可以直接参考。
| 空间 | 起始地址 | 大小 | 实现方式 | 用途 |
|---|---|---|---|---|
| 程序 ROM | 0x0000 | 4KB | M4K 初始化 | 存放点灯机器码 |
| 数据 RAM | 0x0000 | 1KB | M4K 不初始化 | 堆栈与变量 |
| P1 口 | 0x90 | 1 字节 | 顶层连线 | 驱动 LED |
| 复位向量 | 0x0000 | 3 字节 | ROM 首地址 | 跳转到主程序 |
3. 从汇编到比特流:点灯程序的完整落地步骤
3.1 写一段最小可用的点灯汇编
点灯程序的核心是往 P1 口循环写值,配合延时让肉眼能看到闪烁。下面这段汇编用定时器延时,不依赖外部晶振精度,适合第一次跑通。
; led_blink.a51 ; 假设晶振 12MHz,定时器 0 工作在模式 1 ORG 0000H LJMP MAIN ; 复位后跳转到主程序 ORG 0030H MAIN: MOV SP, #07H ; 堆栈指针设在内部 RAM 低端 MOV TMOD, #01H ; 定时器 0 模式 1,16 位定时 LOOP: MOV P1, #00H ; P1 口全低,LED 点亮(低电平有效) ACALL DELAY ; 延时约 500ms MOV P1, #0FFH ; P1 口全高,LED 熄灭 ACALL DELAY SJMP LOOP ; 无限循环 DELAY: MOV TH0, #0FCH ; 12MHz 下 1ms 定时初值高字节 MOV TL0, #018H ; 低字节 SETB TR0 ; 启动定时器 WAIT: JNB TF0, WAIT ; 等待溢出 CLR TF0 ; 清除溢出标志 RET ; 这里只延时 1ms,实际闪烁靠循环次数这段代码里MOV P1, #00H和#0FFH交替,对应 LED 全亮和全灭。延时子程序只做了 1ms,实际闪烁频率靠主循环里反复调用,如果你觉得闪太快,可以在DELAY返回前加一个计数器循环。注意ORG 0000H处必须放跳转指令,因为 8051 复位后从 0x0000 取指,中断向量表也在这附近,别把主程序直接写在 0x0000。
3.2 用汇编器生成 ROM 初始化文件
Quartus 的 M4K 初始化文件是.mif或.hex,你需要把汇编生成的机器码转成这种格式。常见工具链是 SDCC 或 Keil,但 Keil 输出的是 Intel HEX,Quartus 能直接读 HEX,不过要注意地址对齐。我一般用 SDCC 编译出.ihx,再用脚本转成.mif,这样可控性更强。
# 用 SDCC 编译 8051 汇编 sdcc -mmcs51 --code-loc 0x0000 --xram-loc 0x0000 led_blink.a51 # 生成 .ihx 后,用 objcopy 转成二进制 objcopy -I ihex -O binary led_blink.ihx led_blink.bin # 用 Python 脚本把二进制转成 Quartus MIF 格式 python3 bin2mif.py led_blink.bin rom_init.mif 4096bin2mif.py的逻辑是读二进制文件,按字节展开成 MIF 的地址-数据对,不足 4096 字节的部分补 0xFF 或 0x00。注意--code-loc要跟 ROM 起始地址一致,--xram-loc对应数据空间,虽然点灯程序用不到外部 RAM,但链接器需要这个参数才能正确分配。生成的.mif在 Quartus 里通过 MegaWizard 加载到 M4K,综合后 ROM 就有初始内容了。
3.3 顶层 Verilog 例化 OC8051 与 ROM/RAM
顶层模块要做三件事:例化 OC8051 核、例化 ROM 和 RAM、把 P1 口连到 LED 引脚。下面是一个可综合的顶层骨架,省略了 PLL 和复位同步的细节,但关键连接都在。
module de2_oc8051_top ( input clk_50m, // DE2 板载 50MHz input rst_n, // 按键复位,低有效 output [17:0] led // DE2 LED,这里只用低 8 位 ); wire clk_12m; // PLL 输出 12MHz wire rst_sync; // 同步后的复位 wire [15:0] rom_addr; // 程序地址 wire [7:0] rom_data; // 程序数据 wire [15:0] ram_addr; // 数据地址 wire [7:0] ram_dout; // 数据读出 wire [7:0] ram_din; // 数据写入 wire ram_we; // 数据写使能 wire [7:0] p1_out; // P1 口输出 // PLL 降频,具体参数由 MegaWizard 生成 pll_12m u_pll ( .inclk0 (clk_50m), .c0 (clk_12m) ); // 复位同步,两级触发器 reg [1:0] rst_sync_reg; always @(posedge clk_12m or negedge rst_n) begin if (!rst_n) rst_sync_reg <= 2'b00; else rst_sync_reg <= {rst_sync_reg[0], 1'b1}; end assign rst_sync = rst_sync_reg[1]; // OC8051 核例化 oc8051_top u_cpu ( .clk (clk_12m), .rst (rst_sync), .rom_addr (rom_addr), .rom_data (rom_data), .ram_addr (ram_addr), .ram_dout (ram_dout), .ram_din (ram_din), .ram_we (ram_we), .p1_out (p1_out) ); // 程序 ROM,加载 MIF rom_4k u_rom ( .address (rom_addr[11:0]), .clock (clk_12m), .q (rom_data) ); // 数据 RAM,1KB ram_1k u_ram ( .address (ram_addr[9:0]), .clock (clk_12m), .data (ram_din), .wren (ram_we), .q (ram_dout) ); // P1 口驱动 LED,低电平点亮 assign led[7:0] = p1_out; assign led[17:8] = 10'b0; endmodule这段代码里rom_addr[11:0]截取了低 12 位,对应 4KB 空间,高位地址没参与译码是因为 ROM 只有 4KB,如果程序超过 4KB 需要扩展。ram_addr[9:0]对应 1KB,同样只取低位。P1 口直接连 LED,注意 DE2 的 LED 是低电平点亮,所以程序里写 0 才是亮。复位同步用了两级触发器,避免按键抖动导致 CPU 状态机跑飞。PLL 的具体参数在 Quartus 的 MegaWizard 里配置,输入 50MHz,输出 12MHz,锁相环锁定信号可以不接,因为 8051 对时钟精度要求不高。
3.4 Quartus 工程配置与引脚分配
工程建好后,要在 Assignment 里把顶层引脚对应到 DE2 的物理引脚。LED 在 DE2 上对应 PIN_AE23 到 PIN_AF14 等,具体查板子原理图。时钟输入是 PIN_N2,复位按键是 PIN_N25 之类。引脚分配完,编译前还要确认 M4K 初始化文件已经加载到 ROM 模块,否则综合出来的 ROM 全是 0,CPU 取指得到 NOP,程序不会跑。编译通过后生成.sof,用 Quartus Programmer 下载到 FPGA,下载完成后 LED 应该开始闪烁。如果没闪,先检查复位是否释放、时钟是否锁定、ROM 是否真的加载了 MIF。
4. 避坑与排查:点灯不亮的五个血泪经验
4.1 现象:下载后 LED 全亮不闪
原因通常是复位一直有效,CPU 停在复位状态。DE2 的按键复位有的是高有效,有的是低有效,顶层代码里rst_n如果接反,CPU 永远不释放。解决方法是先用 SignalTap 抓rst_sync和rom_addr,看复位释放后rom_addr是否在变化。如果rom_addr一直是 0,说明 CPU 没取指,检查复位极性。
4.2 现象:LED 闪但频率不对
延时子程序的定时初值算错了。12MHz 晶振下,机器周期是 1us,定时器模式 1 是 16 位,1ms 定时初值应该是 65536 - 1000 = 64536 = 0xFC18。如果你写成 0xF C18 但高低字节反了,定时时间会差很多。另外主循环里调用DELAY的次数决定了闪烁周期,只调一次是 1ms,肉眼看不到闪,要加外层循环。
4.3 现象:程序跑飞,LED 乱闪
ROM 初始化文件地址没对齐,或者 MIF 文件里数据宽度设错。Quartus 的 MIF 格式要求地址和数据的位宽匹配,如果 ROM 是 8 位宽,MIF 里数据也必须是 8 位。另外--code-loc如果设成 0x0000 但汇编里ORG写了别的地址,链接器会把代码放到错误位置,CPU 取指到空区域就跑飞。检查方法是看 Quartus 编译报告里 ROM 的初始化是否成功,或者用 SignalTap 抓rom_data看是不是预期机器码。
4.4 现象:综合报错 M4K 资源不够
OC8051 核加 4KB ROM 加 1KB RAM 在 EP2C35 上资源够用,但如果你把 ROM 设成 8KB 或 RAM 设成 4KB,M4K 块可能不够。DE2 的 EP2C35 有 105 个 M4K,每个 4Kbit,4KB ROM 需要 8 个 M4K,1KB RAM 需要 2 个,总共 10 个,余量很大。如果报错,检查是不是例化了多个 ROM 或者 RAM 位宽设成了 16 位。解决方法是压缩 ROM 大小,或者把不用的 M4K 释放出来。
4.5 现象:SignalTap 抓不到信号
SignalTap 的采样时钟必须跟被测信号同步,如果你用 50MHz 抓 12MHz 域的信号,会出现亚稳态或采样丢失。正确做法是用 PLL 输出的 12MHz 作为 SignalTap 的采样时钟,或者用 50MHz 但加同步器。另外 SignalTap 会占用 M4K 资源,如果 ROM 已经占满,可能插不进去。解决方法是先减小 ROM 或 RAM,腾出 M4K 给 SignalTap 用。
5. 进阶技巧:让 OC8051 在 DE2 上跑得更稳
跑通点灯只是第一步,真正让这颗核稳定工作,还要处理几个细节。第一个是时钟域交叉,PLL 输出的 12MHz 和板载 50MHz 如果混用,跨时钟域的信号必须加同步器,否则偶发亚稳态会让 CPU 执行错指令。我一般把所有外设都挂到 12MHz 域,50MHz 只用来驱动 PLL 和下载逻辑。第二个是 ROM 初始化文件的生成脚本,建议写成 Makefile,每次改汇编自动重新生成 MIF 并触发 Quartus 编译,避免手动操作漏步骤。第三个是给 P1 口加寄存器输出,不要直接组合逻辑连 LED,否则毛刺可能让 LED 微亮,看起来像程序跑飞。
验证方法上,除了看 LED,还可以用 SignalTap 抓rom_addr和p1_out,看地址是否按预期递增、P1 口是否在 0x00 和 0xFF 之间切换。如果地址跳变不连续,说明程序里有长跳转或中断,点灯程序没有中断,地址应该基本顺序执行。另外可以写一个简单的测试程序,往 P1 口写 0x55 和 0xAA,这样 LED 会隔一个亮一个,比全亮全灭更容易看出位错。我自己的习惯是每次改完汇编先跑仿真,用 ModelSim 看rom_addr和p1_out的波形,确认逻辑对了再综合下载,这样能省下大量在板子上调试的时间。希望帮到你。
本文还有配套的精品资源,点击获取