2026/8/7 3:46:21

HC32F4A0 EXMC与FPGA并口通信:从ARM驱动到Verilog状态机全解析

HC32F4A0 EXMC与FPGA并口通信:从ARM驱动到Verilog状态机全解析 1. 项目概述与核心价值最近在做一个嵌入式与FPGA协同处理的项目主控用的是华大半导体的HC32F4A0系列ARM芯片需要和一块Xilinx的Artix-7 FPGA进行高速数据交换。数据量不小用SPI或者I2C这种串行总线带宽不够用DMA搬内存又太占CPU资源。翻看HC32F4A0的数据手册发现它内置了一个叫EXMCExternal Memory Controller的外部存储器控制器支持类似8080或6800的并行总线协议这玩意儿不就是为这种场景量身定做的吗EXMC可以直接以访问外部SRAM或NOR Flash的方式通过一组并口数据线、地址线、控制线与FPGA通信时序规整速率可观简直是芯片间通信的“物理外挂”。但这个“外挂”用起来并不像调用一个库函数那么简单。ARM端的EXMC控制器配置只是故事的一半另一半在FPGA这边FPGA需要扮演一个“虚拟的”外部存储器准确无误地响应ARM端EXMC发出的每一个读写时序。时序对不上轻则数据错乱重则系统死锁。网上关于HC32F4A0 EXMC应用的资料零零散散更别提用FPGA来实现从设备接口的完整方案了。踩过几次坑后我决定把从芯片手册解读、ARM端驱动配置到FPGA端Verilog状态机实现的完整流程梳理出来。如果你也在为ARM和FPGA之间的高速数据通道发愁希望这篇结合了芯片手册、驱动代码和RTL设计的实战记录能帮你省下大量调试时间。2. HC32F4A0 EXMC接口深度解析2.1 EXMC模块功能与模式选择HC32F4A0的EXMC模块功能相当强大它并非一个单一接口而是一个支持多种存储类型的控制器主要包括NOR Flash/PSRAM控制器这就是我们实现并口通信要用的模式。它支持异步SRAM-like的协议也就是我们常说的8080或6800时序。NAND Flash控制器带有ECC校验用于连接大容量NAND存储。SDRAM控制器用于连接外置SDRAM扩展系统内存。我们的目标是实现芯片间通信因此核心是NOR/PSRAM控制器模式。在这个模式下EXMC会将一片特定的物理地址空间例如0x60000000~0x6FFFFFFF的访问转化为一组并行的控制信号输出到芯片引脚上。对ARM程序员来说操作FPGA就像在读写一段固定的内存数组一样简单直观。注意EXMC的时钟HCLK是AHB总线时钟它的频率直接决定了总线访问的速度。在配置时序参数时所有的时间计算都需要基于这个HCLK周期。务必在芯片时钟树初始化完成后再初始化EXMC模块。2.2 关键时序参数与寄存器配置EXMC的时序可配置性是其灵活性的体现但也带来了配置的复杂性。我们需要关注几个关键寄存器它们定义了读写操作的“节奏”BCR (Bank Control Register) 存储块控制寄存器用于使能存储块、设置数据总线宽度8/16位、设置存储类型NOR/PSRAM和地址/数据复用模式。对于FPGA通信我们通常选择非复用模式即地址线和数据线分开数据宽度根据需求选择16位以提升吞吐量。BTR (Bank Timing Register) 存储块时序寄存器这是配置的核心每一个参数都对应时序图上的一个时间段。DATLAT: 数据保持时间在读取时尤为重要。BUSTURN: 总线周转时间用于读写操作切换时总线的释放时间。CLKDIV: 时钟分频在异步模式下通常设为0或1表示不依赖额外时钟边沿。DATAST: 数据建立时间即地址有效后到读信号有效或写数据有效的时间。ADDSET: 地址建立时间即片选/块选有效后到地址有效的时间。理解这些参数最好的方式就是对照数据手册中的时序图。以读时序为例一个完整的读周期大致是片选有效-等待ADDSET个HCLK周期地址建立-地址有效-等待DATAST个HCLK周期数据建立-读使能有效-FPGA在DATLAT周期内提供稳定数据-读使能无效-片选无效。配置心得初期调试时建议将所有时间参数ADDSET,DATAST,DATLAT都配置得足够大例如都设为0xF让时序非常宽松。先保证功能正确通信稳定。然后再根据FPGA逻辑的响应速度逐步减小这些参数优化通信效率。这能有效区分是逻辑功能错误还是时序紧张导致的亚稳态问题。2.3 引脚映射与硬件连接设计EXMC的信号线数量较多合理的硬件连接是成功的第一步。我们需要在芯片数据手册的“引脚复用”章节将对应的GPIO配置为EXMC功能。主要信号线包括信号类型信号名称方向 (ARM视角)说明地址线A[25:0]输出地址总线宽度可配决定了可寻址空间大小。数据线D[15:0]双向数据总线8位或16位模式。控制线NE[x]/NCE输出片选或块选信号低有效。每个Bank一个。控制线NOE输出输出使能读使能低有效。控制线NWE输出写使能低有效。控制线NBL[1:0]输出字节使能在16位模式下用于选择高/低字节。硬件连接建议地址和数据线直接连接至FPGA对应的IO引脚。控制线NE, NOE, NWE必须连接。它们是FPGA状态机判断当前操作读、写、空闲的核心依据。上拉电阻对于双向数据线D[15:0]在ARM端可以考虑加上弱上拉电阻避免在总线空闲时引脚悬空引入噪声。电平匹配确保HC32F4A0和FPGA的IO电平标准一致通常都是3.3V LVCMOS。如果FPGA Bank电压不同需使用电平转换芯片。布线等长对于高速应用例如HCLK超过50MHz建议将数据线作为一组地址和控制线作为另一组在PCB布线时尽量保证组内等长以减少信号偏移。3. FPGA端接口逻辑设计与Verilog实现FPGA端的核心任务是设计一个“听话”的从设备精准捕捉ARM端EXMC发出的时序并完成相应的数据读取或写入操作。这里的关键是设计一个稳健的状态机。3.1 接口模块定义与输入输出首先我们定义FPGA模块的接口这直接对应着硬件连线。module exmc_slave_interface ( // 全局信号 input wire fpga_clk, // FPGA主时钟用于同步采样 input wire fpga_rst_n, // FPGA复位低有效 // EXMC物理接口信号 input wire [25:0] exmc_addr, // 地址总线 inout wire [15:0] exmc_data, // 双向数据总线 input wire exmc_ne, // 片选低有效 input wire exmc_noe, // 读使能低有效 input wire exmc_nwe, // 写使能低有效 input wire [1:0] exmc_nbl, // 字节使能低有效 // 用户逻辑接口 output reg [25:0] user_addr, // 锁存后的地址输出给用户逻辑 output reg [15:0] user_wdata, // 写入数据输出给用户逻辑 input wire [15:0] user_rdata, // 读取数据从用户逻辑输入 output reg user_wr_en, // 写使能脉冲高有效 output reg user_rd_en, // 读使能脉冲高有效 output reg user_cs // 片选有效指示 );设计要点fpga_clk的选取理论上只要频率足够高至少数倍于EXMC信号变化速率可以用FPGA的任意稳定时钟来采样异步的EXMC信号。但最佳实践是使用一个与EXMC操作频率相匹配的时钟并进行适当的跨时钟域处理。这里为简化我们假设用一个足够快的时钟如100MHz来同步采样所有EXMC输入信号。exmc_data被声明为inout这意味着我们需要在内部处理它的方向三态控制。3.2 核心状态机设计与时序捕捉EXMC的读写周期本质上是一个固定的序列。我们可以用一个状态机来清晰地描述这一过程。// 状态定义 localparam S_IDLE 3b000; // 空闲等待片选有效 localparam S_ADDR 3b001; // 地址建立期 localparam S_READ 3b010; // 读操作期 localparam S_WRITE 3b011; // 写操作期 localparam S_TURN 3b100; // 总线周转期可选用于处理连续操作 reg [2:0] current_state, next_state; // 状态转移逻辑时序部分 always (posedge fpga_clk or negedge fpga_rst_n) begin if (!fpga_rst_n) begin current_state S_IDLE; end else begin current_state next_state; end end // 状态转移逻辑组合部分 always (*) begin next_state current_state; case (current_state) S_IDLE: begin if (!exmc_ne) begin // 片选有效进入操作周期 if (!exmc_nwe) next_state S_WRITE; // 写使能先有效判定为写 else if (!exmc_noe) next_state S_READ; // 读使能先有效判定为读 else next_state S_ADDR; // 仅地址有效等待读写使能 end end S_ADDR: begin if (!exmc_nwe) next_state S_WRITE; else if (!exmc_noe) next_state S_READ; // 否则保持等待控制信号 end S_READ: begin if (exmc_ne) next_state S_IDLE; // 片选无效操作结束 else if (exmc_noe) next_state S_TURN; // 读使能拉高进入周转或等待下次 end S_WRITE: begin if (exmc_ne) next_state S_IDLE; else if (exmc_nwe) next_state S_TURN; end S_TURN: begin if (exmc_ne) next_state S_IDLE; else if (!exmc_nwe || !exmc_noe) next_state S_ADDR; // 新的操作开始 end default: next_state S_IDLE; endcase end状态机解析S_IDLE初始状态。只有当exmc_ne片选为低时才表示ARM发起了一次总线访问状态机开始工作。通过判断exmc_nwe和exmc_noe哪个先有效来决定是读还是写。这里有一个细节根据EXMC时序写操作时exmc_nwe的下降沿通常较早出现。S_ADDR这是一个中间状态用于捕捉稳定的地址。在ADDSET和DATAST期间地址是有效的但读写使能可能还未生效。这个状态确保了我们能锁存到正确的地址。S_READ/S_WRITE核心操作状态。在此状态下FPGA执行相应的数据驱动或采样操作。S_TURN总线周转状态。在一次操作结束后exmc_ne可能保持有效准备下一次背靠背操作。这个状态用于处理总线释放和准备避免数据冲突。避坑指南状态机的判断条件要严格以**片选exmc_ne**为最高优先级。任何操作都必须在片选有效期内进行。一旦exmc_ne变高必须立即回到S_IDLE并释放数据总线。这是保证多从设备总线兼容性的关键。3.3 数据总线三态控制与读写响应数据总线exmc_data是双向的需要根据操作类型控制其方向。// 数据总线三态控制 reg [15:0] data_out_reg; // 用于驱动数据总线的寄存器 reg data_tri_en; // 三态使能1为高阻0为驱动data_out_reg assign exmc_data (data_tri_en) ? 16hzzzz : data_out_reg; // 三态控制逻辑 always (posedge fpga_clk or negedge fpga_rst_n) begin if (!fpga_rst_n) begin data_tri_en 1b1; // 默认高阻 data_out_reg 16h0000; end else begin case (current_state) S_READ: begin if (!exmc_noe) begin data_tri_en 1b0; // 驱动总线 data_out_reg user_rdata; // 将用户逻辑的数据送上总线 end else begin data_tri_en 1b1; // 读使能无效释放总线 end end S_WRITE, S_IDLE, S_ADDR, S_TURN: begin data_tri_en 1b1; // 非读状态总线为高阻输入 data_out_reg 16h0000; end default: begin data_tri_en 1b1; end endcase end end // 地址、写数据锁存及用户控制信号生成 always (posedge fpga_clk or negedge fpga_rst_n) begin if (!fpga_rst_n) begin user_addr 26b0; user_wdata 16b0; user_wr_en 1b0; user_rd_en 1b0; user_cs 1b0; end else begin user_cs !exmc_ne; // 片选有效指示 user_wr_en 1b0; // 默认拉低生成脉冲 user_rd_en 1b0; case (current_state) S_ADDR: begin user_addr exmc_addr; // 锁存地址 end S_WRITE: begin if (!exmc_nwe) begin // 在写使能有效期间采样数据总线 user_wdata exmc_data; // 可以在写使能上升沿生成一个写脉冲 end // 检测写使能的上升沿生成单周期脉冲 if (current_state S_WRITE exmc_nwe) begin user_wr_en 1b1; end end S_READ: begin // 在读使能有效期间数据已由三态逻辑输出。 // 可以在状态进入时或根据需求生成读脉冲通知用户逻辑准备数据 if (current_state S_READ !exmc_noe) begin user_rd_en 1b1; // 读使能脉冲 end end endcase end end关键实现细节三态总线assign exmc_data (data_tri_en) ? 16hzzzz : data_out_reg;这行代码是精髓。当data_tri_en1时FPGA引脚为高阻态相当于断开连接由ARM端驱动总线写操作。当data_tri_en0时FPGA将data_out_reg的值驱动到总线上读操作。写数据采样在S_WRITE状态当exmc_nwe为低时ARM端的数据是稳定在exmc_data总线上的。此时FPGA应采样这些数据。采样点可以选择在exmc_nwe的下降沿或低电平中期通过寄存器打拍采样能提高稳定性。用户控制脉冲user_wr_en和user_rd_en是单周期脉冲信号用于告知FPGA内部用户逻辑如FIFO、寄存器组、RAM控制器执行一次写或读操作。脉冲的宽度必须与FPGA时钟同步且确保在操作有效期内。字节使能处理上述代码简化了exmc_nbl的处理。在实际16位模式下如果ARM执行字节写只写高8位或低8位exmc_nbl[1]或exmc_nbl[0]会相应变低。FPGA逻辑需要根据这两个信号决定只更新内部存储数据的特定字节。这需要在写数据锁存和RAM写入逻辑中增加字节选择判断。4. ARM端驱动配置与软件访问FPGA逻辑准备就绪后我们需要在ARM端配置EXMC控制器使其能够正确地发起访问。4.1 EXMC初始化代码示例以下基于华大HC32F4A0标准外设库DDL给出初始化代码框架。/** * brief 初始化EXMC NOR Flash模式用于连接FPGA * param None * retval None */ void EXMC_FPGA_Init(void) { stc_exmc_norsram_init_t stcMemInit; stc_exmc_norsram_timing_init_t stcTimingInit; // 1. 开启相关外设时钟 PWC_Fcg2PeriphClockCmd(PWC_FCG2_PERIPH_EXMC, Enable); // 使能EXMC时钟 // 配置相关GPIO为EXMC复用功能此处以Bank1为例具体引脚查数据手册 GPIO_SetFunc(GPIO_PORT_H, GPIO_PIN_00, GPIO_FUNC_5); // PH00 - EXMC_A0 // ... 配置所有用到的地址、数据、控制线引脚 // 2. 配置时序参数保守值HCLK200MHz时一个周期5ns MEM_ZERO_STRUCT(stcTimingInit); stcTimingInit.u8AddrSetupTime 15; // ADDSET 15*5ns 75ns stcTimingInit.u8DataSetupTime 15; // DATAST 15*5ns 75ns stcTimingInit.u8DataLatency 15; // DATLAT 15*5ns 75ns stcTimingInit.u8BusTurnAroundTime 15; // BUSTURN 15*5ns 75ns // CLKDivision和AccessMode根据模式选择异步模式通常AccessMode0 // 3. 配置存储块参数 MEM_ZERO_STRUCT(stcMemInit); stcMemInit.u16DataWidth EXMC_DATA_WIDTH_16BIT; // 16位数据总线 stcMemInit.u16AddressWidth EXMC_ADDRESS_WIDTH_26BIT; // 使用26位地址线 stcMemInit.u16Bank EXMC_BANK1_NOR; // 使用存储块1对应片选NE1 stcMemInit.u16MemoryType EXMC_MEMORY_TYPE_NOR; // NOR类型 stcMemInit.u16AddressDataMux EXMC_ADDRESS_DATA_MUX_DISABLE; // 非复用模式 stcMemInit.pstcTimingInit stcTimingInit; // 4. 初始化并使能 if (LL_OK ! EXMC_NORSRAM_Init(stcMemInit)) { // 初始化错误处理 while(1); } EXMC_NORSRAM_Cmd(EXMC_BANK1_NOR, Enable); } /** * brief 向FPGA指定地址写入一个16位数据 * param addr: 地址偏移相对于EXMC Bank1基地址0x60000000 * param data: 要写入的数据 * retval None */ void FPGA_WriteReg(uint32_t addr, uint16_t data) { volatile uint16_t *fpga_base (volatile uint16_t*)0x60000000; *(fpga_base addr) data; // 地址自动左移一位16位模式 } /** * brief 从FPGA指定地址读取一个16位数据 * param addr: 地址偏移 * retval 读取到的数据 */ uint16_t FPGA_ReadReg(uint32_t addr) { volatile uint16_t *fpga_base (volatile uint16_t*)0x60000000; return *(fpga_base addr); }软件访问要点地址计算当EXMC配置为16位数据宽度时ARM Cortex-M内核访问的地址会自动与物理地址对齐。例如你访问0x60000000实际出现在地址总线A[25:0]上的是0x0。访问0x60000002地址总线是0x1。因此在FPGA端我们收到的地址exmc_addr已经是按字16位索引的地址。在软件中定义指针时使用uint16_t*类型编译器会处理好这个映射。内存屏障在对FPGA进行连续的读写操作时特别是写后立刻读的情况由于ARM内核有写缓冲和指令预取可能需要插入内存屏障指令如__DSB()、__DMB()来确保访问顺序严格按代码执行避免出现时序竞争。字节操作如果需要进行8位字节操作可以利用exmc_nbl信号。在软件端通过访问不同的地址或使用指针类型转换EXMC会自动产生对应的NBL信号。FPGA端需要根据NBL来屏蔽数据线的部分字节。4.2 调试技巧与性能优化初期调试可以遵循以下步骤静态测试ARM端先配置EXMC然后循环写一个固定的数据如0xAA55到一个固定地址。用逻辑分析仪或示波器抓取exmc_ne,exmc_nwe,exmc_addr,exmc_data的波形。对照数据手册的时序图检查地址建立时间、数据建立时间、写使能宽度等是否符合配置。这是验证硬件连接和ARM配置是否正确的最直接方法。FPGA仿真将抓取到的真实波形或根据手册绘制的理想波形作为测试向量在Modelsim或Vivado Simulator中对FPGA的Verilog代码进行仿真。观察状态机跳转、数据锁存、三态控制是否按预期工作。联合调试将编译好的FPGA比特流下载ARM程序也下载运行。ARM端先进行简单的写-读-回环测试写一个数据到FPGA然后立刻读回来检查是否一致。可以从慢速时序开始逐步收紧ADDSET、DATAST等参数测试通信的极限速度。性能优化在时序收敛的前提下可以通过以下方式提升吞吐量使用DMAEXMC支持DMA传输。可以配置DMA将一大块数据从ARM内存搬移到FPGA通过EXMC或者反向操作从而解放CPU。优化FPGA响应FPGA状态机在满足建立保持时间的前提下应尽快响应读写操作减少等待周期。可以适当减少DATLAT的配置值。背靠背操作确保FPGA状态机能够正确处理连续的读写操作S_TURN状态的作用避免在两个操作之间插入不必要的空闲周期。5. 常见问题排查与实战心得5.1 典型问题与解决方案在实际调试中我遇到了以下几个典型问题问题现象可能原因排查方法与解决方案ARM写数据FPGA读不到或数据错误1. EXMC时序配置过于紧张。2. FPGA端写数据采样点不对。3. 地址锁存错误。4. 硬件连接问题虚焊、线接反。1.用逻辑分析仪抓波形重点看exmc_nwe有效期间exmc_data上的数据是否稳定是否满足FPGA寄存器的建立保持时间。2.放宽EXMC时序参数DATAST,ADDSET看问题是否消失。3. 检查FPGA代码中写数据采样是否在exmc_nwe低电平稳定的窗口内进行建议用fpga_clk上升沿采样。4. 核对原理图与PCB确认连线正确。ARM读FPGA读回全是0或0xFFFF1. FPGA数据总线方向控制错误始终为高阻。2. 读使能exmc_noe有效期间FPGA未及时输出数据。3.user_rd_en脉冲未有效触发用户逻辑提供数据。1.抓波形看exmc_noe有效时exmc_data总线是处于高阻电压中间态还是FPGA有驱动输出。2. 检查FPGA状态机在S_READ状态且exmc_noe为低时data_tri_en是否被置为0。3. 仿真验证user_rd_en脉冲能否在exmc_noe有效前或有效初期产生以便用户逻辑有足够时间准备数据。通信不稳定偶尔出错1. 时序余量不足存在亚稳态。2. 电源噪声或地平面不完整。3. 信号完整性差过冲、振铃。1.增加时序约束在FPGA工具中为EXMC输入信号exmc_ne,exmc_nwe,exmc_noe,exmc_addr添加正确的set_input_delay约束。2.测量电源用示波器测量FPGA和ARM芯片的电源引脚看是否有大的毛刺。3.检查PCB确保信号线尤其是数据线有连续的参考地平面避免长距离平行走线。可以在信号串联小电阻如22Ω阻尼反射。字节使能NBL功能不正常FPGA端未处理exmc_nbl信号导致字节写入覆盖了整个16位数据。修改FPGA内部RAM或寄存器的写入逻辑。当exmc_nbl[0]0时只更新低字节当exmc_nbl[1]0时只更新高字节。需要将16位写数据与旧数据按字节掩码进行合并。5.2 个人实战心得与建议工具是王道一个靠谱的逻辑分析仪是调试这种硬件时序问题的“眼睛”。我使用的是带FPGA动态探针的版本可以直接在Vivado中抓取内部信号结合外部IO信号一起看效率倍增。没有逻辑分析仪用示波器的多通道功能勉强可以但解码并行总线非常痛苦。仿真先行在烧录FPGA之前一定要做充分的仿真。编写一个简单的Testbench模拟EXMC的读写时序给自己设计的接口模块灌入数据。这能排除掉大部分控制逻辑上的低级错误。从慢开始千万不要一开始就追求极限速度。先把EXMC的时序参数配得很大几百纳秒让通信绝对稳定。功能正确后再像拧螺丝一样逐步减小时间参数同时用逻辑分析仪观察波形直到找到稳定工作的临界点然后留出足够的余量比如20%-30%。关注跨时钟域如果FPGA处理用户逻辑的时钟域与采样EXMC信号的时钟域不同那么user_wr_en、user_rd_en、user_addr等信号都需要进行跨时钟域处理如双寄存器同步、握手协议、异步FIFO这是避免亚稳态导致随机错误的关键。利用中断可选除了轮询访问也可以设计一个中断机制。例如FPGA完成某项任务后可以通过一个特定的GPIO线拉低触发ARM的外部中断通知ARM来读取结果。这比ARM不断轮询FPGA的状态寄存器要高效得多。这个项目做下来最深的一点体会是软硬件协同设计文档和时序图就是“法律条文”。ARM的数据手册和FPGA的时序约束文件必须逐字逐句地理解透彻。任何一个参数的误解或遗漏都可能让调试过程陷入数天的僵局。当ARM成功地从FPGA中读回第一个正确的数据时那种成就感大概就是嵌入式工程师的快乐吧。希望这份详细的实现笔记能让你在实现自己的HC32F4A0与FPGA并口通信时少走些弯路。