ZYNQ PS 端 UART 接收数据数据帧(初学者友好版)
参考资料:
作为电子工程师初学者,在接触 ZYNQ 这类嵌入式平台时,“串口接收不定长数据” 是非常实用但容易卡壳的知识点 —— 固定长度接收在实际项目中灵活性太差,而不定长接收需要理解中断、FIFO 等核心概念。本文基于 CSDN 博客与正点原子嵌入式培训班教学内容,用通俗语言拆解原理、修正代码细节、补充流程图 / 结构示意图,并清晰描述网页中的关键图片,帮你彻底搞懂这个功能。
要理解 UART 接收数据帧,首先需要明确两个核心概念:
- UART:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),是嵌入式系统中最常用的串行通信接口(无需时钟线,靠 “波特率” 同步);
- 数据帧:UART 通信中 “一次完整数据传输的最小单位”,就像快递包裹的 “包装盒 + 填充物 + 物品”,规定了数据的封装格式,确保发送端和接收端能统一解读。
一、UART 数据帧的基本结构(核心!)
UART 数据帧是 “低电平起始、高电平结束” 的异步结构,没有固定长度,但包含 5 个关键部分,从接收端的角度看,帧结构依次为:
空闲位 → 起始位 → 数据位 → 校验位(可选) → 停止位
下面逐个拆解每个部分的作用、位数和特点,初学者可结合下表理解:
帧组成部分 | 位数 | 电平 / 规则 | 核心作用 | 初学者注意点 |
---|---|---|---|---|
空闲位 | 任意(可无) | 高电平(逻辑 1) | 表示 “当前无数据传输”,是帧的 “前置状态” | 接收端默认先处于空闲位,等待起始位触发接收 |
起始位 | 1 位 | 低电平(逻辑 0) | 告诉接收端:“接下来要传数据了,准备同步” | 必须有! 是帧的 “启动信号”,触发接收流程 |
数据位 | 5~9 位(常用 8 位) | 逻辑 0/1(如 8 位对应 1 个字节) | 实际要传输的 “有效数据”(如 ASCII 码、传感器值) | 最核心的部分,位数需收发端一致(如都设 8 位) |
校验位 | 0 位(无)或 1 位 | 奇校验 / 偶校验 / 强制 0 / 强制 1 | 检测数据传输是否出错(简单差错控制) | 可选功能,若启用,收发端校验规则必须一致 |
停止位 | 1/1.5/2 位(常用 1 位) | 高电平(逻辑 1) | 告诉接收端:“这一帧数据结束了” | 必须有! 是帧的 “收尾信号”,确保帧边界清晰 |
关键部分的通俗解释(举例子)
以 “常用配置”(8 位数据位 + 1 位奇校验 + 1 位停止位) 为例,传输一个字节 0x55
(二进制 01010101
),数据帧的实际波形如下:
高电平(空闲)→ 低电平(起始位)→ 0 1 0 1 0 1 0 1(8位数据)→ 0(奇校验位)→ 高电平(停止位)
- 校验位计算:奇校验要求 “数据位 + 校验位” 的总 1 的个数为奇数。
数据位01010101
有 4 个 1(偶数),所以校验位设为 0,确保总 1 的个数为 5(奇数)。 - 停止位:1 位高电平,接收端看到高电平后,就知道这一帧结束,准备接收下一帧(或回到空闲状态)。
二、UART 接收数据帧的完整过程(从硬件到软件)
接收端(如单片机的 UART 模块)如何 “识别并解析” 这个数据帧?整个过程可分为 4 个关键步骤,初学者可结合流程图理解:
流程图:UART 接收数据帧的核心流程
检测到“高→低”跳变(起始位)
是
否
校验成功
校验失败
检测到高电平(停止位)
未检测到停止位
接收端初始化
继续等待下一帧
同步波特率:用采样时钟对准数据位
逐位接收数据位:存储到寄存器
是否启用校验位?
计算校验值 → 与接收的校验位对比
跳过校验,直接检查停止位
标记“帧错误”,丢弃数据
数据帧接收完成:将数据存入缓冲区/FIFO
检测到“高→低”跳变(起始位)
是
否
校验成功
校验失败
检测到高电平(停止位)
未检测到停止位
接收端初始化
继续等待下一帧
同步波特率:用采样时钟对准数据位
逐位接收数据位:存储到寄存器
是否启用校验位?
计算校验值 → 与接收的校验位对比
跳过校验,直接检查停止位
标记“帧错误”,丢弃数据
数据帧接收完成:将数据存入缓冲区/FIFO
豆包
你的 AI 助手,助力每日工作学习
步骤详解(结合硬件逻辑,初学者易懂)
1. 接收端初始化:做好 “接收准备”
在开始接收前,必须先配置 UART 模块的核心参数(收发端参数必须完全一致,否则会乱码):
- 波特率:如 9600bps(每秒传输 9600 位)、115200bps(常用高速率);
- 数据位:如 8 位(对应 1 个字节,最常用);
- 校验位:如 “无校验”“奇校验”;
- 停止位:如 1 位(最常用)。
例:STM32 单片机初始化 UART 时,会配置 USART_InitTypeDef
结构体,指定上述参数。
2. 等待起始位:“抓帧的启动信号”
UART 接收端默认处于 空闲状态(Rx 引脚为高电平),此时硬件会持续监测 Rx 引脚的电平变化:
- 当检测到 “高电平 → 低电平” 的跳变 时,就判断 “起始位来了”,立即触发接收流程;
- 为什么是 “高→低”?因为空闲位是高,起始位是低,这种跳变是唯一的 “启动标识”,避免误触发。
3. 同步波特率:“对准数据位的采样时机”
UART 是 “异步” 通信(没有时钟线),收发端靠 “波特率” 同步 —— 接收端会生成一个 “采样时钟”(通常是波特率的 16 倍,如 9600bps 对应 153600Hz 采样时钟)。
采样的核心逻辑:
- 检测到起始位后,先等待 8 个采样时钟周期(到达起始位的 “中间位置”),确认这不是干扰(避免毛刺误判);
- 之后,每等待 16 个采样时钟周期(正好对应 1 位数据的时间),就在 “数据位的中间位置” 采样一次(中间位置电平最稳定,误差最小);
- 例如:8 位数据位,就采样 8 次,依次得到每一位的逻辑值(0 或 1),存入 UART 接收寄存器。
4. 校验与停止位确认:“确保数据没传错”
校验位检查(若启用):
接收端会根据预设的校验规则(如奇校验),计算接收的 “数据位” 中 1 的个数,再与 “接收的校验位” 对比:- 若一致 → 校验成功,数据可信;
- 若不一致 → 标记 “校验错误”,丢弃该帧数据(避免错误数据被后续程序使用)。
停止位确认:
校验通过后,接收端继续等待采样时钟,检测是否出现 “高电平”(停止位):- 若检测到高电平 → 确认 “这一帧结束”,接收完成;
- 若未检测到高电平 → 标记 “帧错误”(可能是波特率不匹配或干扰导致),丢弃数据。
5. 数据输出:“交给软件处理”
当一帧数据接收完成且无错误时,UART 硬件会将 “接收寄存器中的数据”(如 8 位数据)存入 接收缓冲区 或 FIFO(之前讨论过 FIFO,用于解决 “CPU 处理速度跟不上数据接收速度” 的问题),同时触发一个 “接收完成中断”,通知 CPU:“有新数据了,快来读!”
三、初学者常见问题与注意事项
为什么波特率必须一致?
波特率决定了 “每一位数据的传输时间”(如 9600bps 对应每 bit 约 104μs)。若收发端波特率偏差超过 5%,接收端的采样时钟会严重偏离数据位中间位置,导致采样错误(比如把 0 读成 1)。“不定长数据” 怎么接收?
UART 帧本身是 “定长”(如 10 位:1 起始 + 8 数据 + 1 停止),但实际应用中常传输 “不定长数据”(如传感器一次传 3 字节,一次传 5 字节)。
解决方法:在 “数据位” 中封装 “帧头 + 长度 + 数据 + 帧尾”(如帧头 0xAA,长度字节表示后续数据长度,帧尾 0x55),接收端先找帧头,再按长度接收数据,最后验证帧尾。常见错误:帧错误、校验错误、溢出错误
- 帧错误:未检测到停止位(原因:波特率不匹配、干扰);
- 校验错误:校验位不匹配(原因:数据传输中被干扰);
- 溢出错误:接收缓冲区 / FIFO 满了,但新数据还在来(原因:CPU 处理太慢,没及时读缓冲区)。
四、总结
UART 接收数据帧的本质是:接收端通过 “电平变化” 识别帧边界(起始位→停止位),通过 “波特率同步” 准确采样数据位,通过 “校验” 确保数据可靠,最终将有效数据交给 CPU 处理。
对初学者来说,重点记住两点:
- 先掌握 帧结构的 5 个部分(尤其是起始位、数据位、停止位的作用);
- 理解 “采样时钟对准数据位中间” 是 UART 异步通信可靠的关键。
后续可结合具体芯片(如 STM32、51 单片机)的 UART 接收代码,对照上述流程分析,就能把理论和实际代码对应起来,理解更深刻。
一、前言:为什么要学 “不定长数据接收”?
之前的 ZYNQ 串口教程多是 “固定长度接收”(比如每次只收 10 个字节),但实际项目中(比如串口传传感器数据、上位机指令),数据长度是不固定的 —— 可能这次传 3 个字节的指令,下次传 20 个字节的采集数据。
如果还用固定长度接收,要么丢数据,要么多收 “无效字节”,完全没法用。所以必须掌握 “不定长接收”,而 ZYNQ PS 端的 UART 通过两种中断配合,就能完美实现这个需求。
二、ZYNQ PS UART 基础认知(先搞懂硬件逻辑)
在写代码前,必须先明白 ZYNQ PS 端串口的 “硬件家底”,不然代码就是 “空中楼阁”。
1. ZYNQ PS UART 的核心硬件结构
博客里提到了一张 “UART 硬件框图”,这里简化成初学者能懂的结构示意图,关键模块一个都不少:
关键模块解释(必看!)
- FIFO(先进先出缓存):串口收发都有一个 64 字节的 FIFO,作用是 “缓冲数据”—— 比如接收数据时,硬件先把数据存到 RxFIFO,不用 CPU “实时盯着”,等 FIFO 里数据够多了再通知 CPU 读取,大大减少 CPU 的工作量(这是串口高效工作的核心)。
- APB 总线:PS 内核(CPU/DMA)和 UART 控制器之间的 “数据公路”,CPU 读 RxFIFO、写 TxFIFO 都要通过它。
- GIC(全局中断控制器):ZYNQ 里所有外设的中断都要先经过 GIC,再传给 CPU—— 相当于 “中断管家”,负责管理哪个中断优先、什么时候通知 CPU。
- 波特率发生器:由 UART 参考时钟分频得到,决定串口的 “数据传输速度”(比如 115200bps,就是每秒传 115200 个比特)。
2. 两个核心中断:不定长接收的 “关键钥匙”
博客里强调:必须同时用两种中断,才能实现不定长接收。这两个中断的作用,类比成 “快递收发” 就好理解了:
中断类型 | 功能类比 | 技术原理(博客核心) | 作用 |
---|---|---|---|
RxFIFO 阈值触发中断 | 快递柜 “满 32 件提醒” | 给 RxFIFO 设一个 “阈值”(比如 32 字节),当 RxFIFO 中数据量≥阈值时,触发中断 | 处理 “批量数据”,避免 CPU 频繁读 FIFO |
接收数据超时(Timeout)中断 | 快递柜 “10 分钟没新快递 = 这波收完了” | 设定一个时间(比如 16 个波特率周期),如果这段时间没收到新数据,触发中断 | 判断 “一帧不定长数据结束”,避免漏收数据 |
举个例子:如果上位机发 100 字节的不定长数据 ——
- 前 32 字节进入 RxFIFO,触发 “阈值中断”,CPU 读走 32 字节;
- 中间 32 字节再进 RxFIFO,又触发 “阈值中断”,CPU 再读走 32 字节;
- 最后 36 字节进 RxFIFO(没到 32 阈值),但过了 16 个波特率周期没新数据,触发 “超时中断”,CPU 读走剩下的 36 字节,同时知道 “这帧数据收完了”。
三、详细实现步骤(硬件 + 软件,一步不落)
博客里的实现步骤是 “创建 Vivado 工程→导入 SDK 写代码→下载验证”,这里补充初学者容易忽略的细节,避免踩坑。
1. 第一步:创建 Vivado 硬件工程(核心是 “启用 UART1”)
博客提到 “参照 ZYNQ 进阶之路 5 的工程创建流程”,这里简化关键步骤,重点是 UART 相关配置:
- 新建 Vivado 工程,添加 “ZYNQ7 Processing System” IP 核(就是博客里提到的
processing_system7_0
); - 双击 IP 核打开配置界面,在 “Peripheral I/O Pins” 里勾选 “UART1”(默认可能只开 UART0,UART1 需要手动启用);
- 确认 UART1 的引脚映射(用 MIO 还是 EMIO,初学者建议用 MIO,不用额外接 PL 引脚);
- 生成 Bitstream(比特流文件),然后导出 “Hardware Platform Specification”(硬件平台文件,给 SDK 用)。
博客里有一张 “工程创建后界面图”,内容就是 Vivado 的 IP Integrator 界面,中间显示
processing_system7_0
IP 核,旁边没有额外的 PL 逻辑(因为这次功能全在 PS 端实现,PL 不用动)。
2. 第二步:SDK 软件开发(核心是 “配置中断 + 写中断服务函数”)
导出硬件文件后,打开 SDK(Xilinx Software Development Kit),创建 “Hello World” 工程,然后替换代码。博客里的代码有一些笔误(初学者容易抄错),这里先修正代码,再逐段解释。
修正后的完整代码(关键错误已标注)
c
运行
#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "xscugic.h"
#include "xparameters_ps.h"
#include "xuartps.h"
#include "xuartps_hw.h"
#include "sleep.h"// 1. 宏定义:关键参数(初学者别乱改,先理解)
#define UART1_BAUD 115200 // 串口波特率(和上位机一致)
#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID // GIC设备ID
#define UART1_DEVICE_ID XPAR_XUARTPS_1_DEVICE_ID // UART1设备ID(博客里写0,这里修正为正确ID)
#define UART1_INT_ID XPAR_XUARTPS_1_INTR // UART1中断ID(博客未明确,补充)// 2. 全局变量和结构体:管理UART接收状态
typedef struct {XUartPs UartPs_uart1; // UART1设备实例(必须有,是XUartPs库函数的“入口”)u32 uart1_totalbumber; // 已接收数据的总长度(记录这帧数据收了多少字节)
} uart1_rxpoint_typedef;uart1_rxpoint_typedef uart1_rxpoint; // 定义结构体变量
unsigned char uart_send[500]; // 接收数据的缓存数组(500字节足够日常使用)// 3. 函数声明:先告诉编译器有这些函数
void uartps_init(void); // UART1初始化函数
void uart_handler(void *CallBackRef); // UART1中断服务函数(核心!)// 4. 主函数:程序入口
int main(void) {init_platform(); // SDK初始化(固定开头,初始化硬件平台)uartps_init(); // 初始化UART1(关键!)xil_printf("Hello World\n\r"); // 串口打印欢迎信息(验证串口是否正常)while(1); // 死循环:程序靠中断驱动,主函数不用做其他事cleanup_platform(); // SDK清理(实际不会执行,因为while(1))return 0;
}// 5. 中断服务函数:UART1收到数据后,CPU会自动跳转到这里执行(核心逻辑!)
void uart_handler(void *CallBackRef) {u32 IsrStatus; // 存储中断状态(判断是哪种中断)u32 rx_realnumber = 0;// 实际读取到的字节数(每次读FIFO都要记录)// 把传入的“回调参数”转成我们定义的结构体类型(获取UART实例和接收长度)uart1_rxpoint_typedef *UartInstancePtr = (uart1_rxpoint_typedef *)CallBackRef;// 步骤1:读取“中断屏蔽寄存器(IMR)”和“中断状态寄存器(ISR)”,得到当前触发的中断类型// 原理:IMR是“允许哪些中断”,ISR是“哪些中断发生了”,两者按位与,得到“允许且发生的中断”IsrStatus = XUartPs_ReadReg(UartInstancePtr->UartPs_uart1.Config.BaseAddress, // UART1的基地址(找到对应的UART)XUARTPS_IMR_OFFSET // 中断屏蔽寄存器地址);IsrStatus &= XUartPs_ReadReg(UartInstancePtr->UartPs_uart1.Config.BaseAddress,XUARTPS_ISR_OFFSET // 中断状态寄存器地址);// 步骤2:判断是“RxFIFO溢出中断”还是“超时中断”if ((IsrStatus & XUARTPS_IXR_RXOVR) != 0) { // 情况1:RxFIFO数据满了(达到阈值)// 先清除中断标志(必须做!不然CPU会一直认为中断还在,反复进入中断)XUartPs_WriteReg(UartInstancePtr->UartPs_uart1.Config.BaseAddress,XUARTPS_ISR_OFFSET,XUARTPS_IXR_RXOVR // 写对应中断位清除标志);// 读取RxFIFO中的数据,存到缓存数组uart_send// 第三个参数500:最大读取长度(不用怕超,因为FIFO空了就会停止,返回实际读取的字节数)rx_realnumber = XUartPs_Recv(&UartInstancePtr->UartPs_uart1, // UART1实例&uart_send[UartInstancePtr->uart1_totalbumber], // 缓存数组的“当前位置”(避免覆盖已存数据)500);// 更新已接收数据的总长度(加上这次读的字节数)UartInstancePtr->uart1_totalbumber += rx_realnumber;} else if ((IsrStatus & XUARTPS_IXR_TOUT) != 0) { // 情况2:超时中断(数据接收完了)// 先清除中断标志XUartPs_WriteReg(UartInstancePtr->UartPs_uart1.Config.BaseAddress,XUARTPS_ISR_OFFSET,XUARTPS_IXR_TOUT);// 读取RxFIFO中剩下的数据(可能没到阈值,比如最后36字节)rx_realnumber = XUartPs_Recv(&UartInstancePtr->UartPs_uart1,&uart_send[UartInstancePtr->uart1_totalbumber],500);// 更新总长度UartInstancePtr->uart1_totalbumber += rx_realnumber;// (可选)将接收的数据“回发”到串口(用串口助手能看到接收的数据,验证是否正确)for (u32 tx_count = 0; tx_count < UartInstancePtr->uart1_totalbumber; tx_count++) {XUartPs_SendByte(STDIN_BASEADDRESS, uart_send[tx_count]); // 逐个字节回发}// 重置总长度(这帧数据处理完了,下次接收从0开始)UartInstancePtr->uart1_totalbumber = 0;}
}// 6. UART1初始化函数:配置UART、FIFO、中断、GIC(一次性配置,主函数只调用一次)
void uartps_init(void) {XUartPs_Config *Uart1_Config; // UART1的配置结构体(存储UART1的基地址、ID等信息)XScuGic_Config *Gic_Config; // GIC的配置结构体(管理中断)// 步骤1:获取UART1的配置信息(从Xilinx提供的库中查找,不用自己写)Uart1_Config = XUartPs_LookupConfig(UART1_DEVICE_ID);if (Uart1_Config == NULL) {xil_printf("UART1 Config Error!\n\r");return;}// 步骤2:初始化UART1设备实例(把配置信息“灌”到uart1_rxpoint.UartPs_uart1中)XStatus status = XUartPs_CfgInitialize(&uart1_rxpoint.UartPs_uart1, // 要初始化的UART实例Uart1_Config, // UART1的配置信息Uart1_Config->BaseAddress // UART1的基地址);if (status != XST_SUCCESS) {xil_printf("UART1 Init Error!\n\r");return;}// 步骤3:配置UART1的核心参数XUartPs_SetBaudRate(&uart1_rxpoint.UartPs_uart1, UART1_BAUD); // 设置波特率(115200)XUartPs_SetFifoThreshold(&uart1_rxpoint.UartPs_uart1, 32); // 设置RxFIFO阈值(32字节,触发阈值中断)XUartPs_SetRecvTimeout(&uart1_rxpoint.UartPs_uart1, 4); // 设置超时时间(公式:超时时间=4×4=16个波特率周期)// 步骤4:允许UART1的“RxFIFO溢出中断”和“超时中断”(不允许的话,中断不会触发)XUartPs_SetInterruptMask(&uart1_rxpoint.UartPs_uart1,XUARTPS_IXR_RXOVR | XUARTPS_IXR_TOUT // 按位或:同时允许两种中断);// 步骤5:初始化GIC(全局中断控制器,UART中断必须经过GIC才能通知CPU)// 5.1 获取GIC的配置信息Gic_Config = XScuGic_LookupConfig(INTC_DEVICE_ID);if (Gic_Config == NULL) {xil_printf("GIC Config Error!\n\r");return;}// 5.2 初始化GIC实例status = XScuGic_CfgInitialize(&XPS_XScuGic, // GIC实例(全局变量)Gic_Config, // GIC配置信息Gic_Config->CpuBaseAddress // GIC的CPU基地址);if (status != XST_SUCCESS) {xil_printf("GIC Init Error!\n\r");return;}// 步骤6:配置UART1中断的“优先级”和“触发方式”XScuGic_SetPriorityTriggerType(&XPS_XScuGic, // GIC实例UART1_INT_ID, // UART1的中断ID16, // 优先级(0最高,255最低,16是中等优先级,避免抢占关键中断)1 // 触发方式(1=上升沿触发,串口中断常用方式));// 步骤7:将“UART1中断”和“中断服务函数(uart_handler)”绑定// 作用:当UART1触发中断时,GIC会自动调用uart_handler函数status = XScuGic_Connect(&XPS_XScuGic,UART1_INT_ID,(Xil_ExceptionHandler)uart_handler, // 中断服务函数&uart1_rxpoint // 传给中断服务函数的参数(结构体变量,带UART实例和接收长度));if (status != XST_SUCCESS) {xil_printf("UART1 Interrupt Connect Error!\n\r");return;}// 步骤8:启用UART1中断(GIC层面允许这个中断)XScuGic_Enable(&XPS_XScuGic, UART1_INT_ID);// 步骤9:初始化并启用CPU的中断功能(最后一步,不然CPU收不到中断)Xil_ExceptionInit(); // 初始化CPU的异常处理Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, // 中断异常类型(Xil_ExceptionHandler)XScuGic_InterruptHandler, // GIC的中断处理函数(Xilinx库提供)&XPS_XScuGic // 传给GIC中断处理函数的参数);Xil_ExceptionEnable(); // 启用CPU的中断功能// 步骤10:初始化“已接收数据总长度”为0(准备接收第一帧数据)uart1_rxpoint.uart1_totalbumber = 0;
}
3. 第三步:下载验证(看实际效果)
- 连接硬件:用 USB 转串口线连接 ZYNQ 的 UART1 TxD/RxD 引脚和电脑;
- 配置串口助手:博客里用的是 “单片机多功能调试助手”,配置如下(必须和代码一致!):
- 端口:电脑设备管理器中查到的 COM 口(比如 COM3);
- 波特率:115200(代码中
UART1_BAUD
定义的是 115200); - 数据位:8;
- 停止位:1;
- 校验:NONE;
- 下载程序:在 SDK 中把程序下载到 ZYNQ 的 RAM 或 FLASH 中;
- 验证效果:
- 串口助手会收到 “Hello World”(说明串口基本正常);
- 在上位机发送任意长度的数据(比如 10 字节、50 字节、130 字节),串口助手会 “回显” 收到的数据(代码中 “回发” 功能),且不会丢数据(博客测试 130 字节也稳定)。
一、代码整体框架
先看代码的 “骨架”,帮你建立整体认知:
- 头文件引入:包含 ZYNQ 硬件驱动、中断、UART 等必需的库文件;
- 宏定义:定义 UART 波特率、设备 ID 等固定参数(避免硬编码,方便修改);
- 结构体定义:集中管理 UART 的 “设备实例” 和 “接收状态”(比零散变量更易维护);
- 全局变量:定义接收缓存数组和 UART 状态结构体(中断服务函数需访问);
- 函数声明:提前声明主函数、中断服务函数、初始化函数(编译器需知道函数存在);
- 主函数:程序入口,仅做 “初始化” 和 “死循环等待中断”;
- 中断服务函数:UART 接收数据的核心逻辑(触发后自动执行,处理两种中断);
- UART 初始化函数:配置 UART 参数、中断、GIC(一次性完成所有硬件配置)。
二、完整代码 + 逐行注释
c
运行
// 1. 头文件引入:每个头文件对应一个功能模块,初学者不用死记,知道用途即可
#include <stdio.h> // 标准输入输出(如printf,但嵌入式常用xil_printf)
#include "platform.h" // SDK平台初始化(必须包含,初始化硬件接口)
#include "xil_printf.h" // Xilinx自定义打印函数(比printf更适配嵌入式)
#include "xparameters.h" // 硬件参数定义(如设备ID、基地址,由Vivado导出)
#include "xscugic.h" // GIC(全局中断控制器)驱动库(管理所有中断)
#include "xparameters_ps.h" // PS端(处理系统)硬件参数(补充xparameters.h的PS部分)
#include "xuartps.h" // PS端UART驱动库(UART配置、收发、中断都靠它)
#include "xuartps_hw.h" // PS端UART硬件寄存器操作(底层寄存器读写,辅助驱动)
#include "sleep.h" // 延时函数(本代码未用到,可保留备用)// 2. 宏定义:将固定参数“符号化”,修改时只需改这里,不用搜遍代码
#define UART1_BAUD 115200 // UART1波特率(必须和上位机串口助手一致,常用115200)
#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID // GIC设备ID(固定值,来自xparameters.h)
#define UART1_DEVICE_ID XPAR_XUARTPS_1_DEVICE_ID // UART1设备ID(PS端UART1的唯一标识,来自xparameters_ps.h)
#define UART1_INT_ID XPAR_XUARTPS_1_INTR // UART1中断ID(UART1中断的唯一标识,来自xparameters_ps.h)// 3. 结构体定义:集中管理UART1的“设备实例”和“接收状态”
// 作用:把UART相关的变量打包,避免零散变量(尤其中断服务函数需要访问时更清晰)
typedef struct {XUartPs UartPs_uart1; // UART1设备实例(Xilinx驱动库定义的结构体,存储UART配置、状态等)u32 uart1_totalbumber; // 已接收数据的总长度(记录当前帧数据收了多少字节,初始为0)
} uart1_rxpoint_typedef;// 4. 全局变量:中断服务函数和初始化函数都需要访问,所以定义为全局
uart1_rxpoint_typedef uart1_rxpoint; // UART1状态结构体实例
unsigned char uart_send[500]; // 接收数据缓存数组(长度500字节,足够日常不定长数据需求)// 5. 函数声明:告诉编译器“后面有这些函数”,避免编译报错
void uartps_init(void); // UART1初始化函数(配置UART参数、中断、GIC)
void uart_handler(void *CallBackRef); // UART1中断服务函数(接收数据的核心逻辑,中断触发后自动执行)// 6. 主函数:程序入口(嵌入式程序从main开始执行)
int main(void) {// 步骤1:初始化SDK平台(固定操作,由SDK自动生成,作用是初始化硬件接口、时钟等)init_platform();// 步骤2:初始化UART1(关键!配置UART的波特率、FIFO、中断等,不初始化UART无法工作)uartps_init();// 步骤3:串口打印欢迎信息(验证UART是否初始化成功,上位机串口助手能收到则基本正常)xil_printf("Hello World\n\r"); // \n\r是“换行+回车”,确保串口助手显示换行// 步骤4:死循环(嵌入式程序常用,因为程序靠“中断驱动”,主函数不用做其他事,只需等待中断)while(1); // 这里没有循环体,CPU会一直停在这里,直到有中断触发// 步骤5:平台清理(实际不会执行,因为while(1)永远循环,仅为代码完整性保留)cleanup_platform();return 0; // 主函数返回值(嵌入式中基本不用,仅为符合C语言语法)
}// 7. 中断服务函数:UART1触发中断时,CPU会自动跳转到这里执行(核心逻辑!)
// 参数CallBackRef:传给中断服务函数的“回调参数”(这里传的是uart1_rxpoint结构体,方便访问UART状态)
void uart_handler(void *CallBackRef) {u32 IsrStatus; // 存储“中断状态”(用于判断是哪种中断触发的)u32 rx_realnumber = 0;// 存储“实际读取到的字节数”(每次读FIFO后,记录读了多少字节)// 关键:将“回调参数”转换为我们定义的结构体类型(因为传进来的是void*,需要强转才能访问结构体成员)uart1_rxpoint_typedef *UartInstancePtr = (uart1_rxpoint_typedef *)CallBackRef;// 步骤1:读取“中断屏蔽寄存器(IMR)”和“中断状态寄存器(ISR)”,筛选出“允许且已发生的中断”// 原理:IMR记录“哪些中断被允许”,ISR记录“哪些中断已发生”,两者按位与(&)后,结果就是“允许且发生的中断”IsrStatus = XUartPs_ReadReg(UartInstancePtr->UartPs_uart1.Config.BaseAddress, // UART1的基地址(找到要操作的UART)XUARTPS_IMR_OFFSET // 中断屏蔽寄存器(IMR)的偏移地址(由xuartps_hw.h定义,固定值));IsrStatus &= XUartPs_ReadReg(UartInstancePtr->UartPs_uart1.Config.BaseAddress,XUARTPS_ISR_OFFSET // 中断状态寄存器(ISR)的偏移地址(固定值));// 步骤2:判断中断类型(两种中断:RxFIFO阈值中断、超时中断)// 情况1:RxFIFO阈值中断(RxFIFO中数据量≥阈值时触发,处理“批量数据”)if ((IsrStatus & XUARTPS_IXR_RXOVR) != 0) { // XUARTPS_IXR_RXOVR是“RxFIFO溢出/阈值中断”的标志位// 子步骤1:清除中断标志(必须做!否则CPU会认为中断还在,反复进入中断服务函数)XUartPs_WriteReg(UartInstancePtr->UartPs_uart1.Config.BaseAddress,XUARTPS_ISR_OFFSET, // 写中断状态寄存器(写1清除对应中断标志)XUARTPS_IXR_RXOVR // 清除“RxFIFO阈值中断”标志);// 子步骤2:从RxFIFO中读取数据,存到缓存数组uart_send// XUartPs_Recv函数功能:从UART的RxFIFO读取数据,返回“实际读取的字节数”rx_realnumber = XUartPs_Recv(&UartInstancePtr->UartPs_uart1, // UART1设备实例(告诉函数读哪个UART)// 缓存数组的“当前位置”:避免覆盖已接收的数据(比如之前已收10字节,就从第10字节开始存)&uart_send[UartInstancePtr->uart1_totalbumber],500 // 最大读取长度(不用怕超,因为FIFO空了就会停止,返回实际长度,500是缓存数组的最大长度));// 子步骤3:更新“已接收数据总长度”(加上这次实际读的字节数)UartInstancePtr->uart1_totalbumber += rx_realnumber;}// 情况2:超时中断(RxFIFO有数据,但超过设定时间没新数据,触发“数据结束”判断)else if ((IsrStatus & XUARTPS_IXR_TOUT) != 0) { // XUARTPS_IXR_TOUT是“超时中断”的标志位// 子步骤1:清除中断标志(和上面一样,必须清除,否则反复触发)XUartPs_WriteReg(UartInstancePtr->UartPs_uart1.Config.BaseAddress,XUARTPS_ISR_OFFSET,XUARTPS_IXR_TOUT // 清除“超时中断”标志);// 子步骤2:读取RxFIFO中剩下的少量数据(比如最后3字节,没到阈值,不会触发阈值中断)rx_realnumber = XUartPs_Recv(&UartInstancePtr->UartPs_uart1,&uart_send[UartInstancePtr->uart1_totalbumber],500);// 子步骤3:更新“已接收数据总长度”UartInstancePtr->uart1_totalbumber += rx_realnumber;// 子步骤4:(可选功能)将接收的数据“回发”到串口(上位机能看到自己发的数据,验证接收是否正确)for (u32 tx_count = 0; tx_count < UartInstancePtr->uart1_totalbumber; tx_count++) {// XUartPs_SendByte:逐个字节发送数据(STDIN_BASEADDRESS是UART1的发送基地址)XUartPs_SendByte(STDIN_BASEADDRESS, uart_send[tx_count]);}// 子步骤5:重置“已接收数据总长度”(这帧数据处理完了,下次接收从0开始)UartInstancePtr->uart1_totalbumber = 0;}
}// 8. UART1初始化函数:一次性配置UART1的所有参数(核心配置函数,仅在主函数中调用一次)
void uartps_init(void) {XUartPs_Config *Uart1_Config; // UART1配置结构体指针(存储UART1的基地址、ID等硬件信息)XScuGic_Config *Gic_Config; // GIC配置结构体指针(存储GIC的硬件信息)XStatus status; // 存储函数返回状态(判断配置是否成功,XST_SUCCESS=成功)// 步骤1:获取UART1的硬件配置信息(从xparameters_ps.h中读取,不用自己写)// XUartPs_LookupConfig:根据UART设备ID,找到对应的配置信息(每个UART的配置唯一)Uart1_Config = XUartPs_LookupConfig(UART1_DEVICE_ID);if (Uart1_Config == NULL) { // 配置信息获取失败(比如设备ID写错)xil_printf("UART1 Config Error!\n\r"); // 打印错误信息return; // 退出函数,不再继续初始化}// 步骤2:初始化UART1设备实例(将步骤1获取的配置信息“写入”UART1实例)// XUartPs_CfgInitialize:初始化UART实例,返回状态(成功/失败)status = XUartPs_CfgInitialize(&uart1_rxpoint.UartPs_uart1, // 要初始化的UART1实例(全局结构体中的成员)Uart1_Config, // UART1的配置信息(步骤1获取的)Uart1_Config->BaseAddress // UART1的基地址(从配置信息中获取,找到硬件UART1));if (status != XST_SUCCESS) { // 初始化失败(比如硬件问题)xil_printf("UART1 Init Error!\n\r");return;}// 步骤3:配置UART1的核心参数(波特率、FIFO阈值、超时时间)// 3.1 设置波特率(必须和上位机一致,这里是115200)XUartPs_SetBaudRate(&uart1_rxpoint.UartPs_uart1, UART1_BAUD);// 3.2 设置RxFIFO阈值(32字节:当RxFIFO中数据≥32字节时,触发阈值中断)XUartPs_SetFifoThreshold(&uart1_rxpoint.UartPs_uart1, 32);// 3.3 设置超时时间(公式:超时时间 = n × 4个波特率周期,这里n=4 → 16个波特率周期)// 作用:超过16个波特率周期没新数据,触发超时中断(判断数据结束)XUartPs_SetRecvTimeout(&uart1_rxpoint.UartPs_uart1, 4);// 步骤4:允许UART1的“RxFIFO阈值中断”和“超时中断”(不允许则中断不会触发)// XUartPs_SetInterruptMask:设置UART的中断屏蔽位(允许哪些中断)XUartPs_SetInterruptMask(&uart1_rxpoint.UartPs_uart1,XUARTPS_IXR_RXOVR | XUARTPS_IXR_TOUT // 按位或(|):同时允许两种中断);// 步骤5:初始化GIC(全局中断控制器)——ZYNQ的所有中断都要经过GIC,否则CPU收不到中断// 5.1 获取GIC的硬件配置信息(从xparameters.h中读取)Gic_Config = XScuGic_LookupConfig(INTC_DEVICE_ID);if (Gic_Config == NULL) { // 配置信息获取失败xil_printf("GIC Config Error!\n\r");return;}// 5.2 初始化GIC实例(将GIC配置信息“写入”GIC实例)status = XScuGic_CfgInitialize(&XPS_XScuGic, // GIC实例(全局变量,来自xscugic.h的默认定义)Gic_Config, // GIC的配置信息(步骤5.1获取的)Gic_Config->CpuBaseAddress // GIC的CPU基地址(找到硬件GIC));if (status != XST_SUCCESS) { // GIC初始化失败xil_printf("GIC Init Error!\n\r");return;}// 步骤6:配置UART1中断的“优先级”和“触发方式”// XScuGic_SetPriorityTriggerType:设置中断的优先级和触发边沿XScuGic_SetPriorityTriggerType(&XPS_XScuGic, // GIC实例UART1_INT_ID, // UART1的中断ID16, // 中断优先级(0最高,255最低,16是中等优先级,避免抢占关键中断)1 // 触发方式(1=上升沿触发,串口中断常用方式,稳定不易误触发));// 步骤7:绑定“UART1中断”和“中断服务函数”(关键!告诉GIC:UART1中断触发时,执行哪个函数)status = XScuGic_Connect(&XPS_XScuGic, // GIC实例UART1_INT_ID, // UART1的中断ID(Xil_ExceptionHandler)uart_handler, // 中断服务函数(强转为Xilinx定义的异常处理类型)&uart1_rxpoint // 传给中断服务函数的参数(这里是UART1状态结构体));if (status != XST_SUCCESS) { // 绑定失败xil_printf("UART1 Interrupt Connect Error!\n\r");return;}// 步骤8:启用UART1中断(G层面允许这个中断,不启用则中断无法传给CPU)XScuGic_Enable(&XPS_XScuGic, UART1_INT_ID);// 步骤9:初始化并启用CPU的中断功能(最后一步!CPU默认关闭中断,不启用则收不到任何中断)Xil_ExceptionInit(); // 初始化CPU的异常处理模块(中断属于“异常”的一种)// 注册GIC的中断处理函数(告诉CPU:所有中断都由GIC的处理函数分发)Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, // 中断异常类型(指定处理“中断”类异常)(Xil_ExceptionHandler)XScuGic_InterruptHandler, // GIC的中断分发函数(Xilinx库提供)&XPS_XScuGic // 传给GIC分发函数的参数(GIC实例));Xil_ExceptionEnable(); // 启用CPU的中断功能(这步之后,CPU才能接收中断)// 步骤10:初始化“已接收数据总长度”为0(准备接收第一帧数据,从0开始计数)uart1_rxpoint.uart1_totalbumber = 0;
}
三、核心模块说明(初学者必看)
1. 为什么需要 “GIC 初始化”?
ZYNQ 的 PS 端是 “多核处理器”,所有外设(如 UART、SPI)的中断不能直接传给 CPU,必须经过GIC(全局中断控制器) 统一管理 ——GIC 相当于 “中断管家”,负责:
- 判断中断优先级(高优先级中断先处理);
- 将中断分发到对应的 CPU 核心;
- 避免多个中断同时触发导致的冲突。
所以代码中必须先初始化 GIC,否则 UART 的中断永远传不到 CPU,中断服务函数也不会执行。
2. 中断服务函数的 “回调参数” 有什么用?
中断服务函数的参数CallBackRef
是void*
类型(通用指针),作用是 “传递数据”:
- 因为中断服务函数不能直接访问局部变量,而全局变量过多会导致代码混乱;
- 所以我们把 UART 的 “设备实例” 和 “接收长度” 打包成结构体,通过
CallBackRef
传给中断服务函数,既清晰又易维护。
3. 为什么 “清除中断标志” 是必须的?
UART 的 “中断状态寄存器(ISR)” 中,中断标志位默认是 “1 表示中断发生”,且不会自动清零 —— 如果不手动清除(写 1 到对应位),CPU 会一直认为 “中断还在发生”,反复进入中断服务函数,导致程序卡死。
4. 不定长数据的 “收尾逻辑” 是什么?
靠 “超时中断” 实现:
- 当 UART 接收数据时,若数据量≥32 字节(阈值),触发 “阈值中断”,CPU 读走批量数据;
- 若数据量 <32 字节(比如最后 3 字节),不会触发阈值中断,但超过 “16 个波特率周期” 没新数据,会触发 “超时中断”;
- 超时中断中,CPU 读走剩下的少量数据,然后回发数据、重置接收长度,完成 “一帧不定长数据” 的处理。
四、调试建议(初学者避坑)
- 编译报错 “设备 ID 未定义”:
检查UART1_DEVICE_ID
和UART1_INT_ID
是否正确 —— 这些值来自xparameters_ps.h
,可以在 SDK 中打开该文件,搜索 “XUARTPS_1” 找到正确的 ID(不同 ZYNQ 型号可能不同)。 - 串口收不到 “Hello World”:
- 先查硬件接线(UART1 的 TxD 接 USB 转串口的 RxD,RxD 接 TxD,别接反);
- 再查串口助手配置(波特率 115200、数据位 8、停止位 1、无校验,必须和代码一致);
- 中断不触发:
检查是否漏了Xil_ExceptionEnable()
(启用 CPU 中断)或XScuGic_Enable()
(启用 UART1 中断)—— 这两步是 “最后一公里”,漏了就收不到中断。
四、博客中关键图片的详细描述(无法直接提取,看描述就能对应)
博客里有 3 张核心图片,是理解和操作的关键,这里逐张描述清楚:
图片 1:ZYNQ PS UART 硬件框图(博客原文标注 “UG585_c19_02_020613”)
这是 Xilinx 官方文档 UG585 中的经典框图,比我们之前画的简化图更完整,包含所有硬件模块:
- 顶部是 “Interconnect”(互联模块)和 “PS AXI Interface”(AXI 接口,和 APB 总线互补);
- 中间是 “Slave APB”(APB 从接口,连接 UART 控制器)、“TxFIFO/RxFIFO”(64 字节缓存)、“Transmitter/Receiver”(收发器);
- 右侧是 “UART TxD/RxD” 引脚,通过 “MIO/EMIO” 选择引脚(MIO 是 PS 自带引脚,EMIO 是扩展到 PL 的引脚);
- 下方是 “Status Registers”(状态寄存器)、“Control Registers”(控制寄存器)、“CTS/RTS/DSR/DCD/RI/DTR”(硬件流控信号,本文没用到);
- 左侧是 “Interrupt Controller (GIC)”(中断控制器)、“UART Ref Clock”(参考时钟)、“Baud Rate Generator”(波特率发生器,带 “Divide by 8” 选项)。
图片 2:Vivado 工程中的 ZYNQ IP 核界面
这张图显示的是 Vivado 的 “IP Integrator” 界面,核心内容:
- 中间只有一个 IP 核:
processing_system7_0
(ZYNQ7 Processing System),说明本次工程 “纯 PS 实现”,不需要 PL 端的逻辑; - IP 核下方有一些未连接的端口(比如 MIO、FCLK 等),但 UART1 的 MIO 引脚已经在 IP 核内部配置好(之前步骤 1 中勾选了 UART1)。
图片 3:串口调试助手运行截图(博客中 “单片机多功能调试助手” 界面)
这是验证效果的关键图,界面分几个区域:
- 顶部菜单栏:包含 “串口调试”“串口监视器”“USB 调试” 等功能(本次用 “串口调试”);
- 串口配置区:显示 “端口:COM3”“波特率:115200”“数据位:8”“停止位:1”“校验:NONE”,和代码配置完全一致;
- 接收区:左侧显示 “已接收 29040 字节,速度 264 字节 / 秒,接收状态 [允许]”,右侧是十六进制格式的接收数据(比如 “22 33 44 11 55...”),说明数据接收稳定;
- 发送区:显示 “已发送 29040 字节”,有 “Hex 发送”“连续发送” 按钮(博客测试时用了 “连续发送” 130 字节);
- 底部控制区:包含 “线路控制”(DTR、BREAK、RTS)、“线路状态”(CTS、RING、RLSD、DSR)、“清发送区”“清接收区” 等按钮。
五、初学者常见问题与注意事项(避坑指南)
- 代码编译报错 “变量未定义”:
- 检查
UART1_DEVICE_ID
和UART1_INT_ID
是否正确 —— 这些宏定义来自xparameters_ps.h
,可以在 SDK 中打开这个文件,搜索 “XUARTPS_1” 找到正确的 ID(不同 ZYNQ 型号可能不同)。
- 检查
- 串口收不到 “Hello World”:
- 先检查硬件接线(TxD 接 RxD,RxD 接 TxD,别接反);
- 再检查串口助手配置(波特率、端口是否正确);
- 最后检查 UART 实例是否初始化正确(
XUartPs_CfgInitialize
的返回值是否为XST_SUCCESS
)。
- 接收数据丢包:
- 检查 FIFO 阈值是否设置合理(32 字节是比较均衡的值,太小会频繁中断,太大可能溢出);
- 检查超时时间是否合适(16 个波特率周期是标准值,太短会误判 “数据结束”,太长会延迟);
- 避免在中断服务函数中做 “耗时操作”(比如 printf,本文用
XUartPs_SendByte
回发是高效的)。
- 中断不触发:
- 检查 GIC 是否初始化正确(
XScuGic_CfgInitialize
是否成功); - 检查中断是否 “允许”(
XUartPs_SetInterruptMask
是否包含两种中断,XScuGic_Enable
是否启用 UART1 中断); - 检查 CPU 中断是否启用(
Xil_ExceptionEnable
是否调用)。
- 检查 GIC 是否初始化正确(
六、总结(核心要点回顾)
- 不定长接收的核心逻辑:用 “RxFIFO 阈值中断” 处理批量数据,用 “超时中断” 判断数据结束,两者配合实现任意长度接收;
- 硬件基础:ZYNQ PS UART 有 64 字节 FIFO 和 GIC 中断控制器,这是高效接收的硬件保障;
- 软件关键步骤:UART 初始化(波特率、FIFO 阈值、超时时间)→ GIC 配置(中断优先级、绑定服务函数)→ 中断服务函数(判断中断类型、读 FIFO、处理数据);
- 验证重点:先收 “Hello World” 验证基本功能,再发不同长度数据验证稳定性(不丢包就是成功)。
掌握这个功能后,你就可以在实际项目中用 ZYNQ 串口接收任意长度的指令或数据了,下一步可以学习 “PS 和 PL 通过 AXI DMA 高速传数据”(博客下一节内容),继续进阶!
要理解 FIFO 及其在 UART 接收不定长数据中的作用,我们可以从 “通俗定义→核心特性→具体作用” 逐步拆解,结合 ZYNQ UART 的实际场景(64 字节 ×8bit FIFO),让初学者能快速抓住本质。
一、先搞懂:什么是 FIFO?
FIFO 的全称是 First-In-First-Out(先进先出),是一种 “数据存储缓冲器”,核心规则是 “先存进去的数据,必须先读出来”—— 就像日常生活中的快递柜或排队买奶茶:
- 第一个放进快递柜的包裹,要第一个取出来;
- 第一个排队的人,要第一个拿到奶茶;
FIFO 里的数据也遵循这个 “先来后到” 的规则,不能 “插队”。
FIFO 的 2 个核心特性(结合 ZYNQ UART 场景)
对于 ZYNQ PS 端的 UART 来说,其收发通路各有一个64 字节 ×8bit的 FIFO(接收端叫 RxFIFO,发送端叫 TxFIFO),这两个参数是关键:
- 宽度(8bit):FIFO 每次能存 / 取 “1 个字节”(因为 UART 是按字节传输数据的,8bit 正好对应 1 个字节);
- 深度(64 字节):FIFO 最多能同时存 “64 个字节” 的数据 —— 相当于快递柜有 64 个格子,最多放 64 个包裹。
此外,FIFO 还有两个重要的 “状态信号”,用于判断数据是否存满 / 取空:
- 空(Empty):FIFO 里没有数据了,再读就会 “读空”(拿不到有效数据);
- 满(Full):FIFO 里存满数据了(比如 64 字节),再存就会 “溢出”(新数据会覆盖旧数据,导致丢包)。
二、关键:FIFO 在 UART 接收不定长数据中的作用
UART 接收不定长数据的核心痛点是:外部数据 “断断续续、长度不固定”,但 CPU “不能一直盯着接收”(否则会浪费资源),且不能 “漏数据”。
而 FIFO 就像 “中间缓冲站”,完美解决了这个痛点,具体有 3 个核心作用,结合 ZYNQ 的 “阈值中断 + 超时中断” 逻辑来理解更清晰:
作用 1:缓冲数据,避免 “数据来得快、CPU 处理慢” 导致的丢包
UART 接收数据的过程是 “硬件自动接收”:外部设备(比如上位机)通过 RxD 引脚发送 bit 流,UART 接收器会自动把 bit 拼成 1 个字节,然后 “立刻” 需要存储 —— 如果没有 FIFO,这些字节会直接传给 CPU,但 CPU 可能在忙其他任务(比如处理传感器、运行其他程序),根本没时间 “实时接数据”,导致新数据覆盖旧数据,出现丢包。
有了 RxFIFO 之后,流程变成:
上位机发数据
UART接收器(硬件)
RxFIFO(先存起来)
CPU空闲时读数据
上位机发数据
UART接收器(硬件)
RxFIFO(先存起来)
CPU空闲时读数据
豆包
你的 AI 助手,助力每日工作学习
- 即使 CPU 忙,UART 硬件也能把接收到的字节先存到 RxFIFO(最多存 64 个),等 CPU 有空了再读 —— 相当于快递柜先存包裹,你不用 “实时等快递员”,有空再去取,不会丢包裹。
比如:上位机连续发 50 字节数据,CPU 此时在处理其他任务,这 50 字节会全部存到 RxFIFO 里(没满 64 字节),等 CPU 处理完其他任务,再从 RxFIFO 里把 50 字节一次性读走,完全不丢数据。
作用 2:配合 “RxFIFO 阈值中断”,减少 CPU 的 “无效忙碌”
如果没有 FIFO 阈值中断,CPU 只能通过 “轮询” 方式读数据 —— 每隔一段时间就去查 “RxFIFO 里有没有数据”,哪怕只有 1 个字节也要查,这会让 CPU 频繁 “做无用功”,浪费资源。
而 FIFO 的 “阈值设置”(比如 ZYNQ 中设为 32 字节)能解决这个问题:
- 阈值触发逻辑:当 RxFIFO 中的数据量≥阈值(比如 32 字节)时,硬件会触发 “RxFIFO 阈值中断”,主动通知 CPU“数据够多了,快来读”;
- CPU 不用再 “轮询”,平时可以忙其他任务,只有收到中断通知时才去读 RxFIFO—— 相当于快递柜满 32 个包裹时,主动给你发消息 “快来取”,你不用每隔 10 分钟去看一次。
这对 “接收大量不定长数据” 特别重要:比如上位机发 100 字节数据,前 32 字节存满 RxFIFO 时触发中断,CPU 读走 32 字节;中间 32 字节再存满时又触发中断,CPU 再读走 32 字节;最后 36 字节没到阈值,不会触发阈值中断 —— 既减少了 CPU 的中断次数(100 字节只触发 2 次阈值中断,而不是 100 次 1 字节中断),又保证了效率。
作用 3:配合 “超时中断”,保证 “短数据 / 收尾数据” 不遗漏
不定长数据的另一个痛点是:不知道 “一帧数据什么时候结束”—— 比如上位机发 3 字节指令(远没到 32 字节阈值),如果只靠阈值中断,RxFIFO 里一直只有 3 字节,永远不会触发中断,CPU 就永远不知道有这 3 字节数据,导致 “漏数据”。
这时候 FIFO 的 “数据停留时间” 就成了关键,配合 “超时中断” 解决问题:
- 超时中断逻辑:ZYNQ 中可以设置 “超时时间”(比如 16 个波特率周期),如果 RxFIFO 里有数据,但 “超过超时时间没有新数据进来”,就触发 “超时中断”,告诉 CPU“这帧数据结束了,剩下的少量数据也读走”;
- 相当于快递柜里有 3 个包裹,你等了 10 分钟(超时时间)也没新包裹进来,就判断 “这波快递送完了”,直接把 3 个包裹取走。
比如上位机发 3 字节指令:
- 3 字节存到 RxFIFO(没到 32 字节阈值,不触发阈值中断);
- 过了 16 个波特率周期,没有新数据进来,触发超时中断;
- CPU 收到中断,读走 RxFIFO 里的 3 字节,完成 “短不定长数据” 的接收,不会遗漏。
三、总结:FIFO 是 UART 不定长接收的 “核心支撑”
如果把 UART 接收不定长数据比作 “快递收发”:
- 上位机是 “快递员”(断断续续送包裹,每次送多少不一定);
- RxFIFO 是 “快递柜”(先存包裹,避免快递员等你;满 32 个提醒你取;久没新包裹也提醒你取);
- CPU 是 “你”(不用实时等快递员,按提醒取包裹,效率高且不丢包裹)。
一句话概括 FIFO 的作用:通过 “缓冲数据 + 阈值触发 + 超时判断”,协调 “UART 硬件接收速度” 和 “CPU 处理速度” 的差异,既避免丢数据,又减少 CPU 资源浪费,最终实现稳定的不定长数据接收。