ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32与FPGA高速并行通信:FSMC接口配置与实战指南

STM32与FPGA高速并行通信:FSMC接口配置与实战指南 简介面向STM32与FPGA高速并口通信场景这份资源提供了基于STM32Cube HAL库的FSMC接口完整工程示例适合需要利用FSMC总线与FPGA进行数据交互的嵌入式开发者参考。包体内含162个文件涵盖HAL库源码c/h、FSMC底层驱动stm32f4xx_ll_fsmc.c、stm32f4xx_hal_sram.c、Keil工程配置uvprojx/uvoptx以及已编译生成的hex、axf、map等输出文件整体压缩包大小9.25MB可帮助读者对照工程快速理解FSMC初始化、GPIO复用与时序参数配置的具体写法。已有2139人学习下载。通过该工程开发者可以直观看到从STM32CubeMX生成ioc配置到HAL库函数调用、再到读写FPGA寄存器的完整链路节省自行搭建和调试FSMC通信环境的时间为后续实现更复杂的并行数据交换提供基础模板。 前阵子接了个项目STM32F407 要从 FPGA 端的高速 ADC 连续搬数据最初用的是 SPI速率怎么算都不够CPU 也被大量占用。后来我换了思路把 FPGA 简化成一块“并口 SRAM”挂到 FSMC 总线上用 STM32Cube 里的 HAL 库配置一个下午就把双向通信跑通了速度翻了不止十倍。这篇就把这套 FSMCFPGA 通信方案的原理、HAL 配置、FPGA 端逻辑和调试过程中踩过的坑从头到尾讲清楚适合手里有 STM32 和 FPGA、想实现高速并行通信的工程师参考。先说结论FSMC 这个东西本质上就是让 STM32 像访问外部存储器一样和一个外部从机打交道。FPGA 只要能模拟出 SRAM 接口时序两边就能无缝通信。整条链路里STM32 侧重点关注 CubeMX 配置和时序参数计算FPGA 侧重点写一个合格的“总线从机”模块。下面按我实际开发的顺序展开。1. 为什么是 FSMC拿 FPGA 当一张“外挂 SRAM”来用1.1 先算一笔带宽账SPI、UART、FSMC 差了多少很多人在 STM32 和 FPGA 之间通信用 SPI简单是简单但跑高速数据流的时候真的是“差一口气”。SPI 接口在 STM32F407 上最高大概能到 42Mbit/s 左右换算下来大约 5MB/s除去协议开销、片选切换、CPU 中断处理实际有效吞吐可能只有 2~3MB/s。如果 FPGA 端是一路 1MSPS、16 位的 ADC光原始数据就是 2MB/sSPI 基本就被吃满了CPU 也没精力干别的。FSMCFlexible Static Memory Controller就不一样了。以 16 位数据宽度为例STM32 一条普通的*(volatile uint16_t *)0x68000000 value;写指令硬件自动完成地址建立、数据建立、片选和读写信号翻转单次访问周期可以做到 100ns 以内。如果时序参数调到比较激进的状态持续读写吞吐到 10MB/s 以上并不难。再用 DMA 搬运数据CPU 几乎可以撒手不管。通信方式典型有效带宽CPU 开销接线数量适合场景UART约 11.5KB/s 115200中2低速调试、指令交互SPI约 2~3MB/s实际中高4~5中低速传感器、小包数据FSMC 并口10MB/s 以上极低DMA20图像采集、高速 ADC、大块数据交换所以如果你的项目里 FPGA 和 STM32 之间要持续搬运上百字节以上的数据块FSMC 是非常值得考虑的方案。1.2 把 FPGA 当成外部存储器的核心思路FSMC 原本的设计目标是接 NOR Flash、SRAM、LCD 控制器、PC Card 这类并行外设。它的好处是接口和访问方式都非常统一CPU 给出地址控制器负责产生片选、读写使能、数据选通时序。对 FPGA 来说只要在内部模拟出和一个异步 SRAM 完全一致的对外接口STM32 就能直接用“读内存地址”和“写内存地址”的方式完成通信。这种“外部从机伪装成存储器”的做法有很实际的好处STM32 端不需要自定义通信协议不需要组帧、拆帧、校验一条访存指令就完成一次数据交换。FPGA 端逻辑非常直接核心就是寄存器文件和读写使能控制。配合 DMA可以实现像“从数组连续读出”一样的批量传输FPGA 端像一块大缓存。当然它也有不适合的场景。比如数据量很小、又分散或者两个设备距离超过十几厘米那 FSMC 这种并行总线接线多、地线要求高、远距离容易受干扰反而没必要。低速简单通信用 UART/SPI 更合适。2. 总线上一次读写到底发生了什么2.1 关键信号线先分清谁是干什么的FSMC 接入 FPGA常用的信号包括几组NEChip Select片选信号低电平有效。只有选中的 Bank 被访问时这个信号才会被拉低。如果 FPGA 只挂在其中一个 Bank 上平时可以基本忽略 NE 上的活动。NOEOutput Enable读使能信号低电平有效。NOE 拉低期间外部从机需要把指定地址的数据放到数据总线上。NWEWrite Enable写使能信号低电平有效。NWE 拉低期间STM32 会把数据放到数据总线上从机在 NWE 上升沿或高电平期间把数据锁存。FSMC_A[15:0]地址线每个 Bank 有独立的地址位宽具体接多少根由外部器件需求决定。FSMC_D[15:0]数据线8 位或 16 位可选。这里有一个很容易踩的坑如果配置成 16 位数据宽度地址线并不是一一对应到内部地址的。FSMC 内部地址最低位是字节选择位并不输出到外部 FSMC_A[0]外部 FSMC_A[0] 实际上对应内部地址的第二位。也就是说即使你只是接 FPGA 内部的普通寄存器也要想清楚“CPU 访问的地址”和“FPGA 引脚看到的地址”之间相差一个字节地址的关系。2.2 读周期与写周期的三段式时序FSMC 的异步 SRAM 访问时序可以粗略拆成三段地址建立期ADDSET地址先放到总线上之后片选和读写信号才拉低。这个时间是为了让从机先稳定地址译码结果。数据建立期DATAST读信号或写信号保持低电平的时间。读时从机必须在这段时间内把数据驱动到总线上写时STM32 在这段时间里把写数据稳定输出。地址保持期ADDHOLD读写信号恢复高电平后地址还会继续保持一小段时间方便从机完成数据锁存。以一次写操作为例STM32 先把地址放到 FSMC_A 上等到地址稳定后拉低 NWE同时把数据放到 FSMC_D 上最后 NWE 拉高。从机可以在 NWE 拉高前后锁存数据和地址。读操作则是STM32 拉低 NOE 后从机把对应地址的数据输出到总线上STM32 在 NOE 上升沿前完成采样。2.3 地址映射哪个 Bank 对应哪段内存STM32F4 系列 FSMC 的 Bank1 被划分为 4 个片选区每一个对应一段固定的内存地址Bank片选信号地址范围Bank1 NE1FSMC_NE10x60000000 - 0x63FFFFFFBank1 NE2FSMC_NE20x64000000 - 0x67FFFFFFBank1 NE3FSMC_NE30x68000000 - 0x6BFFFFFFBank1 NE4FSMC_NE40x6C000000 - 0x6FFFFFFF我常用的是 NE3因为 NE1 有时候会和板载 SRAM/NOR 冲突NE2/NE4 的地址段用得少也比较容易记。比如 FPGA 挂在 NE3 上CPU 访问 0x68000000 就相当于“选中 FPGA 的地址 0”。3. HAL 库配置 FSMCCubeMX 面板上的每一步和背后的时序计算3.1 CubeMX 里的配置项怎么填在 STM32CubeMX 中选好芯片型号后左侧找到 FSMCF4 系列在 CubeMX 里也叫 FMC打开后开启 NOR/SRAM 控制器选择一个 Bank。以 NE3、16 位数据宽度为例主要配置如下Memory type 选 SRAM不要选 NOR Flash。FPGA 模拟异步 SRAM 接口所以按 SRAM 类型走最合适。Memory data width 选 16 bits和硬件接线保持一致。Extended mode 建议打开。这样读写时序可以分开配置调起来灵活一些。Write operation、Asynchronous wait 这些按默认即可WAIT 信号 FPGA 端一般不接保持 Disable。CubeMX 生成代码时会自动把 FSMC 对应的 GPIO 引脚配置成复用功能并调用HAL_SRAM_Init完成初始化。这时候只需要填时序结构体参数。3.2 时序参数到底怎么算给你一套可用的起步值FSMC 的时序参数单位是 HCLK 周期。如果 STM32F407 主频 168MHz一个 HCLK 周期大约是 5.95ns。CubeMX 里填的AddressSetupTime、DataSetupTime、AddressHoldTime分别对应前面说的 ADDSET、DATAST、ADDHOLD。比较保守的起步配置FSMC_NORSRAM_TimingTypeDef Timing {0}; Timing.AddressSetupTime 15; // 约 89ns Timing.AddressHoldTime 15; // 约 89ns Timing.DataSetupTime 15; // 约 89ns Timing.BusTurnAroundDuration 0; Timing.CLKDivision 16; Timing.DataLatency 17; Timing.AccessMode FSMC_ACCESS_MODE_A;这几个值加起来单次访问周期接近 265ns吞吐大概 3~4MB/s能稳定跑通。跑通之后再逐步压缩比如AddressSetupTime8、DataSetupTime6、AddressHoldTime2单次访问约 90ns实际吞吐能到 10MB/s 以上。压缩时序时要注意几个原则地址建立时间不要太短至少保证 FPGA 端地址译码有足够时间稳定。NWE 低电平宽度由DataSetupTime决定写模式下它不能太短否则 FPGA 端采样靠边沿的话容易采错。每次改时序都要用逻辑分析仪或示波器看实际波形别盲调。3.3 HAL 库初始化代码和 CubeMX 生成的基本一致如果你不是用 CubeMX而是手写初始化可以参考下面这段我加了一些注释说明关键点FSMC_NORSRAM_TimingTypeDef ReadTiming {0}; FSMC_NORSRAM_TimingTypeDef WriteTiming {0}; SRAM_HandleTypeDef hsram; hsram.Instance FSMC_NORSRAM_DEVICE; hsram.Extended FSMC_NORSRAM_EXTENDED_DEVICE; hsram.Init.NSBank FSMC_NORSRAM_BANK3; // NE3 hsram.Init.DataAddressMux FSMC_DATA_ADDRESS_MUX_DISABLE; hsram.Init.MemoryType FSMC_MEMORY_TYPE_SRAM; // FPGA 模拟 SRAM hsram.Init.MemoryDataWidth FSMC_NORSRAM_MEM_BUS_WIDTH_16; hsram.Init.BurstAccessMode FSMC_BURST_ACCESS_MODE_DISABLE; hsram.Init.WaitSignalPolarity FSMC_WAIT_SIGNAL_POLARITY_LOW; hsram.Init.WrapMode FSMC_WRAP_MODE_DISABLE; hsram.Init.WaitSignalActive FSMC_WAIT_TIMING_BEFORE_NS; hsram.Init.WriteOperation FSMC_WRITE_OPERATION_ENABLE; hsram.Init.WaitSignal FSMC_WAIT_SIGNAL_DISABLE; hsram.Init.ExtendedMode FSMC_EXTENDED_MODE_ENABLE; hsram.Init.AsynchronousWait FSMC_ASYNCHRONOUS_WAIT_DISABLE; hsram.Init.WriteBurst FSMC_WRITE_BURST_DISABLE; hsram.Init.PageSize FSMC_PAGE_SIZE_NONE; ReadTiming.AddressSetupTime 15; ReadTiming.AddressHoldTime 15; ReadTiming.DataSetupTime 15; ReadTiming.BusTurnAroundDuration 0; ReadTiming.CLKDivision 16; ReadTiming.DataLatency 17; ReadTiming.AccessMode FSMC_ACCESS_MODE_A; WriteTiming.AddressSetupTime 15; WriteTiming.AddressHoldTime 15; WriteTiming.DataSetupTime 15; WriteTiming.BusTurnAroundDuration 0; WriteTiming.CLKDivision 16; WriteTiming.DataLatency 17; WriteTiming.AccessMode FSMC_ACCESS_MODE_A; HAL_SRAM_Init(hsram, ReadTiming, WriteTiming);初始化完成后直接定义两个宏就能读写#define FPGA_BASE_ADDR ((volatile uint16_t *)0x68000000) uint16_t val FPGA_BASE_ADDR[0]; // 读 FPGA 地址 0 FPGA_BASE_ADDR[1] 0x1234; // 写 FPGA 地址 216位模式下地址步长2注意因为是 16 位数据宽度数组下标每加 1实际地址会加 2 字节。4. FPGA 端接口逻辑做一个合格的“总线从机”4.1 寄存器规划先想清楚后面能少走很多弯路FPGA 端要先规划好地址空间。我一般用地址低 8 位直接作为寄存器索引这样最多 256 个 16 位寄存器对大多数通信场景足够。例如FPGA 地址寄存器名方向说明0x00ctrl_reg写控制命令0x01status_reg读状态反馈0x10-0x1Fdata_reg读写数据缓冲区0x20adc_data读高速数据出口从 STM32 角度看如果基地址是 0x68000000那么访问0x68000020对应的就是 FPGA 端地址 0x20。因为 16 位模式下外部 FSMC_A0 对应内部 HADDR[1]所以 CPU 侧寄存器的“数组下标”乘 2 就是实际总线地址。4.2 Verilog 实现的核心骨架片选解析、读写锁存、三态控制FPGA 端模块看起来复杂核心就三件事根据 NE/NOE/NWE 的电平关系识别一次访问。写的时候把总线上数据和地址锁存到相应寄存器。读的时候把对应寄存器的值驱动到数据总线上。一个可用的简化版模块如下module fsmc_slave ( input wire clk, // FPGA 内部时钟 input wire rst_n, input wire ne, // FSMC_NE input wire noe, // FSMC_NOE input wire nwe, // FSMC_NWE input wire [15:0] addr, // FSMC_A[15:0] inout wire [15:0] data // FSMC_D[15:0] ); reg [15:0] data_out; reg data_out_en; reg [15:0] ctrl_reg; reg [15:0] status_reg; reg [15:0] data_reg [0:15]; // 边沿检测对三个控制信号打拍 reg ne_d, noe_d, nwe_d; always (posedge clk or negedge rst_n) begin if (!rst_n) begin ne_d 1b1; noe_d 1b1; nwe_d 1b1; end else begin ne_d ne; noe_d noe; nwe_d nwe; end end wire cs_act ~ne_d; // 片选有效 wire rd_act ~noe_d cs_act; // 读有效 wire wr_act ~nwe_d cs_act; // 写有效 wire nwe_pos nwe ~nwe_d; // NWE 上升沿 // 写寄存器在 NWE 上升沿把地址和数据锁存 always (posedge clk or negedge rst_n) begin if (!rst_n) begin ctrl_reg 16h0; status_reg 16h0; end else begin if (wr_act nwe_pos) begin case (addr[7:0]) 8h00: ctrl_reg data; 8h10, 8h11, 8h12, 8h13, 8h14, 8h15, 8h16, 8h17, 8h18, 8h19, 8h1A, 8h1B, 8h1C, 8h1D, 8h1E, 8h1F: data_reg[addr[3:0]] data; default: ; endcase end end end // 读数据读有效时把对应寄存器值输出 always (*) begin if (rd_act) begin case (addr[7:0]) 8h00: data_out ctrl_reg; 8h01: data_out status_reg; 8h10, 8h11, 8h12, 8h13, 8h14, 8h15, 8h16, 8h17, 8h18, 8h19, 8h1A, 8h1B, 8h1C, 8h1D, 8h1E, 8h1F: data_out data_reg[addr[3:0]]; default: data_out 16hFFFF; endcase end else begin data_out 16h0000; end end // 三态只有读有效时才占用数据总线 assign data rd_act ? data_out : 16hzzzz; endmodule这里有几个需要注意的细节打拍和边沿检测用的时钟要保证频率明显高于 FSMC 的访问速度否则会产生比较大的延迟。一般 FPGA 内部时钟跑到 50MHz 以上配合上面的 FSMC 时序是没问题的。rd_act保持高电平时数据总线一直被 FPGA 驱动。FSMC 读时序结束后 NOE 拉高rd_act变低数据总线自动恢复高阻不会和下一次操作冲突。如果 FPGA 内部数据是异步产生的需要加同步或寄存一拍避免在 GPIO 上出现毛刺。读数据的组合逻辑虽然简单但也要看实际布局布线的延迟稳妥起见可以在输出前再做一级寄存器。4.3 跨时钟域和未接信号的处理FPGA 的内部时钟通常和 STM32 FSMC 的时钟不是同一个源。直接采样外部异步信号会有亚稳态风险。我的做法是所有的 NE、NOE、NWE 都先打两拍同步再做边沿检测。虽然会引入 2 个周期左右的延迟但对 100ns 级别的 FSMC 周期来说如果 FPGA 时钟跑 50MHz20ns 一级两级延迟 40nsFSMC 的数据建立时间只要留够余量就没事。如果 FPGA 端没有使用 WAIT 信号建议把 FSMC 配置里AsynchronousWait关掉同时硬件上不要把 WAIT 引脚悬空。有些板子上这个引脚如果悬空电平不确定FSMC 可能会认为外部设备一直处于忙状态导致读写卡死。最稳妥的做法是硬件上把这个引脚上拉或下拉到一个确定电平然后在 CubeMX 里把 WaitSignal 设为 Disable。5. 实际调试遇到的问题按排查顺序讲给你听5.1 读回来全是 0xFFFF先别急着怀疑时序我第一次上电STM32 读 FPGA 的状态寄存器返回的一直是 0xFFFF。一开始我以为是时序太紧把 ADDSET 和 DATAST 都加了很大仍然无解。后来查了一圈发现是 FPGA 端 data_out_en 逻辑写反了数据总线一直处于高阻状态。总线没人驱动读上来自然全 1。排查这种问题时不要一上来就调软件。先拿逻辑分析仪或者示波器抓 NE、NOE、数据总线的波形。如果 NOE 拉低的时候数据总线上就是高阻电平说明问题在 FPGA 端的三态控制如果 NOE 还没拉低数据线上就已经有数据了那说明时序参数可能确实太紧地址还没有稳定。优先检查顺序硬件引脚连接尤其是 FSMC 数据线有没有接错、接触不良。CubeMX 里引脚复用是否生效GPIO 是否被正确配置为 AF。FPGA 端三态门逻辑读有效信号是否正确拉低。实际波形NOE/NWE 有没有动作。5.2 地址对不上隔一个地址出现重复数据这个问题特别隐蔽。现象是 STM32 读地址 0x68000000 和 0x68000002得到的结果居然一样。原因基本都出在 16 位数据宽度带来的地址映射错位上。FSMC 16 位模式下STM32 内部 HADDR[0] 不输出到外部地址线也就是说外部 FSMC_A[0] 对应内部地址第 1 位。如果你在 FPGA 端用了addr[7:1]而不是addr[7:0]来区分寄存器那么 CPU 访问 0x68000000 和 0x68000002 时FPGA 端看到的地址分别是 0 和 1取addr[7:1]后都是 0自然就访问到同一个寄存器。解决办法很简单FPGA 端用完整的addr[7:0]做寄存器索引同时 STM32 端用FPGA_BASE_ADDR[index]的方式访问让 CPU 编译器去处理步长不要手动把相邻地址减 2 再除 2。如果你真的想对外部地址线做进一步压缩那就在文档里把地址转换关系写清楚否则后面维护代码很容易掉坑。5.3 数据偶尔出错速度一高就挂这个问题的根源十有八九是写时序或读时序余量不够。常见表现是低速读写正常一旦把DataSetupTime压到几个周期或者开启 DMA 高速搬运数据就开始随机出错。我遇到过的一个典型场景是 FPGA 端读数据用的是组合逻辑直接输出而 FSMC 在 NOE 上升沿采样数据时FPGA 的组合逻辑因为路径较长数据还没有完全稳定。解决方法是把读数据改成寄存器输出在 FPGA 内部先用一个寄存器把data_out缓存一拍再输出到三态总线上。代价是多了一个时钟周期的延迟但稳定很多。调整顺序建议先用保守时序把功能跑通确认寄存器读写都正确。再用逻辑分析仪看 NWE 低电平脉宽和 NOE 低电平脉宽确认和配置的理论值一致。逐步减小DataSetupTime每调一次都跑一遍连续读写测试不要只看单次读取。如果压到 5~6 个周期还稳不住检查 FPGA 端组合逻辑延迟而不是继续一味调参数。5.4 DMA 搬运卡死或者漏数据FSMC 配合 DMA 是我推荐的最终形态但也容易出问题。DMA 一旦启动它会按配置连续发起总线访问如果 FSMC 每次访问都执行得很慢或者总线上有异常DMA 会一直等待表现出来就是程序卡在等待 DMA 传输完成或者 DMA 搬运的数据中间有一段丢失。排查 DMA 卡死时先回到最简单的非 DMA 方式CPU 循环读一批地址确认速度和数据都正常。然后再开 DMA。如果 DMA 一开就出问题优先检查FIFO 门限和 burst 长度设置有些 DMA 配置在地址对齐不当时会触发总线错误。FPGA 端数据源是不是连续可读的比如 FIFO 空时DMA 持续读总会读出无效数据。FSMC Bank 基地址和 DMA 外设地址是否匹配16 位模式下 DMA 的 PSIZE/MSIZE 也要同步设为 HalfWord。6. 提升吞吐的几个方向我踩完坑后留下的方案6.1 从寄存器读写升级到连续地址块读一旦寄存器映射验证通过下一步就是把 FPGA 内部做成真正的 RAM 或 FIFO而不是一堆离散寄存器。FPGA 端对连续地址做顺序读STM32 端用 DMA 一次性搬几百字节效率会比单个寄存器操作高一个数量级。实现上很简单FPGA 内部用一个双口 RAM写端口接采集逻辑读端口接 FSMC 地址。STM32 只需要#define FPGA_DATA_BASE ((volatile uint16_t *)0x68000100) HAL_SRAM_Read_DMA(hsram, (uint16_t *)FPGA_DATA_BASE, buffer, 512);只要保证 FPGA 端读地址递增时返回的数据是连续可靠的这个方案就能跑出接近理论极限的吞吐。6.2 乒乓缓冲让采集和搬运同时进行高速数据流场景下我推荐 FPGA 内部做乒乓缓冲两块 RAMA 块被采集逻辑写入B 块通过 FSMC 被 STM32 读取采集满一块后交换角色。这样 STM32 读到的始终是一块完整、连续的数据不会出现“读到一半数据被覆盖”的问题。STM32 侧配合 DMA 双缓冲模式接收完一块后立刻切换缓冲区地址中断里只做标志位翻转。实测下来CPU 占用率几乎可以忽略数据吞吐不再是瓶颈。6.3 最后一点经验先跑通再压时序别一步到位调 FSMCFPGA 通信最大的忌讳就是一上来按手册极限时序配置。手册参数只是理论值实际还要考虑 PCB 走线长度、FPGA 引脚到内部逻辑的组合延迟、电源稳定性。我的习惯是先全部用 15 这种保守值跑通然后确认波形再一步一步把DataSetupTime和AddressHoldTime往下压。每次改动都用连续 10 万次读写来验证不要只测三次。另外硬件上也有些可以优化的点FSMC 数据总线加 33Ω 左右的串联电阻可以改善信号完整性FPGA 和 STM32 尽量放在同一个地平面区域避免跨分割地址线和数据线走线尽量等长长度差不要超过几百 mil。我现在这个项目用的就是 FSMC DMA FPGA 乒乓缓冲稳定跑了大半年每次升级固件基本不用碰通信链路。如果你也正在调 FSMCFPGA先从低速、单寄存器、带逻辑分析仪的方式起步跑通了再往上加 DMA 和乒乓。这套方案属于“笨但有效”的典型掌握之后你会发现嵌入式里很多高速通信问题其实都能用“外挂一块 RAM”的思路解决。本文还有配套的精品资源点击获取
返回列表