ARM核RTL实战:Verilog/VHDL仿真、综合与FPGA验证指南
2026/9/19 9:16:52 网站建设 项目流程

简介:ARM IP核的完整RTL源码包,面向FPGA/ASIC设计工程师、嵌入式系统开发者及数字电路学习者,集中展示ARM处理器内核的寄存器传输级硬件描述。压缩包共45个文件,其中41个vhd文件构成处理器核心逻辑主体,另含C参考模型源文件(.c/.h)、配置文件和备份文件,总大小仅74KB,内部模块划分清晰,适合逐文件研读数据通路、控制状态机及总线接口设计。目前已有224人学习浏览。源码覆盖取指、译码、执行、访存等流水线阶段,还包括缓存、协处理器接口、乘法器/移位器等关键组成,配合C模型可辅助功能仿真与结果比对。这些可综合的VHDL代码既可直接用于FPGA原型验证,也可作为ASIC设计初期的参考实现,帮助有数字电路基础的工程师从寄存器级理解ARM微体系结构的实现细节,快速定位关键路径、定制IP功能,并为SoC集成及教学科研提供可复用的起点。

1. ARM核RTL没你想的那么远,但也没那么近

写这个标题所指向的,大多是这么一件事:想看到、想跑起来、甚至想在FPGA上验证一份ARM处理器的Verilog/VHDL源码。ARM核的完整RTL并不是能随便拿到的东西,商业许可和NDA把它封得严实;能放开的是两个层次:一是ARM官方以学习为目的放出的DesignStart教学RTL(Cortex-M0/M3),二是社区和外企工程师维护的类ARM教学核,多为ARM7/ARM9指令集兼容。标题里那句"arm.rar"大概率就是某个这类包的代称,里面常见内容是RTL源码、仿真脚本、触发器级soc壳和一份说明文档。

对着这类东西,最有价值的动作不是下载解压,而是把"这份RTL能综合到多快""接口怎么对接自有逻辑""跑一个基准程序看性能"这三件事做出来。正文假定你已经会用Vivado/Quartus其中一个,具备Verilog基础,VHDL的经验会在对应章节单独给映射。整篇文章不讲架构课,只讲怎么把ARM核的RTL变成能流片或能上板的现实分系统。

2. 用Verilog和VHDL把ARM核的RTL跑进仿真器:最小环境却不止两步

2.1 先看解压出来的东西里到底有什么

常见的包结构下,里面至少涵盖下面几类,先说规律:

  • 一个顶层,通常叫arm_corecortex_m_top,对外是AMBA总线接口(AHB-Lite或AXI4),带复位、时钟、中断向量。
  • 一堆集中定义参数的全局文件,Verilog里常见define.vconfig_parameters.svh,VHDL侧则是一组package
  • 仿真支撑目录:testbench、memory model、bootloader RAM初始化文件(.hex/.mem)。
  • 综合用的约束模板,以及一个Vivado/Quartus版本的Tcl脚本目录。

标题里同时出现Verilog和VHDL,说明目标是要用自己的IP库做双语适配。常见的做法是:仿真统一用iverilog或Questa,VHDL和Verilog混在同一个work库;综合用Vivado时,混合语言项目直接添加两种文件,工具会自动把VHDL的实体封装成Verilog可调模块。所以第一步先把两种语言文件分目录放,别把VHDL的.vhd和Verilog的.v放同一个文件列表里,那样后仿真会疯狂引错模块。

2.2 最小验证:一个能显示PC和总线读写的testbench

下面这段代码是一个标准AHB-Lite主机侧的总线监视器。套到核顶层的对外接口上,立刻就能看到PC的变化沿着boot地址增长。

`timescale 1ns/1ps module ahb_monitor ( input hclk, input hresetn, input hsel, input [31:0] haddr, input [1:0] htrans, input hwrite, input [31:0] hwdata, input [31:0] hrdata ); reg [31:0] last_addr; integer f; initial begin f = $fopen("bus_trace.log", "w"); if (f == 0) $display("ERROR: cannot open bus_trace.log"); end always @(posedge hclk or negedge hresetn) begin if (!hresetn) begin last_addr <= 0; end else begin if (hsel && htrans[1]) begin if (hwrite) begin $fwrite(f, "%0t WR addr=0x%08x data=0x%08x\n", $time, haddr, hwdata); end else begin $fwrite(f, "%0t RD addr=0x%08x data=0x%08x\n", $time, haddr, hrdata); last_addr <= haddr; end end end end final $fclose(f); endmodule

这段代码挂在ARM核顶层的AHB端口上,每一拍只要有有效事务就完整打印总线读写。htrans2'b10说明是NONSEQ事务,2'b11是SEQ连续突发;总线Trace里出现大量重复PC地址就说明代码卡在循环里。$fwrite按行把波形以外的关键信息落盘,比全程依赖GTKWave看波形要可靠,仿真跑几十万周期时检索效率高得多。

2.3 VHDL侧的门槛:库、封装和子模块映射

混用VHDL与Verilog时,得先处理VHDL的 package 和 library 声明。常见做法是让VHDL文件都引用同一个work库,但iverilog对VHDL的语法支持很有限,实测有包定义和泛型映射就会报错。我一般这么分:

  • 纯模块验证用 iverilog 跑 Verilog 文件,VHDL只用于后仿或门级验证;
  • 真正的混仿用 Vivado/VCS,靠read_vhdl把VHDL文件读进来,再让Verilog顶层通过模块名直接实例化;
  • VHDL的vector端口在跨语言边界时会转成[31:0],方向要预先对着实体声明映射,别在顶层再包一层转换胶水。

综合时,Vivado会用synth_design -top arm_core_top这样的命令自顶而下推断,VHDL的std_logic_vector和Verilog的wire在端口级自动做宽度扩展。最容易踩的坑是位序:VHDL里(0 to 31)(31 downto 0)声明顺序不同,综合出的总线在仿真里恰好反序,能让人对着波形查半小时。无论是AHB还是APB,统一采用big endian命名的信号,写约束时也用同样的位宽表达式,这个问题可以从根上规避。

3. 拆开ARM核的RTL:流水线、AHB/AXI和中断响应在哪能动手

3.1 流水线级:五个阶段的信号走向

在看完仿真结果之后,可预期的下一步是找PC的走向。大多数教学级ARM核把流水线拆成 IF/ID/EX/MEM/WB 五级,Verilog里对应下面这些互相连接的寄存器堆。实际得先在编译选项或宏定义里搜PIPE_EN这个词,不少核里把它做成可综合参数,关掉后变成单周期直通,是逻辑排错时的好开关。

reg [31:0] fetch_pc; // IF阶段 取指地址,复位后指向复位向量 reg [31:0] dec_pc; // ID阶段 当前指令PC reg [31:0] ex_alu_out; // EX阶段 运算结果 reg [31:0] mem_rdata; // MEM阶段 内存读回 reg [31:0] wb_result; // WB阶段 回写寄存器堆的值

如果ex_alu_out长时间不变,多半是译码阶段的立即数扩展向量拼错。典型案例如THUMB立即数在inst[7:0],需要在8位段内做移位扩展,VHDL里需要一个小模块专门处理extend_imm。在流水线上动手时,把fetch_pcdec_pc单独拉出来跨模块观察,能直接规避满屏信号名的干扰。我一般会写一个pipeline_stage的宏开关,在仿真时输出每级有效标志,这样任何一级停顿都能在波形里一眼洞穿。

3.2 总线协议选型:AHB-Lite还是AXI4,用对比表说话

教学级核对外接口多数是AHB-Lite,但如果要把核挂到Zynq的AXI总线上,就得加一段桥接逻辑。下表列出在ARM核RTL实现里两种接口的取舍维度,按实际工程经验给出,没有唯一标准答案。

维度AHB-LiteAXI4
事务粒度单拍或简单burst通道分离,支持乱序返回
握手信号HREADY/HREQ/HADDRAW/W/AR/R 五通道 VALID/READY
写回数据同一拍16/32位数据数据通道可交叠地址通道
RTL复杂度中,容易状态机化高,需要独立读/写状态机
接DDR/DMA需要额外桥原生匹配,接口直接
多主机场景需仲裁矩阵支持原生支持,协议层解决

选择逻辑很简单:若核作为唯一主机,用AHB-Lite最少出问题;若板上有多核或DMA参与,就选AXI4,因为读返回通道和写通道可以同时响应,显著减少总线等待。接口改动时我一般在RTL中做一个状态机,专门负责把命令排队拆分。文件里没有现成桥接模块时,下面这个状态机可以处理AHB-Lite到AXI读通道的搬运:

localparam S_IDLE = 2'b00, S_REQ = 2'b01, S_DATA = 2'b10; reg [1:0] mst_state; reg burst_req; reg [3:0] burst_cnt; always @(posedge hclk or negedge hresetn) begin if (!hresetn) begin mst_state <= S_IDLE; burst_req <= 1'b0; end else begin case (mst_state) S_IDLE: if (hbusreq) begin mst_state <= S_REQ; burst_req <= 1'b1; end S_REQ: if (hgrant && hready) begin mst_state <= S_DATA; burst_cnt <= 4'h0; end S_DATA: if (hready && htrans == 2'b11) begin burst_cnt <= burst_cnt + 1'b1; if (burst_cnt == 4'd15) mst_state <= S_IDLE; end default: mst_state <= S_IDLE; endcase end end

这段状态机的关键点:hgrant只在仲裁授权的那一拍有效,不能当持久信号用;hready拉低代表从机还没准备好,此时htranshaddr必须保持,写数据通道也一样。状态机一旦收到hresp=2'b01(ERROR响应),就应当置起haddr上的错误标志,别在死循环里反复重试。

3.3 中断与异常向量:连到NVIC还是直接挂CPU?

在简化核里,中断一般做成一条nmi和若干外部中断请求线,直接跳入向量表对应地址。故障排查时先确认复位向量处的跳转目标写对没有;再看CPSRI/F位是否被自动置位,异常屏蔽时有中断请求进不来,PC自然不动;最后抓取中断采样点到译码阶段的延迟,若超过3个周期基本是门控时钟没对齐。

VHDL侧的做法是把中断控制器独立成一个实体,内部保持pending[15:0]寄存器,通过通用总线写读取。好处是核间中断域天然隔离,软件写清中断位时也不容易误改内核寄存器。要做出GIC那样带优先级分派的效果,那是芯片级工程师较后期的事,当前直接用priority_encoder实现固定最高优先级触发即可。

4. 验证与综合:AXI读写吞吐、时序收敛和FPGA跑通的组合拳

4.1 验证:不只跑波形,还要用断言抓协议违例

RTL的testbench能跑完全部用例不等于总线没毛病。我至少放三类验证资源:

  • 功能定向测试:覆盖通用寄存器搬移、跳转、异常返回等基本指令,重点检查寄存器堆写回阶段的数据正确性;
  • 总线协议断言:在AHB/AXI接口上,用SVA捕获hsel && !hready && htrans==2'b10之类的典型错误——无事务时htrans应为2'b00,被采样到IDLE下的非零值就直接断言失败;
  • 随机延迟:给流水线控制信号做随机化延迟生成,观察PC是否保持在RAM映射区间内,防止地址越界。

硬件验证里最笨但最有效的手段是:用GCC交叉编译出的固件跑覆盖率,把RTL流水线状态与QEMU的指令级输出逐拍对齐。QEMU执行到某一指令的PC,和RTL里fetch_pc的PC放一起比对,能快速锁定时序错误的位置。另外还有一项建议:写一个协议检查器,专门统计AXI通道上AWVALIDWVALID同时拉高的周期占比,这个指标能间接反映流水线是否够宽。

4.2 综合命令与约束:时序上不去时先看这两行

给Vivado跑综合之前,要先把顶层和约束理顺。常用的Tcl流程是:

read_verilog [glob rtl/*.v] read_vhdl [glob rtl/*.vhd] read_xdc constraints/top.xdc synth_design -top arm_core_top -part xc7a35tcsg324-1 report_timing_summary -max_paths 20 report_utilization

约束里最重要是时钟约束和输入输出延迟:

create_clock -period 10.0 -name sys_clk [get_ports clk] set_input_delay -clock sys_clk 5.0 [get_ports ext_resetn] set_output_delay -clock sys_clk 4.0 [get_ports txd]

-period 10.0代表目标频率100MHz;set_input_delay 5.0表示外部复位从时钟沿到复位PIN的延迟。如果用手拨开关复位,延时基本为0,设大了反而骗时序分析器,导致布局布线偏向乐观。综合出的资源占用通常是:ARM核加桥加内存控制器总面积占Artix-7的20%上下,LUT紧张时先查report_utilization里的BRAM是否被RAM初始化文件挤占。

4.3 FPGA跑通:下载完不亮灯时的排查顺序

板上跑起来后先看两个信号:复位释放后hclk上空闲周期是否超过预计,PC是否停在一个循环地址。排查顺序固定为:

  1. 检查时钟复位:示波器看晶振输出,确认clk引脚没有因为IO标准设错而打不出来;
  2. 检查RAM加载:把固件的起始地址重新对一遍,确认链接脚本的ORIGIN$readmemh的填充起点一致;
  3. 检查总线状态:用ILA抓haddr的2到3根高位总线,看到活跃读地址说明CPU已在取指,没有则优先怀疑时钟门控。

这一步是很多调不通案例的真相:ROM里固件没加载进去,仿真能跑是因为testbench用initial灌了数据,综合后SRAM里面是空的,FPGA一上电PC自然乱飞。加上串口打出来的字符是乱码而不是预期字符串,基本就是这个原因。

5. 进阶玩法:在RTL仿真里跑起CoreMark,再用断言盯住折叠总线

跑基准程序是对ARM核RTL最直接的体检。CoreMark不像Dhrystone那样对编译器优化太敏感,且能在仿真器里跑完整执行周期,作为RTL层面的冒烟标准足够严谨。

实际操作完全依赖前面搭好的仿真加固件链路:把CoreMark源码用arm-none-eabi-gcc交叉编译,注意-O2等级下函数的栈对齐要求,链接脚本里栈指针符号指向RAM高地址,避免和.data段重叠。仿真时若出现PC跑到0xDEADBEEF之类的地址,先确认CoreMark的打印函数不是依赖UART外设,而是直接写入模拟stderr缓冲——否则核既没有外设也永远不会输出分数,只能一直空转。

真正有价值的技巧是把CoreMark每个子项的PORT_GET_RUN_DATA返回值拿出来,每个子项是一个CRC32校验值,与官方参考值逐项对比即可判定RTL实现是否正确。这一步跑通后,写一条AXI事务断言看连续读的burst是否在固定周期内返回;返回超时而写通道没有后续请求,多半是memory controller片选映射没覆盖外设区。

最后提供一个压箱底的小技巧:很多调完的ARM核在综合后跑功能仿真时暴露出IPC(每周期指令数)低的问题。先在波形里统计空闲周期占总周期的百分比,超过30%就去宏配置文件里找BRANCH_PRED这个开关,很多教学级核里藏着简单的2bit分支预测器,打开后能把IPC从0.6拉回0.8,代价只是几十个LUT,够划算。看波形时优先对齐fetch_validexecute_ready两个沿,先看这两个信号再看数据通路,这个核对不对,一眼就知道。

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

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

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

立即咨询