1. 项目缘起当ARM的并口总线遇上FPGA的灵活时序最近在做一个工控项目主控选用了华大半导体的HC32F4A0系列ARM芯片。这颗芯片性能不错外设也丰富其中有一个让我特别关注的功能模块叫EXMCExternal Memory Controller外部存储器控制器。简单来说它就是一个并口总线控制器可以用来连接SRAM、NOR Flash、LCD屏这类需要高速并行数据交换的设备。项目里需要外挂一个自定义的、对时序有特殊要求的传感器接口板用SPI或者I2C速度跟不上用普通的GPIO模拟又太占CPU资源于是很自然地就想到了利用EXMC这个并口。但问题来了传感器接口板的时序要求和EXMC标准支持的SRAM/NOR模式不完全一样。比如它的地址建立时间Address Setup Time要求更短而数据有效后的保持时间Data Hold Time又特别长。直接配置EXMC的标准模式通信要么不稳定要么根本读不到数据。调了几天寄存器发现EXMC的时序参数调整粒度有限有些极端时序无法满足。这时候我的老搭档FPGA就派上用场了。与其让ARM芯片的硬件控制器去勉强适配一个不标准的时序不如用FPGA在中间做一层“翻译”。让ARM的EXMC以它最舒服、最标准的时序比如类SRAM模式去操作FPGA然后在FPGA内部我再设计一个状态机按照传感器接口板要求的精确时序去驱动它。这样ARM端代码简单稳定FPGA端灵活可控完美解耦。这就是本次“用FPGA实现HC32F4A0 EXMC通信时序”项目的核心动机。2. 深入理解HC32F4A0的EXMC接口在动手写FPGA代码之前必须先把ARM这边的情况吃透。HC32F4A0的EXMC模块功能强大支持多种存储器类型但我们最关心的是它与FPGA对接时那些看得见、摸得着的引脚信号和它们之间的时间关系。2.1 EXMC接口信号线全解析EXMC连接FPGA我们通常使用其“SRAM/NOR Flash Bank1”的接口因为它控制信号最丰富。我们需要关注以下几组信号这决定了FPGA端模块的输入输出端口定义地址总线A[25:0]最多26位地址线。实际使用多少位取决于你在ARM端将FPGA映射到哪个地址空间以及你需要多大的寻址范围。例如如果你只用了A[15:0]这16根线那么在FPGA看来ARM就是在访问一个64KB2^16的空间。数据总线D[15:0]16位数据线。EXMC也支持8位模式但16位更常见传输效率更高。FPGA端需要实现双向数据总线。片选信号NEx对于Bank1片选信号是NE1。当ARM访问映射到该Bank的地址时NE1会拉低。这是FPGA开始响应操作的总开关。读使能NOE输出使能低电平有效。当ARM发起读操作时此信号拉低FPGA应当在此期间将有效数据放到数据总线D[15:0]上。写使能NWE写使能低电平有效。当ARM发起写操作时此信号拉低FPGA应当在NWE的上升沿或根据配置锁存此时数据总线上的值。字节选择NBL[1:0]字节使能信号。NBL0对应数据低字节D[7:0]NBL1对应数据高字节D[15:8]。当进行16位操作时两者同时有效。这在实现8位/16位可切换接口时有用。这些信号是物理连接的基础。在原理图设计阶段就必须确保这些信号线从ARM芯片引脚连接到FPGA的通用IO上并且考虑电平标准通常是3.3V LVCMOS的匹配。2.2 EXMC时序模型与关键参数ARM通过配置EXMC的寄存器来生成读写时序。我们需要关注几个关键的时间参数它们决定了ARM端信号的“波形形状”。以SRAM模式1Mode1为例主要参数如下这些名词在芯片参考手册的EXMC章节有明确定义MEMxSET地址建立时间。指地址线有效后到片选NEx或NOE/NWE有效之间的延迟。MEMxWAIT地址保持时间。指NOE/NWE无效后地址线继续保持有效的时间。MEMxHOLD数据保持时间。特指写操作时NWE无效后数据线继续保持有效的时间。OE/WE 低电平宽度由MEMxSET、MEMxWAIT和时钟分频等参数共同决定。在ARM端的驱动代码里我们通过初始化结构体配置这些参数。例如用STM32 HAL库风格伪代码表示// HC32F4A0 SDK 风格示例 stc_exmc_sram_init_t sramInit; sramInit.u16ReadAddrSetupTime 2; // 读地址建立时间 (HCLK周期) sramInit.u16ReadAddrHoldTime 1; // 读地址保持时间 sramInit.u16ReadDataSetupTime 4; // 读数据建立时间影响OE宽度 sramInit.u16WriteAddrSetupTime 2; // 写地址建立时间 sramInit.u16WriteAddrHoldTime 1; // 写地址保持时间 sramInit.u16WriteDataSetupTime 2; // 写数据建立时间影响WE宽度 sramInit.u16WriteDataHoldTime 1; // 写数据保持时间 sramInit.u16BusTurnAroundTime 0; // 总线周转时间 EXMC_SRAM_Init(EXMC_BANK1, sramInit);配置完这些ARM端操作FPGA就变得像操作内部内存一样简单*(volatile uint16_t*)0x60000000 0x1234;就是写操作value *(volatile uint16_t*)0x60000000;就是读操作。EXMC硬件会自动产生所有波形。关键理解FPGA逻辑设计的输入就是上述EXMC产生的、符合这些参数约束的波形。我们的任务是“读懂”这个波形并据此生成给传感器板的另一套波形。3. FPGA端接口逻辑设计从EXMC信号到内部状态FPGA作为中间桥梁其核心是一个“协议转换器”。输入是标准的EXMC SRAM时序输出是自定义的传感器板时序。我们首先设计FPGA与EXMC对接的这部分逻辑。3.1 顶层模块与端口定义我们使用Verilog或VHDL进行设计。首先定义顶层模块例如exmc_to_sensor_interface的端口这直接对应了FPGA芯片的物理引脚。module exmc_to_sensor_interface ( // 来自 HC32F4A0 EXMC 的接口信号 input wire i_emc_clk, // 可选如果使用同步模式可能需要时钟 input wire [15:0] i_emc_addr, // EXMC 地址总线根据实际连接调整宽度 inout wire [15:0] io_emc_data, // EXMC 双向数据总线 input wire i_emc_ne1, // 片选低有效 input wire i_emc_noe, // 读使能低有效 input wire i_emc_nwe, // 写使能低有效 input wire [1:0] i_emc_nbl, // 字节使能 // 连接到外部传感器板的接口信号 output reg [7:0] o_sensor_addr, // 给传感器的8位地址线举例 output reg [15:0] o_sensor_data, // 给传感器的16位数据线写操作时 input wire [15:0] i_sensor_data, // 来自传感器的16位数据线读操作时 output reg o_sensor_cs_n, // 传感器片选低有效 output reg o_sensor_rd_n, // 传感器读使能低有效 output reg o_sensor_wr_n, // 传感器写使能低有效 output reg o_sensor_ale // 传感器地址锁存使能如果需要 );注意io_emc_data是inout类型意味着它既是输入也是输出。在Verilog中需要妥善处理三态逻辑这是FPGA与并口总线交互的关键也是容易出错的地方。3.2 EXMC读写周期检测与解码EXMC的读写操作本质上是i_emc_ne1、i_emc_noe、i_emc_nwe三个信号状态的组合。我们需要一个组合逻辑或者时序逻辑来检测操作的开始和类型。一个简单可靠的方法是使用边沿检测结合状态机。但针对EXMC这种异步总线更直接的方式是利用i_emc_ne1片选作为使能条件根据i_emc_noe和i_emc_nwe判断操作。// 检测写操作有效周期片选有效且写使能变低 wire write_cycle_active (i_emc_ne1 1‘b0) (i_emc_nwe 1’b0); // 检测读操作有效周期片选有效且读使能变低 wire read_cycle_active (i_emc_ne1 1‘b0) (i_emc_noe 1’b0); // 锁存地址和数据。注意在写周期数据在NWE上升沿时稳定在读周期需要在NOE有效期间将数据输出。 always (posedge i_emc_nwe or negedge i_emc_ne1) begin // 一种边沿触发锁存方式 if (!i_emc_ne1) begin // 片选有效期间锁存地址。实际中地址可能在NWE/NOE变化前就稳定了。 r_latched_addr i_emc_addr[7:0]; // 假设我们只使用低8位地址 end end always (negedge i_emc_nwe) begin // 在NWE的下降沿数据可能还未完全稳定这不是最佳实践 // 更好的做法是在NWE的上升沿锁存数据 end // 更稳健的做法使用一个主时钟例如内部的50MHz时钟来同步采样这些异步信号 reg [1:0] sync_ne1, sync_noe, sync_nwe; always (posedge clk_50m) begin sync_ne1 {sync_ne1[0], i_emc_ne1}; sync_noe {sync_noe[0], i_emc_noe}; sync_nwe {sync_nwe[0], i_emc_nwe}; end // 检测边沿 wire ne1_falling (sync_ne1[1] ~sync_ne1[0]); // 片选有效 wire nwe_rising (~sync_nwe[1] sync_nwe[0]); // 写使能释放数据在此刻稳定 wire noe_falling (sync_noe[1] ~sync_noe[0]); // 读使能有效 // 当检测到NWE上升沿且片选有效时锁存写数据 always (posedge clk_50m) begin if (ne1_falling) begin r_latched_addr i_emc_addr[7:0]; end if (ne1_falling nwe_rising) begin // 写周期结束 r_latched_wr_data io_emc_data; r_write_pending 1‘b1; // 置位写请求标志 end if (ne1_falling noe_falling) begin // 读周期开始 r_read_pending 1’b1; // 置位读请求标志 end end通过同步化处理我们将异步的EXMC信号转换成了FPGA内部同步时钟域下的控制标志r_write_pending,r_read_pending和锁存数据。这是连接两个不同时钟域ARM的HCLK通过EXMC产生的异步信号 vs FPGA内部时钟的标准做法能有效避免亚稳态。3.3 双向数据总线的三态控制处理io_emc_data双向端口是重点。原则是ARM读操作时FPGA驱动数据线ARM写操作或空闲时FPGA释放总线高阻态。reg [15:0] r_emc_data_out; // FPGA准备输出给EXMC的数据 reg r_emc_data_oe; // FPGA数据输出使能1-驱动0-高阻 // 三态总线赋值 assign io_emc_data r_emc_data_oe ? r_emc_data_out : 16‘hzzzz; // 输出使能和数据产生的逻辑 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin r_emc_data_oe 1’b0; r_emc_data_out 16‘h0000; end else begin if (r_read_pending) begin // 当有读请求并且处于EXMC读使能有效期内我们需要驱动数据 // 这里需要根据EXMC的读时序NOE低电平宽度来决定何时驱动。 // 我们可以用一个计数器或状态机在NOE变低后延迟几个时钟周期模拟读取时间然后输出数据。 r_emc_data_oe 1’b1; r_emc_data_out r_sensor_read_data; // 假设r_sensor_read_data是从传感器读回的数据 end else begin r_emc_data_oe 1‘b0; // 非读周期释放总线 end end end关键点r_emc_data_oe使能的时机必须精确。它必须在i_emc_noe有效后、ARM芯片采样数据之前有效并且要在i_emc_noe无效前保持。这需要根据EXMC配置的ReadDataSetupTime等参数在FPGA内部做精确的延迟控制。如果使能太晚ARM会读到错误数据如果释放太晚可能会和下一个写周期的数据冲突。4. 核心面向自定义传感器的时序状态机设计这是项目的精髓所在。FPGA锁存了ARM的指令读/写地址数据后需要生成一套全新的时序来控制传感器板。我们假设传感器板需要一个类似8080并口的时序先输出地址并锁存然后进行读或写操作。4.1 传感器时序要求分析与状态定义假设传感器接口时序要求如下具体数值根据传感器手册写操作序列o_sensor_cs_n拉低。输出地址到o_sensor_addr拉高o_sensor_ale地址锁存使能至少50ns。拉低o_sensor_ale输出数据到o_sensor_data。拉低o_sensor_wr_n至少100ns。拉高o_sensor_wr_n数据在上升沿被锁存。拉高o_sensor_cs_n。读操作序列o_sensor_cs_n拉低。输出地址到o_sensor_addr拉高o_sensor_ale至少50ns。拉低o_sensor_ale。拉低o_sensor_rd_n至少150ns传感器需要更长的数据输出时间。在o_sensor_rd_n低电平期间采样i_sensor_data。拉高o_sensor_rd_n。拉高o_sensor_cs_n。根据这个要求我们可以设计一个有限状态机FSM。状态定义如下localparam [3:0] S_IDLE 4‘d0, // 空闲 S_WR_ADDR 4’d1, // 写操作-输出地址 S_WR_ALE_H 4‘d2, // 写操作-ALE高电平保持 S_WR_DATA 4’d3, // 写操作-输出数据 S_WR_PULSE 4‘d4, // 写操作-WR_N低电平脉冲 S_WR_DONE 4’d5, // 写操作完成 S_RD_ADDR 4‘d6, // 读操作-输出地址 S_RD_ALE_H 4’d7, // 读操作-ALE高电平保持 S_RD_WAIT 4‘d8, // 读操作-等待地址建立后 S_RD_PULSE 4’d9, // 读操作-RD_N低电平脉冲 S_RD_SAMPLE 4‘d10,// 读操作-采样数据 S_RD_DONE 4’d11; // 读操作完成 reg [3:0] r_current_state, r_next_state;4.2 状态转移与定时控制状态机的跳转由r_write_pending和r_read_pending触发。每个需要延时的状态如S_WR_ALE_H、S_WR_PULSE内部都需要一个计数器。reg [15:0] r_delay_cnt; reg r_delay_done; // 状态转移逻辑 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin r_current_state S_IDLE; end else begin r_current_state r_next_state; end end always (*) begin r_next_state r_current_state; case (r_current_state) S_IDLE: begin if (r_write_pending) begin r_next_state S_WR_ADDR; end else if (r_read_pending) begin r_next_state S_RD_ADDR; end end S_WR_ADDR: r_next_state S_WR_ALE_H; S_WR_ALE_H: begin if (r_delay_done) r_next_state S_WR_DATA; end S_WR_DATA: r_next_state S_WR_PULSE; S_WR_PULSE: begin if (r_delay_done) r_next_state S_WR_DONE; end S_WR_DONE: begin // 清除写请求标志返回空闲 r_next_state S_IDLE; end // ... 读操作状态转移类似 S_RD_DONE: begin // 清除读请求标志返回空闲 r_next_state S_IDLE; end default: r_next_state S_IDLE; endcase end // 延时计数器逻辑 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin r_delay_cnt 16‘d0; r_delay_done 1’b0; end else begin r_delay_done 1‘b0; if (r_current_state ! r_next_state) begin // 状态即将改变复位计数器 r_delay_cnt 16’d0; end else begin case (r_current_state) S_WR_ALE_H: begin // 需要50ns 50MHz时钟即 50ns / 20ns 2.5个周期取3个周期 if (r_delay_cnt 16‘d2) begin r_delay_done 1’b1; end else begin r_delay_cnt r_delay_cnt 16‘d1; end end S_WR_PULSE: begin // 需要100ns即5个周期 if (r_delay_cnt 16’d4) begin r_delay_done 1‘b1; end else begin r_delay_cnt r_delay_cnt 16’d1; end end // ... 其他需要延时的状态 default: begin r_delay_cnt 16‘d0; end endcase end end end4.3 输出逻辑生成在每个状态下根据传感器时序要求驱动对应的输出信号。// 传感器接口信号输出逻辑 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin o_sensor_cs_n 1‘b1; o_sensor_ale 1’b0; o_sensor_wr_n 1‘b1; o_sensor_rd_n 1’b1; o_sensor_addr 8‘h00; o_sensor_data 16’h0000; r_sensor_read_data 16‘h0000; r_write_pending 1’b0; // 状态机完成后清除请求 r_read_pending 1‘b0; end else begin case (r_current_state) S_IDLE: begin o_sensor_cs_n 1’b1; o_sensor_ale 1‘b0; o_sensor_wr_n 1’b1; o_sensor_rd_n 1‘b1; end S_WR_ADDR: begin o_sensor_cs_n 1’b0; // 片选有效 o_sensor_ale 1‘b0; o_sensor_addr r_latched_addr; // 输出锁存的地址 end S_WR_ALE_H: begin o_sensor_ale 1’b1; // 拉高ALE end S_WR_DATA: begin o_sensor_ale 1‘b0; // 拉低ALE锁存地址 o_sensor_data r_latched_wr_data; // 输出要写的数据 end S_WR_PULSE: begin o_sensor_wr_n 1’b0; // 产生写脉冲 end S_WR_DONE: begin o_sensor_wr_n 1‘b1; o_sensor_cs_n 1’b1; r_write_pending 1‘b0; // 清除标志通知主逻辑完成 end S_RD_PULSE: begin o_sensor_rd_n 1’b0; end S_RD_SAMPLE: begin o_sensor_rd_n 1‘b1; r_sensor_read_data i_sensor_data; // 在RD_N上升沿后采样数据 // 注意有些器件要求在RD_N低电平期间采样具体看器件手册 end S_RD_DONE: begin o_sensor_cs_n 1’b1; r_read_pending 1‘b0; // 清除标志 // 此时r_sensor_read_data已经保存可以用于驱动回EXMC的数据总线 end // ... 其他状态 endcase end end这个状态机完美地将ARM端简单的“地址-数据”读写翻译成了传感器能理解的、带有时序要求的复杂操作序列。FPGA内部的时钟如50MHz提供了精确的定时基础。5. 系统集成、仿真与实测调试设计完成RTL代码后工作只完成了一半。接下来的仿真和调试是保证项目成功的关键。5.1 编写Testbench进行功能仿真在将代码下载到FPGA之前必须用仿真工具如ModelSim, Vivado Simulator验证逻辑的正确性。我们需要编写一个模拟HC32F4A0 EXMC行为的Testbench。timescale 1ns / 1ps module tb_exmc_to_sensor(); reg clk_50m; reg rst_n; // EXMC 接口信号 reg [15:0] tb_emc_addr; wire [15:0] tb_emc_data; reg tb_emc_ne1; reg tb_emc_noe; reg tb_emc_nwe; reg [1:0] tb_emc_nbl; // 传感器接口信号 wire [7:0] tb_sensor_addr; wire [15:0] tb_sensor_data; reg [15:0] tb_sensor_data_in; wire tb_sensor_cs_n, tb_sensor_rd_n, tb_sensor_wr_n, tb_sensor_ale; // 实例化被测模块 exmc_to_sensor_interface uut ( ... ); // 端口连接省略 // 生成时钟和复位 initial begin clk_50m 0; forever #10 clk_50m ~clk_50m; end // 50MHz initial begin rst_n 0; #100; rst_n 1; end // 模拟EXMC写操作任务 task exmc_write; input [15:0] addr; input [15:0] data; begin tb_emc_ne1 1‘b1; // 初始片选无效 tb_emc_noe 1’b1; tb_emc_nwe 1‘b1; tb_emc_addr addr; #100; // 一些初始延迟 tb_emc_ne1 1’b0; // 片选有效 #50; // 模拟地址建立时间 MEMxSET tb_emc_nwe 1‘b0; // 写使能有效 tb_emc_data_drive data; // 驱动数据总线 #100; // 模拟写数据建立时间 tb_emc_nwe 1’b1; // 写使能无效数据在上升沿被FPGA采样 #50; // 模拟数据保持时间 MEMxHOLD tb_emc_ne1 1‘b1; // 片选无效 tb_emc_data_drive 16’hzzzz; // 释放总线 #200; // 操作间隔 end endtask // 模拟EXMC读操作任务 task exmc_read; input [15:0] addr; begin tb_emc_ne1 1‘b1; tb_emc_noe 1’b1; tb_emc_nwe 1‘b1; tb_emc_addr addr; #100; tb_emc_ne1 1’b0; #50; tb_emc_noe 1‘b0; // 读使能有效 // 此时我们期望FPGA模块驱动tb_emc_data #150; // 模拟读数据建立时间 // 可以在这里检查tb_emc_data上的值是否正确 $display(“Read data: %h”, tb_emc_data); tb_emc_noe 1’b1; #50; tb_emc_ne1 1‘b1; #200; end endtask initial begin // 初始化 tb_emc_data_drive 16’hzzzz; tb_sensor_data_in 16‘h0000; #200; // 等待复位完成 // 执行测试序列 exmc_write(16’h1234, 16‘hABCD); // 向地址0x1234写入数据0xABCD // 此时在仿真波形中应该能看到传感器接口上出现完整的写时序 #1000; // 设置传感器模拟返回值 tb_sensor_data_in 16’h5678; exmc_read(16‘h5678); // 从地址0x5678读取数据期望读到0x5678 #1000; $finish; end endmodule通过仿真我们可以清晰地看到状态机的跳转、各个信号的时序关系是否满足传感器要求以及EXMC数据总线的三态控制是否正确。这是排查逻辑错误最有效的手段。5.2 上板实测与逻辑分析仪抓取波形仿真通过后将程序综合、实现、生成比特流下载到FPGA开发板。连接好HC32F4A0开发板和传感器板或模拟负载。基础连通性测试在ARM端编写最简单的测试程序向固定的FPGA映射地址写入一个已知值然后读回。如果读写数据一致说明EXMC到FPGA的路径基本通畅。时序波形验证这是调试的核心。必须使用逻辑分析仪或示波器带多通道数字功能同时抓取关键信号。抓取点1ARM-FPGA接口连接NE1、NWE、NOE、A[0]、D[0]等信号。验证ARM端发出的EXMC时序是否符合预期地址建立、写脉冲宽度等。抓取点2FPGA-传感器接口连接o_sensor_cs_n,o_sensor_wr_n,o_sensor_ale,o_sensor_addr[0],o_sensor_data[0]等。这是验证我们FPGA状态机成果的关键。测量ALE高电平时间、WR_N低电平时间等看是否满足传感器手册要求。对比分析触发一次ARM写操作同时观察两个抓取点的波形。你会发现ARM端一个简单的NWE负脉冲在FPGA端被“翻译”成了一段包含CS_N、ALE、WR_N的复杂序列。通过测量时间参数可以精确调整状态机中的延时计数器值。常见问题与调试技巧问题ARM读数据总是0xFF或0x00。排查首先检查FPGA代码中r_emc_data_oe使能信号。在逻辑分析仪上看NOE变低后FPGA的io_emc_data引脚是否从高阻态变为驱动状态。如果没有说明输出使能逻辑有问题。如果有看驱动的数据是否正确。可能是传感器读回的数据r_sensor_read_data没有正确赋值或者状态机在NOE有效期间还未完成读传感器操作。问题传感器不响应。排查重点看FPGA-传感器接口波形。CS_N是否拉低ALE和WR_N/RD_N的时序、脉宽是否达标地址和数据线在对应时刻的值是否正确用逻辑分析仪对照传感器数据手册的时序图逐一检查。问题通信不稳定偶尔出错。排查可能是时序余量不足。适当增加状态机中的延时计数器值特别是ALE和WR_N/RD_N的脉冲宽度。检查PCB走线过长或过细的走线可能引起信号完整性问题。在FPGA引脚约束中可以尝试增加输出驱动电流或调整IO标准。5.3 性能考量与优化吞吐率整个操作从ARM发出指令到传感器操作完成的延迟由状态机的周期数决定。如果对速度要求高可以优化状态机合并一些状态或者用更快的FPGA时钟。但要注意传感器本身可能有最低时序要求不能无限制缩短。资源占用这个设计主要消耗查找表LUT和寄存器FF以及一些Block RAM如果实现了数据缓冲。对于中等规模的FPGA如Artix-7系列来说绰绰有余。扩展性本设计是针对特定传感器时序的。如果未来要支持多种传感器可以将时序参数如ALE宽度、脉冲宽度设计成可配置的寄存器由ARM通过EXMC写入FPGA内部的配置空间进行动态设置从而实现一个通用的“可编程时序转换器”。通过以上步骤我们成功地在FPGA上搭建了一座桥梁让时序严格的HC32F4A0 EXMC接口能够灵活地与各种非标准时序的并行设备通信。这种设计思路不仅适用于传感器还可以扩展到连接自定义的DSP、ASIC、或者老式的并行接口设备上体现了FPGA在系统集成中解决接口适配问题的强大能力。整个过程中对双方接口时序的深刻理解、稳健的状态机设计以及严谨的仿真调试是项目成功不可或缺的环节。