简介:这份资源面向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初始化、软核在DE2上的下载配置与LED验证的完整思路。
1. 在 DE2 上跑通 OC8051 点灯:从拿到源码到看见 LED 闪烁
手里有一块 DE2 开发板,想跑 8051 又不想外挂一颗真芯片,OC8051 是绕不开的选择。它是一个用 Verilog 写的 8051 软核,把 MCS-51 的指令集、定时器、串口、中断这些外设全部综合进 FPGA 逻辑里,配合 Altera 的 Quartus 工具链,能在 Cyclone II 上跑出一个"虚拟 8051"。点灯程序看着简单,但它一次性验证了三件事:软核能不能正确取指、存储器映射对不对、GPIO 有没有接对引脚。很多人卡在"综合过了但灯不亮",问题往往不在 8051 代码,而在 SoC 顶层和引脚约束。这篇笔记按我实际拆包复现的顺序写,从工程结构、存储器初始化、Quartus 编译到板级验证,把能抄的步骤和容易翻车的地方都摊开。适合手上有 DE2、想学软核 SoC 落地的同学,也适合已经跑过 NIOS 想换 8051 练手的工程师。
2. OC8051 软核与 DE2 工程结构:先搞清楚谁在驱动谁
2.1 OC8051 到底是个什么东西
OC8051 最早是一套开源的 8051 IP,核心文件用 Verilog-2001 写成,包含 CPU 主体、定时器/计数器、UART、中断控制器和若干特殊功能寄存器。它不是一个"能直接下载到 FPGA 就完事"的成品,而是一堆 RTL 加一个可选的测试平台。真正要跑起来,你得自己搭一个最小 SoC:把 OC8051 核、程序存储器(ROM)、数据存储器(RAM)和 GPIO 外设用片内总线连起来。
在 DE2 这种 Cyclone II EP2C35 上,常见做法是用 Altera 的片上存储器(M4K 块)来放程序和数据。8051 是哈佛结构,程序和数据分开编址,所以 ROM 和 RAM 要分别例化。程序存储器一般用altsyncram配成 ROM,初始化文件是.mif或.hex;数据存储器用另一块altsyncram配成 RAM。GPIO 最简单,一个assign把某个 SFR 的写信号接到 LED 引脚就行。
这里有个容易忽略的点:OC8051 的取指周期和普通 CPU 不一样,它对 ROM 的读延迟敏感。如果你用异步读的 ROM,综合工具可能给你插一堆逻辑,时序会紧。常见做法是把 ROM 配成同步读、加一级输出寄存器,然后在顶层把psen信号对齐好。这个细节不处理,综合能过,但上板跑飞。
2.2 DE2 工程的目录长什么样
我拿到的包解压后大致是这么几层:
oc8051_de2/ ├── rtl/ │ ├── oc8051_defines.v │ ├── oc8051_top.v │ ├── oc8051_alu.v │ ├── oc8051_control.v │ ├── oc8051_timer.v │ ├── oc8051_uart.v │ └── oc8051_sfr.v ├── soc/ │ ├── oc8051_soc_top.v │ ├── rom_1024x8.v │ ├── ram_256x8.v │ └── gpio.v ├── sw/ │ ├── led_blink.asm │ └── led_blink.hex ├── quartus/ │ ├── oc8051_de2.qpf │ ├── oc8051_de2.qsf │ └── oc8051_de2.sdc └── doc/ └── pin_map.txtrtl/是软核本体,soc/是我要重点看的顶层和存储器,sw/是 8051 汇编和编译出来的 hex,quartus/是工程文件。注意oc8051_defines.v里有一堆宏,比如OC8051_ROM、OC8051_RAM的地址宽度,改存储器大小的时候要同步改这里,否则地址对不上。
2.3 存储器映射:为什么你的灯可能"亮错位置"
OC8051 的 SFR 空间里,P0、P1、P2、P3 是四个 8 位端口,地址分别是 0x80、0x90、0xA0、0xB0。点灯一般用 P1,因为 DE2 上 LED 多,接起来直观。但 OC8051 核本身只给出 SFR 的读写信号,具体哪个端口接到哪些引脚,是你在 SoC 顶层决定的。
我一般会这么接:P1 的写使能p1_wr和写数据p1_data引到gpio.v,在gpio.v里打一拍输出到LEDG[7:0]。这样 8051 执行MOV P1, #0x55的时候,LED 就是 01010101。如果你把 P1 接到了LEDR,那也没问题,只是颜色不同。真正要命的是地址译码写错,比如把 P1 的地址写成 0xA0(那是 P2),程序里写 P1 实际动的是 P2,灯当然不按预期亮。
提示:先别急着改 8051 代码,拿示波器或者 Quartus 的 SignalTap 抓一下
p1_wr和p1_data,确认 SFR 写信号有没有出来。这一步能省掉后面大量瞎猜。
3. 从汇编到 MIF:点灯程序的编译与存储器初始化
3.1 点灯汇编怎么写才不"玄学"
8051 点灯的核心就是一个循环:写端口、延时、翻转、再延时。但 OC8051 的定时器精度和真芯片有差异,延时循环的常数不能照抄。我一般先用一个粗略的软件延时,跑通之后再换定时器中断。
; led_blink.asm ; 在 OC8051 上让 P1 端口的 LED 交替闪烁 ORG 0000H LJMP MAIN ORG 0030H MAIN: MOV SP, #07H ; 设置堆栈指针,避开工作寄存器区 MOV P1, #0FFH ; 先全灭(DE2 LED 低电平点亮时相反) LOOP: MOV P1, #55H ; 01010101 ACALL DELAY MOV P1, #0AAH ; 10101010 ACALL DELAY SJMP LOOP DELAY: MOV R0, #0FFH ; 外层循环 D1: MOV R1, #0FFH ; 内层循环 D2: DJNZ R1, D2 DJNZ R0, D1 RET END这段代码里MOV SP, #07H容易被忽略。OC8051 复位后 SP 是 07H,但如果你在中断里压栈,不改 SP 可能覆盖工作寄存器。点灯用不到中断,但养成习惯没坏处。DELAY的双层循环在 12MHz 等效时钟下大概几百毫秒,具体值要看你给 OC8051 的分频比。
3.2 把 hex 转成 MIF:别让 ROM 初始化成空
Quartus 的altsyncram认.mif格式,而汇编器一般吐.hex。转换脚本我一般用 Python 写一个,逻辑很直白:读 hex 的每一行,按地址填进 MIF 的表格。
# hex2mif.py # 把 8051 汇编器输出的 Intel HEX 转成 Quartus MIF # 用法: python hex2mif.py led_blink.hex led_blink.mif 1024 import sys def hex_to_mif(hex_path, mif_path, depth): mem = [0xFF] * depth # 未初始化的 ROM 填 0xFF(NOP 附近) with open(hex_path, 'r') as f: for line in f: line = line.strip() if not line.startswith(':'): continue byte_count = int(line[1:3], 16) addr = int(line[3:7], 16) rec_type = int(line[7:9], 16) if rec_type != 0: # 只处理数据记录 continue for i in range(byte_count): data = int(line[9 + i*2: 11 + i*2], 16) if addr + i < depth: mem[addr + i] = data with open(mif_path, 'w') as f: f.write('DEPTH = %d;\n' % depth) f.write('WIDTH = 8;\n') f.write('ADDRESS_RADIX = HEX;\n') f.write('DATA_RADIX = HEX;\n') f.write('CONTENT\nBEGIN\n') for i in range(depth): f.write('%04X : %02X;\n' % (i, mem[i])) f.write('END;\n') if __name__ == '__main__': hex_to_mif(sys.argv[1], sys.argv[2], int(sys.argv[3]))参数说明:depth要和rom_1024x8.v里的深度一致,DE2 上一般用 1024 或 2048。0xFF填充是因为 8051 的MOVC取到 0xFF 会当 NOP 处理,不会跑飞太远。如果你的汇编器输出的是.bin,那就按字节读,逻辑一样。
3.3 在 Quartus 里挂上 MIF
ROM 的例化在rom_1024x8.v里,关键参数是init_file:
// rom_1024x8.v 片段 altsyncram #( .operation_mode("ROM"), .width_a(8), .widthad_a(10), .numwords_a(1024), .outdata_reg_a("CLOCK0"), // 输出打一拍,改善时序 .init_file("led_blink.mif"), // 路径相对于 Quartus 工程 .lpm_type("altsyncram") ) rom_inst ( .address_a(rom_addr), .clock0(clk), .q_a(rom_data) );outdata_reg_a("CLOCK0")这行是血泪经验:不加的话,ROM 读延迟是组合逻辑,OC8051 的取指时序会紧,综合报告里psen路径容易出 setup violation。加了之后多一拍延迟,但 OC8051 的状态机本身有等待周期,能吃掉这一拍。
注意:MIF 文件路径在 Quartus 里是相对工程目录的。如果你把 MIF 放在
sw/下,init_file要写../sw/led_blink.mif,否则综合时找不到文件,ROM 全填 0,程序从 0x0000 开始执行一条NOP然后一路滑下去,灯永远不亮。
4. Quartus 编译与引脚约束:让信号真的走到 LED 上
4.1 工程设置里几个不能默认的项
打开oc8051_de2.qpf之后,先别点编译。在 Assignments → Device 里确认器件是 EP2C35F672C6,这是 DE2 的标配。然后在 Settings → Compiler Settings 里把Verilog HDL Input的版本设成 SystemVerilog-2001 或 Verilog-2001,OC8051 的代码用了generate和localparam,老版本解析会报错。
时钟方面,DE2 的 50MHz 晶振直接给 OC8051 太快。我一般用一个分频计数器把它降到 1MHz 左右,再给软核。分频逻辑放在oc8051_soc_top.v里:
// 50MHz -> 约 1MHz,供 OC8051 使用 reg [5:0] clk_div; reg cpu_clk; always @(posedge clk_50m) begin if (clk_div == 6'd24) begin clk_div <= 0; cpu_clk <= ~cpu_clk; end else begin clk_div <= clk_div + 1; end end6'd24是分频系数,50MHz / (24+1) / 2 ≈ 1MHz。这个值不用太精确,8051 的延时循环本来就是粗调。但如果你后面要跑 UART,分频比要重新算,波特率会偏。
4.2 引脚约束:pin_map 里的坑
DE2 的 LED 引脚在pin_map.txt里列了,但 Quartus 的.qsf要自己写。我一般把 P1 的 8 位接到LEDG[7:0]:
set_location_assignment PIN_AE22 -to LEDG[0] set_location_assignment PIN_AF22 -to LEDG[1] set_location_assignment PIN_AB21 -to LEDG[2] set_location_assignment PIN_AC21 -to LEDG[3] set_location_assignment PIN_AD21 -to LEDG[4] set_location_assignment PIN_AD22 -to LEDG[5] set_location_assignment PIN_AG22 -to LEDG[6] set_location_assignment PIN_AE23 -to LEDG[7] set_location_assignment PIN_N25 -to clk_50m这些引脚号是 DE2 的固定映射,别自己改。如果你用的是别的板子,去翻对应的手册。clk_50m一定要接对,接错了整个设计没时钟,SignalTap 也抓不到东西。
4.3 编译报告里看什么
编译完之后,重点看两个地方:一是 Fitter 报告里的资源占用,OC8051 大概吃 2000 多个 LE,ROM 和 RAM 各占几个 M4K,DE2 的 EP2C35 绰绰有余;二是 Timing Analyzer 里的psen和p1_wr路径,如果 setup slack 是负的,先把 ROM 的输出寄存器加上,再不行就降 CPU 时钟。
我遇到过一种情况:综合过了,时序也过,但上板灯就是不亮。后来用 SignalTap 抓p1_wr,发现它一直是低。查了半天是oc8051_defines.v里OC8051_SFR_P1的地址宏被改过,和gpio.v里的译码对不上。这种问题没有捷径,只能对着地址表一行行核。
5. 上板验证与常见问题排查:灯不亮的时候先查什么
5.1 下载与 SignalTap 抓信号
Quartus 的 Programmer 里加载.sof文件,确认 JTAG 链识别到 EP2C35。下载完之后,如果 LED 没反应,先别拔线,打开 SignalTap,把cpu_clk、rom_addr、p1_wr、p1_data加进去,采样深度设 1024,触发条件设p1_wr上升沿。
正常情况你应该看到rom_addr在 0x0000 到 0x0030 之间跳,p1_wr周期性拉高,p1_data在 0x55 和 0xAA 之间交替。如果rom_addr一直是 0,说明 CPU 没取指,检查复位信号和时钟;如果rom_addr在跳但p1_wr不动,说明程序跑飞了,大概率是 ROM 初始化不对或者堆栈溢出。
5.2 常见问题排查清单
现象一:LED 全亮或全灭,不闪烁。原因:P1 端口初始值或者延时循环没生效。OC8051 复位后 P1 是 0xFF,如果你的 LED 是低电平点亮,那上电就是全亮。检查MOV P1, #0FFH之后有没有进LOOP,以及DELAY的循环常数是不是太小导致肉眼看不出来。 解决:把DELAY的R0和R1都改成 0xFF,或者用 SignalTap 看p1_data有没有变化。如果p1_data在变但 LED 不变,查引脚约束。
现象二:编译报错 "Can't find init_file"。原因:MIF 路径写错,或者 MIF 文件没生成。 解决:确认hex2mif.py跑过,MIF 在工程目录下。Quartus 的init_file路径是相对.qpf所在目录的,不是相对.v文件。我一般把 MIF 直接放在quartus/下,省得路径绕。
现象三:时序违例,setup slack 为负。原因:ROM 组合读延迟太大,或者 CPU 时钟太快。 解决:给altsyncram加outdata_reg_a("CLOCK0"),把 CPU 时钟从 1MHz 降到 500kHz 试试。如果还不行,检查oc8051_top.v里的状态机是不是有长组合路径,必要时插流水。
现象四:SignalTap 抓不到信号,或者采样全是 0。原因:SignalTap 的采样时钟接错,或者被综合优化掉了。 解决:采样时钟用cpu_clk,别用 50MHz 的clk_50m,否则采样率对不上。另外在 Quartus 里把要抓的信号标记为keep,防止被优化。
现象五:程序跑飞,rom_addr跳到奇怪的地方。原因:ROM 里未初始化的区域填了 0x00 而不是 0xFF,0x00 是NOP,但连续 NOP 之后会滑到数据区。或者堆栈指针设错,ACALL压栈覆盖了程序代码。 解决:确认hex2mif.py里mem = [0xFF] * depth,别填 0x00。堆栈设在 0x07 以上、程序区以下,一般 0x30 到 0x7F 是安全的。
提示:排查顺序永远是"时钟 → 复位 → 取指 → SFR 写 → 引脚"。从前往后查,别一上来就改 8051 代码。
6. 进阶:用定时器中断替代软件延时,顺便验证中断系统
软件延时的点灯只能证明 CPU 在跑,证明不了中断和定时器。OC8051 的定时器 0 在模式 1 下是 16 位定时,配合中断可以做出更精确的闪烁。我一般会这么改:定时器每 50ms 中断一次,在中断服务程序里翻转一个标志位,主循环根据标志位写 P1。
; timer_blink.asm ; 用定时器 0 中断实现精确闪烁 ORG 0000H LJMP MAIN ORG 000BH ; 定时器 0 中断入口 LJMP T0_ISR ORG 0030H MAIN: MOV SP, #07H MOV TMOD, #01H ; 定时器 0,模式 1(16 位) MOV TH0, #3CH ; 50ms @ 1MHz,初值 0x3CB0 MOV TL0, #0B0H SETB ET0 ; 开定时器 0 中断 SETB EA ; 开总中断 SETB TR0 ; 启动定时器 MOV P1, #0FFH LOOP: SJMP LOOP ; 主循环空转,等中断 T0_ISR: MOV TH0, #3CH ; 重装初值 MOV TL0, #0B0H CPL P1.0 ; 翻转 P1.0 RETI ENDTH0/TL0的初值按 1MHz 时钟算:50ms 需要 50000 个机器周期,65536 - 50000 = 15536 = 0x3CB0。如果你的 CPU 时钟不是 1MHz,这个值要重算。中断服务程序里重装初值是因为模式 1 不是自动重载,忘了重装就只中断一次。
验证中断有没有生效,还是用 SignalTap 抓p1_data的 bit0,看它是不是每 50ms 翻转一次。如果翻转频率不对,先查时钟分频,再查定时器初值。中断优先级和嵌套在点灯场景用不到,但如果你后面要加串口,IP寄存器要配一下。
从那以后我每次跑新软核,都强制先走一遍"时钟 → 复位 → 取指 → SFR → 引脚"的检查链,再去看应用代码。这套流程帮我省掉了大量对着 LED 发呆的时间。希望帮到你。
本文还有配套的精品资源,点击获取