第一次接触GD32的EXMC总线时,确实有点懵。这玩意儿说白了就是单片机用来跟外部存储器打交道的专用通道,而我们要做的就是让FPGA"假扮"成SRAM存储器。GD32F450的EXMC跟STM32的FSMC几乎是一个模子刻出来的,所以STM32的经验在这里完全适用。
EXMC总线最厉害的地方在于它能直接映射外部存储器的地址空间到单片机的内存空间。想象一下,你在代码里写个指针操作,实际上是在跟FPGA通信,这种体验简直不要太爽!我用的GD32F450ZIT6有16位数据总线,最高能跑到90MHz,实测下来传输速度相当给力。
这里有个关键点要理解:EXMC支持多种存储器类型,包括NOR Flash、PSRAM、SRAM等。我们要用的是SRAM模式,因为FPGA模拟SRAM最简单。SRAM模式又分为同步和异步两种,考虑到FPGA的灵活性,异步模式更适合我们的场景。
官方例程里只有SDRAM的初始化代码,SRAM的得自己折腾。下面这个初始化函数是我调试通过的版本,关键配置都加了注释:
c复制void exmc_sram_init() {
exmc_norsram_parameter_struct sdram_init_struct;
exmc_norsram_timing_parameter_struct read_write_timing;
// 先开启相关时钟
rcu_periph_clock_enable(RCU_EXMC);
rcu_periph_clock_enable(RCU_GPIOB);
// 省略其他GPIO时钟使能...
// GPIO复用配置
gpio_af_set(GPIOD, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_3);
gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1);
// 省略其他GPIO配置...
// 时序参数配置
read_write_timing.asyn_address_setuptime = 0x0U; // 地址建立时间
read_write_timing.asyn_address_holdtime = 0x0U; // 地址保持时间
read_write_timing.asyn_data_setuptime = 0x08U; // 数据保持时间
// SRAM初始化结构体
sdram_init_struct.norsram_region = EXMC_BANK0_NORSRAM_REGION0;
sdram_init_struct.memory_type = EXMC_MEMORY_TYPE_SRAM;
sdram_init_struct.databus_width = EXMC_NOR_DATABUS_WIDTH_16B;
// 其他参数配置...
exmc_norsram_init(&sdram_init_struct);
exmc_norsram_enable(EXMC_BANK0_NORSRAM_REGION0);
delay_1ms(10);
}
配置过程中踩过几个坑:
时序配置是EXMC通信最头疼的部分。刚开始我完全看不懂那些建立时间、保持时间到底该设多少,后来发现官方用户手册里的这张时序图才是救命稻草:
重点参数有三个:
实测发现,GD32F450在72MHz主频下,这些参数可以这么设:
用示波器抓波形时要注意:
FPGA这边要做的事情其实更简单,就是根据EXMC的时序要求,在收到读/写信号时做出响应。Verilog核心代码大概长这样:
verilog复制module sram_emulator(
input wire clk,
input wire [15:0] addr,
inout wire [15:0] data,
input wire noe,
input wire nwe,
input wire ne0
);
reg [15:0] mem[0:65535];
reg [15:0] data_out;
// 三态控制
assign data = (!noe && !ne0) ? data_out : 16'hzzzz;
// 写操作
always @(negedge nwe or negedge ne0) begin
if(!ne0 && !nwe) begin
mem[addr] <= data;
end
end
// 读操作
always @(*) begin
if(!noe && !ne0) begin
data_out = mem[addr];
end
end
endmodule
调试FPGA时要注意:
搞这种跨器件通信,不出问题才怪。分享几个我遇到的典型问题:
问题1:读到的全是0xFF
问题2:数据偶尔出错
问题3:写操作不生效
有个调试小技巧:先在FPGA里实现一个固定返回0x55AA的简单逻辑,验证基础通信是否正常,再逐步完善功能。
当基础功能调通后,我开始琢磨怎么提升通信效率。试了几种方案:
突发传输模式:
DMA传输:
双缓冲机制:
最终我的选择是:小数据包用直接访问,大数据块上DMA。突发模式虽然快,但项目周期紧张就先搁置了。
去年做的工业控制器就用了这套方案。单片机要实时读取FPGA处理好的传感器数据,同时下发控制指令。具体实现是这样的:
地址空间划分:
通信协议设计:
错误处理:
这套系统连续运行半年多,通信错误率低于0.001%,完全满足工业级要求。关键是要把异常处理考虑周全,毕竟实际现场什么奇葩问题都可能出现。