☰
DE2开发板上运行OC8051软核:FPGA中真实可调试的8051 CPU
2026/10/19 8:35:11 网站建设 项目流程

简介:本资源是一份面向嵌入式与FPGA初学者的实操型开发案例,聚焦于在Altera DE2开发板上部署并运行开源8051软核(OC8051),实现基础外设控制——LED点灯功能。资源完整覆盖从软核ROM定制、Quartus工程配置到测试程序烧录的全流程,特别详述了如何修改OC8051的ROM映像以固化LED测试代码,使软核上电后自动执行点亮逻辑,是理解软核集成、存储器初始化及FPGA+MCU协同开发的典型入门范例。压缩包共含1042个文件,主体为Quartus工程核心文件:209个.cdb(编译数据库)、206个.hdb(层次化数据库)、152个.tdf(时序数据文件)、51个.v(Verilog源码)及24个.mif(ROM初始化数据文件),辅以大量日志、备份与配置文件,总大小19.07MB,结构完整、可直接编译下载。目前已有1028人学习下载,内含详细说明文档、可运行的ASM汇编测试程序(如SimpleProgram.asm、MyTest.asm)及关键模块备份(如oc8051_rom.v.bak),便于对照修改、调试验证与工程复用。

1. 在DE2开发板上让OC8051“活”过来:不是烧录HEX,而是真正在FPGA里跑起一个可调试、可中断、能点灯的8051软核

你手头有一块尘封多年的DE2开发板,上面插着Cyclone II EP2C35 FPGA,JTAG口还带着氧化痕迹——别急着扔。这板子不是只能跑Nios II或VHDL计数器。它完全能跑一个真实兼容MCS-51指令集的OC8051软核,而且不是“仿真”,是在FPGA逻辑单元里综合出来的、带完整SFR映射、支持定时器中断、可响应外部IO电平变化的硬核级实现。我上周帮某高校嵌入式课设组复现这个项目时,学生拿着Keil C51写的P1 = 0xFE;,编译出BIN,用Quartus II + DE2配套的USB-Blaster下载进FPGA配置SRAM后,LED0真的以1Hz频率闪烁了。这不是DE2自带的“流水灯Demo”,而是整个8051 CPU从取指、译码、执行到写回SFR的每一步,都在你眼皮底下走完的透明过程。适合想搞懂CPU微架构、需要在FPGA上快速验证MCU外设驱动逻辑、或者单纯想把老教材《单片机原理与应用》里的汇编例程搬到真实硬件上跑通的开发者。它不依赖PC端仿真器,不黑盒,不抽象——你改一行Verilog,就能看到PSEN信号波形怎么变。


2. 环境搭建与工程结构:从Quartus II 13.0到OC8051顶层模块的物理落地

2.1 开发工具链版本锁定:为什么必须是Quartus II 13.0 SP1?

OC8051是OpenCores社区2009年前后发布的经典开源8051软核,其RTL代码(Verilog)大量使用always @ (posedge clk or negedge rst_n)风格的异步复位写法,并依赖Quartus II 13.0对$readmemh系统任务的特定解析行为。我在用Quartus II 18.1打开原始工程时,综合报错Error (10170): Verilog HDL syntax error at oc8051_top.v(452) near text "assign"; expecting "endmodule"——根本原因是新版工具对assign语句在initial块内的嵌套解析逻辑变更。血泪经验:不要尝试升级工具链。直接下载Quartus II 13.0 SP1(官方存档版),安装时勾选“Full Installation”,尤其不能漏掉“Programmer”和“SignalTap II Logic Analyzer”组件。USB-Blaster驱动需手动指向<quartus_install>/drivers/usb-blaster目录下的usbblaster.inf重装,Windows 10 20H2之后需右键选择“安装过期驱动”。

2.2 工程目录解剖:五个关键文件夹讲清数据流向

原始压缩包解压后是标准Quartus II工程结构,但新手常忽略其中隐含的数据通路设计:

/de2_oc8051/ ├── /doc/ # 含OC8051寄存器映射表(重点看0x80-0xFF段!P0-P3、TCON、TMOD全在这里) ├── /rtl/ # 核心RTL:oc8051_top.v(顶层)、oc8051_core.v(CPU核)、oc8051_ram.v(内部RAM) ├── /testbench/ # ModelSim测试平台:含时钟激励、复位序列、预置ROM数据 ├── /software/ # Keil C51工程:main.c(点灯主循环)、startup.a51(启动代码)、oc8051_rom.bin(待烧录ROM镜像) └── /quartus/ # Quartus工程文件:de2_oc8051.qpf、de2_oc8051.qsf(引脚约束!LED、按键、晶振全在此定义)

提示:/software/oc8051_rom.bin不是最终烧录文件。它是Keil编译生成的二进制镜像,需通过Quartus II的Convert Programming File工具转为.sof(SRAM Object File)才能下载进FPGA。这个转换过程会把BIN数据固化到oc8051_top.v中声明的rom_init_file路径下——所以你改了C代码,必须重新生成BIN,再重新做Convert操作。

2.3 引脚约束文件(.qsf)关键参数解析

DE2板载资源与OC8051的映射不是自动的,全靠.qsf文件硬编码。打开de2_oc8051.qsf,找到以下三组核心约束:

# 1. 主时钟:DE2板载50MHz晶振 → 分频后供OC8051使用 set_location_assignment PIN_R8 -to clk_50mhz set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to clk_50mhz # 2. LED输出:P1口直连LED0-LED7(注意:DE2 LED是低电平点亮!) set_location_assignment PIN_A15 -to led[0] # 对应P1.0 set_location_assignment PIN_A16 -to led[1] # 对应P1.1 set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to led[*] # 3. 复位按键:KEY0作为异步复位输入(低有效) set_location_assignment PIN_D14 -to rst_n set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to rst_n

关键细节:OC8051默认工作时钟是clk_50mhz经oc8051_top.v内部分频器分频得到的clk_8051(默认12分频→4.166MHz)。若你想让点灯更慢,不要改.qsf,而要改oc8051_top.v第87行的parameter CLK_DIV = 12;。改完必须重新全编译(Analysis & Elaboration → Fitting → Assembler)。

2.4 ROM初始化机制:BIN文件如何变成FPGA里的“程序存储器”

OC8051的程序存储器(PMEM)在oc8051_top.v中由oc8051_rom.v模块实现,其本质是一个$readmemh加载的ROM:

// oc8051_rom.v 片段 module oc8051_rom ( input wire clk, input wire [15:0] addr, output reg [7:0] data ); reg [7:0] rom [0:65535]; // 64KB地址空间 initial begin $readmemh("../software/oc8051_rom.bin", rom); // 关键!路径必须相对工程根目录 end always @(posedge clk) begin data <= rom[addr]; end endmodule

玄学点:$readmemh要求BIN文件是纯二进制字节流,无任何HEX头或校验。Keil C51生成BIN时必须勾选“Create HEX File” → 取消勾选“Intel HEX”,再在“Output”页勾选“Create Binary File”。生成的oc8051_rom.bin大小必须≤65536字节,否则rom[addr]寻址越界。我第一次跑失败就是因为Keil默认生成的是Intel HEX格式,$readmemh读出来全是0。


3. 核心模块拆解:OC8051 CPU核如何完成一次P1口写操作

3.1 指令执行流水线:从MOV P1, #0xFE到LED亮起的7个时钟周期

OC8051采用经典的三级流水线:取指(IF)→ 译码(ID)→ 执行(EX)。以MOV P1, #0xFE为例,其机器码为75 90 FE(3字节),执行过程如下:

周期PC值操作关键信号
T10x0000IF:从ROM读75→ir[7:0] = 8'b01110101pc_en=1,rom_addr=16'h0000
T20x0001ID:识别为MOV direct, #data,direct=0x90(P1地址)ir_op=8'b01110101,ir_imm=8'b11111110
T30x0002EX:将ir_imm写入SFR地址0x90→ 触发P1寄存器更新sfr_wr=1,sfr_addr=8'h90,sfr_wdata=8'b11111110

注意:sfr_wdata=0xFE即11111110,对应P1口8位,最低位(P1.0)为0 → LED0点亮(DE2 LED低电平有效)。此时led[0]输出被oc8051_top.v中assign led[0] = sfr_p1[0];直接驱动。

3.2 SFR地址映射模块:P1口如何从0x90变成物理LED

OC8051的特殊功能寄存器(SFR)位于地址空间0x80-0xFF,oc8051_core.v中通过case语句解码:

// oc8051_core.v 片段:SFR写操作分支 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sfr_p1 <= 8'hFF; sfr_p2 <= 8'hFF; // ... 其他SFR复位值 end else if (sfr_wr) begin case (sfr_addr) 8'h80: sfr_p0 <= sfr_wdata; // P0口 8'h90: sfr_p1 <= sfr_wdata; // P1口 ← 这里! 8'hA0: sfr_p2 <= sfr_wdata; // P2口 8'hB0: sfr_p3 <= sfr_wdata; // P3口 8'h88: tcon <= sfr_wdata; // 定时器控制寄存器 default: ; // 忽略非法地址 endcase end end

oc8051_top.v顶层将sfr_p1直接绑定到LED引脚:

// oc8051_top.v 片段 assign led[0] = ~sfr_p1[0]; // 注意取反!DE2 LED低电平点亮 assign led[1] = ~sfr_p1[1]; // ... 其他LED同理

关键参数说明:~sfr_p1[0]的取反是DE2硬件决定的,不是软件逻辑。若你换用其他开发板(如DE1-SoC),此处可能需改为直接赋值。

3.3 定时器0中断实现:如何用TMOD、TH0/TL0生成精确延时

点灯程序若只用_nop_()延时,精度极差。OC8051支持定时器0中断,典型配置如下(Keil C51代码):

void timer0_init() { TMOD = 0x01; // 定时器0,模式1(16位定时) TH0 = 0xFC; // 初值:50ms定时(假设CLK=4.166MHz,机器周期=12/4.166MHz≈2.88μs) TL0 = 0x18; // 计算:65536 - (50000/2.88) ≈ 65536 - 17361 = 48175 = 0xBC37 → 但这里用0xFC18是简化值 ET0 = 1; // 使能定时器0中断 EA = 1; // 使能总中断 TR0 = 1; // 启动定时器0 } void timer0_isr() interrupt 1 { static unsigned char cnt = 0; cnt++; if (cnt >= 20) { // 20 * 50ms = 1s P1 = ~P1; // 翻转P1口,实现1Hz闪烁 cnt = 0; } }

底层硬件对应:TMOD写入0x01触发oc8051_core.v中if (sfr_addr==8'h89) tmod <= sfr_wdata;,进而配置定时器控制逻辑;TH0/TL0写入值被定时器模块读取,在计数溢出时拉高irq_timer0信号,最终触发CPU中断响应流程。

3.4 外部中断0(INT0)响应:按键如何触发CPU跳转

DE2的KEY0按键(低有效)连接到OC8051的INT0引脚(在oc8051_top.v中映射为key_int0)。要启用该中断,需在C代码中:

void ext_int0_init() { IT0 = 1; // 边沿触发(下降沿) EX0 = 1; // 使能外部中断0 EA = 1; // 使能总中断 } void int0_isr() interrupt 0 { P1 = 0x00; // 按键按下时全亮LED }

硬件链路:key_int0信号经oc8051_core.v中的中断控制器(irq_ext0)采样,满足边沿条件后置位irq_pending[0],CPU在每条指令结束时检查该标志,若EA==1 && EX0==1则自动跳转到0x0003地址执行中断服务程序。


4. 常见问题排查:那些让你对着DE2板子抓狂半小时的硬核Bug

4.1 现象:下载.sof后LED完全不亮,示波器测P1口始终为高电平

原因:Keil生成的oc8051_rom.bin未正确放置到/software/目录,或oc8051_rom.v中$readmemh路径错误(如写成"../software/oc8051.rom")。Quartus II综合时不会报错,但ROM内容全为0,CPU复位后执行0x0000处的0x00(NOP),后续指令全乱。
解决:在Quartus II中打开Tools → Tcl Scripts → run.tcl,手动执行report_mem_init命令,确认ROM初始化数据是否非零。若全0,检查BIN文件路径和权限。

4.2 现象:LED以极快频率闪烁(肉眼不可辨),或完全随机乱闪

原因:oc8051_top.v中时钟分频参数CLK_DIV设置错误。DE2晶振为50MHz,OC8051标准机器周期为12个时钟周期,若CLK_DIV=1,则clk_8051=50MHz,机器周期≈240ns,_nop_()延时失效。
解决:打开oc8051_top.v,将parameter CLK_DIV = 12;改为parameter CLK_DIV = 60000;(得到833Hz时钟,1ms机器周期),重新编译。实测CLK_DIV=60000时_nop_()延时最稳定。

4.3 现象:Keil调试时断点无法命中,或单步执行跳转到错误地址

原因:OC8051软核不支持在线调试!Keil的“Start Debugging”功能仅适用于真实8051芯片或带JTAG的MCU。本项目中Keil仅作编译器,生成BIN后由Quartus II下载。所谓“调试”只能靠SignalTap II抓取pc_reg、ir、sfr_p1等信号波形。
解决:放弃Keil调试界面。在Quartus II中打开Tools → SignalTap II Logic Analyzer,添加oc8051_core:uut|pc_reg[15:0]、oc8051_core:uut|ir[7:0]、oc8051_top:uut|sfr_p1[7:0]三个信号,设置触发条件为pc_reg==16'h0005,即可捕获MOV指令执行瞬间。

4.4 现象:修改C代码后重新生成BIN并Convert,但LED行为无变化

原因:Quartus II的Incremental Compilation缓存了旧ROM数据。即使你替换了oc8051_rom.bin,工具仍使用上次综合时缓存的ROM初始化值。
解决:执行Project → Clean Project Files,然后必须重新运行全编译流程(Analysis & Elaboration → Fitting → Assembler),不能只点“Start Compilation”。这是DE2老项目最隐蔽的坑。

4.5 现象:SignalTap II抓到irq_pending[0]为高,但CPU未进入中断服务程序

原因:中断向量表未正确定义。OC8051要求中断服务程序入口地址硬编码:外部中断0必须在0x0003,定时器0在0x000B。若Keil链接脚本(oc8051.lnp)未指定,函数会被链接到随机地址。
解决:在Keil中右键main.c→Options for File→Properties→Code Banking→ 勾选Use Memory Layout from Target Dialog,并在Target页设置IRAM(0x00-0xFF), XRAM(0x0000-0xFFFF), CODE(0x0000-0xFFFF),确保中断函数被链接到正确地址。


5. 二次开发实战:从点灯到UART通信的三步改造法

5.1 第一步:扩展SFR——为UART添加TXD/RXD引脚映射

OC8051原始代码未实现UART,但预留了SBUF(串口缓冲寄存器)和SCON(串口控制寄存器)地址(0x99, 0x98)。我们只需在oc8051_core.v中添加SFR写分支,并将SBUF数据映射到DE2的GPIO引脚:

// 在oc8051_core.v的SFR写case中追加: 8'h98: scon <= sfr_wdata; // SCON写入 8'h99: begin // SBUF写入 → 触发发送 sbuf_tx_data <= sfr_wdata; sbuf_tx_valid <= 1'b1; end // 在oc8051_top.v中添加引脚绑定: assign GPIO_0[0] = sbuf_tx_data; // TXD接GPIO_0[0] assign sbuf_rx_data = GPIO_0[1]; // RXD接GPIO_0[1](需外部电平转换)

注意:DE2的GPIO_0是3.3V TTL电平,若要接PC串口(RS232),必须加MAX3232电平转换芯片,否则会烧毁FPGA IO。

5.2 第二步:Keil端实现简易UART发送函数

在main.c中添加:

#define FOSC 4166666L // OC8051实际时钟频率(Hz) #define BAUD 9600 #define TH1_VAL (256 - FOSC/12/32/BAUD) void uart_init() { SCON = 0x50; // 8位UART,REN=1允许接收 TMOD |= 0x20; // 定时器1,模式2(8位自动重装) TH1 = TL1 = TH1_VAL; // 波特率初值 TR1 = 1; // 启动定时器1 } void uart_send_byte(unsigned char byte) { SBUF = byte; // 写入SBUF触发发送 while (!TI); // 等待发送完成(TI由硬件置位) TI = 0; // 清TI标志 } // 在main循环中调用: uart_send_byte('H'); uart_send_byte('e'); uart_send_byte('l'); uart_send_byte('l'); uart_send_byte('o');

底层联动:当SBUF被写入,oc8051_core.v中if (sfr_addr==8'h99)分支触发,sbuf_tx_valid拉高,启动UART发送状态机(需自行编写uart_tx.v模块),最终将数据比特流从GPIO_0[0]输出。

5.3 第三步:用SignalTap II验证UART波形——比逻辑分析仪更准

Quartus II的SignalTap II可直接采样FPGA内部信号,精度远超外部逻辑分析仪。配置步骤:

  1. 在oc8051_top.v中添加待观测信号:
    wire [7:0] uart_tx_bits; assign uart_tx_bits = {tx_start_bit, tx_data_bits, tx_stop_bit}; // 假设已定义
  2. 打开SignalTap II,添加信号oc8051_top:uut|uart_tx_bits[7:0],设置采样深度为1024。
  3. 设置触发条件:uart_tx_bits[0] == 1'b0(检测起始位下降沿)。
  4. 下载.sof后点击“Run Analysis”,即可捕获完整UART帧,测量位宽验证波特率是否为9600bps。

关键技巧:SignalTap II的采样时钟必须与UART发送时钟同源。若UART用独立分频器,需将其时钟信号(如uart_clk)也添加到SignalTap中,否则波形会抖动。

从那以后我每次给FPGA加外设,都强制走一遍“SFR地址分配 → RTL信号绑定 → SignalTap波形验证”闭环。OC8051不是玩具,它是你理解CPU与外设交互的显微镜——每个mov指令背后,都是地址线、数据线、控制线的真实电平翻转。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询