手把手教你用FPGA(EP4CE10)和STM32F103实现双向UART数据转发(含完整Verilog与C代码)
FPGA与STM32双向UART通信实战:从硬件搭建到代码解析
在嵌入式系统开发中,FPGA和MCU的协同工作越来越常见。FPGA擅长并行处理和硬件加速,而STM32这类微控制器则更适合控制逻辑和协议处理。本文将带你实现一个完整的FPGA(EP4CE10)与STM32F103之间的双向UART数据转发系统,包含硬件连接、Verilog和C代码的逐行解析,以及实际调试中的关键技巧。
1. 系统架构与硬件准备
这个项目的核心目标是建立一个双向通信桥梁:上位机发送的数据可以通过STM32转发到FPGA,也可以直接发送到FPGA再转发给STM32。这种架构在工业控制、数据采集等场景中非常实用。
所需硬件清单:
- FPGA开发板:EP4CE10F17C8核心板
- STM32开发板:STM32F103RCT6(正点原子或野火系列均可)
- USB转TTL模块(用于连接上位机)
- 杜邦线若干
硬件连接示意图:
上位机 <--UART--> STM32(UART2) <--UART--> FPGA(UART1) <--UART--> 上位机关键连接细节:
- FPGA的UART1_TX连接STM32的UART1_RX(PA10)
- FPGA的UART1_RX连接STM32的UART1_TX(PA9)
- STM32的UART2(PA2/PA3)连接上位机
- FPGA的UART2连接另一个上位机通道(可选)
注意:所有UART接口的电平必须匹配,通常是3.3V TTL电平。如果使用5V设备,需要电平转换电路。
2. FPGA端的Verilog实现
FPGA作为通信中间节点,需要处理两路UART的收发以及数据缓冲。我们采用FIFO作为数据中转站,确保在数据突发时不会丢失。
2.1 顶层模块设计
module test( input sys_clk, // 50MHz系统时钟 input sys_rst_n, // 低电平复位 // UART1接口 input uart1_rxd, // FPGA接收STM32数据 output uart1_txd, // FPGA发送数据到STM32 // UART2接口 input uart2_rxd, // FPGA接收上位机数据 output uart2_txd // FPGA发送数据到上位机 );时钟分频处理:由于UART波特率通常较低(如115200bps),我们需要对50MHz系统时钟进行分频:
wire clk_1m_w; pll_clk u_pll_clk( .inclk0(sys_clk), .c0(clk_1m_w) // 生成1MHz时钟用于调试 );2.2 双FIFO数据缓冲机制
数据流向控制是核心难点,我们采用两个独立的FIFO分别处理不同方向的数据:
- uart2_to_uart1 FIFO:存储从上位机(UART2)接收要转发给STM32(UART1)的数据
- uart1_to_uart2 FIFO:存储从STM32(UART1)接收要转发给上位机(UART2)的数据
// UART2到UART1的FIFO uart2_uart1 u_uart2_uart1( .wrclk(sys_clk), .wrreq(uart2_wrreq), .data(uart2_recv_data), .wrfull(uart2_wrfull), .rdclk(sys_clk), .rdreq(uart2_rdreq), .q(uart2_data_out), .rdempty(uart2_rdempty) ); // UART1到UART2的FIFO uart1_uart2 u_uart1_uart2( .wrclk(sys_clk), .wrreq(uart1_wrreq), .data(uart1_recv_data), .wrfull(uart1_wrfull), .rdclk(sys_clk), .rdreq(uart1_rdreq), .q(uart1_data_out), .rdempty(uart1_rdempty) );2.3 UART收发模块关键代码
UART接收模块需要准确检测起始位并在数据位中点采样:
always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin rxdata <= 8'd0; end else if(rx_flag) begin if (clk_cnt == BPS_CNT/2) begin // 在数据位中点采样 case (rx_cnt) 4'd1: rxdata[0] <= uart_rxd_d1; 4'd2: rxdata[1] <= uart_rxd_d1; // ... 其他数据位 4'd8: rxdata[7] <= uart_rxd_d1; default:; endcase end end end发送模块则需要注意忙信号处理,防止数据覆盖:
always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin uart_txd <= 1'b1; end else if (tx_flag) begin case(tx_cnt) 4'd0: uart_txd <= 1'b0; // 起始位 4'd1: uart_txd <= tx_data[0]; // ... 其他数据位 4'd9: uart_txd <= 1'b1; // 停止位 default:; endcase end end3. STM32端的C语言实现
STM32作为系统的另一个智能节点,需要配置两个UART接口并实现数据转发逻辑。
3.1 UART初始化配置
使用STM32CubeMX可以快速生成初始化代码,但我们还是分析关键配置:
void uart1_init(u32 bound) { // GPIO端口设置 GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 使能USART1和GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); // 配置USART1_TX (PA9)为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置USART1_RX (PA10)为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // USART参数配置 USART_InitStructure.USART_BaudRate = bound; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); // 使能USART1接收中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }3.2 中断服务程序实现
STM32通过中断方式接收数据,提高系统响应效率:
void USART1_IRQHandler(void) { u8 Res; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { Res = USART_ReceiveData(USART1); if((USART1_RX_STA & 0x8000) == 0) { // 接收未完成 USART1_RX_BUF[USART1_RX_STA & 0X3FFF] = Res; USART1_RX_STA++; if(Res == 0x65) // 检测到结束标志 USART1_RX_STA |= 0x8000; else if(USART1_RX_STA > (USART1_REC_LEN-1)) USART1_RX_STA = 0; // 错误处理 } } }3.3 主循环数据转发逻辑
主程序不断检查接收状态标志,实现数据转发:
while(1) { if(USART2_RX_STA & 0x8000) { // UART2收到完整数据 len = USART2_RX_STA & 0x3fff; for(t=0; t<len; t++) { USART1->DR = USART2_RX_BUF[t]; // 转发到UART1 while((USART1->SR & 0X40) == 0); // 等待发送完成 } USART2_RX_STA = 0; } else if(USART1_RX_STA & 0x8000) { // UART1收到完整数据 len = USART1_RX_STA & 0x3fff; for(t=0; t<len; t++) { USART2->DR = USART1_RX_BUF[t]; // 转发到UART2 while((USART2->SR & 0X40) == 0); // 等待发送完成 } USART1_RX_STA = 0; } else { delay_ms(10); // 短暂延时降低CPU占用 } }4. 系统调试与性能优化
实际部署时,以下几个关键点需要特别注意:
4.1 波特率匹配问题
常见问题:
- FPGA和STM32的波特率设置不一致
- 高波特率下的时钟误差累积
解决方案:
- 双方使用相同的波特率(推荐115200或256000)
- 在FPGA端精确计算波特率分频系数:
parameter CLK_FREQ = 50000000; // 50MHz系统时钟 parameter UART_BPS = 115200; // 目标波特率 localparam BPS_CNT = CLK_FREQ/UART_BPS; // 分频系数- 使用示波器测量实际波特率,检查起始位、停止位和数据的脉宽
4.2 FIFO缓冲区的深度优化
根据数据流量特点调整FIFO深度:
| 应用场景 | 推荐FIFO深度 | 考虑因素 |
|---|---|---|
| 低频控制指令 | 16-32字节 | 指令长度短,间隔时间长 |
| 中速数据采集 | 64-128字节 | 保证不会因处理延迟丢数据 |
| 高速数据流 | 256字节以上 | 应对突发数据 |
4.3 错误处理机制增强
在实际项目中,我们需要增加以下保护措施:
- 超时机制:当FIFO长时间不满时强制发送
- 数据校验:添加CRC校验字段
- 流量控制:硬件流控(RTS/CTS)或软件流控(XON/XOFF)
- 错误计数:统计通信错误率,超过阈值报警
// 增强型接收状态判断 if(USART_GetITStatus(USART1, USART_IT_RXNE)) { Res = USART_ReceiveData(USART1); if(USART_GetFlagStatus(USART1, USART_FLAG_PE|USART_FLAG_FE|USART_FLAG_NE)) { error_count++; // 统计错误 USART_ClearFlag(USART1, USART_FLAG_PE|USART_FLAG_FE|USART_FLAG_NE); } else { // 正常数据处理 } }4.4 性能测试数据
在不同波特率下的实测性能对比:
| 波特率(bps) | FPGA资源占用(LUT) | 最大稳定吞吐量 | STM32 CPU占用率 |
|---|---|---|---|
| 9600 | 120 | 0.8KB/s | <5% |
| 115200 | 150 | 10KB/s | 15% |
| 256000 | 180 | 22KB/s | 30% |
| 921600 | 220 | 75KB/s | 65% |
提示:当波特率超过500kbps时,建议使用DMA方式传输,降低CPU负载
5. 扩展应用与进阶设计
这个基础框架可以扩展出多种实际应用:
5.1 协议转换网关
在数据转发过程中添加协议转换逻辑:
- Modbus RTU转ASCII:工业设备互联
- 自定义二进制协议转JSON:物联网应用
- 数据加密/解密:安全通信
// 简单的异或加密示例 always @(posedge sys_clk) begin if(uart2_recv_en) begin encrypted_data <= uart2_recv_data ^ 8'hAA; // 异或加密 uart2_wrreq <= 1; end else begin uart2_wrreq <= 0; end end5.2 多设备组网
通过UART扩展可以实现更复杂的网络拓扑:
[上位机] | [STM32] / \ [FPGA1] [FPGA2] | | [设备A] [设备B]5.3 高速数据采集系统
结合FPGA的并行处理能力:
- FPGA负责高速ADC数据采集和预处理
- STM32负责数据打包、协议处理和网络传输
- 通过UART发送控制命令和接收状态信息
// STM32控制FPGA采集的指令格式 void send_acq_command(uint8_t mode, uint16_t sample_count) { uint8_t cmd[5] = {0xAA, mode, sample_count>>8, sample_count&0xFF, 0x55}; for(int i=0; i<5; i++) { USART1->DR = cmd[i]; while((USART1->SR & USART_FLAG_TXE) == 0); } }在实现这些扩展功能时,FPGA端的Verilog代码需要相应增加状态机和数据处理逻辑,而STM32端则需要完善协议栈和错误处理机制。通过这种灵活的组合,可以构建出适应各种工业场景的可靠通信系统。
