HC32F4A0 EXMC与FPGA并口通信:从ARM驱动到Verilog状态机全解析
2026/8/7 3:46:18 网站建设 项目流程

1. 项目概述与核心价值

最近在做一个嵌入式与FPGA协同处理的项目,主控用的是华大半导体的HC32F4A0系列ARM芯片,需要和一块Xilinx的Artix-7 FPGA进行高速数据交换。数据量不小,用SPI或者I2C这种串行总线带宽不够,用DMA搬内存又太占CPU资源。翻看HC32F4A0的数据手册,发现它内置了一个叫EXMC(External 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的时序可配置性是其灵活性的体现,但也带来了配置的复杂性。我们需要关注几个关键寄存器,它们定义了读写操作的“节奏”:

  1. BCR (Bank Control Register) 存储块控制寄存器:用于使能存储块、设置数据总线宽度(8/16位)、设置存储类型(NOR/PSRAM)和地址/数据复用模式。对于FPGA通信,我们通常选择非复用模式(即地址线和数据线分开),数据宽度根据需求选择16位以提升吞吐量。

  2. 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 = 3'b000; // 空闲,等待片选有效 localparam S_ADDR = 3'b001; // 地址建立期 localparam S_READ = 3'b010; // 读操作期 localparam S_WRITE = 3'b011; // 写操作期 localparam S_TURN = 3'b100; // 总线周转期(可选,用于处理连续操作) 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

状态机解析

  1. S_IDLE:初始状态。只有当exmc_ne(片选)为低时,才表示ARM发起了一次总线访问,状态机开始工作。通过判断exmc_nweexmc_noe哪个先有效,来决定是读还是写。这里有一个细节:根据EXMC时序,写操作时exmc_nwe的下降沿通常较早出现。
  2. S_ADDR:这是一个中间状态,用于捕捉稳定的地址。在ADDSETDATAST期间,地址是有效的,但读写使能可能还未生效。这个状态确保了我们能锁存到正确的地址。
  3. S_READ/S_WRITE:核心操作状态。在此状态下,FPGA执行相应的数据驱动或采样操作。
  4. 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) ? 16'hzzzz : data_out_reg; // 三态控制逻辑 always @(posedge fpga_clk or negedge fpga_rst_n) begin if (!fpga_rst_n) begin data_tri_en <= 1'b1; // 默认高阻 data_out_reg <= 16'h0000; end else begin case (current_state) S_READ: begin if (!exmc_noe) begin data_tri_en <= 1'b0; // 驱动总线 data_out_reg <= user_rdata; // 将用户逻辑的数据送上总线 end else begin data_tri_en <= 1'b1; // 读使能无效,释放总线 end end S_WRITE, S_IDLE, S_ADDR, S_TURN: begin data_tri_en <= 1'b1; // 非读状态,总线为高阻输入 data_out_reg <= 16'h0000; end default: begin data_tri_en <= 1'b1; end endcase end end // 地址、写数据锁存及用户控制信号生成 always @(posedge fpga_clk or negedge fpga_rst_n) begin if (!fpga_rst_n) begin user_addr <= 26'b0; user_wdata <= 16'b0; user_wr_en <= 1'b0; user_rd_en <= 1'b0; user_cs <= 1'b0; end else begin user_cs <= !exmc_ne; // 片选有效指示 user_wr_en <= 1'b0; // 默认拉低,生成脉冲 user_rd_en <= 1'b0; 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 <= 1'b1; end end S_READ: begin // 在读使能有效期间,数据已由三态逻辑输出。 // 可以在状态进入时或根据需求生成读脉冲,通知用户逻辑准备数据 if (current_state == S_READ && !exmc_noe) begin user_rd_en <= 1'b1; // 读使能脉冲 end end endcase end end

关键实现细节

  • 三态总线assign exmc_data = (data_tri_en) ? 16'hzzzz : data_out_reg;这行代码是精髓。当data_tri_en=1时,FPGA引脚为高阻态,相当于断开连接,由ARM端驱动总线(写操作)。当data_tri_en=0时,FPGA将data_out_reg的值驱动到总线上(读操作)。
  • 写数据采样:在S_WRITE状态,当exmc_nwe为低时,ARM端的数据是稳定在exmc_data总线上的。此时FPGA应采样这些数据。采样点可以选择在exmc_nwe的下降沿或低电平中期,通过寄存器打拍采样能提高稳定性。
  • 用户控制脉冲user_wr_enuser_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. 配置时序参数(保守值,HCLK=200MHz时,一个周期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根据模式选择,异步模式通常AccessMode=0 // 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 调试技巧与性能优化

初期调试,可以遵循以下步骤:

  1. 静态测试:ARM端先配置EXMC,然后循环写一个固定的数据(如0xAA55)到一个固定地址。用逻辑分析仪或示波器抓取exmc_ne,exmc_nwe,exmc_addr,exmc_data的波形。对照数据手册的时序图,检查地址建立时间、数据建立时间、写使能宽度等是否符合配置。这是验证硬件连接和ARM配置是否正确的最直接方法。
  2. FPGA仿真:将抓取到的真实波形(或根据手册绘制的理想波形)作为测试向量,在Modelsim或Vivado Simulator中对FPGA的Verilog代码进行仿真。观察状态机跳转、数据锁存、三态控制是否按预期工作。
  3. 联合调试:将编译好的FPGA比特流下载,ARM程序也下载运行。ARM端先进行简单的写-读-回环测试:写一个数据到FPGA,然后立刻读回来,检查是否一致。可以从慢速时序开始,逐步收紧ADDSETDATAST等参数,测试通信的极限速度。
  4. 性能优化:在时序收敛的前提下,可以通过以下方式提升吞吐量:
    • 使用DMA:EXMC支持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 个人实战心得与建议

  1. 工具是王道:一个靠谱的逻辑分析仪是调试这种硬件时序问题的“眼睛”。我使用的是带FPGA动态探针的版本,可以直接在Vivado中抓取内部信号,结合外部IO信号一起看,效率倍增。没有逻辑分析仪,用示波器的多通道功能勉强可以,但解码并行总线非常痛苦。
  2. 仿真先行:在烧录FPGA之前,一定要做充分的仿真。编写一个简单的Testbench,模拟EXMC的读写时序,给自己设计的接口模块灌入数据。这能排除掉大部分控制逻辑上的低级错误。
  3. 从慢开始:千万不要一开始就追求极限速度。先把EXMC的时序参数配得很大(几百纳秒),让通信绝对稳定。功能正确后,再像拧螺丝一样,逐步减小时间参数,同时用逻辑分析仪观察波形,直到找到稳定工作的临界点,然后留出足够的余量(比如20%-30%)。
  4. 关注跨时钟域:如果FPGA处理用户逻辑的时钟域与采样EXMC信号的时钟域不同,那么user_wr_enuser_rd_enuser_addr等信号都需要进行跨时钟域处理(如双寄存器同步、握手协议、异步FIFO),这是避免亚稳态导致随机错误的关键。
  5. 利用中断(可选):除了轮询访问,也可以设计一个中断机制。例如,FPGA完成某项任务后,可以通过一个特定的GPIO线拉低触发ARM的外部中断,通知ARM来读取结果。这比ARM不断轮询FPGA的状态寄存器要高效得多。

这个项目做下来,最深的一点体会是:软硬件协同设计,文档和时序图就是“法律条文”。ARM的数据手册和FPGA的时序约束文件必须逐字逐句地理解透彻。任何一个参数的误解或遗漏,都可能让调试过程陷入数天的僵局。当ARM成功地从FPGA中读回第一个正确的数据时,那种成就感,大概就是嵌入式工程师的快乐吧。希望这份详细的实现笔记,能让你在实现自己的HC32F4A0与FPGA并口通信时,少走些弯路。

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

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

立即咨询