尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F407与FPGA的FSMC并行总线通信设计与调试实战

STM32F407与FPGA的FSMC并行总线通信设计与调试实战 简介针对STM32F407与FPGA通过FSMC总线高速通信的性能测试工程面向嵌入式开发者和硬件工程师解决MCU与可编程逻辑器件间地址映射、时序控制与DMA传输验证等问题。压缩包内含360个文件大小约14.17MB覆盖STM32端C源码含HAL库驱动如stm32f4xx_hal_tim.c、FPGA侧Verilog设计如FSMC_Ctrl.v、Quartus工程与IP配置.qip/.qpf、编译产生的axf/hex/map固件及调试信息以及批量处理脚本和说明文档构成从代码到可执行文件的完整交付链大量.h/.c/.v文件体现了MCU与FPGA协同开发的真实工程结构。已有396人学习适合研究STM32 FSMC接口与FPGA协同设计的中高级开发者可依据这些资料复现实验环境深入分析FSMC时序参数、存储区映射、DMA传输性能和异常排查方法并根据配套脚本调整优化自身项目。整体目录结构清晰完整展现了从硬件配置、软件驱动到性能测试与排错要点的实践路径。1. FSMC总线在STM32F407与FPGA之间到底传什么数据一块iCore3板卡摆在调测台上主控是STM32F407ZGT6旁边躺着一颗FPGA两者之间没有经过SPI、没有经过UART而是直接拉了一组并行总线。这个名为“iCore3_FSMC_TEST_REV1.0”的工程目的就是把STM32F407上的FSMC外设当作一个无胶合SRAM接口接到FPGA上用最朴素的寄存器读写来验证整条链路是否好用。FSMC在多数工程师手里是用来驱动TFT-LCD或者NOR Flash的很少有人会把它当成一颗“可编程并行总线桥”来使用。但在STM32F407FPGA这种异构板卡上FSMC的价值恰恰在于它能把CPU的普通内存读写操作原封不动转换成片选、读写使能、地址线和双向数据线的电平时序FPGA只需要按SRAM的规矩接住就行不需要额外实现任何通信协议。所以这个测试工程要解决的不是“怎么点亮LCD”而是三个更底层的问题PCB上的FSMC引脚连线是否焊接正确、STM32侧时序参数是否能满足FPGA的逻辑延迟、以及后续驱动层应该把FPGA寄存器挂在哪个内存地址上。这篇文章顺着这个思路把原理、标准库配置、FPGA端从机接口和排错方法整体过一遍。2. 先摸清地址映射STM32F407的FSMC异步时序与iCore3引脚分配2.1 Bank1四个Region与HADDR左移一位的映射规则FSMC的NOR/PSRAM控制器把外部存储器空间分成4个Bank每个Bank容量256MB。其中Bank1专门用于NOR Flash、PSRAM或者SRAM类存储设备又被拆成NE1到NE4四个片选区域每个Region独立管理64MB地址空间。在STM32F407上Bank1的基地址固定是0x60000000四个Region的地址范围依次是0x60000000、0x64000000、0x68000000和0x6C000000开头。iCore3上如果用NE3连接FPGA那么FPGA在CPU眼中的基地址就是0x6C000000。这里有一个特别容易踩的坑FSMC外部地址线FSMC_A[25:0]与芯片内部HADDR[25:1]并不是逐位对应的。对于16位外部存储器STM32内部的字节地址HADDR会右移一位再送到FSMC_A引脚上。换句话说你写*(volatile uint16_t *)(0x6C000000 2)时外部地址线FSMC_A上呈现的地址编码其实是0x01而不是0x02。这个映射关系直接决定了FPGA端寄存器偏移的设计方式。如果FPGA内部把FSMC_A[0]当成寄存器地址的最低位那么C语言里连续的uint16_t指针偏移0、1、2、3到了FPGA端看到的就是寄存器索引0、1、2、3两边完全对齐。但是如果你拿了数据手册里的字节地址去硬套就会看到访问地址凭空多了一位。后面第5章会给一张速查表这里只需要记住结论用uint16_t指针的下标访问FSMC映射地址比直接算字节偏移更安全。2.2 异步SRAM模式A的ADDSET、DATAST与HOLD参数怎么定FSMC的异步访问模式有Mode 1、Mode A、Mode 2等好几种iCore3这类板卡驱动FPGA时最常用的是Mode A。Mode A有一个关键特征在访问周期开始时会拉低NADV信号用来给外部器件提供地址锁存脉冲。FPGA如果内部没有独立的地址锁存逻辑可以靠NADV的下降沿把地址抓进寄存器后续读写期间地址变化就不影响已经锁存的内容。时序参数由三个主要字段控制。FSMC字段含义FPGA场景建议说明ADDSET地址建立时间单位HCLK周期1地址有效到读写信号有效之间的时间DATAST数据建立时间单位HCLK周期3读信号有效到数据被采样的时间写则对应写脉冲宽度ADDRESSHOLD地址保持时间单位HCLK周期0或1读写信号结束后地址保持的时间FPGA一般不做要求BUS TURNAROUND总线方向切换等待周期0高阻窗口不足时适当增大避免双向总线冲突当STM32F407跑在168MHz主频时一个HCLK周期大约是5.95ns。ADDSET1表示地址信号先于读写信号约6ns建立DATAST3表示读写脉冲宽度约18ns加上FSMC自身的一个HCLK周期整个访问周期大约是5个HCLK也就是30ns左右。对FPGA来说这个速度并不算快组合逻辑足够在一个周期内完成地址译码但这里要小心的是FPGA内部如果加了同步打拍逻辑读数据会被延迟一拍这时候靠DATAST3可能不够。另外不要忽略写入时的HOLD时间。STM32F407的FSMC在写周期结束后会很快释放数据总线而FPGA侧如果是靠NWE上升沿写入那么写入数据需要在NWE上升沿之后继续保持一段时间。标准库结构体里没有单独的写保持时间字段但可以通过FSMC_BusTurnAroundDuration来做补偿或者在FPGA端用NADV锁存地址、用数据线自身的延迟来兜底。2.3 iCore3上FSMC信号与FPGA的接口关系这块板卡在原理图上的接法并不复杂实际调试时只需要确认一组对应关系。常用的信号分配如下表所示。STM32F407引脚FSMC功能接FPGA侧信号作用PD7FSMC_NE3FPGA_FSMC_CS_N片选低有效PD4FSMC_NOEFPGA_FSMC_RD_N读使能低有效PD5FSMC_NWEFPGA_FSMC_WR_N写使能低有效PD0FSMC_A2FPGA_FSMC_ADDR[2]寄存器地址线PE7~PE15FSMC_D[7:15]FPGA_FSMC_DATA[7:15]数据总线高9位PD14~PD15FSMC_D[0:1]FPGA_FSMC_DATA[0:1]数据总线低2位PD11FSMC_NADVFPGA_FSMC_ADV_N地址锁存信号不同版本的iCore3板卡在地址线宽度上可能不一样有的把FSMC_A[10:0]全部引出有的只引低8位。测试工程里一般只操作低地址空间所以不必关心高端地址线是否连接。检查原理图时重点是确认NE3对应的确实是0x6C000000以及NADV到底有没有接出来。有些精简设计会直接省略NADV这时FPGA端就不能靠地址锁存脉冲必须保证地址在整个读写周期内保持稳定。3. 工程落地配置FSMC标准库驱动并完成最小读写测试3.1 时钟与GPIO复用使能这个测试工程沿用标准库很多人找不到stm32f407标准库的下载入口其实Keil的Pack Installer里把STM32F4xx的固件包装上源码目录下就会有stm32f4xx_fsmc.c。标准库和HAL库在FSMC这部分差别不大寄存器位定义完全一致熟悉的工程师完全可以对照HAL库的初始化代码反向改到标准库工程里。开启时钟的代码放在主函数初始化开头void FSMC_GPIO_Config(void) { RCC_AHB1PeriphClockCmd( RCC_AHB1Periph_GPIOD | RCC_AHB1Periph_GPIOE | RCC_AHB1Periph_GPIOF | RCC_AHB1Periph_GPIOG, ENABLE); RCC_AHB3PeriphClockCmd(RCC_AHB3Periph_FSMC, ENABLE); }这段代码先把GPIOD、GPIOE、GPIOF、GPIOG四组引脚的时钟打开再单独打开FSMC外设所在AHB3总线时钟。FSMC外设挂在AHB3上很多人只配置了GPIO时钟却忘掉这一步结果寄存器写不进值。标准库中AHB3外设时钟使能函数是RCC_AHB3PeriphClockCmd不是AHB1要注意区分。GPIO复用的配置也是老生常谈但要用对AF编号。FSMC引脚在STM32F407上对应的复用功能是GPIO_AF_FSMC也就是AF12。每组引脚都需要单独初始化GPIO_InitTypeDef GPIO_InitStructure; GPIO_PinAFConfig(GPIOD, GPIO_PinSource0, GPIO_AF_FSMC); GPIO_PinAFConfig(GPIOD, GPIO_PinSource4, GPIO_AF_FSMC); GPIO_PinAFConfig(GPIOD, GPIO_PinSource5, GPIO_AF_FSMC); GPIO_PinAFConfig(GPIOD, GPIO_PinSource7, GPIO_AF_FSMC); GPIO_PinAFConfig(GPIOD, GPIO_PinSource11, GPIO_AF_FSMC); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOD, GPIO_InitStructure);GPIO模式要选GPIO_Mode_AF输出类型是推挽速度直接给100MHz。上拉电阻建议打开FPGA端引脚悬空时不会读到不确定电平。上面代码只写了GPIOD的初始化实际工程还应该按同样的方式把E、F、G三组用到的引脚全部配置完否则数据线不完整读写高低字节会错乱。3.2 时序参数结构体赋值与Bank1 Region3使能标准库中FSMC的初始化由两个结构体配合完成FSMC_NORSRAMTimingInitTypeDef负责时序FSMC_NORSRAMInitTypeDef负责存储类型与使能控制。先填时序参数FSMC_NORSRAMTimingInitTypeDef TimingStructure; FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure; TimingStructure.FSMC_AddressSetupTime 0x01; TimingStructure.FSMC_AddressHoldTime 0x00; TimingStructure.FSMC_DataSetupTime 0x03; TimingStructure.FSMC_BusTurnAroundDuration 0x00; TimingStructure.FSMC_CLKDivision 0x00; TimingStructure.FSMC_DataLatency 0x00; TimingStructure.FSMC_AccessMode FSMC_AccessMode_A; FSMC_NORSRAMInitStructure.FSMC_Bank FSMC_Bank1_NORSRAM3; FSMC_NORSRAMInitStructure.FSMC_DataAddressMux FSMC_DataAddressMux_Disable; FSMC_NORSRAMInitStructure.FSMC_MemoryType FSMC_MemoryType_SRAM; FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode FSMC_BurstAccessMode_Disable; FSMC_NORSRAMInitStructure.FSMC_AsynchronousWait FSMC_AsynchronousWait_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity FSMC_WaitSignalPolarity_Low; FSMC_NORSRAMInitStructure.FSMC_WrapMode FSMC_WrapMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive FSMC_WaitSignalActive_BeforeWaitState; FSMC_NORSRAMInitStructure.FSMC_WriteOperation FSMC_WriteOperation_Enable; FSMC_NORSRAMInitStructure.FSMC_WaitSignal FSMC_WaitSignal_Disable; FSMC_NORSRAMInitStructure.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WriteBurst FSMC_WriteBurst_Disable; FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct TimingStructure; FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct TimingStructure; FSMC_NORSRAMInit(FSMC_NORSRAMInitStructure); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM3, ENABLE);这里的关键参数是FSMC_AddressSetupTime1、FSMC_DataSetupTime3对应Mod A下大约30ns的总访问周期。FSMC_AccessMode必须与接口设计一致iCore3上FPGA端如果实现了NADV锁存就用Mode A如果只靠地址保持Mode 1也能用但后面FPGA代码里锁存逻辑要对应调整。FSMC_MemoryType_SRAM表示外部器件按异步SRAM方式访问FSMC_MemoryDataWidth_16b配合芯片实际的16位数据总线。FSMC_ExtendedMode_Disable表示读时序和写时序共用一个结构体测试阶段这样最简单如果要分别优化读与写速度可以把读写结构体分离。3.3 向FPGA寄存器写0xA5A5再读回的验证流程初始化完成之后FSMC设备就已经挂在总线上了。现在定义FPGA的映射基地址和常用寄存器#define FPGA_FSMC_BASE ((volatile uint16_t *)0x6C000000u) #define FPGA_REG_ID 0x00 #define FPGA_REG_VERSION 0x02 #define FPGA_REG_DATA 0x04 FPGA_FSMC_BASE[FPGA_REG_ID] 0xA5A5; uint16_t version FPGA_FSMC_BASE[FPGA_REG_VERSION];这段代码中FPGA_FSMC_BASE被声明为volatile uint16_t指针编译后对它的读写会直接转换成外部总线访问。写FPGA_REG_ID时CPU向0x6C000000地址发起16位写操作FSMC自动拉低NE3、NWE并把0xA5A5放到数据线上。读FPGA_REG_VERSION时FSMC拉低NOE等待数据稳定后把总线上的16位数据返回给CPU。如果FPGA端暂时没有任何逻辑可以先用线与回环的方式验证把数据总线的低8位通过跳线短接到高8位写0x1234后读回如果读值恰好是0x3412的某种变形说明数据线焊接顺序有问题如果读回0xFFFF说明NOE没有真正拉低或FPGA没有驱动数据线如果始终为0x0000检查NE3片选地址是不是错到了别的Region。4. FPGA端要接住FSMCSRAM-like从机接口与读时序排错4.1 FPGA侧对外总线模型片选、读写、地址锁存FPGA端对FSMC总线的处理方式直接决定了这套系统是否稳定。最常见的错误是把FSMC的NWE信号直接连到寄存器组的时钟端用always (posedge fsmc_nwe)来写数据。这样做功能仿真可能通过但综合时会产生门控时钟时序收敛困难而且NWE上的毛刺很容易造成误写入。更好的方案是让FPGA把FSMC信号当成普通异步输入在系统时钟域内做同步打拍产生读写使能窗口。一个典型的从机接口代码结构如下module fsmc_slave #( parameter AWIDTH 8, parameter DWIDTH 16 )( input wire clk, input wire fsmc_ne_n, input wire fsmc_noe_n, input wire fsmc_nwe_n, input wire fsmc_nadv_n, input wire [AWIDTH-1:0] fsmc_addr, inout wire [DWIDTH-1:0] fsmc_data ); reg [AWIDTH-1:0] addr_lat; reg [DWIDTH-1:0] regfile [0:(1AWIDTH)-1]; reg ne_r, noe_r, nwe_r; always (posedge clk) begin ne_r fsmc_ne_n; noe_r fsmc_noe_n; nwe_r fsmc_nwe_n; end always (posedge clk) begin if (!fsmc_nadv_n) addr_lat fsmc_addr; end wire write_en (!ne_r !nwe_r); wire read_en (!ne_r !noe_r); always (posedge clk) begin if (write_en) regfile[addr_lat] fsmc_data; endne_r、nwe_r是FSMC信号同步打拍后的结果write_en在NWE有效且片选有效时拉高此时所有FSMC信号已经稳定寄存器写入可靠。NADV下降沿用于锁存地址确保后续读写期间地址线变化不影响索引值。同步打拍带来的副作用是FSMC信号整体延迟了两个时钟周期FPGA的读响应也会晚两拍这个延迟需要靠FSMC侧的DATAST参数来补偿。4.2 双向数据线方向切换与turnaround时间读数据的三态控制是这个接口最容易出问题的地方。fsmc_data是双向总线FPGA只有在片选有效且NOE有效时才应该驱动数据线其他时间必须置为高阻reg [DWIDTH-1:0] read_data; always (posedge clk) begin if (read_en) read_data regfile[addr_lat]; end assign fsmc_data read_en ? read_data : {DWIDTH{1bz}}; endmodule这里read_data在read_en有效时寄存一次输出到总线。问题在于read_en由同步打拍后的信号产生FSMC侧的NOE时序期望FPGA在NOE拉低后尽快输出数据如果延迟过大STM32F407在NOE上升沿前采样不到稳定数据读回来的值就不对。调试时可以先把read_data的组合逻辑输出直接挂到总线上即assign fsmc_data read_en ? regfile[addr_lat] : {DWIDTH{1bz}};牺牲一点时序收敛性换取读速度。等逻辑分析仪确认采样窗口足够后再换回寄存输出。三态释放也要留意NOE拉高后如果FPGA还继续驱动总线下一笔写操作到来时STM32F407的数据线还没恢复两个驱动源直接冲突极端情况下会损坏IO。必要时把FSMC_BusTurnAroundDuration设置成1或2在读写操作之间插入总线空闲周期。4.3 用逻辑分析仪抓波形定位读时序错误调试FSMC接口逻辑分析仪比示波器更好用。采样率至少100MHz优先抓FSMC_NE3、FSMC_NWE、FSMC_NOE、FSMC_A[2:0]和FSMC_D[15:0]。操作步骤如下让STM32F407循环写FPGA_REG_ID逻辑分析仪连续采样观察NWE下降沿时地址线是否已经稳定。触发条件设为NE3下降沿抓一段读操作找到NOE下降沿之后数据线上出现有效数据的时刻。对比预期时序图和实际波形确认NADV低脉冲是否出现在地址变化之后因为Mode A要求地址先建立NADV再拉低。如果写操作正常但读回固定值0xFFFF多半是NOE没接对或者FPGA内部read_en逻辑中片选信号极性取反。当读数据总是比预期晚一个周期时直接把FSMC_DataSetupTime从3改成4或5重新测试。每增加一个HCLK约6nsFPGA端同步打拍的延迟通常需要2到3个HCLK才能覆盖。5. 把TEST工程变成可用组件循环校验、吞吐测试与地址错位处理5.1 批量写读比较与DMA压测单个寄存器读写只能证明通路存在验证总线稳定性还得靠批量循环。可以准备一段固定Pattern数组通过DMA连续搬运到FPGA映射地址再读回来逐字节比对#define TEST_COUNT 1024 uint16_t tx_buf[TEST_COUNT]; uint16_t rx_buf[TEST_COUNT]; void DMATest_Start(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_DeInit(DMA2_Stream0); DMA_InitStructure.DMA_Channel DMA_Channel_0; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)tx_buf; DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)FPGA_FSMC_BASE; DMA_InitStructure.DMA_DIR DMA_DIR_MemoryToMemory; DMA_InitStructure.DMA_BufferSize TEST_COUNT; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Enable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Disable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_Init(DMA2_Stream0, DMA_InitStructure); DMA_Cmd(DMA2_Stream0, ENABLE); }DMA把tx_buf里的数据连续写到以0x6C000000开头的地址空间FPGA端每收到一次写使能就按地址递增顺序把数据寄存到内部RAM。PeripheralInc_Enable表示源地址自增MemoryInc_Disable表示目标地址固定因为我们把整个FPGA地址空间当成一个FIFO入口来用。压测期间CPU可以在主循环里读一个统计用的GPIO来估算耗时数完一轮后再启动DMA从FPGA读回数据比对两个数组差异。吞吐量可以通过循环访问一个固定的FPGA寄存器来测用DWT-CYCCNT记录100万次写操作消耗的CPU周期数再乘以访问周期对应的字节数。以168MHz主频算单次16位写约30ns理论吞吐约66MB/s实际在50MB/s左右属于正常范围。5.2 16位模式下地址错位与寄存器偏移速查开发过程中最隐蔽的坑是地址错位尤其在FPGA端寄存器地址定义不统一时。下面这张表列出C语言指针偏移与FPGA实际看到地址编码的对应关系C指针偏移内部字节地址FSMC_A引脚编码FPGA寄存器索引00x000x00010x020x01120x040x02230x060x03340x080x044如果FPGA端按字节偏移地址来译码而C语言用指针下标访问就会出现所有寄存器地址都是双倍的错位现象。解决的方法是统一约定在工程里直接定义一个宏#define FPGA_REG(addr) (*(volatile uint16_t *)(0x6C000000u (addr)))使用FPGA_REG(0)、FPGA_REG(2)这些宏时传入的是外部FSMC_A的编码值而不是字节地址。这样FPGA端可以继续用FSMC_A[0]作为寄存器最低位两边语义一致。验证地址是否正确时可以写一个只有低4位地址回环的FPGA小模块把写入地址的低4位值放到数据寄存器里再读回对比看到底是0x6C000000里的哪一位地址线接错了。这个技巧能在一分钟内区分是焊接问题、地址映射问题还是时序问题后续扩展成FPGA协处理器访问接口时这套调试路径可以直接复用。本文还有配套的精品资源点击获取
返回列表