适用场景:复杂电子系统 / 高可靠FPGA-MCU协同设计
关键词:FPGA · STM32 · EBI · LocalBus · 异步SRAM
1. 为什么选EBI而非SPI?
在高可靠嵌入式系统中,STM32与FPGA通信的常见方式有SPI、I2C、UART等串行接口,以及EBI(External Bus Interface)等并行总线。虽然SPI实现简单,但在需要高带宽、低延迟、内存映射访问的场景下,EBI并行总线具有显著优势:
| 对比维度 | SPI | EBI |
|---|---|---|
| 数据宽度 | 1~4 bit (QSPI) | 8/16 bit |
| 理论带宽 | ~50 MB/s (QSPI) | ~100+ MB/s |
| 访问方式 | 寄存器操作 | 内存映射 (直接读写) |
| DMA支持 | 有限 | 原生支持 |
| 适用场景 | 控制命令、低速数据 | 图像缓冲、FFT数据流、航电总线 |
对于需要FPGA处理高速数据流(如传感器采集、信号处理)并将结果回传MCU的场景,EBI允许STM32像访问内部SRAM一样直接读写FPGA内部寄存器或FIFO,无需复杂的协议栈开销。这种"内存映射"方式使得CPU可以通过简单的指针操作完成数据交换,几乎零额外软件开销。
2. EBI系统框图


3. EBI开发手册下载
见文章末尾
4. EBI读写时序
EBI读时序

EBI写时序

5. EBI硬件连接与信号定义
以STM32F407 + FPGA(Xilinx/Altera)为例,采用16位数据宽度、非复用地址模式,典型连接如下:
5.1 信号连接表
| STM32 引脚 | FPGA 引脚 | 信号说明 |
|---|---|---|
| PD14~PD15, PE7~PE15 | IO_EBI_data[15:0] | EBI数据总线 (16位双向) |
| PF0~PF5, PG0~PG13 | I_EBI_addr[23:0] | EBI地址总线 (24位,可寻址16MB) |
| PG9 | I_EBI_CS_n | EBI片选 (低有效) |
| PD4 | I_EBI_OE_n | EBI读使能 (低有效) |
| PD5 | I_EBI_WE_n | EBI写使能 (低有效) |
| — | I_sys_clk | FPGA系统时钟 (≥100MHz) |
| — | I_rst_n | 系统复位 (低有效) |
地址映射:Bank1_NE2 → 0x6400_0000 ~ 0x64FF_FFFF (16MB空间)
5.2 硬件设计注意事项
⚠️ 关键提示
- FPGA的IO电平必须与STM32匹配(通常 3.3V LVTTL/LVCMOS33)
- 数据线建议串联 22~47Ω 电阻抑制信号反射,尤其是高速运行时
- 地址线若不需要全部使用,可接地或接固定电平,减少FPGA引脚占用
- 若使用等待信号(NWAIT),需在EBI配置中使能,并在FPGA中正确驱动
- 航空电子场景需特别关注 EMC/EMI 设计,建议增加滤波和屏蔽
6. FPGA端Verilog接口设计
FPGA侧的核心任务是模拟一个"SRAM设备",响应STM32的读写时序。以下是基于异步SRAM模式的完整Verilog接口模块,直接对应你的实际工程代码:
6.1 模块接口定义
module EBI_LOCALBUS (
/* --------------------- Input Signals --------------------- */
input I_sys_clk, // 系统时钟
input I_rst_n, // 系统复位
input I_EBI_CS_n, // EBI片选
input I_EBI_WE_n, // EBI写信号
input I_EBI_OE_n, // EBI读信号
input [23 : 00] I_EBI_addr, // EBI地址 (24位)
inout [15 : 00] IO_EBI_data, // EBI数据总线 (16位双向)
input [15 : 00] I_EBI_RD_data, // CPU读数据 (来自内部模块)
/* --------------------- Output Signals -------------------- */
output [15 : 00] O_EBI_WR_data, // CPU写数据 (输出到内部模块)
output O_EBI_CS_n, // 片选转接
output reg O_EBI_WE_n, // 写信号同步输出
output O_EBI_RD_n, // 读信号同步输出
output [23 : 00] O_EBI_addr // 地址转接
);6.2 核心设计要点
6.2.1 信号直通与数据三态控制
// EBI片选直通
assign O_EBI_CS_n = I_EBI_CS_n;
// EBI地址直通
assign O_EBI_addr = I_EBI_addr;
// 写数据:EBI总线 → FPGA内部 (写操作时数据由STM32驱动)
assign O_EBI_WR_data = IO_EBI_data;
// 读数据:FPGA内部 → EBI总线 (仅在片选有效且读使能有效时驱动)
assign IO_EBI_data = ( (I_EBI_CS_n == 1'b0) && (S_EBI_RD_N_d2 == 1'b0) )
? I_EBI_RD_data : 16'bz;关键设计:
O_EBI_CS_n和O_EBI_addr为直通信号,不做时钟域转换,确保地址和片选的最小延迟IO_EBI_data采用三态控制:仅在CS_n == 0且RD_N_d2 == 0时驱动读数据,否则置高阻态,避免总线冲突O_EBI_WR_data直接采样IO_EBI_data,因为写操作时数据总线由STM32驱动,无需三态判断
3.2.2 写信号同步
// EBI写信号打拍同步
always @(posedge I_sys_clk or negedge I_rst_n) begin
if (I_rst_n == 1'b0) begin
O_EBI_WE_n <= 1'b1; // 复位后写无效
end
else begin
O_EBI_WE_n <= I_EBI_WE_n;
end
end设计意图:
- 写信号经过1级同步,从异步EBI域转入FPGA系统时钟域
- 同步后的
O_EBI_WE_n可直接用于内部模块的写使能,配合O_EBI_addr和O_EBI_WR_data完成一次完整的写操作
6.2.3 读信号同步
reg S_EBI_RD_N_d0;
reg S_EBI_RD_N_d1;
reg S_EBI_RD_N_d2;
// EBI读信号三级打拍
always @(posedge I_sys_clk or negedge I_rst_n) begin
if (I_rst_n == 1'b0) begin
S_EBI_RD_N_d0 <= 1'b1;
S_EBI_RD_N_d1 <= 1'b1;
S_EBI_RD_N_d2 <= 1'b1;
end
else begin
S_EBI_RD_N_d0 <= I_EBI_OE_n;
S_EBI_RD_N_d1 <= S_EBI_RD_N_d0;
S_EBI_RD_N_d2 <= S_EBI_RD_N_d1;
end
end
assign O_EBI_RD_n = S_EBI_RD_N_d2;为什么读信号需要3级同步?
读信号采用三级延迟(而非写信号的1级),原因如下:
| 同步级数 | 作用 |
|---|---|
S_EBI_RD_N_d0 | 第一级同步,消除亚稳态 |
S_EBI_RD_N_d1 | 第二级同步,稳定信号 |
S_EBI_RD_N_d2 | 第三级延迟,为FPGA内部模块预留数据准备时间 |
读操作需要FPGA内部模块在检测到读使能后,将数据放到 I_EBI_RD_data 上,再通过三态门驱动到 IO_EBI_data。三级延迟确保了:
- 内部模块有足够的时间响应读请求
- 数据在
S_EBI_RD_N_d2 == 0时已经稳定 - 满足STM32 EBI的数据建立时间要求
7. 内部模块连接示例
EBI_LOCALBUS 模块本身是一个总线桥接器,需要配合内部功能模块使用。以下是一个典型的寄存器文件连接示例:
module internal_regfile (
input I_sys_clk,
input I_rst_n,
input I_cs_n, // 来自 EBI_TO_LOCALBUS.O_EBI_CS_n
input I_we_n, // 来自 EBI_TO_LOCALBUS.O_EBI_WE_n
input I_rd_n, // 来自 EBI_TO_LOCALBUS.O_EBI_RD_n
input [23:0] I_addr, // 来自 EBI_TO_LOCALBUS.O_EBI_addr
input [15:0] I_wr_data, // 来自 EBI_TO_LOCALBUS.O_EBI_WR_data
output reg [15:0] O_rd_data // 送到 EBI_TO_LOCALBUS.I_EBI_RD_data
);
reg [15:0] reg_file [0:255]; // 256个16位寄存器
integer i;
initial begin
for (i = 0; i < 256; i = i + 1)
reg_file[i] = 16'd0;
end
// 写操作:WE_n下降沿 (同步后的信号)
always @(posedge I_sys_clk or negedge I_rst_n) begin
if (!I_rst_n) begin
// 复位逻辑
end
else if (!I_cs_n && !I_we_n) begin
reg_file[I_addr[7:0]] <= I_wr_data;
end
end
// 读操作:组合逻辑输出 (确保在RD_n有效时数据已稳定)
always @(*) begin
if (!I_cs_n && !I_rd_n)
O_rd_data = reg_file[I_addr[7:0]];
else
O_rd_data = 16'h0000;
end
endmodule顶层连接:
module top_fpga_stm32 (
input sys_clk,
input sys_rst_n,
// EBI物理接口
inout [15:0] fmc_data,
input [23:0] fmc_addr,
input fmc_cs_n,
input fmc_oe_n,
input fmc_we_n
);
wire [15:0] ebi_rd_data;
wire [15:0] ebi_wr_data;
wire ebi_cs_n;
wire ebi_we_n;
wire ebi_rd_n;
wire [23:0] ebi_addr;
EBI_LOCALBUS u_ebi_bridge (
.I_sys_clk (sys_clk),
.I_rst_n (sys_rst_n),
.I_EBI_CS_n (fmc_cs_n),
.I_EBI_WE_n (fmc_we_n),
.I_EBI_OE_n (fmc_oe_n),
.I_EBI_addr (fmc_addr),
.IO_EBI_data (fmc_data),
.I_EBI_RD_data (ebi_rd_data),
.O_EBI_WR_data (ebi_wr_data),
.O_EBI_CS_n (ebi_cs_n),
.O_EBI_WE_n (ebi_we_n),
.O_EBI_RD_n (ebi_rd_n),
.O_EBI_addr (ebi_addr)
);
internal_regfile u_regfile (
.I_sys_clk (sys_clk),
.I_rst_n (sys_rst_n),
.I_cs_n (ebi_cs_n),
.I_we_n (ebi_we_n),
.I_rd_n (ebi_rd_n),
.I_addr (ebi_addr),
.I_wr_data (ebi_wr_data),
.O_rd_data (ebi_rd_data)
);
endmodule8. STM32端HAL配置与驱动
STM32侧使用HAL库配置FMC/EBI为异步SRAM模式。以下代码基于STM32F407,使用Bank1_NE2(对应地址 0x64000000):
8.1 GPIO初始化
#include "stm32f4xx_hal.h"
#define FPGA_BASE_ADDR 0x64000000
// 内存映射指针 (16位访问)
volatile uint16_t *fpga_regs = (volatile uint16_t *)FPGA_BASE_ADDR;
static void FMC_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
// 数据总线 D0~D15: PD14/15, PE7~15
GPIO_InitStruct.Pin = GPIO_PIN_14 | GPIO_PIN_15;
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_FMC;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
GPIO_InitStruct.Pin = 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;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
// 地址总线 A0~A5: PF0~PF5 (可扩展至A23)
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 |
GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
// 控制信号: NOE(PD4), NWE(PD5), NE2(PG9)
GPIO_InitStruct.Pin = GPIO_PIN_4 | GPIO_PIN_5;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_9;
HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
}8.2 FMC异步SRAM模式初始化
void FMC_FPGA_Init(void)
{
SRAM_HandleTypeDef hsram = {0};
FMC_NORSRAM_TimingTypeDef Timing = {0};
FMC_GPIO_Init();
__HAL_RCC_FMC_CLK_ENABLE();
hsram.Instance = FMC_NORSRAM_DEVICE;
hsram.Extended = FMC_NORSRAM_EXTENDED_DEVICE;
// Bank1_NE2, 16位数据宽度, SRAM类型
hsram.Init.NSBank = FMC_NORSRAM_BANK2;
hsram.Init.DataAddressMux = FMC_DATA_ADDRESS_MUX_DISABLE; // 非复用
hsram.Init.MemoryType = FMC_MEMORY_TYPE_SRAM;
hsram.Init.MemoryDataWidth = FMC_NORSRAM_MEM_BUS_WIDTH_16;
hsram.Init.BurstAccessMode = FMC_BURST_ACCESS_MODE_DISABLE;
hsram.Init.WaitSignalPolarity = FMC_WAIT_SIGNAL_POLARITY_LOW;
hsram.Init.WrapMode = FMC_WRAP_MODE_DISABLE;
hsram.Init.WaitSignalActive = FMC_WAIT_TIMING_BEFORE_WS;
hsram.Init.WriteOperation = FMC_WRITE_OPERATION_ENABLE;
hsram.Init.WaitSignal = FMC_WAIT_SIGNAL_DISABLE;
hsram.Init.ExtendedMode = FMC_EXTENDED_MODE_DISABLE;
hsram.Init.AsynchronousWait = FMC_ASYNCHRONOUS_WAIT_DISABLE;
hsram.Init.WriteBurst = FMC_WRITE_BURST_DISABLE;
hsram.Init.ContinuousClock = FMC_CONTINUOUS_CLOCK_SYNC_ONLY;
// 时序配置 (HCLK = 168MHz, 1周期 ≈ 5.95ns)
// 需根据FPGA时序报告调整,满足建立/保持时间
Timing.AddressSetupTime = 2; // 地址建立: ~12ns
Timing.AddressHoldTime = 1; // 地址保持
Timing.DataSetupTime = 5; // 数据建立: ~30ns
Timing.BusTurnAroundDuration = 1; // 总线周转
Timing.CLKDivision = 2;
Timing.DataLatency = 2;
Timing.AccessMode = FMC_ACCESS_MODE_A;
HAL_SRAM_Init(&hsram, &Timing, &Timing);
}8.3 读写驱动函数
/**
* @brief 向FPGA寄存器写入16位数据
*/
static inline void FPGA_WriteReg(uint16_t reg, uint16_t value)
{
fpga_regs[reg] = value;
__DSB(); // 数据同步屏障,确保写入完成
}
/**
* @brief 从FPGA寄存器读取16位数据
*/
static inline uint16_t FPGA_ReadReg(uint16_t reg)
{
uint16_t val = fpga_regs[reg];
__DSB();
return val;
}
/**
* @brief 批量数据写入
*/
void FPGA_WriteBuffer(uint16_t reg_base, uint16_t *data, uint32_t len)
{
for (uint32_t i = 0; i < len; i++) {
fpga_regs[reg_base + i] = data[i];
}
__DSB();
}
/**
* @brief 批量数据读取
*/
void FPGA_ReadBuffer(uint16_t reg_base, uint16_t *data, uint32_t len)
{
for (uint32_t i = 0; i < len; i++) {
data[i] = fpga_regs[reg_base + i];
}
__DSB();
}9. 读写时序分析
9.1 读时序 (FPGA → STM32)
读时序要点:
I_EBI_OE_n下降沿后,FPGA内部模块开始准备数据- 经过3个时钟周期(
d0 → d1 → d2),S_EBI_RD_N_d2变为低电平 - 三态门在
S_EBI_RD_N_d2 == 0时使能,将I_EBI_RD_data驱动到IO_EBI_data - STM32在
NOE上升沿采样数据,需确保数据在采样前已稳定(满足DataSetupTime)
9.2 写时序 (STM32 → FPGA)
写时序要点:
I_EBI_WE_n下降沿表示写周期开始,STM32地址和数据已稳定O_EBI_WE_n在1个时钟周期后同步到低电平,内部模块在时钟上升沿检测!O_EBI_WE_n && !O_EBI_CS_n执行写操作O_EBI_WR_data直接采样IO_EBI_data,无需额外延迟,因为写操作时数据总线由STM32持续驱动
10. 关键设计决策说明
10.1 为什么写信号只用1级同步,读信号用3级?
| 信号 | 同步级数 | 原因 |
|---|---|---|
I_EBI_WE_n → O_EBI_WE_n | 1级 | 写操作对FPGA是"被动接收",数据总线由STM32驱动,FPGA只需在时钟沿采样稳定的数据即可 |
I_EBI_OE_n → S_EBI_RD_N_d2 | 3级 | 读操作对FPGA是"主动输出",需要给内部模块预留时间准备数据,再通过三态门驱动到总线 |
10.2 三态门控制逻辑
assign IO_EBI_data = ( (I_EBI_CS_n == 1'b0) && (S_EBI_RD_N_d2 == 1'b0) )
? I_EBI_RD_data : 16'bz;使用 S_EBI_RD_N_d2 而非 I_EBI_OE_n 的原因:
- 直接使用
I_EBI_OE_n会导致FPGA在STM32还未准备好接收数据时就驱动总线,可能产生总线冲突 - 使用延迟后的
S_EBI_RD_N_d2确保:① 内部数据已准备好;② STM32的读使能确实有效;③ 满足数据建立时间
10.3 地址直通 vs 同步
assign O_EBI_addr = I_EBI_addr; // 直通,无时钟延迟地址采用组合逻辑直通而非同步,原因:
- 地址在EBI访问期间保持稳定,无需跨时钟域处理
- 减少地址到内部模块的延迟,提高访问速度
- 地址稳定性由STM32硬件保证
