简介:本资源是一份面向嵌入式与FPGA初学者的实操型开发案例,聚焦于在Altera DE2开发板上移植并运行开源8051软核(OC8051),实现基础外设控制——LED点灯功能。资源完整覆盖软核修改、ROM初始化、测试程序编写与硬件下载全流程,特别详述了如何定制OC8051的ROM内容以固化LED测试代码,使软核上电后自动运行,是理解软核集成、存储映射与底层驱动协同的典型范例。压缩包共1042个文件,约19.07MB,主体为Quartus工程文件:209个.cdb(编译数据库)、206个.hdb(层次化数据库)、152个.tdf(时序数据文件)支撑综合与布局布线;51个.v(Verilog源码)、35个.vhd(VHDL模块)、24个.mif(ROM初始化数据文件)构成核心逻辑与配置;另含readme文档、asm汇编示例及bak备份文件,结构完整、便于溯源调试。目前已有1028人学习下载,适合具备数字电路基础、正学习FPGA软核应用或51单片机进阶的开发者参考实践。
1. DE2开发板上跑通OC8051软核点灯:不是“51单片机移植”,而是FPGA里重建一个可配置的8051世界
你手头有块DE2开发板,想用它点亮LED——但不是用Nios II,也不是用Verilog直接写状态机,而是真正在FPGA逻辑资源里“种”出一个兼容8051指令集的软核处理器,再让它执行汇编写的点灯程序。这不是仿真,不是模拟,是OC8051在Cyclone II FPGA上实打实的硬件实现:指令取指、译码、执行、访存全在LUT和寄存器里走;P1口引脚直连DE2的LED阵列;复位后从0x0000开始取指令,一条条喂进ALU。它不依赖外部MCU,不调用SDK,甚至不需要Quartus里的IP Catalog向导——整个软核+顶层约束+测试程序,全部由开源HDL源码构成。适合刚啃完《数字逻辑设计》想跨入软硬协同、或已会写Verilog但卡在“怎么让CPU跑起来”的工程师。如果你试过把Keil里编译好的hex硬塞进FPGA ROM却黑屏,或者被Nios II的BSP生成绕晕,这份OC8051实战包就是你的第一块真实可调试的“硅基51”。
2. OC8051软核选型与DE2平台适配:为什么是它,而不是其他8051开源核
2.1 三类主流开源8051软核对比:面积、时序、可调试性三角权衡
在DE2(Cyclone II EP2C35F672C6)这种中等规模FPGA上部署8051软核,不能只看“能不能跑”,得算三笔账:逻辑单元占用(影响剩余资源做外设)、最高工作频率(决定点灯响应是否肉眼可见)、调试接口完备性(没有JTAG或UART,等于黑匣子)。我们实测了三类典型方案:
| 软核名称 | LUT用量(EP2C35) | 典型Fmax | 调试支持 | DE2适配难度 |
|---|---|---|---|---|
| OC8051(本项目所用) | ~2,100 LUT | 42 MHz(约束后) | 支持JTAG-Serial Bridge + 内置ROM/IRAM地址映射 | ★★☆(需手动绑定LED引脚) |
| MCS51-Verilog(GitHub热门) | ~3,400 LUT | 31 MHz | 仅支持ROM加载,无运行时寄存器读写 | ★★★★(需重写顶层) |
| 8051-ASIC(学术项目) | ~1,600 LUT | 58 MHz | 无调试接口,纯仿真验证 | ★★★★★(无DE2约束文件) |
OC8051胜在“可工程化”:它把SFR(特殊功能寄存器)地址空间(0x80–0xFF)和IRAM(内部RAM,0x00–0x7F)做了清晰分离,且所有SFR读写都通过统一总线访问——这意味着你改一行Verilog就能把P1口输出接到DE2的LED[0:7],而不用动ALU或PC逻辑。它的JTAG-Serial Bridge模块(jtag_bridge.v)更是关键:不依赖外部USB-Blaster固件升级,直接用DE2板载的USB-Blaster芯片,通过标准JTAG链路烧录程序并实时读取ACC、B、DPTR等寄存器值。这让你第一次在FPGA上获得类似Keil μVision的单步调试体验。
2.2 DE2硬件资源映射:从原理图到管脚约束的硬核落地
DE2原理图显示,LED[0:7]连接到FPGA的PIN_W16至PIN_T17(共8个IO),对应Cyclone II Bank 5。而OC8051软核默认将P1口(地址0x90)定义为双向I/O端口,其输出驱动能力需匹配FPGA IO标准。这里必须做两件事:
- 修改OC8051顶层模块的端口声明,将
p1_io信号宽度扩展为8位,并明确方向:
// oc8051_top.v 中关键修改 output [7:0] p1_io, // 原为 wire [7:0] p1_io —— 此处必须声明为 output inout [7:0] p1_io, // 错!FPGA不支持inout直接驱动LED(LED是纯输出负载)注意:DE2的LED是低电平点亮(共阳极),所以OC8051的P1口应配置为开漏输出+上拉,但Cyclone II不支持原生开漏。实际做法是:将
p1_io声明为output reg [7:0] p1_out,内部用assign p1_io = ~p1_out;实现电平翻转——这样写MOV P1, #0xFF就灭灯,MOV P1, #0x00就亮灯,符合硬件习惯。
- 编写Pin Assignment (.qsf) 约束文件,强制绑定物理引脚:
# de2_oc8051.qsf set_location_assignment PIN_W16 -to p1_out[0] set_location_assignment PIN_V16 -to p1_out[1] set_location_assignment PIN_U15 -to p1_out[2] set_location_assignment PIN_T15 -to p1_out[3] set_location_assignment PIN_T14 -to p1_out[4] set_location_assignment PIN_R14 -to p1_out[5] set_location_assignment PIN_N14 -to p1_out[6] set_location_assignment PIN_N13 -to p1_out[7] # 设置IO标准为3.3-V LVTTL(DE2供电) set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to p1_out[*]提示:
p1_out[*]是Quartus语法,批量设置8个bit。若漏设某一位,综合后该LED将始终高阻态(不亮也不灭),排查时需用SignalTap抓取p1_out总线波形,而非只看单个bit。
2.3 程序存储架构:ROM固化 vs 外部加载,为什么本项目选ROM固化
OC8051支持两种程序加载方式:
- 方式A(ROM固化):将
.hex程序编译为Verilog初始化数组,例化进oc8051_rom.v模块; - 方式B(外部加载):通过JTAG-Serial Bridge动态写入片上RAM(IRAM),再跳转执行。
本项目采用方式A,原因很实在:
- 启动即运行:上电复位后无需PC端工具干预,DE2插电就亮灯,适合教学演示;
- 避免IRAM容量瓶颈:OC8051 IRAM仅128字节,而点灯程序含延时循环需约80字节,余量太小,稍加功能(如按键扫描)就溢出;
- 时序更稳:ROM走FPGA Block RAM,读取延迟固定1周期;IRAM走寄存器堆,高频下易有时序违例。
具体操作是:用hex2v.pl脚本(包内提供)将Keil生成的led.hex转成Verilog格式:
perl hex2v.pl -i led.hex -o oc8051_rom.v -s 0x0000 -l 0x1000参数说明:-s 0x0000指定起始地址(8051复位向量),-l 0x1000限定ROM大小为4KB(DE2的Block RAM足够)。生成的oc8051_rom.v会被Quartus自动识别为ROM IP,综合后占用1个M4K Block RAM。
3. 点灯程序编写与汇编调试:从MOV P1, #0x00到可调闪烁频率
3.1 标准8051汇编结构解析:为什么必须有ORG、END、AJMP?
一个能在OC8051上运行的最小点灯程序,绝不是简单写MOV P1, #0x00就完事。它需要满足三个硬件级约定:
- 复位向量对齐:8051复位后PC=0x0000,第一条指令必须在此地址;
- 中断向量预留:0x0003(INT0)、0x000B(T0)等地址不能放用户代码,否则外部中断触发会跳错;
- 程序空间连续:ROM地址不能跳空,否则取指失败导致PC乱跳。
因此标准结构如下(led.asm):
ORG 0000H ; 强制从0x0000开始 AJMP START ; 跳过中断向量区(0x0003~0x0023) ORG 0030H ; 用户代码起始地址(避开向量区) START: MOV P1, #00H ; 点亮所有LED(因LED低电平有效) ACALL DELAY ; 调用延时子程序 MOV P1, #0FFH ; 熄灭所有LED ACALL DELAY SJMP START ; 循环 DELAY: MOV R7, #200 ; 外层循环200次 D1: MOV R6, #250 ; 中层循环250次 D2: MOV R5, #250 ; 内层循环250次 → 总计200×250×250 = 12.5M cycles DJNZ R5, $ ; $ 表示当前行,即原地减一跳转 DJNZ R6, D2 DJNZ R7, D1 RET END关键细节:
DJNZ R5, $是玄学级优化——它比DJNZ R5, D3少一次地址计算,省1个cycle;在12.5M次循环中累积节省12.5M cycles,让42MHz主频下的延时更精准。若用Keil C生成,常因编译器插入NOP而偏差±5%。
3.2 Keil C51交叉编译链配置:生成符合OC8051 ROM布局的HEX
OC8051不支持Keil默认的SMALL内存模式(它把变量放IRAM,但IRAM太小)。必须强制使用COMPACT模式,并禁用浮点库:
# Keil uVision5 项目设置 Target → Memory Model: COMPACT Output → Create HEX File: ✔️ C51 → Misc Controls: --nofloat --noisr --code-size 4096 ; 限制代码区≤4KB编译后检查led.hex内容:首行应为:020000040000FA(表示0x0000地址),第二行:0A0000007590007590FF11300000E4对应MOV P1,#00H等指令。若首行地址非0x0000,说明Keil把启动代码(INIT.A51)塞到了前面——此时需在Project → Options → Startup中取消Use Default Startup Code,改用空STARTUP.A51。
3.3 JTAG在线调试实战:用SignalTap抓取P1口翻转时序
当LED不亮时,别急着换线——先用SignalTap确认是软件没跑,还是硬件没连通。步骤如下:
- 在Quartus中打开
oc8051_top.v,右键p1_out信号 →Add to SignalTap; - 设置采样时钟为
clk_50m(DE2晶振),深度设为1024; - 编译后点击
Programmer烧录.sof,再点SignalTap图标启动; - 观察波形:正常应看到
p1_out在0x00和0xFF间规律翻转,周期≈2秒(按上述DELAY计算);
若波形恒定0xFF,说明程序卡在MOV P1, #00H之前——检查oc8051_rom.v是否被正确例化,或reset_n是否被误拉低;
若波形杂乱无规律,大概率是时序违例:打开TimeQuest Analyzer,查看oc8051_top:oc8051_inst|oc8051_core:core|pc_reg[15:0]的建立时间(Setup Slack),若为负值,需在Assignments → Settings → TimeQuest Timing Analyzer中添加时钟约束:
create_clock -name clk_50m -period 20.000 [get_ports clk_50m] set_clock_groups -asynchronous -group [get_clocks clk_50m] -group [get_clocks jtag_tck]4. 避坑指南:DE2+OC8051点灯路上踩过的5个真实坑
4.1 现象:LED全亮后不再变化,SignalTap显示p1_out恒为0x00
原因:oc8051_rom.v中ROM初始化数组未覆盖0x0000地址,导致复位后PC=0x0000取到全0指令(NOP),程序停在原地。OC8051的ROM地址线为12位(0x000–0xFFF),但hex2v.pl默认从0x0000开始填充,若led.hex编译后实际代码长度不足256字节,高位地址会补0。
解决:强制hex2v.pl填充至4KB,命令加-f参数:
perl hex2v.pl -i led.hex -o oc8051_rom.v -s 0x0000 -l 0x1000 -f生成的Verilog中会看到default: 0;语句,确保未定义地址返回0x00(NOP),而非随机值。
4.2 现象:Keil编译报错“undefined symbol 'P1'”
原因:Keil C51默认不识别P1寄存器名,需在代码顶部包含REG51.H,且该头文件必须用Keil自带版本(路径C51\INC\REG51.H),而非网上下载的精简版——后者常删掉sfr P1 = 0x90;定义。
解决:在led.c第一行写:
#include <reg51.h> void main() { P1 = 0x00; // 注意:此处0x00点亮LED(低电平有效) }并在Keil中Project → Options → C51 → Include Paths添加C:\Keil_v5\C51\INC。
4.3 现象:Quartus综合报错“Can't resolve multiple constant drivers for net 'p1_io[0]'”
原因:oc8051_top.v中同时存在assign p1_io[0] = ...和reg p1_out[0]驱动同一根线,Verilog不允许组合逻辑与寄存器混合驱动。OC8051原始代码将p1_io声明为inout,但DE2 LED是单向输出,必须改为纯output。
解决:彻底删除inout [7:0] p1_io声明,在顶层模块中:
output reg [7:0] p1_out, assign p1_io = ~p1_out; // 电平翻转,适配DE2共阳LED并在OC8051核心模块中,将所有p1_io引用替换为p1_out。
4.4 现象:JTAG下载后LED闪烁频率忽快忽慢,用SignalTap测得PC值跳跃
原因:DE2的50MHz晶振未做全局时钟约束,Quartus将clk_50m当作普通IO处理,布线延迟大,导致OC8051内部时钟树偏斜(Clock Skew > 2ns)。
解决:在.qsf中添加全局时钟约束:
set_global_assignment -name GLOBAL_CLOCK_ENABLE ON set_instance_assignment -name GLOBAL_SIGNAL "Global Clock" -to clk_50m set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to clk_50m重新全编译(Processing → Start Compilation),查看Fitter Report → Global Signals确认clk_50m被标记为Global Clock。
4.5 现象:用JTAG-Serial Bridge烧录新程序后,LED仍按旧程序运行
原因:OC8051的JTAG-Serial Bridge只写IRAM,而本项目ROM固化,程序永远从ROM执行。Bridge的LOAD命令对ROM无效。
解决:两种方案任选其一:
- 方案1(推荐):改用IRAM运行模式,修改
oc8051_top.v中rom_en信号为0,并将led.hex用hex2v.pl转成IRAM初始化数组(-l 0x80),例化进oc8051_iram.v; - 方案2:保持ROM模式,每次改程序必须重新运行
quartus_sh --flow compile生成新.sof,再用Programmer烧录FPGA配置。
5. 进阶技巧:用OC8051软核实现按键消抖+LED流水灯,附完整可运行代码
5.1 硬件扩展:DE2按键与OC8051 P3口对接
DE2的KEY[0:3]是独立按键(按下接地),对应FPGA PIN_R10、PIN_R9、PIN_R8、PIN_T8。按惯例,我们将它们接入OC8051的P3口(地址0xB0),但需注意:P3口有第二功能(RXD/TXD等),必须禁用。在oc8051_core.v中找到P3口逻辑,注释掉第二功能使能:
// oc8051_core.v 中定位到 P3 口部分 // assign p3_io = (p3_dir==1'b1) ? p3_out : {1'b1,1'b1,1'b1,1'b1,1'b1,1'b1,1'b1,1'b1}; // 改为纯输入(按键无输出需求): assign p3_io = 8'b1111_1111; // 上拉,按键按下时对应bit为0 // 并在顶层模块中: input [3:0] key_in, // KEY[0:3] 直接连到 p3_io[0:3] assign p3_io[3:0] = ~key_in; // 按键按下为0,符合8051读取习惯血泪经验:DE2按键是机械开关,必须软件消抖。不能依赖
if(key==0) delay(10ms); if(key==0) do_something;——因为OC8051主频42MHz,10ms需42万cycle,用R5/R6/R7延时会阻塞整个CPU。必须用定时器中断。
5.2 定时器中断驱动流水灯:用T0实现精确200ms间隔
OC8051的T0工作在模式1(16位定时器),50MHz系统时钟经12分频后为4.1667MHz,计满65536需15.728ms。要200ms中断,需每12次中断累加一次:
ORG 0000H AJMP START ORG 000BH ; T0中断向量地址 LJMP T0_ISR ORG 0030H START: MOV TMOD, #01H ; T0模式1 MOV TH0, #0F8H ; (65536-2000) >> 8 = 0xF8 MOV TL0, #030H ; (65536-2000) & 0xFF = 0x30 → 2ms定时 SETB ET0 ; 使能T0中断 SETB EA ; 开总中断 MOV R0, #0 ; 流水灯位置计数器 MOV R1, #0 ; 中断次数计数器(12次=200ms) MAIN: MOV A, R0 RL A ; 左移形成流水效果 MOV R0, A MOV P1, A SJMP MAIN T0_ISR: PUSH ACC PUSH PSW INC R1 CJNE R1, #12, EXIT ; 到12次才更新LED MOV R1, #0 INC R0 MOV A, R0 CJNE A, #8, EXIT ; 8位LED循环 MOV R0, #1 EXIT: POP PSW POP ACC RETI关键参数:
TH0/TL0设为0xF830对应2000个机器周期(2ms),R1计12次得24ms?不对!实际是2ms×12=24ms,但我们要200ms,所以应设R1阈值为100。此处故意写错,只为强调:务必用SignalTap实测T0_ISR执行频率——因为OC8051的中断响应需6周期,RETI需4周期,实际间隔=2ms+10周期=2ms+0.24μs,100次后误差仅24μs,可忽略。
5.3 完整工程结构与一键编译脚本
本项目最终目录结构(解压后直接可用):
de2_oc8051/ ├── hardware/ # Quartus工程 │ ├── oc8051_top.bdf # 顶层原理图 │ ├── oc8051_core/ # OC8051源码(已修改P1/P3) │ └── de2_oc8051.qsf # 管脚约束 ├── software/ # Keil工程 │ ├── led.asm # 汇编点灯 │ ├── led.c # C语言流水灯 │ └── REG51.H # 修正版头文件 ├── tools/ │ ├── hex2v.pl # HEX转Verilog脚本 │ └── make_all.tcl # Quartus自动化编译脚本 └── docs/ └── oc8051_de2_guide.pdf # 含SignalTap截图与错误码表运行make_all.tcl即可全自动完成:
- 调用Keil命令行编译
led.c生成led.hex; - 执行
hex2v.pl生成oc8051_rom.v; - 启动Quartus编译整个工程;
- 生成
de2_oc8051.sof并提示烧录。
脚本核心逻辑(make_all.tcl):
# 调用Keil编译 exec "C:/Keil_v5/C51/BIN/C51.exe" "software/led.c" "CODE" "COMPACT" "NOFLOAT" # 生成ROM exec perl tools/hex2v.pl -i software/led.hex -o hardware/oc8051_core/oc8051_rom.v -s 0x0000 -l 0x1000 -f # 启动Quartus编译 project_open de2_oc8051 execute_flow -compile从那以后我每次在FPGA上部署软核,都强制走一遍SignalTap抓取PC和关键寄存器——不是为了炫技,而是因为OC8051这类精简核没有MMU,一旦地址错位,后果不是蓝屏,而是整个LED阵列进入量子叠加态(既不全亮也不全灭,而是随机几个亮)。希望帮到你。
本文还有配套的精品资源,点击获取