当前位置: 首页 > news >正文

ZYNQ PS 端 UART 接收数据数据帧(初学者友好版)

参考资料:

作为电子工程师初学者,在接触 ZYNQ 这类嵌入式平台时,“串口接收不定长数据” 是非常实用但容易卡壳的知识点 —— 固定长度接收在实际项目中灵活性太差,而不定长接收需要理解中断、FIFO 等核心概念。本文基于 CSDN 博客与正点原子嵌入式培训班教学内容,用通俗语言拆解原理、修正代码细节、补充流程图 / 结构示意图,并清晰描述网页中的关键图片,帮你彻底搞懂这个功能。

要理解 UART 接收数据帧,首先需要明确两个核心概念:

  1. UART:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),是嵌入式系统中最常用的串行通信接口(无需时钟线,靠 “波特率” 同步);
  2. 数据帧: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:“有新数据了,快来读!”

三、初学者常见问题与注意事项

  1. 为什么波特率必须一致?
    波特率决定了 “每一位数据的传输时间”(如 9600bps 对应每 bit 约 104μs)。若收发端波特率偏差超过 5%,接收端的采样时钟会严重偏离数据位中间位置,导致采样错误(比如把 0 读成 1)。

  2. “不定长数据” 怎么接收?
    UART 帧本身是 “定长”(如 10 位:1 起始 + 8 数据 + 1 停止),但实际应用中常传输 “不定长数据”(如传感器一次传 3 字节,一次传 5 字节)。
    解决方法:在 “数据位” 中封装 “帧头 + 长度 + 数据 + 帧尾”(如帧头 0xAA,长度字节表示后续数据长度,帧尾 0x55),接收端先找帧头,再按长度接收数据,最后验证帧尾。

  3. 常见错误:帧错误、校验错误、溢出错误

    • 帧错误:未检测到停止位(原因:波特率不匹配、干扰);
    • 校验错误:校验位不匹配(原因:数据传输中被干扰);
    • 溢出错误:接收缓冲区 / FIFO 满了,但新数据还在来(原因:CPU 处理太慢,没及时读缓冲区)。

四、总结

UART 接收数据帧的本质是:接收端通过 “电平变化” 识别帧边界(起始位→停止位),通过 “波特率同步” 准确采样数据位,通过 “校验” 确保数据可靠,最终将有效数据交给 CPU 处理

对初学者来说,重点记住两点:

  1. 先掌握 帧结构的 5 个部分(尤其是起始位、数据位、停止位的作用);
  2. 理解 “采样时钟对准数据位中间” 是 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 字节的不定长数据 ——

  1. 前 32 字节进入 RxFIFO,触发 “阈值中断”,CPU 读走 32 字节;
  2. 中间 32 字节再进 RxFIFO,又触发 “阈值中断”,CPU 再读走 32 字节;
  3. 最后 36 字节进 RxFIFO(没到 32 阈值),但过了 16 个波特率周期没新数据,触发 “超时中断”,CPU 读走剩下的 36 字节,同时知道 “这帧数据收完了”。

三、详细实现步骤(硬件 + 软件,一步不落)

博客里的实现步骤是 “创建 Vivado 工程→导入 SDK 写代码→下载验证”,这里补充初学者容易忽略的细节,避免踩坑。

1. 第一步:创建 Vivado 硬件工程(核心是 “启用 UART1”)

博客提到 “参照 ZYNQ 进阶之路 5 的工程创建流程”,这里简化关键步骤,重点是 UART 相关配置:

  1. 新建 Vivado 工程,添加 “ZYNQ7 Processing System” IP 核(就是博客里提到的processing_system7_0);
  2. 双击 IP 核打开配置界面,在 “Peripheral I/O Pins” 里勾选 “UART1”(默认可能只开 UART0,UART1 需要手动启用);
  3. 确认 UART1 的引脚映射(用 MIO 还是 EMIO,初学者建议用 MIO,不用额外接 PL 引脚);
  4. 生成 Bitstream(比特流文件),然后导出 “Hardware Platform Specification”(硬件平台文件,给 SDK 用)。

博客里有一张 “工程创建后界面图”,内容就是 Vivado 的 IP Integrator 界面,中间显示processing_system7_0IP 核,旁边没有额外的 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. 第三步:下载验证(看实际效果)

  1. 连接硬件:用 USB 转串口线连接 ZYNQ 的 UART1 TxD/RxD 引脚和电脑;
  2. 配置串口助手:博客里用的是 “单片机多功能调试助手”,配置如下(必须和代码一致!):
    • 端口:电脑设备管理器中查到的 COM 口(比如 COM3);
    • 波特率:115200(代码中UART1_BAUD定义的是 115200);
    • 数据位:8;
    • 停止位:1;
    • 校验:NONE;
  3. 下载程序:在 SDK 中把程序下载到 ZYNQ 的 RAM 或 FLASH 中;
  4. 验证效果
    • 串口助手会收到 “Hello World”(说明串口基本正常);
    • 在上位机发送任意长度的数据(比如 10 字节、50 字节、130 字节),串口助手会 “回显” 收到的数据(代码中 “回发” 功能),且不会丢数据(博客测试 130 字节也稳定)。

一、代码整体框架

先看代码的 “骨架”,帮你建立整体认知:

  1. 头文件引入:包含 ZYNQ 硬件驱动、中断、UART 等必需的库文件;
  2. 宏定义:定义 UART 波特率、设备 ID 等固定参数(避免硬编码,方便修改);
  3. 结构体定义:集中管理 UART 的 “设备实例” 和 “接收状态”(比零散变量更易维护);
  4. 全局变量:定义接收缓存数组和 UART 状态结构体(中断服务函数需访问);
  5. 函数声明:提前声明主函数、中断服务函数、初始化函数(编译器需知道函数存在);
  6. 主函数:程序入口,仅做 “初始化” 和 “死循环等待中断”;
  7. 中断服务函数:UART 接收数据的核心逻辑(触发后自动执行,处理两种中断);
  8. 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. 中断服务函数的 “回调参数” 有什么用?

中断服务函数的参数CallBackRefvoid*类型(通用指针),作用是 “传递数据”:

  • 因为中断服务函数不能直接访问局部变量,而全局变量过多会导致代码混乱;
  • 所以我们把 UART 的 “设备实例” 和 “接收长度” 打包成结构体,通过CallBackRef传给中断服务函数,既清晰又易维护。

3. 为什么 “清除中断标志” 是必须的?

UART 的 “中断状态寄存器(ISR)” 中,中断标志位默认是 “1 表示中断发生”,且不会自动清零 —— 如果不手动清除(写 1 到对应位),CPU 会一直认为 “中断还在发生”,反复进入中断服务函数,导致程序卡死。

4. 不定长数据的 “收尾逻辑” 是什么?

靠 “超时中断” 实现:

  • 当 UART 接收数据时,若数据量≥32 字节(阈值),触发 “阈值中断”,CPU 读走批量数据;
  • 若数据量 <32 字节(比如最后 3 字节),不会触发阈值中断,但超过 “16 个波特率周期” 没新数据,会触发 “超时中断”;
  • 超时中断中,CPU 读走剩下的少量数据,然后回发数据、重置接收长度,完成 “一帧不定长数据” 的处理。

四、调试建议(初学者避坑)

  1. 编译报错 “设备 ID 未定义”
    检查UART1_DEVICE_IDUART1_INT_ID是否正确 —— 这些值来自xparameters_ps.h,可以在 SDK 中打开该文件,搜索 “XUARTPS_1” 找到正确的 ID(不同 ZYNQ 型号可能不同)。
  2. 串口收不到 “Hello World”
    • 先查硬件接线(UART1 的 TxD 接 USB 转串口的 RxD,RxD 接 TxD,别接反);
    • 再查串口助手配置(波特率 115200、数据位 8、停止位 1、无校验,必须和代码一致);
  3. 中断不触发
    检查是否漏了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)、“清发送区”“清接收区” 等按钮。

五、初学者常见问题与注意事项(避坑指南)

  1. 代码编译报错 “变量未定义”
    • 检查UART1_DEVICE_IDUART1_INT_ID是否正确 —— 这些宏定义来自xparameters_ps.h,可以在 SDK 中打开这个文件,搜索 “XUARTPS_1” 找到正确的 ID(不同 ZYNQ 型号可能不同)。
  2. 串口收不到 “Hello World”
    • 先检查硬件接线(TxD 接 RxD,RxD 接 TxD,别接反);
    • 再检查串口助手配置(波特率、端口是否正确);
    • 最后检查 UART 实例是否初始化正确(XUartPs_CfgInitialize的返回值是否为XST_SUCCESS)。
  3. 接收数据丢包
    • 检查 FIFO 阈值是否设置合理(32 字节是比较均衡的值,太小会频繁中断,太大可能溢出);
    • 检查超时时间是否合适(16 个波特率周期是标准值,太短会误判 “数据结束”,太长会延迟);
    • 避免在中断服务函数中做 “耗时操作”(比如 printf,本文用XUartPs_SendByte回发是高效的)。
  4. 中断不触发
    • 检查 GIC 是否初始化正确(XScuGic_CfgInitialize是否成功);
    • 检查中断是否 “允许”(XUartPs_SetInterruptMask是否包含两种中断,XScuGic_Enable是否启用 UART1 中断);
    • 检查 CPU 中断是否启用(Xil_ExceptionEnable是否调用)。

六、总结(核心要点回顾)

  1. 不定长接收的核心逻辑:用 “RxFIFO 阈值中断” 处理批量数据,用 “超时中断” 判断数据结束,两者配合实现任意长度接收;
  2. 硬件基础:ZYNQ PS UART 有 64 字节 FIFO 和 GIC 中断控制器,这是高效接收的硬件保障;
  3. 软件关键步骤:UART 初始化(波特率、FIFO 阈值、超时时间)→ GIC 配置(中断优先级、绑定服务函数)→ 中断服务函数(判断中断类型、读 FIFO、处理数据);
  4. 验证重点:先收 “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),这两个参数是关键:

  1. 宽度(8bit):FIFO 每次能存 / 取 “1 个字节”(因为 UART 是按字节传输数据的,8bit 正好对应 1 个字节);
  2. 深度(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 字节指令:

  1. 3 字节存到 RxFIFO(没到 32 字节阈值,不触发阈值中断);
  2. 过了 16 个波特率周期,没有新数据进来,触发超时中断;
  3. CPU 收到中断,读走 RxFIFO 里的 3 字节,完成 “短不定长数据” 的接收,不会遗漏。

三、总结:FIFO 是 UART 不定长接收的 “核心支撑”

如果把 UART 接收不定长数据比作 “快递收发”:

  • 上位机是 “快递员”(断断续续送包裹,每次送多少不一定);
  • RxFIFO 是 “快递柜”(先存包裹,避免快递员等你;满 32 个提醒你取;久没新包裹也提醒你取);
  • CPU 是 “你”(不用实时等快递员,按提醒取包裹,效率高且不丢包裹)。

一句话概括 FIFO 的作用:通过 “缓冲数据 + 阈值触发 + 超时判断”,协调 “UART 硬件接收速度” 和 “CPU 处理速度” 的差异,既避免丢数据,又减少 CPU 资源浪费,最终实现稳定的不定长数据接收


文章转载自:

http://fyjuCA88.mrckk.cn
http://XBgx6kHb.mrckk.cn
http://1HdlcfLM.mrckk.cn
http://obs19Xw5.mrckk.cn
http://1upOAp91.mrckk.cn
http://zxxAXIne.mrckk.cn
http://ALQomqv9.mrckk.cn
http://EDcotfas.mrckk.cn
http://kwZZtZkJ.mrckk.cn
http://NPQMbFge.mrckk.cn
http://PFFj747p.mrckk.cn
http://D6OeYZfe.mrckk.cn
http://jwQB2jAS.mrckk.cn
http://5YXzEuX0.mrckk.cn
http://53VKnlQe.mrckk.cn
http://WzPevhoy.mrckk.cn
http://yaLjPYMT.mrckk.cn
http://jnpagShY.mrckk.cn
http://2q9OLcZ8.mrckk.cn
http://OOsnTm9f.mrckk.cn
http://q7Q2k7AH.mrckk.cn
http://R795PjnP.mrckk.cn
http://wMLqOXOQ.mrckk.cn
http://Djbp8O3G.mrckk.cn
http://VJytdEf3.mrckk.cn
http://5GUiPqph.mrckk.cn
http://m3Rolzu9.mrckk.cn
http://j4KgTaRo.mrckk.cn
http://FGBQtxpF.mrckk.cn
http://JDoR8GRM.mrckk.cn
http://www.dtcms.com/a/375745.html

相关文章:

  • 【ARM-day03】
  • TI-92 Plus计算器:单位换算功能介绍
  • TDengine 选择函数 Max() 用户手册
  • 总结 IO、存储、硬盘、文件系统相关常识
  • MATLAB基于GM(灰色模型)与LSTM(长短期记忆网络)的组合预测方法
  • cnn,vit,mamba是如何解决医疗影像问题的
  • 数据库连接池:性能优化的秘密武器
  • 鸿蒙(HarmonyOS) 历史
  • 华为Ai岗机考20250903完整真题
  • 机器人控制器开发(文章总览)
  • 怎么选适合企业的RPA财务机器人?
  • Vite:Next-Gen Frontend Tooling 的高效之道——从原理到实践的性能革命
  • 常用优化器及其区别
  • 【Ansible】管理变量和事实知识点
  • 2025-09-08升级问题记录:app提示“此应用专为旧版Android打造..”或“此应用与最新版 Android 不兼容”
  • 网络通信的“地址”与“门牌”:详解IP地址与端口号的关系
  • 基于Python的旅游数据分析可视化系统【2026最新】
  • Nginx 优化与防盗链全解析:从性能调优到资源保护
  • 【AI】Tensorflow在jupyterlab中运行要注意的问题
  • (论文速读)从语言模型到通用智能体
  • 3-9〔OSCP ◈ 研记〕❘ WEB应用攻击▸利用REST API提权
  • Kafka面试精讲 Day 15:跨数据中心复制与灾备
  • 数据库之间如何同步
  • YOLO学习笔记
  • 3.Python高级数据结构与文本处理
  • LeetCode热题 42.接雨水
  • diffusion model(0.2) DDPM
  • 广州物业管理宣传片拍摄:以专业服务传递城市温度
  • 4、Python面向对象编程与模块化设计
  • 服务注册发现高可用设计:从三次典型雪崩事故到故障免疫体系