GD32与FPGA的EXMC接口实战:从原理到高速数据传输
发布时间:2026/9/28 14:09:34来源:尧图网络
GD32和FPGA的搭配在工业控制、仪器仪表、视觉检测这些行当里越来越常见。FPGA负责高速采样、并行预处理GD32做主控跑逻辑、通信、显示两边一拍即合。但真正把这两个器件焊到一块板子上让数据在它们之间流畅跑起来中间那层接口往往是最磨人的。有人用SPI一秒钟几兆字节顶天了有人用IO口模拟总线写驱动写得想砸键盘还有人直接上EXMC却发现时序不对、读写不稳定来回调参调到头秃。这期内容就围绕GD32与FPGA的EXMC接口把它从原理到实战拆个通透。我会结合自己调试过的实际项目讲清楚为什么选EXMC而不是SPI或并口IO模拟EXMC在GD32里到底是怎么工作的FPGA侧又该怎么配合以及我在联调时踩过哪些坑、最后是怎么爬出来的。无论是准备做异构计算的嵌入式工程师还是正在被总线和时序折磨的FPGA玩家这篇文章都值得你花几分钟看完。1. 为什么是EXMC并行接口的选型逻辑1.1 常见MCU与FPGA交互方案对比做GD32和FPGA的通信第一件事不是画原理图而是确定用哪种接口。这个决定直接决定了后续的吞吐量上限、驱动复杂度、引脚占用和调试难度。我把几种主流方案拉了一张表方便你对照自己的应用场景做取舍。接口方案理论带宽8位典型主频引脚占用驱动复杂度典型应用场景SPI从模式1-10 MB/s4-6低小数据量控制、参数下传UART0.1-1 MB/s2极低调试、低速率遥测IO模拟并行受IO翻转速度限制8-16中高无硬件总线时的临时方案EXMC/FSMC几十到上百MB/s20-30中图像、高速采集、大块数据传输PCIe/RapidIO数GB/s以上复杂极高高端软件无线电、高速信号处理从表里能直观看到EXMC在MCU级方案里属于“带宽和复杂度平衡得最好”的那个。SPI简单但对大数据量传输不友好UART就更不用说了。如果你只是给FPGA传几个配置参数SPI完全够用但如果是传送一帧1280x720的图像哪怕是灰度图也有近1MB的数据量SPI跑满也就是一秒几帧的水平EXMC则是几十帧起步的吞吐量完全不是一个量级。1.2 EXMC到底解决了什么问题EXMC的英文全称是External Memory Controller从名字就能看出它的定位——它本身就是给MCU扩展外部存储器设计的控制器。标准的NOR Flash、SRAM、PSRAM都在它的支持列表里。FPGA虽然没有“记忆”但它的逻辑模块可以被设计成类似SRAM的接口时序于是EXMC就顺理成章地成了MCU与FPGA之间最高效的桥梁之一。我见过很多人在GD32和FPGA之间用GPIO模拟总线理论上也能跑但实际用起来会有几个非常难受的问题。首先是时序不可控IO翻转的延迟受中断、系统调度影响很大跑起来带宽忽高忽低其次是代码效率低每一次读写都要手动控制片选、读写使能、地址和数据线的电平顺序写一个简单寄存器操作都要十几条语句。EXMC就不一样了它把时序控制全部交给硬件外设你在GD32里写一次地址总线访问硬件自动完成片选拉低、地址建立、数据读写、释放总线这一整套动作对CPU来说就像访问内部SRAM一样简单。2. EXMC接口机制与原理解析2.1 EXMC存储映射与访问模型GD32的EXMC把外部存储器映射到ARM核心的4GB地址空间里一共分成6个片选区Bank。每个片选区有独立的片选信号NE[x]可以接不同的外部设备比如Bank0接NOR FlashBank1接SRAMBank2接FPGA互不干扰。以GD32F407为例外部存储器的起始地址是从0x60000000开始的每个Bank大小为256MB。对FPGA来说最关键的是理解EXMC的两种访问模式NOR Flash模式和SRAM模式。NOR Flash模式保留了地址锁存信号NADV适合复用地址/数据总线的器件SRAM模式则简化为独立的地址线和数据线。GD32与FPGA交互时我更推荐使用SRAM模式因为FPGA内部逻辑并不需要地址锁存独立地址/数据总线反而更容易设计和排查问题。访问模型上EXMC支持8位、16位和32位数据宽度。16位是常用的平衡点因为8位模式带宽减半32位模式引脚占用太多。选定16位宽度后有个容易踩坑的地方外部地址线需要从A1开始连接A0被内部用来区分高字节和低字节不再对外输出。换句话说GD32的地址线要整体左移一位接到FPGA16位地址访问到的实际是FPGA上32KB的寄存器空间每次读写都对齐到16位边界。2.2 EXMC时序参数与读写周期理解EXMC时序是调通FPGA接口的前提。GD32的EXMC控制器在NOR/SRAM模式下一次读操作大致分为三个阶段地址建立、数据建立、地址保持。对应到寄存器里就是三个可调参数ADDSET地址建立时间、DATAST数据建立时间、ADDHOLD地址保持时间。具体时间含义是这么算的地址建立时间就是片选拉低、地址线送出到读使能信号拉低之间的时间数据建立时间则是读信号拉低到数据被采样之间的时间。这些参数都以AHB时钟周期为单位GD32F407跑120MHz时一个周期大约8.3nsADDSET设2个周期、DATAST设4个周期就能得到约50ns的完整读周期。为什么时序参数要单独说因为在FPGA内部从检测到读请求到真正把数据放到总线上存在一个组合逻辑加寄存器输出的延迟。如果没有给足数据建立时间高速时钟下就会出现读回的数据是上一个地址残留值的情况。我习惯的调校方式是先把参数放到最宽松比如ADDSET15DATAST15确认功能无误后再逐步压缩时序参数直到找到稳定工作的最小值再保留20%的余量。2.3 GD32与STM32在EXMC/FSMC上的差异提醒很多做STM32移植到GD32的老手会在这里翻车。GD32的EXMC在寄存器布局和功能逻辑上整体兼容STM32的FSMC但有一些微妙的区别。比如GD32的某些系列默认AHB频率更高同样的延迟参数跑起来实际读周期更短对FPGA侧的时序要求就更严另外GD32的GPIO复用功能配置与STM32有差异EXTI中断、重映射机制都不同不能直接照搬HAL库代码。如果是从STM32的HAL库工程转过来建议不要直接复制初始化代码。STM32用HAL_SRAM_Init配置FSMCGD32则是在标准外设库或新版的GD32固件库里用exmc_norsram_parameter_struct结构体去配置。两者的参数命名和默认值都有出入我遇到过复制FSMC代码在GD32上跑发现NOR模式初始化一直进不了就绪状态的情况最后查了勘误手册才发现是时序寄存器设置阈值不同导致的。老老实实对照GD32参考手册的时序配置章节重新算一遍问题就消失了。3. GD32侧软件配置核心流程3.1 标准工程建立与GPIO复用配置在动手写EXMC驱动前工程模板得先搭好。GD32的开发环境比较多Keil MDK、GD32 Embedded Builder、VS Code搭配EIDE插件都是常用的组合。习惯用VS Code写代码的可以选EIDE插件管理编译和下载都很方便想要开箱即用、省去插件配置折腾的官方GD32 Embedded Builder也还凑合就是界面略丑。从工程角度看核心是要选对器件型号然后把启动文件、系统时钟初始化和标准外设库加进来。关于GD32的时钟配置这里有个很多人问过的问题GD32能不能像STM32那样直接用外部晶振和PLL跑高主频答案是可以但要注意GD32的USBD、CAN等外设对时钟频率比较敏感如果这些外设和EXMC同时工作建议用官方推荐的120MHz主频并将APB总线频率合理分频避免EXMC的时序基准频率过高导致参数设置困难。GPIO复用配置是EXMC初始化的第一道门槛。以GD32F407为例EXMC用到的引脚分散在多个GPIO口上包括地址线、数据线、控制线。每个引脚都要设置为复用推挽输出模式并且复用功能号要选对。这里特别提醒GD32的AFIO复用映射和STM32不完全一致比如PD4、PD5这些引脚在GD32F407上的EXMC功能对应的是AF0或AF12具体查阅数据手册的复用功能表别凭经验照搬。/* GD32F407 EXMC GPIO初始化示例 */ void exmc_gpio_init(void) { rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_GPIOD); rcu_periph_clock_enable(RCU_GPIOE); rcu_periph_clock_enable(RCU_AF); /* 数据线 D0-D15PD14, PD15, PD0-PD7, PE7-PE15 */ gpio_af_set(GPIOD, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15); gpio_af_set(GPIOE, GPIO_AF_12, GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15); gpio_mode_set(GPIOE, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15); gpio_output_options_set(GPIOE, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); /* 地址线 A0-A18PE0-PE6, PD11-PD13 */ gpio_af_set(GPIOE, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6); gpio_af_set(GPIOD, GPIO_AF_12, GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13); gpio_mode_set(GPIOE, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6); gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13); /* 控制线NE1(PD7), NOE(PD4), NWE(PD5), NWAIT(PD6) */ gpio_af_set(GPIOD, GPIO_AF_12, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); }3.2 EXMC时序参数计算与实践配置GPIO配置完之后重点就是EXMC控制器本身的初始化。GD32的EXMC初始化结构体里包含NOR/SRAM模式配置和时序配置两部分。模式选择上如果FPGA侧实现了SRAM接口就把模式设为SRAM如果FPGA用NOR Flash模型就选NOR模式。地址和数据宽度设为16位数据总线设为复用模式。时序参数的计算要结合自己的AHB时钟频率。我用的GD32F407跑120MHzAHB时钟也就是120MHz一个时钟周期约8.3ns。参考GD32数据手册SRAM读操作要求地址建立时间最小约10ns数据建立时间最小约30ns换算成周期数分别是2和4。保留余量后我通常会设ADDSET3、DATAST5总体读周期接近66ns稳定性很好。在带宽敏感的场景再逐步减小这两个参数直到读写开始出错前再回退一档。/* EXMC初始化结构体示例 */ void exmc_init(void) { exmc_norsram_parameter_struct exmc_norsram_init; exmc_norsram_parameter_struct exmc_norsram_timing_init; /* 使能EXMC时钟 */ rcu_periph_clock_enable(RCU_EXMC); /* 配置扩展模式关闭、写使能、SRAM模式、16位数据宽度 */ exmc_norsram_init.norsram_region EXMC_BANK0_REGION0; exmc_norsram_init.write_mode EXMC_ASYN_WRITE; exmc_norsram_init.extended_mode DISABLE; exmc_norsram_init.data_width EXMC_NORSRAM_DATA_WIDTH_16B; exmc_norsram_init.memory_type EXMC_MEMORY_TYPE_SRAM; exmc_norsram_init.norsram_ws DISABLE; /* 核心时序参数 */ exmc_norsram_timing_init.adv_hold_time 0; exmc_norsram_timing_init.addr_setup_time 3; exmc_norsram_timing_init.data_setup_time 5; exmc_norsram_timing_init.bus_latency 0; exmc_norsram_timing_init.address_hold_time 1; exmc_norsram_init.read_write_timing exmc_norsram_timing_init; exmc_norsram_init.write_timing exmc_norsram_timing_init; exmc_norsram_init(exmc_norsram_init); }这里补充一个容易忽视的细节address_hold_time在SRAM模式下虽然用不上但也要设为非零值。我试过把这个参数设为0结果读操作总是多出几个时钟周期的毛刺导致FPGA侧状态机偶发错乱。设为1个周期就一切正常了这是数据手册里没写明白的细节。3.3 基于指针的寄存器映射访问当EXMC初始化完成后访问FPGA就像访问一个外部数组。通过指针将Bank0的基地址映射到一个结构体上就能用结构体成员的语法来读写FPGA的寄存器空间。这种做法既直观又高效编译器还能自动生成LDR/STR指令不会产生额外的函数调用开销。/* FPGA寄存器映射结构体 */ typedef struct { volatile uint16_t CTRL; /* 0x00: 控制寄存器 */ volatile uint16_t STATUS; /* 0x02: 状态寄存器 */ volatile uint16_t DATA_FIFO; /* 0x04: 数据FIFO端口 */ volatile uint16_t DMA_LEN; /* 0x06: DMA长度寄存器 */ volatile uint16_t IRQ_MASK; /* 0x08: 中断屏蔽寄存器 */ uint16_t RESERVED[3]; volatile uint16_t DATA_BUF[128]; /* 0x10-0x10F: 数据缓冲 */ } fpga_regs_t; #define FPGA_BASE_ADDR ((uint32_t)0x60000000) #define FPGA_REG ((volatile fpga_regs_t *)FPGA_BASE_ADDR) /* 写控制寄存器示例 */ void fpga_write_ctrl(uint16_t value) { FPGA_REG-CTRL value; } /* 从FIFO读一个16位数据 */ uint16_t fpga_read_fifo(void) { return FPGA_REG-DATA_FIFO; }需要注意的是编译器可能对结构体内的填充字段做对齐优化如果你改了数据宽度或加了字段建议用#pragma pack(1)或者显式添加RESERVED数组来保证地址偏移符合FPGA侧的寄存器布局。另外访问外部存储器的变量建议加上volatile修饰防止编译器优化成寄存器缓存导致读到的数据始终是第一遍的值这是一个我非常容易踩的隐蔽坑。4. FPGA侧逻辑实现与交互设计4.1 FPGA内部总线接口模块设计FPGA侧的工作量其实不输于GD32。GD32发来的读请求FPGA端必须在一个可控的时间窗口内返回有效数据。设计上我一般会把接口模块拆成三个子模块地址译码与寄存器堆、读写状态机、数据FIFO。地址译码的逻辑比较简单把EXMC送来的地址线信号经过组合逻辑判断当前访问的是哪个寄存器或FIFO。但是这里要注意不要使用组合逻辑直接产生寄存器的写使能信号容易出现毛刺。正确做法是让读写状态机产生经过同步的写使能脉冲用这个脉冲去锁存数据总线上的值。我的实现思路大致是这样的每当检测到NWE下降沿说明GD32正在执行一次写操作把此时数据总线上的值写入地址对应的寄存器检测到NOE下降沿则把目标寄存器的值驱动到双向数据总线上。// FPGA侧EXMC接口状态机核心逻辑简化版 reg [15:0] reg_ctrl; reg [15:0] reg_status; reg [15:0] data_fifo_out; // 写事务核心 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin reg_ctrl 16h0; end else if (exmc_nwe 1b0) begin case (exmc_addr) 16h00: reg_ctrl exmc_data_in; // CTRL寄存器 16h06: dma_len_reg exmc_data_in; // DMA长度寄存器 default: ; endcase end end // 读事务核心输出寄存器数据到双向总线 always (*) begin case (exmc_addr) 16h02: fifo_rd_data reg_status; // STATUS寄存器 16h04: fifo_rd_data data_fifo_out; // FIFO端口 default: fifo_rd_data 16hDEAD; endcase end4.2 双向数据总线的控制细节双向数据总线是整个接口最容易出问题的地方。FPGA的IO引脚要同时承担输入和输出功能必须通过方向信号来控制三态缓冲器。这个方向信号通常由NOE信号转换而来NOE为低时FPGA向外驱动数据NOE为高时FPGA释放总线让GD32驱动。有些初学者会直接让数据总线恒为输出结果一读一写就冲突严重的还会损坏引脚。正确做法是采用高阻态控制。在Verilog里就是在读有效期间把寄存器数据赋给总线的同时用assign exmc_data (exmc_noe 1b0) ? fifo_rd_data : 16hzzzz;这样的三态赋值语句。这个思想说起来简单但实际联调时外部上拉、下拉电阻和GD32侧IO配置会干扰总线状态如果读到0xFF或0x00这类固定值多半就是总线方向没控制对。4.3 通过FIFO实现高速数据缓存在真实项目中GD32与FPGA之间往往不是简单读几个寄存器而是持续搬运大批量数据。比如FPGA内部ADC采样模块以50MHz频率不断产生12位数据GD32不可能每个周期都去读就需要在FPGA内部设计一个FIFO把数据缓存起来。FIFO的深度取决于GD32的读取节奏。假设GD32每5微秒通过DMA搬运一次数据每次搬运256字节那么FIFO深度至少要能容纳两个这样批次的数据才能避免DMA初始化期间数据溢出。我常用的配置是异步FIFO写时钟用FPGA侧采样时钟读时钟用EXMC的读周期时钟宽度16位深度512。FIFO的空满标志对外映射到状态寄存器的bit0和bit1。GD32侧在读FIFO前先读状态寄存器判断FIFO是否非空再启动DMA读取。这样软件与FPGA配合既能保证不读空也不至于丢掉数据。条件允许的话FPGA还可以拉高EXMC的NWAIT信号来动态插入等待周期让GD32在FIFO为空时自动等待直到新数据到来再完成读周期这个功能需要EXMC的扩展模式配合复杂度稍高但确实是效率最高的交互方式。5. 联调中常见问题与排查经验5.1 读写数据异常与总线冲突定位联调阶段遇到的问题五花八门我按出现频率从高到低列了一张快速排查表方便你对照参考。现象可能原因排查方向读回全为0xFFFFFPGA未驱动总线三态门方向反了检查NOE信号是否连对用示波器测电平读回全为0x0000FPGA内部寄存器复位值不对或数据总线被下拉检查FPGA寄存器复位逻辑和IO配置写入不生效写使能NWE和地址建立时序不满足增大地址建立时间ADDSET检查NWE脉宽偶发读写错位数据总线双向切换产生毛刺在GD32侧配置IO为慢速翻转加长总线周转时间访问整个Bank死机地址线A0接错了16位模式下外部A0必须悬空或接固定电平我印象最深刻的一个问题是GD32一访问EXMC地址就进入HardFault。现象很诡异读别的寄存器都正常一访问0x60000000就死。排查到最后发现是NE片选信号没接对FPGA侧根本没拉低片选EXMC控制器等了超时后进入异常。把NE1挂到逻辑分析仪上一看电平始终为高顺藤摸瓜查原理图才发现FPGA引脚分配错了。这类硬件连接问题最浪费时间建议画板前就用Excel列一张完整的信号对应表把GD32引脚、FPGA引脚、网络名、功能一一对应能省下大半天调试时间。5.2 FPGA复位信号与亚稳态处理联调中另一个不可忽视的点是复位信号处理。如果FPGA用GD32的某个GPIO输出的电平做异步复位而这个复位信号没有做同步处理很容易在时钟沿附近变化时产生亚稳态。复位释放时离时钟上升沿太近可能导致状态机进入非法状态表现就是接口时好时坏。解决方法是把外部复位信号先打两拍再做异步复位、同步释放处理。这在很多FPGA工程里是标配但和EXMC联动时尤其明显因为GD32启动时GPIO初始电平是不确定的如果没有正确的复位时序FPGA的寄存器在GD32发出第一次EXMC访问时可能还没就绪。我建议FPGA侧加一个上电延时计数器让内部逻辑等系统时钟稳定200ms后再释放复位同时GD32初始化代码在启动DMA搬运前先往控制寄存器写一个握手标志FPGA收到后才进入正常工作状态。5.3 GD32锁住后的解锁方法与固件更新调试过程中还容易遇到一个和接口本身无关但足以让人崩溃的问题GD32芯片被锁住读保护或调试口禁用。有几次我在修改EXMC引脚配置时把SWD调试引脚误配成了普通IO结果Keil报错无法连接目标芯片。万幸GD32不像某些MCU那样锁死就报废常用的解锁方法是按住复位键在Keil设置里选择Connect under Reset模式然后在烧录算法里加上整片擦除选项利用芯片上电默认SWD功能正常的窗口期把Flash清空。如果你的板子上有BOOT0引脚引出来也可以通过设置BOOT0为高电平进入系统存储器Bootloader模式用串口ISP工具重新下载程序。但对于已经焊接在设备里的板子我强烈建议在固件里保留一个简单的IAP升级功能或者至少不要把SWD引脚配置成无法恢复的模式。调试接口禁用的问题我至少遇到过三次每次都要靠短按复位和Connect under Reset的组合来救回来。6. 高速数据搬运实践DMA与EXMC的结合6.1 GD32 DMA配置与关键参数EXMC接口的吞吐量上限很高但如果每次都靠CPU去读寄存器再存内存实际带宽要大打折扣。真正发挥EXMC优势的用法是配合DMA把从FPGA FIFO读取的数据自动搬运到GD32的内存缓冲区全程不占CPU。GD32的DMA控制器配置和STM32类似但有些细节需要留意。首先是外设地址和数据地址的递增模式EXMC作为外设源地址时地址寄存器通常设为固定递增模式源地址是FPGA FIFO的固定地址目的地址是内存缓冲区的递增地址。数据宽度要匹配EXMC的数据宽度16位传输就设半字模式。DMA传输完成后会在DMA中断里发起新一轮搬运请求形成流水线操作。/* GD32 DMA读取FPGA FIFO示例 */ void dma_exmc_to_memory_init(uint16_t *buf, uint16_t len) { dma_parameter_struct dma_init; rcu_periph_clock_enable(RCU_DMA0); dma_deinit(DMA0, DMA_CH3); dma_init.periph_addr (uint32_t)FPGA_REG-DATA_FIFO; dma_init.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init.memory_addr (uint32_t)buf; dma_init.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init.direction DMA_PERIPH_TO_MEMORY; dma_init.number len; dma_init.periph_width DMA_PERIPHERAL_WIDTH_16BIT; dma_init.memory_width DMA_MEMORY_WIDTH_16BIT; dma_init.priority DMA_PRIORITY_HIGH; dma_init.periph_memory_sel DMA_PERIPH_SEL_EXMC; dma_init.memory_periph_sel DMA_MEMORY_SEL_SRAM; dma_init.memory_sel DMA_MEMORY_0; dma_init.memory_to_periph DMA_MEMORY_TO_PERIPHERAL_DISABLE; dma_init.memory_data_align DMA_MEMORY_DATA_ALIGN_NORMAL; dma_init.periph_data_align DMA_PERIPH_DATA_ALIGN_NORMAL; dma_init.circular_mode DMA_CIRCULAR_MODE_DISABLE; dma_init.block_half_interrupt DMA_BLOCK_HALF_INTERRUPT_ENABLE; dma_init.block_full_interrupt DMA_BLOCK_FULL_INTERRUPT_ENABLE; dma_init(DMA0, DMA_CH3, dma_init); dma_circulation_enable(DMA0, DMA_CH3); dma_channel_enable(DMA0, DMA_CH3); }值得说明的是GD32的DMA外设请求映射和STM32并不完全相同。在GD32F407上DMA0的通道3可以映射到EXMC的读请求但需要确认选型手册里的DMA请求映射表。我之前在STM32移植过来的工程里直接沿用DMA1通道4的映射结果GD32平台完全无法触发DMA传输查了半天才发现外设请求源编号不一样。6.2 环形缓冲与乒乓结构设计当DMA持续从FPGA搬运数据到内存时内存管理用简单的单缓冲区很容易出现数据竞争主循环在处理上一帧数据的同时DMA已经在往同一个缓冲区写入新数据了。我常用的解法是双缓冲或环形缓冲。双缓冲实现起来很直接定义两个大小相同的缓冲区DMA交替填充其中一个CPU处理另一个。GD32 DMA支持传输完成中断和半传输中断利用这两个中断可以精确知道缓冲区哪一半被填满了。在DMA半传输中断里处理第一个半块在DMA全传输中断里处理第二个半块这样连续流数据就能像两条流水线一样无缝衔接。配合前面提到的FPGA状态寄存器判断FIFO空满状态整个数据链路可以做到每秒钟搬运几十MB数据CPU占用率依然很低。我实测过一组数据用SPI方式从FPGA读取同样大小的图像数据GD32主频120MHz时只跑出约3MB/s的吞吐量而且CPU占用率接近满负荷切换到EXMCDMA方案后吞吐量直接提升到40MB/s以上CPU占用率降到不足15%。这个差距让你的应用是卡成PPT还是顺畅显示立竿见影。7. 从原型到产品的几个进阶建议7.1 硬件设计上的信号完整性细节原型验证没问题之后进入PCB设计阶段就要开始关注信号完整性了。EXMC总线的地址线和数据线是并行同步信号走线长度如果差距太大高速翻转时会引起建立时间和保持时间的窗口恶化。实际的规则是同一组数据线D0-D15的走线长度差控制在100mil以内地址线组和控制线组的长度差控制在200mil以内。地平面的完整性也比很多人想象的更重要。EXMC高速信号切换时需要一个连续的参考平面来抑制EMI如果信号正好跨越了电源分割区返回路径被切断波形质量会明显变差。我用过一次四层板把EXMC总线全部布在顶层第二层完整地平面第三层电源底层散线信号质量比之前的两层板好了不止一个量级。如果预算允许做四层板不是浪费是省事。7.2 通信协议与可扩展性设计EXMC接口只解决了物理传输的问题真正让系统跑得顺还要靠协议层设计。写寄存器偏移地址时要有意识地把控制寄存器、状态寄存器、FIFO端口、中断控制这些功能模块分开布局同时预留一些保留寄存器方便后续固件升级。不要把所有功能都挤在一两个寄存器里用位域硬编码后续加功能会非常痛苦。另一个经验是给关键寄存器操作加上握手流程。FPGA内部有些状态转换需要时间比如启动采集后A/D模块需要几个时钟周期才能稳定输出如果GD32写完启动命令立刻去读数据大概率读到无效值。我习惯在FPGA侧为这类操作设置一个BUSY标志位GD32轮询到BUSY拉低后才继续下一步。虽然轮询会占用少量CPU周期但比盲目延时等待可靠得多也方便排查问题。7.3 调试工具链与效率提升方法最后说说调试工具。很多做MCU的工程师习惯用JTAG/SWD调试器但调GD32与FPGA接口时我强烈建议你配一台逻辑分析仪这种工具可以并行观察几十路信号的时序关系。我用的是24通道的逻辑分析仪把NWE、NOE、NE、地址线、数据线全部加上标签配合EXMC的时序参考图只需要一次就能定位到是地址建立时间不够还是数据总线方向切换有问题。如果没有逻辑分析仪用示波器的多通道模式也能凑合但效率真的差很多。特别是在排查询时序问题、总线竞争问题时逻辑分析仪的波形视图比示波器直观十倍不止。还有个小心得在GD32代码里加一个调试用的寄存器读写函数直接把测试值写到EXMC总线FPGA捕获后用板载LED或串口输出当前状态这种“软件打点”的方法在初期快速验证物理链路时非常管用比一上来就跑完整DMA流程省心得多。我在实际项目里还养成了一个习惯每次修改时序参数或FPGA逻辑后先固化一组基准测试数据写一段测试代码连续读写固定模式跑上十万次再检查有无错误。这样改一版测一版回归问题能第一时间暴露而不是等整个系统联调时才发现数据偶发损坏。稳定压倒一切这个理念在高速接口调试里尤其重要。从GPIO模拟到EXMCDMA本质上是用硬件控制器替换掉CPU的低效轮询和手动时序操作。GD32负责逻辑决策和通信FPGA扛下高速并行的数据吞吐EXMC作为中间的高速通道把两者的优势充分发挥出来。做完一次这样的项目之后你对嵌入式系统中处理器与可编程逻辑器件的分工、总线协议设计、时序约束这些概念的理解都会比看书深刻得多。有空把你手上那些用SPI慢慢传数据的板子重新翻出来看看认真评估一下是否值得升级到EXMC方案性能提升可能会给你不小的惊喜。
网站建设高端定制企业官网