STM32 FSMC模拟AXI总线与FPGA高效通信实战
1. 为什么需要FSMC模拟AXI总线
在嵌入式系统开发中,STM32和FPGA的协同工作越来越常见。ZYNQ这类SoC芯片内置了AXI总线,可以非常方便地实现处理器系统(PS)和可编程逻辑(PL)之间的高速数据交互。但对于传统的STM32+FPGA架构,要实现类似的高效通信就需要一些技巧了。
我最近在一个工业控制项目中就遇到了这个问题。项目需要STM32H743作为主控,与Xilinx Artix-7 FPGA进行大量数据交换,包括传感器数据采集和PWM波形输出。最初尝试了SPI和I2C,但速度完全达不到要求。后来改用FSMC接口,实测传输速率可以达到50MB/s以上,完全满足项目需求。
FSMC(Flexible Static Memory Controller)是STM32提供的一个非常灵活的静态存储器接口,它最大的特点是可以配置成多种总线模式。通过合理配置,我们可以让它模拟出类似AXI总线的行为,实现与FPGA的高速并行通信。相比原生AXI总线,这种方案有几点优势:
- 成本更低:不需要专门的SoC芯片
- 灵活性高:适用于各种STM32和FPGA组合
- 性能足够:16位模式下实测传输速率可达50MB/s以上
- 开发简单:STM32标准库提供完整的FSMC驱动支持
2. FSMC接口的硬件设计与配置
2.1 硬件连接方案
要实现FSMC与FPGA的稳定通信,硬件设计是关键。我采用的是16位数据总线宽度,这也是大多数STM32芯片支持的最佳模式。具体引脚连接如下:
- 地址总线:使用A0-A15(实际只用到了A0-A8)
- 数据总线:D0-D15
- 控制信号:
- NOE(读使能)
- NWE(写使能)
- NE1(片选)
- NADV(地址有效)
这里有个硬件设计上的坑要特别注意:STM32的FSMC接口电压要与FPGA的IO电压匹配。我遇到过因为3.3V和2.5V不匹配导致通信不稳定的问题,最后通过添加电平转换芯片解决。
2.2 STM32端FSMC初始化
STM32的FSMC初始化相对复杂,但标准库已经帮我们封装好了大部分工作。以下是基于STM32H743的初始化代码关键部分:
void FSMC_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; FSMC_NORSRAM_TimingTypeDef Timing = {0}; // 使能时钟 __HAL_RCC_FSMC_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); // 配置GPIO复用功能 GPIO_InitStruct.Pin = GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF12_FSMC; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 类似配置其他GPIO... // 时序配置 Timing.AddressSetupTime = 1; Timing.AddressHoldTime = 0; Timing.DataSetupTime = 4; Timing.BusTurnAroundDuration = 0; Timing.CLKDivision = 0; Timing.DataLatency = 0; Timing.AccessMode = FSMC_ACCESS_MODE_A; // FSMC初始化 FSMC_NORSRAM_InitTypeDef Init = {0}; Init.NSBank = FSMC_NORSRAM_BANK1; Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE; Init.MemoryType = FSMC_MEMORY_TYPE_SRAM; Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16; Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE; Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW; Init.WrapMode = FSMC_WRAP_MODE_DISABLE; Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS; Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE; Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE; Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE; Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE; Init.WriteBurst = FSMC_WRITE_BURST_DISABLE; Init.ReadWriteTimingStruct = &Timing; Init.WriteTimingStruct = &Timing; HAL_FSMC_NORSRAM_Init(&Init); }这里有几个关键参数需要注意:
- DataSetupTime:根据FPGA的响应速度调整,太小会导致数据不稳定
- MemoryDataWidth:设置为16位模式
- AddressSetupTime:地址建立时间,一般1-2个时钟周期即可
3. FPGA端双端口RAM设计与实现
3.1 Verilog核心模块设计
FPGA端需要实现一个双端口RAM作为数据缓冲区。我采用Xilinx的Block Memory Generator生成IP核,然后在外面包装一层接口逻辑。以下是核心代码:
module fsmc_interface ( input [8:0] ab, inout [15:0] db, input wrn, input rdn, input csn, input clk, input rst_n ); wire rd_en = !(csn | rdn); wire wr_en = !(csn | wrn); reg [15:0] ram_out; assign db = rd_en ? ram_out : 16'hzzzz; // 双端口RAM实例化 blk_mem_gen_0 ram_inst ( .addra(ab), .clka(clk), .dina(db), .douta(ram_out), .wea(wr_en), .ena(1'b1) ); // 地址解码逻辑 reg [15:0] reg_file[0:31]; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin // 复位寄存器文件 end else if (wr_en) begin reg_file[ab[4:0]] <= db; end end endmodule这个设计有几个关键点:
- 异步信号同步化:将FSMC的异步控制信号同步到FPGA时钟域
- 三态总线处理:正确实现双向数据总线
- 地址解码:支持寄存器映射访问模式
3.2 时序约束与优化
为了保证通信稳定性,必须在FPGA工程中添加适当的时序约束:
create_clock -period 10.000 -name clk [get_ports clk] set_input_delay -clock clk -max 3.000 [get_ports {db[*] ab[*]}] set_output_delay -clock clk -max 3.000 [get_ports db[*]]实测发现,当STM32主频超过100MHz时,FPGA端的setup时间容易违规。解决方法有两种:
- 降低FSMC时钟分频系数
- 在FPGA端添加输入寄存器
4. 软件层通信协议实现
4.1 地址映射与数据访问
STM32端访问FPGA寄存器非常简单,只需要定义一个宏:
#define FPGA_REG(addr) (*(volatile uint16_t *)(0x60000000 | ((addr) << 1)))这里有个重要细节:由于使用的是16位数据宽度,FSMC会自动将地址右移1位。所以我们在定义地址时需要左移1位补偿。
使用方法示例:
// 写入FPGA寄存器 FPGA_REG(0x10) = 0xABCD; // 读取FPGA寄存器 uint16_t status = FPGA_REG(0x11);4.2 批量数据传输优化
对于大量数据传输(如图像、音频等),可以使用DMA+FSMC组合。以下是DMA配置示例:
void FSMC_DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); DMA_HandleTypeDef hdma; hdma.Instance = DMA2_Stream0; hdma.Init.Channel = DMA_CHANNEL_0; hdma.Init.Direction = DMA_MEMORY_TO_MEMORY; hdma.Init.PeriphInc = DMA_PINC_ENABLE; hdma.Init.MemInc = DMA_MINC_ENABLE; hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma.Init.Mode = DMA_NORMAL; hdma.Init.Priority = DMA_PRIORITY_HIGH; hdma.Init.FIFOMode = DMA_FIFOMODE_ENABLE; hdma.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; hdma.Init.MemBurst = DMA_MBURST_INC4; hdma.Init.PeriphBurst = DMA_PBURST_INC4; HAL_DMA_Init(&hdma); __HAL_LINKDMA(&hsram, hdma, hdma); }使用DMA传输可以大幅降低CPU负载,实测传输1KB数据只需要不到20us。
5. 调试技巧与常见问题
5.1 信号完整性检查
在调试阶段,首先要用示波器检查关键信号:
- 时钟信号是否干净
- 数据/地址线是否有过冲
- 控制信号时序是否符合预期
我遇到过因为PCB走线过长导致信号振铃的问题,解决方法是在FSMC输出端添加33欧姆串联电阻。
5.2 常见错误排查
- 数据错位:检查地址线连接是否正确,特别是A0是否接对
- 写入失败:检查NWE信号是否正常,FPGA端的setup/hold时间是否满足
- 读取全零:检查NOE信号和FPGA的三态输出控制
一个实用的调试技巧是在FPGA端添加ILA核,实时监控总线活动:
ila_0 ila_inst ( .clk(clk), .probe0(ab), .probe1(db), .probe2({wrn, rdn, csn}) );6. 性能优化实战
6.1 时序参数调优
FSMC的性能很大程度上取决于时序参数的配置。通过调整以下几个参数,我成功将传输速率从30MB/s提升到52MB/s:
Timing.AddressSetupTime = 0; // 从1降到0 Timing.DataSetupTime = 2; // 从4降到2 Timing.AccessMode = FSMC_ACCESS_MODE_B; // 更快的访问模式注意:每次修改后都要用逻辑分析仪验证数据是否正确。
6.2 突发传输实现
虽然STM32的FSMC不支持真正的突发传输,但我们可以通过软件模拟:
void burst_write(uint32_t addr, uint16_t *data, uint32_t len) { volatile uint16_t *ptr = (uint16_t *)(0x60000000 | (addr << 1)); while(len--) { *ptr++ = *data++; } }配合FPGA端的流水线设计,可以实现接近突发传输的效果。
7. 项目实战:高速数据采集系统
最近完成的一个项目就采用了这种方案:STM32H743通过FSMC与Artix-7 FPGA通信,实现8通道16位1MSPS的数据采集系统。关键实现点包括:
- 乒乓缓冲:FPGA端实现双缓冲机制,确保不会丢失数据
- DMA传输:STM32使用DMA将数据从FSMC接口搬运到内存
- 硬件触发:通过额外的GPIO实现精确的采集触发
系统稳定运行时的实测性能:
- 持续采集速率:48MB/s
- 延迟:<5us
- CPU占用率:<10%
这个方案最大的优势是开发周期短——从硬件设计到软件调试只用了两周时间,而且成本只有ZYNQ方案的三分之一。
