简介:面向嵌入式开发者的GD32与FPGA通信参考工程,以STM32F4标准库的FMC功能程序为基础,成功移植到GD32F470的EXMC外设,实现两者间异步NORSRAM模式A通信。工程包含完整单片机端源码,FPGA部分配合博客阐述,适合需要掌握GD32 EXMC配置、时序匹配及跨平台代码迁移的工程师参考。压缩包共209个文件,核心代码由41个h头文件与35个c源文件构成,另有编译生成的d、o、crf中间文件、uvprojx等工程文件以及hex烧录文件,可分别用于源码阅读、工程复现与固件烧写,整体约6.96MB。已有2050人学习下载,代码结构完整,可直接导入工程查看GPIO复用、EXMC时序寄存器及NOR/PSRAM控制逻辑,对理解FMC到EXMC的寄存器差异和实际板级调试具有明确参考价值。
1. 为什么选 EXMC 和 GD32 做 FPGA 通信桥
做嵌入式这些年,GD32 和 FPGA 打交道是我觉得性价比最高的一组组合。FPGA 擅长并行处理和自定义时序,GD32 擅长跑逻辑控制和协议栈,两者互补之后能处理很多单芯片搞不定的场景,比如高速数据采集、图像预处理、多通道传感器同步。而连接它们的接口,我首选 EXMC,也就是 GD32 的外部存储器控制器,相当于 STM32 里的 FSMC/FMC,但它有自己的寄存器体系和时序逻辑。
选 EXMC 而不是 SPI、UART 或者 I2C,核心原因就一个:带宽和实时性。SPI 理论速率看着还行,但一上协议、握手、应答,实际有效吞吐掉得厉害;UART 更不用提,做控制没问题,做批量数据搬运就是折磨。EXMC 不一样,它是存储器总线接口,从 GD32 的角度看,FPGA 就是一块“内存”,你在代码里写一个地址,读一个地址,数据就过去了,不需要一堆通讯协议栈,也不占用 CPU 大量时间做分包组包,而是靠地址映射和片选信号直接读写,吞吐能力和响应速度都能拉满。
这个项目我做的时候用的是 GD32F450 系列,最高主频 200MHz,EXMC 支持 8 位或 16 位数据总线宽度,可以寻址多个存储区域,每个区域有独立的片选信号。FPGA 端我用的是入门级器件,逻辑资源不需要太强,但它能提供灵活的外部时序响应。GD32 这边只需要配置好 EXMC 的时序参数,然后像读 SRAM 一样操作地址,FPGA 通过地址译码和数据总线拿到数据,整个过程干净利落。
如果你原来用过 STM32 的 FSMC 和 FPGA 通信,那你会发现 GD32 的 EXMC 基本思路是类似的,但细节上有不少需要重新确认的地方。接下来我把这个项目的全部设计思路、关键配置和踩坑过程写下来,希望对正在做类似方案的你有点参考价值。
2. EXMC 通信方案的核心设计思路
2.1 EXMC 与 FPGA 对接的本质
EXMC 和 FPGA 对接,本质上是把一个并行总线设备挂在 GD32 的总线上,FPGA 扮演外部器件的角色,通过总线接口解析地址和数据。GD32 的 EXMC 通常支持 NOR Flash、PSRAM、SRAM 和 NAND Flash 这类设备,我们利用的是 SRAM 模式,因为它的时序最简单,读写操作都是异步的,FPGA 端容易实现。
可以把 EXMC 理解成一个快递柜,GD32 是发件人,FPGA 是收件人,地址线是柜号,数据线是包裹内容,读信号和写信号就是送货和取货的动作。GD32 说“我要往这个格子放东西”,FPGA 听见了就把数据取走;反过来 GD32 说“我要取某个格子的东西”,FPGA 就把数据放到总线上。整个流程不需要额外的通讯协议,只要双方都遵守约定的时序,数据就能正确流动。
这种设计带来的最大优势是,GD32 可以用最简单的代码访问 FPGA 内部寄存器或缓冲区。比如一个数据采集系统,FPGA 不断采集 ADC 的数据并放入内部双口 RAM,GD32 只需周期性地从固定地址读取,就能拿到最新的数据。反过来,GD32 给 FPGA 下发配置参数,也只需要写几个地址即可,不需要串口一帧一帧地解析。
2.2 总线宽度规划:8 位还是 16 位
设计第一步是决定数据总线宽度。GD32 EXMC 支持 8 位和 16 位两种模式。我建议根据两个因素考虑:一是通信数据量的大小,二是 FPGA 引脚资源的富余程度。
如果只是传控制命令和状态量,比如几十个寄存器的读写,8 位总线完全够用,接线也少,FPGA 引脚可以省下来做别的事情。如果是批量数据交互,比如图像数据、波形采样数据,16 位总线效率翻倍,一次读写就能搬运两个字节,CPU 的循环次数少一半。
我的项目里有图像数据传输需求,所以选了 16 位模式。这意味着 FPGA 端的双向数据总线有 16 根,加上地址线、控制线,总共大约 25 根左右,布局布线时要注意总线等长和信号完整性,特别是数据速率较高的时候,线长差太大会导致建立保持时间不够。
另外还需要注意字节序的问题。16 位模式下,GD32 是小端格式,低字节在偶地址,高字节在奇地址,FPGA 端在做数据拼接时要保持一致,否则会出现高低字节互换的经典 bug。
2.3 Bank 区域映射与地址规划
GD32 的 EXMC 通过不同片选信号区分多个外部存储区域。具体到 F4 系列,通常有多个 BANK,每个 BANK 对应一个片选引脚,地址空间是固定的。我在项目里只使用了一个 BANK,把 FPGA 挂在片选 0 上,对应的起始地址是 0x60000000。这部分地址空间被映射到 GD32 的内部寻址范围,CPU 直接读写该地址就能触发 EXMC 总线周期。
地址规划上,我把 FPGA 内部的寄存器按功能划分到不同的地址偏移。比如偏移 0x0000-0x00FF 放控制寄存器,偏移 0x0100 以后放数据缓冲区。FPGA 解码地址线时,只关注低位地址,用高位地址做区域选择,这样可以扩展更多的内部地址空间。一定要注意地址对齐的问题,16 位模式下地址线错位规则和 8 位模式不同,通常是 A0 不参与数据对齐,内部偏移从 A1 开始计算,FPGA 端译码时也要对应处理。
这在调试时特别容易出问题,我在第一次上电测试时,往地址 0x60000000 写值,FPGA 端看到地址出现在 0x00000001 上,排查了很久才发现是地址线错位导致。这个问题后面会在常见问题里详细展开。
3. 硬件连接与关键信号设计
3.1 信号线清单和连接要点
GD32 的 EXMC 和 FPGA 连接,核心信号可以分为四组,下面是我实际用的连接方式:
| 信号类型 | 信号名 | 方向 | 说明 |
|---|---|---|---|
| 数据线 | DATA[15:0] | 双向 | 16 位数据总线,必须加双向缓冲控制 |
| 地址线 | ADDR[15:0] | 输出 | 用于内部寄存器或缓冲区寻址,按需连接 |
| 控制线 | EXMC_NE | 输出 | 片选信号,低有效,选中 FPGA |
| 控制线 | EXMC_NOE | 输出 | 读使能,低有效 |
| 控制线 | EXMC_NWE | 输出 | 写使能,低有效 |
时序上,GD32 EXMC 在写操作时先拉低片选,然后地址稳定,再拉低写使能,数据在写使能期间保持有效,最后释放写使能,完成锁存。读操作类似,GD32 发送地址后拉低读使能,FPGA 需要在一个规定时间内把数据放到总线上,然后 GD32 在上升沿采样数据。双方必须严格匹配这些时间参数,尤其是 FPGA 的数据输出延迟。
硬件连接上,我有几个建议。第一,数据总线一定要加上下拉电阻或总线缓冲器,防止总线悬浮导致 FPGA 端误判电平。第二,FPGA 端的数据总线 IO 必须设置为三态,并在片选和读写信号无效时输出高阻态,否则多个设备驱动总线会造成冲突。第三,GD32 和 FPGA 之间如果距离比较远,建议在关键控制线上加串联电阻,通常 22Ω 到 33Ω,可以减少信号反射。
3.2 FPGA 端总线时序逻辑怎么设计
FPGA 端是整个通信的从机,需要解析 GD32 发起的总线操作。我的做法是用 Verilog 写一个简易的从机接口模块,核心是一个状态机,监控片选信号、读信号和写信号的变化。当检测到片选有效且写信号出现下降沿时,锁存地址总线和数据总线,并根据地址把数据写入对应的内部寄存器组,这个很好理解。
读操作需要注意时序。GD32 给出地址并拉低读使能后,FPGA 必须在一个很短的时间内完成地址译码并把数据推到数据总线上。这个时间通常只有几十纳秒,如果 FPGA 内部逻辑链路过长,很容易超时。解决方法是在 FPGA 内部做一次流水线,提前把地址译码结果准备好,或者使用 Block RAM 的异步读端口,能极大缩短输出延迟。
时钟方面,如果 GD32 和 FPGA 有独立的时钟源,建议在 FPGA 端用同步逻辑重新采样控制信号,避免亚稳态。我当时用的是 GD32 的 EXMC 时钟作为 FPGA 端参考时钟,通过一个全局时钟引脚进入 FPGA,保证控制信号和内部逻辑同步,调试时省了很多麻烦。
4. 软件代码实现与配置要点
4.1 初始化 EXMC 控制器的关键配置
GD32 的固件库对 EXMC 做了封装,但不同系列的库函数名和结构体会略有差异。以我的 GD32F450 项目为例,关键配置代码如下:
void exmc_init(void) { exmc_gpio_config(); exmc_norsram_parameter_struct exmc_norsram_init_struct; exmc_norsram_struct_para_init(&exmc_norsram_init_struct); exmc_norsram_init_struct.norsram_region = EXMC_BANK0_NORSRAM_REGION0; exmc_norsram_init_struct.write_mode = EXMC_ACCESS_MODE_A; exmc_norsram_init_struct.data_width = EXMC_NORSRAM_DATA_WIDTH_16B; exmc_norsram_init_struct.memory_type = EXMC_MEMORY_TYPE_SRAM; exmc_norsram_init_struct.address_setup_time = 5; exmc_norsram_init_struct.address_hold_time = 5; exmc_norsram_init_struct.data_setup_time = 8; exmc_norsram_init_struct.write_setup_time = 5; exmc_norsram_init_struct.write_hold_time = 5; exmc_norsram_init_struct.data_latency_time = 0; exmc_norsram_init_struct.read_bus_latency_time = 0; exmc_norsram_init_struct.write_bus_latency_time = 0; exmc_norsram_init_struct.address_data_mux = DISABLE; exmc_norsram_init_struct.burst_mode = DISABLE; exmc_norsram_init_struct.wait_polarity = EXMC_CONTROL_WAIT_POLARITY_LOW; exmc_norsram_init_struct.write_enable = ENABLE; exmc_norsram_init_struct.wait_signal = DISABLE; exmc_norsram_init_struct.extend_mode = DISABLE; exmc_norsram_init(&exmc_norsram_init_struct); exmc_norsram_enable(); }注意看这里我用的memory_type是 SRAM,不是 NOR Flash。这个区别很重要,NOR Flash 模式会有额外的命令锁存时序,SRAM 模式就是纯粹的地址锁存和读写,FPGA 端实现起来更直接。
ACCESS_MODE_A和ACCESS_MODE_B的区别在于读和写时序的差异,Mode A 适合 SRAM,Mode B 更适合 NOR Flash。我选 Mode A,也建议你用 Mode A。时序参数address_setup_time、data_setup_time等,需要根据 FPGA 端的实际响应速度来调整,这个没有固定答案,只能测试中优化。
4.2 GPIO 复用配置
GPIO 配置是很多人容易忽略的地方。EXMC 引脚不像普通 GPIO 那样设置输入输出就行,必须配置为复用推挽模式。GD32 的固件库中 GPIO 初始化结构体里,gpio_mode要设为GPIO_MODE_AF_PP,然后调用gpio_pin_af_config把引脚复用到 EXMC 功能上。
我曾经看到有人把数据引脚配置成普通推挽输出,结果读数据死活不对,因为普通输出模式下引脚没有三态能力,当 FPGA 往总线放数据时,GD32 的引脚还在主动驱动总线,两个输出必然打架。所以AF_PP这个配置是必须的,不是为了好看,是电气特性上的硬性要求。
引脚分配上,不同型号的 GD32 支持的引脚映射不一样,甚至同一型号不同封装也有差异。设计 PCB 之前一定先查数据手册的引脚定义表,确认使用的引脚号支持 EXMC 复用功能,避免后续飞线或者改板。
4.3 读写操作封装
初始化完成后,EXMC 的读写就是访问指针,非常简单,但建议封装成函数,方便做错误处理和调试日志:
#define FPGA_BASE_ADDR (0x60000000u) #define FPGA_REG_CTRL (0x0000u) #define FPGA_REG_STATUS (0x0002u) #define FPGA_DATA_BUFFER (0x0100u) static volatile uint16_t *fpga_reg_ctrl = (volatile uint16_t *)(FPGA_BASE_ADDR + FPGA_REG_CTRL); static volatile uint16_t *fpga_reg_status = (volatile uint16_t *)(FPGA_BASE_ADDR + FPGA_REG_STATUS); static volatile uint16_t *fpga_data_buffer = (volatile uint16_t *)(FPGA_BASE_ADDR + FPGA_DATA_BUFFER); uint16_t fpga_read_reg(uint32_t offset) { return *(volatile uint16_t *)(FPGA_BASE_ADDR + offset); } void fpga_write_reg(uint32_t offset, uint16_t value) { *(volatile uint16_t *)(FPGA_BASE_ADDR + offset) = value; }这套封装很薄,但好处是后续如果要加缓存、统计读写次数、或者做断言检查,只需要在函数内部增加逻辑,所有调用方不用改动。我在实际项目中还用volatile关键字避免编译器优化导致漏读漏写,这也是低级但致命的错误。
读操作返回后,建议先判断一下状态寄存器的值,验证 FPGA 是否已经准备好数据。我们这边踩过坑,GD32 读取数据时 FPGA 刚好在更新缓冲区,拿到一半新一半旧的数据。后来在 FPGA 端加了数据有效标志,GD32 先查询标志再读数据,问题就解决了。
5. 调试记录:波形分析与典型问题排查
5.1 用逻辑分析仪验证时序是否匹配
接口通信调试,最忌讳的就是闷头猜问题。我调试 EXMC 和 FPGA 通信,第一件事永远是接逻辑分析仪,把片选、读使能、写使能、地址线、数据线全部抓出来,跟数据手册上的时序图做对比。GD32 的 EXMC 时序可以在参考手册里找到对应模式下的波形图,对照检查地址建立时间、数据建立时间是否符合预期。
我第一次上板调试,写操作波形没有任何问题,但读操作出现数据一直读到 0xFFFF 的情况。用逻辑分析仪抓波形发现,FPGA 的数据总线根本就没被驱动起来,一直停留在高阻态。后来定位到 FPGA 端读操作的三态门控制逻辑写反了,片选无效时将数据输出了,片选有效时反而高阻。这种问题如果没有逻辑分析仪,很难定位到 FPGA 内部。
5.2 地址错位问题排查
16 位总线模式下,地址线对齐是个高频问题。GD32 的 EXMC 在 16 位宽度时,内部地址总线的处理方式和 8 位不同。如果用 16 位模式,低位地址线 A0 不参与寻址,实际上会偏移一位。比如你想访问偏移 0x0002,需要给的地址线信号是 FROM 0x0001 对应的地址线状态。
FPGA 端译码时,要么主动把地址线右移一位再解析,要么在定义寄存器偏移时就考虑到这个错位。我的做法是 FPGA 端在顶层接口把地址线addr[15:1]拿出来做内部译码,这样内部寄存器偏移定义和 GD32 端完全一致,不会搞混。
这个问题的隐蔽性在于,如果你只写地址 0x0000 和 0x0001,它们对应的物理地址线完全相同,根本测不出问题,但一旦访问地址 0x0002 和 0x0003,就会出乱子。我建议在联调初期先用一个读写测试函数,逐个地址写一个固定值再读回来,比对结果,这样能快速发现错位。
5.3 时序裕量与速度优化
EXMC 的时序参数不是越大越好,也不是越小越好,需要找到双方都能接受的平衡点。参数太大,单次读写耗时过长,高速传输时 CPU 被拖累;参数太小,建立时间不够,数据采样不稳定。
我在实际调优时,先用相对宽松的参数保证功能正确,然后用逻辑分析仪观察信号建立和保持的裕量。比如数据建立时间从data_setup_time = 8开始,功能正常后逐步减小到 5、4,每次修改后跑一轮压力测试,验证上千次读写没有错误,才算稳定。硬件上如果布线质量差,信号上出现过冲和振铃,稍微宽松的参数可以掩盖这些问题,但产品的长期可靠性不能依赖这个,还是要回归到信号完整性上解决。
另外提到 GD32 和 FPGA 之间的通信,volatile和内存屏障的问题也要注意。虽然你的读写是直接映射的地址访问,但编译器可能会因为优化而调整访问顺序,尤其是在做批量读写时。我的建议是,对于 FPGA 端一些有前后依赖关系的寄存器操作,逗留之间不要依赖编译顺序,必要的时候可以用__DMB()或者直接不优化某些函数,避免底层硬件操作被编译器重排。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 写入后 FPGA 端数据全为 0 | 地址线错位或写使能时序不满足 | 检查 EXMC 的地址映射,抓波形确认 NWE 时序 |
| 读回数据一直是 0xFFFF | FPGA 没有驱动数据总线 | 检查 FPGA 端三态门逻辑,确认读操作时序 |
| 偶发读写数据错误 | 时序裕量不足或信号质量问题 | 适当增加 data_setup_time,检查线路串阻和地线 |
| 系统卡死,程序跑飞 | EXMC 总线访问超时导致硬件异常 | 确认 BANK 地址空间是否正确,是否访问了不存在的地址 |
| 和 FPGA 握手正常但性能远低于预期 | 每次读写之间等待过长 | 开启 EXMC 的突发模式,或者用 DMA 搬运数据 |
阅读项目时,很多人还会混淆 EXMC 的 Bank 0 和 Bank 1 及不同寄存器配置。GD32 的 EXMC 在不同型号上支持的区域数量不完全一致,有的只有 Bank0,有的支持多 Bank。如果设计中要用到多个片选,必须确认芯片手册里的区域划分,并检查 Bank 间地址是否重叠。
6. 后续扩展:DMA 搬运和大数据块传输
EXMC 通信一个非常值得扩展的方向是利用 DMA 做数据搬运。GD32 的 DMA 控制器可以配置为内存到内存或者内存到外设模式,这里我们可以配置为从 FPGA 的地址空间读取数据到内部 SRAM,而不经过 CPU 逐字搬运,大幅提高吞吐效率。配置 DMA 时要注意几件事:数据宽度必须和 EXMC 总线宽度一致,传输方向不要搞混,外设地址就是 FPGA 的基地址,内存地址是缓冲区地址,传输大小按字节或半字计算。
我用 DMA 配合 EXMC 做了一次 64KB 数据块的批量读取测试,效果对比非常明显。在 CPU 循环读取模式下,读 64KB 数据耗时约 2 毫秒,切换 DMA 后降到几十微秒级别,差距超过一个数量级。如果你的项目里有实时性要求高的批量数据传输场景,强烈建议加 DMA,不要省这个功能。
还有一个可以扩展的点是用 EXMC 的突发传输模式。GD32 的 EXMC 支持同步突发读取,在 NOR Flash 模式下可以配置突发长度,但 SRAM 模式下的支持力度要看具体芯片型号。我那边最终没有用突发模式,因为 FPGA 端实现同步时序相对复杂,DMA 方案已经满足需求,但如果你对带宽有极致要求,可以深入研究一下。
7. 关于接口选择的一点个人体会
项目做完之后,如果再让我选一次,我还是会选 EXMC 而不是 SPI 或者并口 GPIO 模拟。GPIO 模拟总线的方案听起来简单灵活,但实际速率根本提不上去,而且 CPU 被占得死死的,完全没有扩展性。SPI 虽然接线少,但在双方向高吞吐场景下,协议开销和中断处理成本都不低。
EXMC 最大的价值在于,它把 FPGA 完美地抽象成了一块内存,读和写变成了最简单的地址操作,整个软件框架清晰了很多。与此同时,它也有代价,就是对硬件设计的要求高,时序调试相对复杂,算法工程师需要快思考慢思考,调整参数时要多看波形。
最后分享一个小经验:第一次做 EXMC 和 FPGA 通信的时候,不要一上来就追求复杂功能,先做一个最小系统,点亮一个 LED 或者读写一个寄存器,验证时序通了再扩展功能。高速路上栽跟头,往往是地基没打好。当时我为了验证时序,写了一个简单的寄存器回环测试,GD32 写一个值到 FPGA 的寄存器,FPGA 把值原样读出来,经过几百次循环比对无误后,才敢继续往下加功能。这个习惯,希望你也有。
本文还有配套的精品资源,点击获取