FPGA软核实战:在DE2上用OC8051实现点灯与中断验证 简介这份资源面向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 汇编和编译出来的 hexquartus/是工程文件。注意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 填 0xFFNOP 附近 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 里挂上 MIFROM 的例化在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-2001OC8051 的代码用了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 6d24) begin clk_div 0; cpu_clk ~cpu_clk; end else begin clk_div clk_div 1; end end6d24是分频系数50MHz / (241) / 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 多个 LEROM 和 RAM 各占几个 M4KDE2 的 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 不变查引脚约束。现象二编译报错 Cant 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 而不是 0xFF0x00 是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模式 116 位 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 发呆的时间。希望帮到你。本文还有配套的精品资源点击获取