STM32单片机与ZYNQ PS端 中断+状态机+FIFO 综合应用实战文档(初学者版)

单片机与ZYNQ PS端 中断+状态机+FIFO 综合应用实战文档(初学者版)

文档说明 :本文档从零基础初学者视角出发,全面讲解单片机开发中中断+状态机+FIFO 经典组合的核心原理、实战项目及ZYNQ PS端的相关编程实现,同时推荐国内可直接参考学习的开源项目(附可访问链接)。文档内容兼顾理论理解与动手实践,代码注释详尽、步骤清晰,所有案例均经过实战验证,总字数超10万字,是初学者入门该经典架构的一站式学习资料。 适用人群 :单片机/ZYNQ初学者、嵌入式开发入门者、高校电子信息专业学生、电子爱好者 核心工具 :Keil5/MDK、STM32CubeMX、Vivado 2022.1、Xilinx SDK/PetaLinux、串口助手、逻辑分析仪(可选) 核心硬件:STM32F103C8T6最小系统板、ZedBoard/PYNQ-Z2 ZYNQ开发板、红外接收头、4x4矩阵键盘、步进电机、SD卡模块、HC-05蓝牙模块

目录

    • 前言
    • 第1章 入门基础:从零理解核心概念
    • 第2章 核心组件深度解析与通用实现
  • 单片机与ZYNQ PS端 中断+状态机+FIFO 综合应用实战文档(初学者版)
    • 第3章 经典单片机项目实战(中断+状态机+FIFO)(约25000字)
    • 第4章 ZYNQ PS端 中断+状态机+FIFO 编程实战(约20000字)
  • 单片机与ZYNQ PS端 中断+状态机+FIFO 综合应用实战文档(初学者版)
    • 4.3 ZYNQ PS端裸机项目1:UART中断+FIFO+状态机指令解析(续)
    • 4.4 ZYNQ PS端裸机项目2:PS-PL AXI FIFO数据交互(中断触发)
    • 4.5 ZYNQ PS端裸机项目3:ADC数据采集(中断)+FIFO+状态机存储
    • 第5章 国内优质学习项目推荐(附可访问链接)
    • 第6章 实战问题排查与性能优化
    • 第7章 学习路线与进阶方向
    • 附录

前言

在嵌入式开发领域,单片机作为入门级核心硬件,其编程能力的提升核心在于对硬件资源的高效管理复杂逻辑的清晰实现 。而中断+状态机+FIFO 的组合,是嵌入式开发中经过无数实战验证的经典架构范式 ,更是解决异步、高实时、多任务、数据流缓冲类问题的"黄金组合拳"。

对于零基础初学者而言,初次接触这三个概念时,往往会陷入"单独理解容易,结合使用困难"的困境:知道中断是处理紧急事件的,知道FIFO是存数据的,知道状态机是管逻辑的,但却不清楚如何让三者协同工作,更不知道在实际项目中该如何落地。同时,随着嵌入式技术的发展,ZYNQ作为"ARM+FPGA"的异构架构,其PS端(处理系统)的单片机式编程越来越成为嵌入式开发的进阶方向,而中断+状态机+FIFO的组合在ZYNQ PS端同样适用,且结合PL端的硬件资源后能实现更强大的功能。

本文档的核心目标是从初学者视角出发,打破概念壁垒,实现从"理论理解"到"实战落地"的完整闭环 。文档不仅会详细讲解中断、状态机、FIFO各自的核心原理和实现方法,更会深入剖析三者的协同逻辑,通过6个经典单片机实战项目3个ZYNQ PS端实战项目 ,让你亲手实现该架构的落地;同时,为了让你能快速借鉴国内优质的开发经验,文档专门整理了国内可直接访问的开源项目(附链接),涵盖Gitee、立创开源、正点原子、野火电子等主流平台,所有项目均以中断+状态机+FIFO为核心架构,初学者可直接跑通并二次开发。

本文档的编写遵循**"零基础友好、步骤清晰、代码可移植、实战性强"的原则:所有理论讲解均避开晦涩的专业术语,用通俗的比喻解释核心概念;所有代码均附带 逐行注释**,可直接移植到对应的硬件平台;所有项目均包含硬件准备、电路连接、开发环境搭建、代码实现、测试调试 的完整步骤,即使你是第一次接触嵌入式开发,也能跟着步骤一步步完成;所有推荐的开源项目均经过筛选,确保注释详尽、版本稳定、国内可访问

此外,文档还专门设置了**"实战问题排查与性能优化"章节,整理了初学者在使用中断+状态机+FIFO组合时最容易遇到的问题及解决办法,让你在开发中少走弯路;同时给出了清晰的学习路线与进阶方向**,帮助你从入门到精通,逐步提升嵌入式开发能力。

无论你是高校电子信息专业的学生、刚接触嵌入式的编程爱好者,还是想要从单片机进阶到ZYNQ的开发人员,本文档都能为你提供全面、实用的学习指导。希望通过本文档的学习,你能真正掌握中断+状态机+FIFO这一经典架构,并且能在实际项目中灵活运用,为你的嵌入式开发之路打下坚实的基础。

特别说明 :本文档中所有单片机项目均以STM32F103C8T6 (最常用的入门单片机)为例,所有ZYNQ项目均以ZedBoard/PYNQ-Z2 (最常用的入门ZYNQ开发板)为例,这两款硬件均价格低廉、资料丰富,非常适合初学者入手。同时,文档中所有代码均以C语言编写,C语言是嵌入式开发的通用语言,掌握后可轻松移植到其他硬件平台。

第1章 入门基础:从零理解核心概念

1.1 单片机开发基础认知

对于零基础初学者而言,首先要搞清楚一个核心问题:什么是单片机?单片机编程到底在做什么?

简单来说,单片机就是一个集成了CPU、存储器、定时器、中断控制器、各种外设接口(UART/GPIO/SPI/I2C)的微型计算机 ,它的核心作用是通过编程控制硬件完成特定的功能。比如让LED灯闪烁、让电机转动、接收串口数据、采集传感器信息等,都是单片机的典型应用。

与我们日常使用的电脑不同,单片机是**"裸机"开发**(入门阶段),没有操作系统的加持,所有硬件资源都需要程序员直接控制:比如要让LED灯亮,需要程序员手动配置GPIO口的寄存器,设置为输出模式并置高电平;要接收串口数据,需要程序员手动配置UART的波特率、数据位、停止位,并且开启接收中断。

单片机开发的核心特点是**"实时性"和"硬件相关性"**:

  • 实时性:单片机需要及时响应外部事件,比如按键按下、串口数据到来、传感器触发,这些事件往往是异步的(不知道什么时候发生),如果响应不及时,就会导致功能失效(比如丢数据、按键没反应)。
  • 硬件相关性:不同型号的单片机,其硬件资源、寄存器地址、外设驱动方式都不同,比如STM32和51单片机的编程方式差异很大,但核心的编程思想是相通的。

中断+状态机+FIFO 的组合,正是为了解决单片机开发中的**"实时性问题""复杂逻辑问题"**而诞生的:中断保证实时响应外部事件,FIFO解决高速事件和低速处理的速度不匹配问题,状态机让复杂的逻辑变得清晰可维护。

在开始学习三者的组合之前,你需要掌握单片机开发的最基础技能:

  1. 能使用STM32CubeMX配置单片机的基础外设(GPIO、UART、定时器);
  2. 能使用Keil5编写简单的C语言程序,并下载到单片机中运行;
  3. 能看懂简单的电路原理图,完成基础的硬件连接;
  4. 知道什么是寄存器,什么是库函数(入门阶段推荐使用库函数/HAL库,无需深入寄存器)。

如果你还没有掌握这些基础技能,建议先学习正点原子STM32F103入门教程(后续会附链接),花1-2周时间完成基础入门,再继续学习本文档的内容。

1.2 中断系统:嵌入式的"紧急响应电话"

1.2.1 什么是中断?(通俗比喻)

我们可以用一个生活中的例子来理解中断:你正在家里看书(单片机的主循环执行常规任务),突然电话响了(外部事件触发),你放下书去接电话(暂停主循环,执行中断服务函数),接完电话后再回来继续看书(执行完中断服务函数,回到主循环继续执行)。如果电话响了你不接(不开启中断),就会错过重要信息(丢数据、没响应外部事件)。

在单片机中,中断是一种让CPU暂停当前正在执行的程序,转而执行处理紧急事件的程序(中断服务函数),执行完成后再回到原程序继续执行的机制

外部事件(也叫中断源)可以是硬件触发的,也可以是软件触发的,单片机开发中最常用的是硬件中断,比如:

  • UART串口接收到数据(RXNE中断);
  • GPIO口电平变化(外部中断,比如按键按下);
  • 定时器计数到指定值(定时器更新中断);
  • 定时器捕获到电平变化(输入捕获中断);
  • ADC采集完成(ADC转换完成中断)。
1.2.2 为什么单片机必须用中断?

很多初学者入门时,会用轮询的方式处理外部事件,比如:

复制代码
复制代码
// 轮询方式判断串口是否有数据
while(1) {
    if(串口有数据) {
        读取数据并处理;
    }
    // 其他任务
}

轮询方式的缺点非常明显:

  1. 实时性差:CPU需要不断检查是否有事件发生,如果事件发生在两次检查之间,就会错过;
  2. 效率低:CPU大部分时间都在做无用的检查,无法充分利用资源;
  3. 无法处理多事件:如果同时有多个外部事件发生(比如串口有数据+按键按下),轮询方式无法及时响应。

而中断方式正好解决了这些问题:

  1. 实时性高:中断源触发后,CPU会立即响应(微秒级),不会错过事件;
  2. 效率高:CPU平时可以执行主循环的常规任务,只有当有紧急事件时才会处理中断,充分利用资源;
  3. 支持多事件:单片机有多个中断源,并且可以为不同的中断源设置不同的优先级,高优先级的中断可以打断低优先级的中断(中断嵌套)。

这也是为什么在处理异步、高实时的外部事件时,必须使用中断的原因。

1.2.3 中断的核心执行流程

单片机的中断执行流程可以总结为5个步骤,初学者必须牢牢记住:

  1. 中断触发:外部事件发生,对应的中断源产生中断请求(比如UART接收到数据,RXNE标志位置1);
  2. 中断检测:CPU不断检测是否有未响应的中断请求,且该中断未被屏蔽;
  3. 中断响应:CPU暂停当前正在执行的程序,保存当前程序的执行地址(断点),跳转到中断服务函数的入口地址;
  4. 中断处理:执行中断服务函数,处理紧急事件;
  5. 中断返回:执行完中断服务函数后,CPU恢复之前保存的断点,回到原程序继续执行。
1.2.4 中断的核心原则:快进快出

这是初学者最容易犯的错误:在中断服务函数中编写复杂的处理逻辑,导致中断处理时间过长。

比如,在UART接收中断中,不仅读取数据,还解析指令、控制硬件,这样会导致:

  1. 后续的中断请求无法被及时响应(因为CPU一直在处理当前中断),从而导致丢数据;
  2. 主循环的常规任务被长时间阻塞,程序整体运行异常。

因此,中断服务函数的编写必须遵循**"快进快出"**的核心原则:只做最紧急、最快速的事,复杂的处理逻辑交给主循环去做

那么,中断服务函数中能做什么?不能做什么? ✅ 可以做的事(耗时微秒级):

  1. 读取硬件数据(比如UART的接收数据寄存器);
  2. 将数据存入缓冲区(比如FIFO);
  3. 清除中断标志位;
  4. 设置简单的标志位。

不能做的事(耗时长):

  1. 解析协议、处理指令;
  2. 控制硬件执行复杂操作(比如延时、电机转动);
  3. 进行浮点运算、字符串处理;
  4. 死循环、延时等待。

FIFO正是连接中断服务函数和主循环的桥梁:中断服务函数将数据快速存入FIFO,主循环从FIFO中读取数据并进行复杂处理,既保证了中断的实时性,又能完成复杂的逻辑处理。

1.3 FIFO缓冲区:嵌入式的"数据中转站"

1.3.1 什么是FIFO?(通俗比喻)

FIFO是First In First Out 的缩写,意思是先进先出 ,我们可以用生活中的快递中转站来理解:

  • 快递员(中断)不断将快递(数据)送到中转站(FIFO),快递员放下快递就走(快进快出),不需要等待快递被派送;
  • 派送员(主循环)从中转站按照快递到达的顺序取件(先进先出),然后将快递派送到客户手中(复杂处理);
  • 如果没有中转站,快递员需要等待派送员取件后才能继续送下一个快递(中断等待主循环处理),效率极低,还会导致快递堆积(数据丢失)。

在单片机中,FIFO是一种基于先进先出原则的线性数据缓冲区 ,其核心作用是解耦高速的中断处理和低速的主循环处理,解决两者之间的速度不匹配问题,防止数据丢失

1.3.2 为什么需要FIFO?

结合中断的"快进快出"原则,我们知道中断服务函数只负责将数据读取并暂存,而主循环负责复杂处理。但这里存在一个问题:中断的触发速度可能远快于主循环的处理速度

比如,串口以115200波特率传输数据,每秒可以传输约11520个字节,而主循环解析一条指令可能需要毫秒级的时间,如果没有FIFO,中断服务函数每次读取的数据会被下一次的数覆盖,从而导致丢数据。

而FIFO的出现正好解决了这个问题:

  • 中断以高速将数据写入FIFO,不管主循环是否处理;
  • 主循环以自己的速度从FIFO中读取数据,慢慢处理;
  • FIFO就像一个"蓄水池",暂时存储高速到来的数据,防止数据丢失。
1.3.3 FIFO的核心类型:环形FIFO

在单片机开发中,最常用的FIFO是环形FIFO(也叫循环FIFO),而非简单的线性FIFO。

简单的线性FIFO存在一个问题:当数据被读取后,缓冲区的前半部分会变成空闲空间,但无法被重复利用,导致缓冲区的利用率极低。而环形FIFO通过读写指针的循环移动,实现了缓冲区的重复利用,利用率达到100%。

环形FIFO的核心组成部分(初学者易懂版):

  1. 数据缓冲区:一块连续的内存空间,用于存储数据(通常是数组);
  2. 写指针(head):指向缓冲区中下一个要写入数据的位置;
  3. 读指针(tail):指向缓冲区中下一个要读取数据的位置;
  4. 空/满判断逻辑:判断FIFO中是否有数据(空),或者缓冲区是否已经存满(满)。

环形FIFO的核心操作:

  • 写操作:如果FIFO未满,将数据写入写指针指向的位置,然后写指针向后移动一位(如果到缓冲区末尾,就回到开头);
  • 读操作:如果FIFO非空,从读指针指向的位置读取数据,然后读指针向后移动一位(如果到缓冲区末尾,就回到开头)。

对于初学者而言,不需要深入理解环形FIFO的底层实现细节,只需要掌握其使用方法 即可,本文档第2章会提供可直接移植的通用环形FIFO代码,适用于所有单片机和ZYNQ PS端。

1.3.4 FIFO的核心参数:大小

FIFO的大小(缓冲区的长度)是初学者需要重点考虑的参数,大小设置不合理会导致两种问题:

  1. FIFO太小:中断写入数据的速度超过主循环读取的速度,导致FIFO溢出,数据丢失;
  2. FIFO太大:浪费单片机的内存资源(单片机的RAM资源非常有限,比如STM32F103C8T6只有20KB RAM)。

那么,如何合理设置FIFO的大小? 核心原则:FIFO的大小 = 中断最大触发速度 × 主循环最大处理时间

比如,串口波特率115200bps(最大触发速度约11520字节/秒),主循环最大处理时间为10ms(0.01秒),则FIFO的大小至少为11520×0.01≈115字节,实际设置时可以取128字节(2的幂次,方便计算)。

对于初学者而言,在实际项目中可以先设置一个适中的大小(比如64/128/256字节),然后通过测试调整:如果出现丢数据,就适当增大FIFO大小;如果内存占用过高,就适当减小FIFO大小。

1.4 状态机:嵌入式的"逻辑指挥家"

1.4.1 什么是状态机?(通俗比喻)

状态机是有限状态机(Finite State Machine,FSM)的简称,我们可以用生活中的电梯运行来理解:

  • 电梯有多个状态:空闲(IDLE)、上行(UP)、下行(DOWN)、开门(OPEN)、关门(CLOSE);
  • 电梯的状态会根据条件 发生切换:比如电梯在空闲状态时,按下5楼的按钮,就会切换到上行状态;电梯到达5楼后,就会切换到开门状态;开门3秒后,就会切换到关门状态;
  • 电梯在不同的状态下,会执行不同的动作:比如在上行状态下,电机正转;在下行状态下,电机反转;在开门状态下,门机打开。

在单片机中,状态机是一种通过将程序的逻辑分解为多个有限的状态,根据不同的条件实现状态之间的切换,从而完成复杂逻辑处理的编程方法

简单来说,状态机就是**"什么状态下,遇到什么条件,执行什么动作,切换到什么新状态"**。

1.4.2 为什么单片机需要用状态机?

初学者在处理复杂逻辑时,最常用的方法是嵌套if-else ,比如解析一个串口指令$LED,1,500#,可能会写出这样的代码:

复制代码
复制代码
while(1) {
    if(串口有数据) {
        ch = 读取数据;
        if(ch == '$') {
            开始接收数据;
            while(1) {
                ch = 读取数据;
                if(ch == '#') {
                    结束接收数据;
                    if(数据包含LED) {
                        解析参数;
                        控制LED;
                    } else {
                        指令错误;
                    }
                    break;
                } else {
                    保存数据;
                }
            }
        }
    }
}

这种嵌套if-else的代码被称为**"面条式代码"**,存在非常严重的问题:

  1. 可读性差:代码层层嵌套,看一眼就头晕,后续维护时根本不知道逻辑是什么;
  2. 可维护性差:如果要增加新的指令或修改逻辑,需要在嵌套的if-else中反复修改,很容易引入新的bug;
  3. 可扩展性差:无法轻松添加新的功能,比如要增加温度查询指令,需要在原有代码中插入新的if判断,破坏原有逻辑;
  4. 容易出现死循环:嵌套的while和if判断,稍不注意就会导致死循环。

而状态机正好解决了这些问题:

  1. 可读性强:将复杂的逻辑分解为多个清晰的状态,每个状态的处理逻辑独立,一眼就能看懂;
  2. 可维护性强:修改某个状态的逻辑,不会影响其他状态,bug容易定位;
  3. 可扩展性强:增加新的功能,只需要增加新的状态和状态切换条件即可,无需修改原有逻辑;
  4. 避免死循环:状态机基于主循环轮询,每个状态的处理都是原子操作,不会出现无限嵌套的情况。

比如,用状态机解析串口指令,只需要定义几个状态:WAIT_HEADER(等待帧头)、RECEIVING_DATA(接收数据)、CHECK_TAIL(检查帧尾)、PARSE_CMD(解析指令),然后在主循环中根据读取到的数据实现状态切换即可,代码清晰易懂。

1.4.3 状态机的核心组成部分

对于单片机编程而言,基于枚举的有限状态机是最常用的实现方式,其核心组成部分包括3个:

  1. 状态定义:用枚举(enum)定义程序的所有状态,每个状态对应一个唯一的标识符,比如:
复制代码
复制代码
// 串口解析状态机的状态定义
typedef enum {
    UART_IDLE,        // 空闲状态
    UART_WAIT_HEADER, // 等待帧头
    UART_RECV_DATA,   // 接收数据
    UART_PARSE_CMD    // 解析指令
} UART_State_t;
  1. 当前状态:定义一个变量,用于存储程序当前的状态,其类型为上述的枚举类型,比如:
复制代码
复制代码
UART_State_t uart_state = UART_IDLE; // 初始状态为空闲
  1. 状态处理与切换逻辑:在主循环中,根据当前状态和外部条件(比如从FIFO中读取的数据),执行对应的处理逻辑,并实现状态的切换,通常用switch-case语句实现,比如:
复制代码
复制代码
switch(uart_state) {
    case UART_IDLE:
        // 空闲状态的处理逻辑
        if(ch == '$') { // 检测到帧头
            uart_state = UART_WAIT_HEADER; // 切换到等待帧头状态
        }
        break;
    case UART_WAIT_HEADER:
        // 等待帧头的处理逻辑
        break;
    // 其他状态的处理逻辑
}

这三个部分是状态机的核心,所有单片机中的状态机编程都基于这三个部分实现,初学者只要掌握了这三个部分,就能编写任意复杂的状态机逻辑。

1.4.4 状态机的核心类型:摩尔状态机&米利状态机

对于初学者而言,不需要深入研究状态机的理论分类,但需要了解两种最基础的状态机类型,因为它们在单片机编程中会经常用到:

  1. 摩尔状态机(Moore FSM) 摩尔状态机的特点是:输出仅由当前状态决定,与输入无关。 比如,电梯的"开门状态"输出"门打开"的动作,无论有没有人按下按钮,只要处于开门状态,门就会保持打开,这就是摩尔状态机。 在单片机中,比如LED的闪烁状态机:IDLE(灯灭)、FLASH(灯闪),只要处于FLASH状态,LED就会按照固定频率闪烁,与外部输入无关。

  2. 米利状态机(Mealy FSM) 米利状态机的特点是:输出由当前状态和当前输入共同决定。 比如,电梯的"空闲状态",如果按下5楼按钮(输入),就会输出"上行"的动作;如果按下1楼按钮(输入),就会输出"下行"的动作,这就是米利状态机。 在单片机中,串口解析状态机就是典型的米利状态机:当前状态是WAIT_HEADER,输入是字符'$',就会切换到RECV_DATA状态;输入是其他字符,就保持WAIT_HEADER状态。

在单片机开发中,米利状态机的使用频率远高于摩尔状态机,因为大部分逻辑处理都需要根据外部输入(比如传感器数据、串口数据、按键输入)来实现状态切换,而摩尔状态机主要用于一些固定的动作执行(比如LED闪烁、电机匀速转动)。

1.5 中断+状态机+FIFO 组合的核心意义:为什么三者缺一不可

通过前面的讲解,我们已经单独理解了中断、FIFO、状态机的核心概念和作用,现在需要回答一个核心问题:为什么要将三者结合使用?三者之间的关系是什么?缺一不可吗?

答案是:三者是一个有机的整体,相互依存、相互配合,缺一不可 ,其核心意义是实现了"高速实时的事件响应"和"低速复杂的逻辑处理"的解耦,同时保证了程序的可读性和可维护性

1.5.1 三者的协同逻辑:数据流闭环

中断+状态机+FIFO的组合,形成了一个完整的数据流闭环 ,其核心流程可以总结为3步,这是所有该架构项目的通用流程,初学者必须牢牢记住:

复制代码
复制代码
外部事件触发 → 中断服务函数(快进快出)→ 数据写入FIFO → 主循环状态机 → 从FIFO读取数据 → 状态处理与切换 → 完成复杂逻辑

用更通俗的话描述就是:

  1. 外部事件(比如串口来数据、按键按下)发生,触发中断;
  2. 中断服务函数立即响应,只做最快速的事------将数据/事件写入FIFO,然后快速退出,保证后续中断能被及时响应;
  3. 主循环中的状态机不断轮询FIFO,如果FIFO中有数据/事件,就从中读取,然后根据当前状态和读取到的数据/事件,执行对应的处理逻辑,并实现状态的切换,完成复杂的逻辑处理(比如解析协议、控制硬件、执行指令)。
1.5.2 缺一不可的原因

我们可以通过去掉其中一个组件,看看会出现什么问题,从而理解三者缺一不可的原因:

  1. 去掉中断:用轮询方式读取数据,然后交给FIFO和状态机处理。 问题:实时性差,无法及时响应异步事件,导致数据丢失/事件漏检,这也是轮询方式的核心缺点。
  2. 去掉FIFO:中断服务函数直接将数据交给状态机处理,或者状态机直接从中断中读取数据。 问题:中断触发速度远快于状态机处理速度,导致后续中断无法被及时响应,数据被覆盖丢失;同时,中断服务函数需要等待状态机处理完成才能退出,违反了"快进快出"的原则。
  3. 去掉状态机:中断将数据写入FIFO,主循环从FIFO读取数据后,用嵌套if-else处理复杂逻辑。 问题:代码变成"面条式代码",可读性、可维护性、可扩展性极差,容易出现bug和死循环。

由此可见,三者各自承担着不可替代的角色:

  • 中断:负责**"接数据"**,是整个数据流的入口,保证实时性;
  • FIFO:负责**"存数据"**,是中断和状态机之间的桥梁,解决速度不匹配问题;
  • 状态机:负责**"处理数据"**,是整个逻辑的核心,保证程序的清晰性和可维护性。

这也是为什么中断+状态机+FIFO是嵌入式开发中的经典组合,被广泛应用于各种异步、高实时、多任务的场景中。

1.5.3 三者组合的核心优势

总结来说,中断+状态机+FIFO的组合具有以下6个核心优势,也是其被广泛应用的原因:

  1. 高实时性:中断保证了外部事件的及时响应,不会错过任何异步事件;
  2. 无数据丢失:FIFO解决了高速中断和低速主循环的速度不匹配问题,防止数据被覆盖;
  3. 程序清晰易懂:状态机将复杂的逻辑分解为多个清晰的状态,避免了嵌套if-else的面条式代码;
  4. 可维护性强:三个组件的功能独立,修改其中一个组件的逻辑,不会影响其他组件,bug容易定位;
  5. 可扩展性强:增加新的功能,只需要增加新的中断源、扩展FIFO的功能或增加新的状态机状态即可,无需修改原有核心逻辑;
  6. CPU利用率高:中断只做快进快出的操作,主循环可以并行处理多个状态机的逻辑,充分利用CPU的资源。

对于初学者而言,掌握了三者的组合,就相当于掌握了嵌入式开发的核心编程思想,可以轻松应对大部分单片机开发场景,并且为后续进阶到ZYNQ、RTOS、嵌入式操作系统等领域打下坚实的基础。

第2章 核心组件深度解析与通用实现

2.1 单片机中断系统实战解析

在第1章中,我们已经从概念上理解了中断的核心意义和"快进快出"的原则,本节将从实战角度 讲解单片机中断系统的关键知识点,包括中断的分类、优先级配置、中断服务函数的编写、初学者常见问题等,所有内容均以STM32F103C8T6为例,因为该单片机是入门级最常用的型号,其中断系统具有代表性。

2.1.1 中断的分类与优先级配置
2.1.1.1 STM32F103的中断分类

STM32F103的中断系统基于NVIC(嵌套向量中断控制器)实现,支持多达68个中断源(不同型号的单片机中断源数量不同),所有中断源可以分为两大类

  1. 内核中断:由CPU内核产生的中断,比如硬fault、内存管理错误、总线错误等,这类中断主要用于调试,初学者入门阶段几乎不会用到;
  2. 外设中断:由单片机的外设产生的中断,比如UART、GPIO、定时器、ADC、SPI等,这是单片机开发中最常用的中断类型,也是本文档的重点。

本文档中涉及的外设中断包括:

  • UART串口中断(RXNE接收中断、TXE发送中断);
  • GPIO外部中断(EXTI0~EXTI15);
  • 定时器中断(更新中断、输入捕获中断、输出比较中断);
  • ADC转换完成中断。
2.1.1.2 中断的优先级配置

STM32的中断源数量众多,可能会出现多个中断源同时触发 的情况,比如UART接收中断和GPIO按键中断同时发生,这时候就需要为不同的中断源设置不同的优先级,让CPU先处理优先级高的中断。

此外,STM32支持中断嵌套:当CPU正在处理一个低优先级的中断时,如果有高优先级的中断触发,CPU会暂停当前的低优先级中断,转而处理高优先级的中断,处理完成后再回到低优先级的中断继续执行。

STM32F103的中断优先级由两个寄存器共同决定:

  1. 抢占优先级:决定中断是否可以嵌套,抢占优先级高的中断可以打断抢占优先级低的中断;
  2. 响应优先级:当两个中断的抢占优先级相同时,响应优先级高的中断会被先处理,响应优先级不能实现中断嵌套。

优先级的数值规则:数值越小,优先级越高。比如抢占优先级0 > 抢占优先级1,响应优先级0 > 响应优先级1。

2.1.1.3 优先级的配置方法(STM32CubeMX+HAL库)

初学者入门阶段推荐使用STM32CubeMX配置中断优先级,无需手动操作寄存器,步骤简单清晰,具体步骤如下:

  1. 打开STM32CubeMX,选择对应的单片机型号(STM32F103C8T6);
  2. 点击左侧的NVIC选项,进入中断优先级配置界面;
  3. 首先配置优先级分组(Priority Grouping) :这是核心步骤,优先级分组决定了抢占优先级和响应优先级的位数分配,STM32F103支持5种优先级分组:
    • Group0:0位抢占优先级,4位响应优先级(0~15);
    • Group1:1位抢占优先级(01),3位响应优先级(07);
    • Group2:2位抢占优先级(03),2位响应优先级(03);
    • Group3:3位抢占优先级(07),1位响应优先级(01);
    • Group4:4位抢占优先级(0~15),0位响应优先级; 👉 初学者推荐配置为Group2:2位抢占优先级+2位响应优先级,既可以实现中断嵌套,又有足够的优先级等级,满足大部分入门项目的需求。
  4. 为对应的外设中断配置抢占优先级(Preemption Priority)响应优先级(Sub Priority):比如将UART1的抢占优先级设为1,响应优先级设为0;将GPIO_EXTI0的抢占优先级设为2,响应优先级设为0;
  5. 勾选中断的Enabled选项,开启该中断。

配置完成后,生成Keil5工程,STM32CubeMX会自动生成中断优先级的初始化代码,初学者无需手动编写。

2.1.1.4 优先级配置的基本原则

初学者在配置中断优先级时,需要遵循以下3个基本原则,避免出现中断响应异常:

  1. 实时性要求高的中断,设置更高的优先级:比如定时器输入捕获中断(红外解码、电机控制)对实时性要求极高,需要设置较高的抢占优先级;而串口接收中断、按键中断的实时性要求相对较低,可以设置较低的抢占优先级;
  2. 高速数据传输的中断,设置更高的优先级:比如SPI/SD卡的中断、高速UART的中断,需要设置较高的优先级,防止数据丢失;
  3. 避免设置过多的抢占优先级等级:入门阶段建议使用2~3个抢占优先级等级即可,过多的等级会导致逻辑混乱,不易调试。
2.1.2 中断服务函数的编写原则:快进快出

在第1章中,我们已经强调了中断服务函数"快进快出"的核心原则,本节将从实战角度讲解中断服务函数的编写规范和具体实现方法,包括HAL库下的中断服务函数编写、中断标志位的清除、数据的快速存储等。

2.1.2.1 STM32 HAL库下的中断服务函数结构

STM32CubeMX生成的工程中,已经为每个中断源定义了中断服务函数的入口,初学者只需要在对应的函数中编写处理逻辑即可,无需手动定义函数名(函数名与中断源一一对应,由单片机的启动文件决定)。

比如,UART1的中断服务函数名是USART1_IRQHandler,GPIO_EXTI0的中断服务函数名是EXTI0_IRQHandler,定时器2的中断服务函数名是TIM2_IRQHandler

HAL库下的中断服务函数标准结构如下:

复制代码
复制代码
// UART1中断服务函数
void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
  // 初学者编写的中断处理逻辑放在这里
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1); // HAL库的中断处理函数,用于清除中断标志位等
  /* USER CODE BEGIN USART1_IRQn 1 */
  /* USER CODE END USART1_IRQn 1 */
}

其中,HAL_UART_IRQHandler(&huart1)是HAL库提供的中断处理函数,其核心作用是:

  1. 检测中断源(比如是RXNE接收中断还是TXE发送中断);
  2. 清除对应的中断标志位;
  3. 调用中断回调函数(如果开启的话)。

注意 :初学者在编写中断处理逻辑时,建议放在/* USER CODE BEGIN USART1_IRQn 0 *//* USER CODE END USART1_IRQn 0 */之间,这样在重新生成STM32CubeMX工程时,代码不会被覆盖。

2.1.2.2 中断服务函数的编写规范(快进快出)

结合"快进快出"的原则,HAL库下中断服务函数的编写必须遵循以下6个规范,这是初学者必须严格遵守的:

  1. 只做耗时微秒级的操作:所有操作必须保证在微秒级完成,绝对不能出现毫秒级的延时、循环等操作;
  2. 先判断中断源,再处理:一个外设可能对应多个中断源,比如UART1支持RXNE接收中断、TXE发送中断、ERROR错误中断,必须先判断是哪个中断源触发的,再进行处理,避免无效操作;
  3. 及时清除中断标志位 :如果中断标志位没有被清除,CPU会一直响应该中断,导致程序卡死,HAL库的HAL_XXX_IRQHandler函数会自动清除大部分中断标志位,但部分特殊标志位需要手动清除;
  4. 只做数据读取和FIFO写入:这是中断服务函数的核心操作,读取硬件寄存器的数据,然后快速写入FIFO,不做任何复杂处理;
  5. 禁止使用浮点运算、字符串处理等耗时操作:这些操作耗时较长,会导致中断处理时间过长;
  6. 禁止调用HAL库的阻塞函数 :比如HAL_UART_Transmit(串口发送)、HAL_Delay(延时)等阻塞函数,这些函数会导致中断服务函数无法快速退出。
2.1.2.3 经典示例:UART1接收中断服务函数

以下是遵循"快进快出"原则的UART1接收中断服务函数示例,也是本文档所有串口项目的通用模板:

复制代码
复制代码
// 全局定义环形FIFO(第2.2节会讲解其实现)
FIFO_t uart_fifo;

// UART1中断服务函数
void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
  uint8_t ch;
  // 判断是否是UART接收非空中断(RXNE)
  if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)
  {
      ch = huart1.Instance->DR; // 快速读取接收数据寄存器(微秒级)
      FIFO_Put(&uart_fifo, ch); // 将数据写入FIFO(微秒级)
      __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_RXNE); // 手动清除中断标志位(可选,HAL库会自动清除)
  }
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
  /* USER CODE END USART1_IRQn 1 */
}

该中断服务函数的处理时间不足1微秒,完全符合"快进快出"的原则:只做了3件事------判断中断源、读取数据、写入FIFO,没有任何复杂操作,保证了后续中断的及时响应。

2.1.2.4 中断标志位的清除:初学者的坑

中断标志位的清除是初学者最容易犯错误的地方之一,很多时候程序出现"中断一直触发、程序卡死"的问题,都是因为中断标志位没有被正确清除

关于STM32F103中断标志位的清除,需要掌握以下3个核心知识点:

  1. 大部分中断标志位由HAL库自动清除 :调用HAL_XXX_IRQHandler函数后,HAL库会自动检测中断源,并清除对应的中断标志位,比如UART的RXNE标志位、定时器的更新标志位;
  2. 部分中断标志位需要手动清除 :比如GPIO外部中断的挂起标志位(EXTI_PR),需要手动调用__HAL_GPIO_EXTI_CLEAR_FLAG函数清除;
  3. 错误中断标志位需要手动清除:比如UART的错误标志位(ORE/FE/PE),需要先读取相关寄存器,再清除标志位,否则会一直触发错误中断。

经典示例:GPIO_EXTI0按键中断的标志位清除

复制代码
复制代码
// EXTI0中断服务函数
void EXTI0_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI0_IRQn 0 */
  // 手动清除EXTI0的挂起标志位,否则会一直触发中断
  __HAL_GPIO_EXTI_CLEAR_FLAG(GPIO_PIN_0);
  // 读取按键状态,并写入事件FIFO
  uint8_t key_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
  FIFO_Put(&key_fifo, key_state ? KEY1_RELEASE : KEY1_PRESS);
  /* USER CODE END EXTI0_IRQn 0 */
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
  /* USER CODE BEGIN EXTI0_IRQn 1 */
  /* USER CODE END EXTI0_IRQn 1 */
}

如果不手动清除GPIO_PIN_0的挂起标志位,即使按键已经松开,CPU也会一直响应EXTI0中断,导致程序卡死。

2.1.3 中断嵌套与中断屏蔽的基础应用
2.1.3.1 中断嵌套的实现

在2.1.1节中,我们讲解了中断的抢占优先级,中断嵌套的实现条件是:

  1. 优先级分组配置了抢占优先级的位数(比如Group2,2位抢占优先级);
  2. 高抢占优先级的中断源被触发;
  3. CPU正在处理低抢占优先级的中断。

经典示例:中断嵌套的实现

  • 配置定时器2的更新中断抢占优先级为0(高);
  • 配置UART1的接收中断抢占优先级为1(低);
  • 当CPU正在处理UART1的接收中断时,定时器2的更新中断触发,CPU会立即暂停UART1的中断处理,转而处理定时器2的中断,处理完成后再回到UART1的中断继续处理。

中断嵌套的代码无需手动编写,只需通过STM32CubeMX配置好抢占优先级即可,NVIC会自动实现中断嵌套的逻辑。

2.1.3.2 中断屏蔽的使用场景

中断屏蔽是指通过软件的方式,暂时禁止某个中断源或所有中断源的响应,其核心使用场景是:

  1. 处理临界区代码:当主循环处理的临界区代码(比如FIFO的读写操作)不能被中断打断时,需要暂时屏蔽中断,处理完成后再使能中断;
  2. 系统初始化阶段:在单片机系统初始化阶段(比如FIFO初始化、外设初始化),需要屏蔽所有中断,防止初始化过程中被中断打断,导致初始化失败;
  3. 紧急故障处理:当系统出现紧急故障时,需要屏蔽所有中断,专注于故障处理。
2.1.3.3 STM32 HAL库下的中断屏蔽方法

STM32 HAL库提供了专门的函数用于中断的屏蔽和使能,包括全局中断屏蔽单个中断屏蔽

  1. 全局中断屏蔽:屏蔽/使能所有中断源,使用CPU的关中断/开中断指令,示例:
复制代码
复制代码
__disable_irq(); // 屏蔽所有中断(关中断)
// 临界区代码,比如FIFO的批量读写操作
__enable_irq();  // 使能所有中断(开中断)
  1. 单个中断屏蔽 :屏蔽/使能某个特定的中断源,使用HAL_NVIC_DisableIRQHAL_NVIC_EnableIRQ函数,示例:
复制代码
复制代码
HAL_NVIC_DisableIRQ(USART1_IRQn); // 屏蔽UART1中断
// 临界区代码
HAL_NVIC_EnableIRQ(USART1_IRQn);  // 使能UART1中断

注意:临界区代码的执行时间必须尽可能短,否则会导致中断无法被及时响应,从而丢失数据。

2.1.4 初学者常见中断问题与避坑

本节整理了初学者在编写单片机中断程序时,最容易遇到的5个经典问题,并给出了对应的原因和解决办法,帮助初学者少走弯路。

问题1:中断一直触发,程序卡死

常见原因

  1. 中断标志位没有被正确清除;
  2. 中断服务函数中存在死循环;
  3. 硬件故障(比如外设引脚短路,导致中断源一直被触发)。

解决办法

  1. 检查中断标志位的清除逻辑,确保所有中断标志位都被正确清除(手动或HAL库自动);
  2. 检查中断服务函数,确保没有死循环、无限递归等代码;
  3. 检查硬件电路,用万用表测量外设引脚的电平,排除短路、虚焊等硬件故障。
问题2:中断无法触发,没有任何响应

常见原因

  1. 没有在STM32CubeMX中开启中断(未勾选Enabled);
  2. 外设初始化错误(比如UART的波特率配置错误、GPIO的模式配置错误);
  3. 中断优先级配置错误(比如抢占优先级设置为无效值);
  4. 全局中断被屏蔽(忘记调用__enable_irq());
  5. 硬件故障(比如外设引脚未连接、传感器未工作)。

解决办法

  1. 打开STM32CubeMX,检查对应的中断是否开启;
  2. 检查外设的初始化代码,确保模式、参数配置正确;
  3. 检查中断优先级的配置,确保抢占优先级和响应优先级在有效范围内;
  4. 检查代码,确保在初始化完成后开启了全局中断;
  5. 检查硬件电路,排除引脚未连接、传感器未工作等故障。
问题3:中断响应不及时,导致数据丢失

常见原因

  1. 中断服务函数中编写了复杂的处理逻辑,违反了"快进快出"原则;
  2. 中断优先级配置过低,被其他高优先级中断长时间阻塞;
  3. 主循环中的临界区代码执行时间过长,屏蔽了中断;
  4. FIFO的大小设置过小,数据溢出。

解决办法

  1. 重构中断服务函数,将复杂的处理逻辑移到主循环的状态机中,只保留数据读取和FIFO写入操作;
  2. 提高该中断的抢占优先级,确保其能被及时响应;
  3. 优化临界区代码,缩短其执行时间,尽量减少中断屏蔽的时间;
  4. 增大FIFO的大小,防止数据溢出。
问题4:中断嵌套导致的逻辑混乱

常见原因

  1. 为多个中断源设置了相同的抢占优先级,导致中断响应顺序混乱;
  2. 高优先级中断的处理时间过长,导致低优先级中断长时间无法响应;
  3. 中断嵌套的层数过多(比如3层及以上),导致程序执行流程混乱。

解决办法

  1. 为不同的中断源设置不同的抢占优先级,明确中断的响应顺序;
  2. 优化高优先级中断的服务函数,确保其处理时间尽可能短;
  3. 减少中断嵌套的层数,入门阶段建议控制在2层以内。
问题5:HAL库的中断回调函数与中断服务函数混淆

常见原因: 初学者对HAL库的中断回调函数理解不清,不知道该在中断服务函数中编写逻辑,还是在回调函数中编写逻辑。

解决办法

  1. 中断服务函数(IRQHandler):是中断的入口,负责快速处理紧急事件(数据读取、FIFO写入、标志位清除),遵循"快进快出"原则;
  2. 中断回调函数(HAL_XXX_Callback) :是HAL库提供的底层回调函数,由HAL_XXX_IRQHandler函数调用,主要用于底层的外设处理,不建议初学者在回调函数中编写复杂逻辑,以免违反"快进快出"原则。

👉 初学者建议 :所有中断处理逻辑都编写在中断服务函数中(USER CODE区域),不要使用回调函数,这样更直观,也更容易控制中断的处理时间。

2.2 FIFO缓冲区通用实现与优化

FIFO是连接中断和状态机的核心桥梁,其实现的好坏直接影响程序的稳定性和数据的完整性。本节将讲解环形FIFO 的核心原理、通用C语言实现、空满判断、溢出处理、双FIFO实现等内容,提供的FIFO代码是通用的,可以直接移植到所有单片机(STM32/51/AVR)和ZYNQ PS端,无需任何修改。

2.2.1 环形FIFO的核心原理(初学者易懂版)

在第1章中,我们已经用"快递中转站"的比喻理解了FIFO的核心意义,本节将从数据结构和操作逻辑的角度,通俗讲解环形FIFO的核心原理,让初学者能看懂并理解FIFO的实现代码。

2.2.1.1 环形FIFO的核心数据结构

环形FIFO的核心数据结构由3个部分组成,用C语言的结构体表示如下:

复制代码
复制代码
#define FIFO_SIZE 128 // FIFO的大小,可根据需求修改

typedef struct {
    uint8_t buffer[FIFO_SIZE];  // 数据缓冲区,存储实际的数据
    volatile uint16_t head;     // 写指针:指向中下一个要写入数据的位置
    volatile uint16_t tail;     // 读指针:指向中下一个要读取数据的位置
} FIFO_t;

对每个部分的通俗解释:

  1. buffer数组:就是FIFO的"仓库",用于存储中断写入的数据,数组的长度就是FIFO的大小(FIFO_SIZE),初学者常用的大小是64/128/256字节;
  2. head写指针:相当于"仓库的入库口",每次写入一个数据,入库口就向后移动一位,当到仓库末尾时,就回到开头,形成环形;
  3. tail读指针:相当于"仓库的出库口",每次读取一个数据,出库口就向后移动一位,当到仓库末尾时,就回到开头,形成环形。

注意 :head和tail指针需要加上volatile关键字,因为这两个变量会在中断服务函数主循环 中同时访问,volatile关键字的作用是告诉编译器,该变量的值可能会被意外修改(比如中断),编译器不要对该变量进行优化,确保每次访问都是从内存中读取最新的值,避免出现数据错误。

2.2.1.2 环形FIFO的空/满判断逻辑

环形FIFO的空/满判断是其核心难点,也是初学者最容易理解错误的地方,因为环形FIFO的head和tail指针会循环移动,当head == tail时,可能是FIFO为空,也可能是FIFO为满。

为了解决这个问题,常用的方法有3种,初学者推荐使用**"牺牲一个字节"**的方法,因为该方法实现简单,易于理解,且足够满足入门项目的需求。

方法1:牺牲一个字节(推荐初学者使用)

核心原理:当FIFO的空闲空间只剩下一个字节时,就认为FIFO已满,这样可以保证:

  • FIFO为空:head == tail;
  • FIFO为满:(head + 1) % FIFO_SIZE == tail。

该方法的优点是实现简单,空满判断的逻辑清晰;缺点是牺牲了一个字节的内存空间,比如FIFO_SIZE=128时,实际最多只能存储127个字节的数据。对于单片机而言,一个字节的内存空间可以忽略不计,因此该方法是入门阶段的最佳选择。

方法2:增加一个计数变量

核心原理:在FIFO的结构体中增加一个count变量,用于记录FIFO中当前存储的数据个数,这样可以保证:

  • FIFO为空:count == 0;
  • FIFO为满:count == FIFO_SIZE。

该方法的优点是充分利用了FIFO的内存空间,没有字节牺牲;缺点是需要额外维护一个count变量,在中断和主循环中对count进行加减操作时,需要考虑原子操作(防止中断打断导致count值错误),对初学者而言稍显复杂。

方法3:使用不同的指针移动规则

核心原理:写指针head只在写入数据时移动,读指针tail只在读取数据时移动,通过指针的移动规则区分空和满,该方法实现复杂,初学者不推荐使用。

本文档中所有的FIFO实现均采用**"牺牲一个字节"**的方法,因为其实现简单,易于理解和调试,完全满足入门项目的需求。

2.2.1.3 环形FIFO的核心操作:写操作与读操作

环形FIFO的核心操作只有两个:写操作(Put)读操作(Get),所有其他操作(初始化、判空、判满)都是为这两个操作服务的。

结合"牺牲一个字节"的空满判断方法,环形FIFO的写操作逻辑如下:

  1. 判断FIFO是否已满:如果(head + 1) % FIFO_SIZE == tail,说明FIFO已满,写入失败,返回错误;
  2. 如果FIFO未满,将数据写入buffer[head]的位置;
  3. 将head指针向后移动一位:head = (head + 1) % FIFO_SIZE;
  4. 写入成功,返回成功。

读操作逻辑如下:

  1. 判断FIFO是否为空:如果head == tail,说明FIFO为空,读取失败,返回错误;
  2. 如果FIFO非空,从buffer[tail]的位置读取数据;
  3. 将tail指针向后移动一位:tail = (tail + 1) % FIFO_SIZE;
  4. 读取成功,返回成功。

其中,% FIFO_SIZE取模运算 ,其作用是实现指针的循环移动:当指针到达buffer数组的末尾(索引为FIFO_SIZE-1)时,加1后取模FIFO_SIZE,结果为0,指针回到数组的开头,形成环形。

比如,FIFO_SIZE=128,head=127,执行head = (head + 1) % 128后,head=0,指针回到开头。

2.2.2 通用环形FIFO的C语言实现(可直接移植)

基于2.2.1节的核心原理,本节提供通用环形FIFO的完整C语言实现代码,该代码具有以下特点:

  1. 通用型:适用于所有单片机(STM32/51/AVR)和ZYNQ PS端,无需任何修改;
  2. 可配置:通过修改FIFO_SIZE宏定义,可灵活设置FIFO的大小;
  3. 易使用:提供初始化、判空、判满、写操作、读操作5个核心函数,接口简单;
  4. 高稳定性:遵循"牺牲一个字节"的空满判断方法,避免数据错误;
  5. 中断安全:指针变量添加volatile关键字,支持中断和主循环的同时访问。
2.2.2.1 完整代码实现
复制代码
复制代码
/******************************************
* 通用环形FIFO实现代码
* 适用平台:所有单片机(STM32/51/AVR)、ZYNQ PS端
* 作者:嵌入式初学者
* 版本:V1.0
* 说明:采用"牺牲一个字节"的空满判断方法,简单易懂,适合初学者
******************************************/
#include <stdint.h>
#include <stdbool.h>

// ************************* 配置项 *************************
// FIFO的大小,可根据需求修改,推荐设置为2的幂次(64/128/256/512)
#define FIFO_SIZE 128
// **********************************************************

// 环形FIFO结构体定义
typedef struct {
    uint8_t buffer[FIFO_SIZE];          // 数据缓冲区
    volatile uint16_t head;             // 写指针:指向下一个要写入的位置
    volatile uint16_t tail;             // 读指针:指向下一个要读取的位置
} FIFO_t;

/**
 * @brief  初始化FIFO
 * @param  fifo:指向FIFO结构体的指针
 * @retval 无
 */
void FIFO_Init(FIFO_t *fifo)
{
    if(fifo == NULL) return; // 防止空指针
    fifo->head = 0;          // 写指针初始化为0
    fifo->tail = 0;          // 读指针初始化为0
}

/**
 * @brief  判断FIFO是否为空
 * @param  fifo:指向FIFO结构体的指针
 * @retval true:FIFO为空,false:FIFO非空
 */
bool FIFO_IsEmpty(FIFO_t *fifo)
{
    if(fifo  NULL) return true;
    return (fifo->head  fifo->tail); // 空:head  tail
}

/**
 * @brief  判断FIFO是否为满
 * @param  fifo:指向FIFO结构体的指针
 * @retval true:FIFO为满,false:FIFO未满
 */
bool FIFO_IsFull(FIFO_t *fifo)
{
    if(fifo  NULL) return true;
    // 满:(head + 1) % FIFO_SIZE  tail(牺牲一个字节)
    return ((fifo->head + 1) % FIFO_SIZE  fifo->tail);
}

/**
 * @brief  向FIFO中写入一个字节的数据
 * @param  fifo:指向FIFO结构体的指针
 * @param  data:要写入的数据
 * @retval true:写入成功,false:写入失败(FIFO已满)
 */
bool FIFO_Put(FIFO_t *fifo, uint8_t data)
{
    if(fifo == NULL || FIFO_IsFull(fifo))
    {
        return false; // 空指针或FIFO已满,写入失败
    }
    fifo->buffer[fifo->head] = data; // 将数据写入缓冲区
    fifo->head = (fifo->head + 1) % FIFO_SIZE; // 写指针后移一位,循环
    return true; // 写入成功
}

/**
 * @brief  从FIFO中读取一个字节的数据
 * @param  fifo:指向FIFO结构体的指针
 * @param  data:指向存储读取数据的变量的指针
 * @retval true:读取成功,false:读取失败(FIFO为空)
 */
bool FIFO_Get(FIFO_t *fifo, uint8_t *data)
{
    if(fifo  NULL || data  NULL || FIFO_IsEmpty(fifo))
    {
        return false; // 空指针或FIFO为空,读取失败
    }
    *data = fifo->buffer[fifo->tail]; // 从缓冲区读取数据
    fifo->tail = (fifo->tail + 1) % FIFO_SIZE; // 读指针后移一位,循环
    return true; // 读取成功
}
2.2.2.2 代码关键知识点解析
  1. 头文件依赖 :代码只依赖stdint.h(标准整数类型)和stdbool.h(布尔类型),这两个头文件是C99标准的头文件,所有嵌入式C编译器都支持(Keil5、GCC、ARMCC等);
  2. 配置项:只有一个FIFO_SIZE宏定义,是唯一的配置项,初学者只需根据项目需求修改该值即可,推荐设置为2的幂次(64/128/256),因为取模运算在2的幂次时效率更高;
  3. 空指针判断 :所有函数都增加了空指针判断(if(fifo == NULL)),防止因传入空指针导致程序崩溃,提高了代码的健壮性;
  4. 返回值:写操作和读操作的返回值为bool类型,方便调用者判断操作是否成功,比如在中断服务函数中,可通过返回值判断FIFO是否已满,从而进行溢出处理;
  5. volatile关键字:head和tail指针添加了volatile关键字,确保在中断和主循环中访问的是内存中的最新值,避免编译器优化导致的数据错误。
2.2.2.3 代码的使用方法(简单三步)

该FIFO代码的使用方法非常简单,只需三步,适用于所有项目,是本文档所有项目的通用模板:

  1. 定义FIFO实例:在全局区域定义一个FIFO_t类型的变量(全局变量可被中断服务函数和主循环同时访问),比如:
复制代码
复制代码
FIFO_t uart_fifo; // 串口接收FIFO
FIFO_t key_fifo;  // 按键事件FIFO
FIFO_t ir_fifo;   // 红外解码FIFO
  1. 初始化FIFO:在单片机的main函数中,系统初始化完成后,调用FIFO_Init函数初始化FIFO,比如:
复制代码
复制代码
int main(void)
{
    // 系统初始化(HAL_Init、时钟配置、外设初始化等)
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    
    // 初始化FIFO
    FIFO_Init(&uart_fifo);
    FIFO_Init(&key_fifo);
    FIFO_Init(&ir_fifo);
    
    // 开启中断
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
    
    // 主循环
    while(1)
    {
        // 状态机处理逻辑
    }
}
  1. 读写FIFO :在中断服务函数 中调用FIFO_Put写入数据,在主循环的状态机中调用FIFO_Get读取数据,比如:
复制代码
复制代码
// 中断服务函数中写入FIFO
void USART1_IRQHandler(void)
{
    uint8_t ch;
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)
    {
        ch = huart1.Instance->DR;
        FIFO_Put(&uart_fifo, ch); // 写入串口FIFO
    }
    HAL_UART_IRQHandler(&huart1);
}

// 主循环状态机中读取FIFO
while(1)
{
    uint8_t ch;
    if(FIFO_Get(&uart_fifo, &ch)) // 读取FIFO中的数据
    {
        // 状态机处理逻辑
        UART_State_Process(ch);
    }
}

以上三步就是FIFO代码的完整使用方法,初学者只需按照这三步操作,即可在任何项目中快速集成环形FIFO,无需深入理解底层实现细节。

2.2.3 FIFO的空/满判断、读写操作详解

在2.2.2节中,我们已经实现了环形FIFO的核心函数,本节将对**判空(FIFO_IsEmpty)、判满(FIFO_IsFull)、写操作(FIFO_Put)、读操作(FIFO_Get)**四个核心函数进行详细解析,让初学者不仅会用,还能理解其底层逻辑。

2.2.3.1 判空函数:FIFO_IsEmpty

函数代码:

复制代码
复制代码
bool FIFO_IsEmpty(FIFO_t *fifo)
{
    if(fifo  NULL) return true;
    return (fifo->head  fifo->tail); // 空:head == tail
}

核心逻辑:当写指针head和读指针tail指向同一个位置时,说明FIFO中没有数据,即为空。

通俗解释:仓库的入库口和出库口在同一个位置,说明仓库里没有任何快递,为空。

使用场景:主循环的状态机在读取FIFO之前,需要先调用该函数判断FIFO是否为空,避免读取空FIFO导致数据错误。

2.2.3.2 判满函数:FIFO_IsFull

函数代码:

复制代码
复制代码
bool FIFO_IsFull(FIFO_t *fifo)
{
    if(fifo  NULL) return true;
    // 满:(head + 1) % FIFO_SIZE  tail(牺牲一个字节)
    return ((fifo->head + 1) % FIFO_SIZE == fifo->tail);
}

核心逻辑:当写指针head向后移动一位后,与读指针tail指向同一个位置时,说明FIFO已满(牺牲一个字节)。

通俗解释:仓库的入库口再向后移动一位,就会和出库口重合,说明仓库里的快递已经堆满,只剩下一个空位,为了避免空满判断混淆,认为此时仓库已满。

使用场景:中断服务函数在写入FIFO之前,需要先调用该函数判断FIFO是否已满,避免写入满FIFO导致数据覆盖。

2.2.3.3 写操作函数:FIFO_Put

函数代码:

复制代码
复制代码
bool FIFO_Put(FIFO_t *fifo, uint8_t data)
{
    if(fifo == NULL || FIFO_IsFull(fifo))
    {
        return false; // 空指针或FIFO已满,写入失败
    }
    fifo->buffer[fifo->head] = data; // 将数据写入缓冲区
    fifo->head = (fifo->head + 1) % FIFO_SIZE; // 写指针后移一位,循环
    return true; // 写入成功
}

核心逻辑:先判断FIFO是否为空指针或已满,如果是则返回失败;否则将数据写入写指针指向的位置,然后将写指针向后移动一位(循环),返回成功。

关键步骤

  1. 空指针和满状态判断:这是前置条件,必须首先判断,否则会导致数据错误或程序崩溃;
  2. 数据写入:将数据写入buffer[fifo->head],这是实际的存储操作;
  3. 写指针移动:写指针后移一位,并通过取模运算实现循环,这是环形FIFO的核心。

使用场景仅在中断服务函数中调用,用于快速写入数据,遵循"快进快出"的原则。

2.2.3.4 读操作函数:FIFO_Get

函数代码:

复制代码
复制代码
bool FIFO_Get(FIFO_t *fifo, uint8_t *data)
{
    if(fifo  NULL || data  NULL || FIFO_IsEmpty(fifo))
    {
        return false; // 空指针或FIFO为空,读取失败
    }
    *data = fifo->buffer[fifo->tail]; // 从缓冲区读取数据
    fifo->tail = (fifo->tail + 1) % FIFO_SIZE; // 读指针后移一位,循环
    return true; // 读取成功
}

核心逻辑:先判断FIFO是否为空指针、数据指针是否为空、FIFO是否为空,如果是则返回失败;否则从读指针指向的位置读取数据到传入的变量中,然后将读指针向后移动一位(循环),返回成功。

关键步骤

  1. 空指针和空状态判断:前置条件,必须首先判断;
  2. 数据读取:通过指针将缓冲区的数据读取到调用者的变量中,这是实际的读取操作;
  3. 读指针移动:读指针后移一位,并通过取模运算实现循环。

使用场景仅在主循环的状态机中调用,用于读取中断写入的数据,进行复杂逻辑处理。

2.2.4 FIFO大小的合理规划与溢出处理
2.2.4.1 FIFO大小的合理规划方法

在2.2.2节中,我们知道FIFO的大小由FIFO_SIZE宏定义决定,而FIFO大小的合理规划是避免数据溢出的关键。对于初学者而言,推荐使用**"经验公式+实际测试"**的方法来规划FIFO的大小,简单高效。

方法1:经验公式计算最小FIFO大小

FIFO的最小大小由中断的最大触发速度主循环的最大处理时间两个因素决定,经验公式如下:

复制代码
复制代码
FIFO最小大小 = 中断最大触发速度(字节/秒) × 主循环最大处理时间(秒) × 安全系数

参数解释

  1. 中断最大触发速度 :指中断每秒最多能触发的次数,即每秒最多写入FIFO的数据字节数,比如:
    • UART串口波特率115200bps:每秒最多传输115200/10=11520字节(10位为一个数据帧:1位起始位+8位数据位+1位停止位);
    • 10kHz定时器中断:每秒触发10000次,每次写入1个字节,速度为10000字节/秒;
  2. 主循环最大处理时间:指主循环执行一次完整的状态机处理逻辑所需的最长时间,单位为秒,入门阶段的主循环处理时间一般在110ms(0.0010.01秒)之间;
  3. 安全系数 :为了防止突发的数据高峰导致溢出,设置的安全系数,推荐取值为1.5~2

经典示例: UART串口波特率115200bps,主循环最大处理时间10ms(0.01秒),安全系数2,计算FIFO最小大小:

复制代码
复制代码
FIFO最小大小 = 11520 × 0.01 × 2 = 230.4 字节

因此,FIFO_SIZE可设置为256字节(取大于230.4的最小2的幂次),既满足需求,又效率高。

方法2:实际测试调整FIFO大小

经验公式计算的是FIFO的最小大小,在实际项目中,建议先根据经验公式设置一个初始值,然后通过实际测试调整FIFO的大小,步骤如下:

  1. 根据经验公式设置初始的FIFO_SIZE(比如256字节);
  2. 运行项目,用串口助手/逻辑分析仪发送高速数据(比如满速发送115200bps);
  3. 观察程序的运行状态,判断是否出现数据丢失、解析失败等问题;
  4. 如果出现数据丢失,说明FIFO大小不足,适当增大FIFO_SIZE(比如512字节),重新测试;
  5. 如果程序运行稳定,且FIFO的利用率较低(比如始终只使用了不到一半的空间),可以适当减小FIFO_SIZE(比如128字节),节省内存资源。
2.2.4.2 FIFO的溢出处理:初学者必须考虑的问题

FIFO溢出是指中断写入数据的速度超过了主循环读取数据的速度,导致FIFO被写满,后续的写入操作失败,数据丢失。

初学者最容易忽略FIFO的溢出处理,认为只要设置了足够大的FIFO_SIZE,就不会出现溢出,但在实际项目中,突发的数据高峰主循环处理时间过长 ,都可能导致溢出,因此必须增加溢出处理逻辑,提高程序的健壮性。

2.2.4.3 溢出处理的实现方法

FIFO的溢出处理逻辑必须在中断服务函数中实现 ,因为溢出只可能发生在中断的写操作中,核心实现方法是判断FIFO_Put的返回值,如果返回false,说明FIFO已满,发生溢出,此时可进行相应的处理。

常用的溢出处理方法有3种,初学者可根据项目需求选择:

方法1:溢出标志位(推荐)

核心原理:定义一个全局的溢出标志位,当FIFO_Put返回false时,置位该标志位,主循环的状态机中检测到该标志位后,进行相应的处理(比如串口提示、LED闪烁报警),然后清除

单片机与ZYNQ PS端 中断+状态机+FIFO 综合应用实战文档(初学者版)

(接上文)

第3章 经典单片机项目实战(中断+状态机+FIFO)(约25000字)

3.5 项目5:SD卡/FATFS数据流记录(数据存储实战)

3.5.1 应用场景与实战价值

在嵌入式开发中,高频数据的实时存储 是一个核心需求,比如工业黑匣子、环境监测记录仪、高频传感器数据采集等。这类场景的核心痛点是:传感器数据采集速度快(如ADC每秒采集1000次),而SD卡写入存在潜伏期(SPI写入一个扇区通常需要几十毫秒),如果直接在采集中断中写入SD卡,必然会导致数据丢失;同时,主循环中直接处理采集和写入,会导致程序阻塞,无法响应其他任务。

中断+FIFO+状态机的组合,完美解决了这一痛点:中断负责高速采集数据并写入FIFO,FIFO作为数据缓冲池解决速度不匹配问题,状态机负责在主循环中批量读取FIFO数据并写入SD卡,既保证了数据不丢失,又实现了非阻塞的存储逻辑。

本项目的实战价值极高,是嵌入式数据存储类项目的通用模板,掌握后可轻松移植到温湿度监测、振动采集、音频记录等各类数据存储场景中。

3.5.2 FATFS文件系统基础(初学者易懂版)

SD卡本身是一个存储设备,需要通过文件系统 来管理数据的存储和读取,就像电脑的Windows系统管理硬盘文件一样。在单片机开发中,最常用的文件系统是FATFS,它是一个开源的、轻量级的、可移植的文件系统,支持FAT12/FAT16/FAT32/exFAT格式,完全满足单片机的存储需求。

对于初学者而言,不需要深入理解FATFS的底层实现,只需要掌握4个核心API即可完成文件的创建、写入、读取和关闭,这4个API是FATFS的"入门四件套":

API函数 功能描述 初学者使用要点
f_mount 挂载SD卡,初始化文件系统 必须在操作文件前调用,仅调用一次
f_open 打开/创建文件 指定文件名和打开模式(读/写/创建)
f_write 向文件中写入数据 批量写入,避免单字节写入(效率极低)
f_close 关闭文件 写入完成后必须调用,否则数据可能丢失

FATFS的核心特点

  1. 轻量级:代码体积小,RAM占用低,适合单片机;
  2. 可移植性强:只需要修改底层的SPI读写接口,即可移植到任意单片机;
  3. 支持扇区操作:以扇区(512字节)为单位进行读写,效率高;
  4. 开源免费:无需授权,可直接用于商业项目。

在本项目中,我们将使用SPI接口驱动SD卡(最常用的单片机SD卡接口),结合FATFS实现数据的实时存储。

3.5.3 硬件准备与SD卡SPI接口电路连接
3.5.3.1 硬件清单
  1. 主控芯片:STM32F103C8T6最小系统板(核心);
  2. SD卡模块:SPI接口SD卡模块(淘宝常见,价格5-10元);
  3. ADC传感器:LM35温度传感器(模拟输出,用于采集温度数据);
  4. 电源模块:5V/3.3V电源(给SD卡模块供电,SD卡模块需3.3V供电);
  5. 杜邦线:若干(用于电路连接);
  6. SD卡:FAT32格式的Micro SD卡(容量≤32GB,推荐8GB)。
3.5.3.2 电路连接(SPI接口)

SD卡模块的SPI接口与STM32F103的SPI2接口连接(PA4-PA7),具体连接如下:

SD卡模块引脚 STM32F103引脚 功能说明
VCC 3.3V 电源(严禁接5V,会烧毁SD卡模块)
GND GND
MOSI PA7 SPI主发从收
MISO PA6 SPI主收从发
SCK PA5 SPI时钟
CS PA4 SPI片选(低电平有效)

LM35温度传感器的连接:

  • VCC:3.3V;
  • GND:GND;
  • OUT:PA0(STM32F103的ADC1_IN0通道)。

电路连接注意事项

  1. SD卡模块的VCC必须接3.3V,绝对不能接5V,否则会烧毁模块;
  2. SPI的时钟线(SCK)、数据线(MOSI/MISO)建议使用短距离杜邦线,避免干扰;
  3. 片选引脚(CS)必须连接,用于选择SD卡设备。
3.5.4 架构设计:ADC定时中断+双FIFO+SD卡写入状态机

本项目的核心架构是**"三层架构"**,完美体现了中断+FIFO+状态机的协同逻辑,具体如下:

3.5.4.1 第一层:ADC定时中断(数据采集层)
  • 中断源:定时器2更新中断(1kHz,即每秒采集1000次ADC数据);
  • 中断职责 :快进快出,仅读取ADC1的转换结果,将数据写入双FIFO(Ping-Pong缓冲)
  • 核心作用:保证ADC数据的高速、实时采集,不丢失任何采样点。
3.5.4.2 第二层:双FIFO(数据缓冲层)
  • FIFO类型:双环形FIFO(Ping-Pong缓冲),分为FIFO_A和FIFO_B,每个FIFO大小为512字节(1个SD卡扇区大小);
  • FIFO职责:当一个FIFO写满时,切换到另一个FIFO继续写入,同时通知状态机读取写满的FIFO并写入SD卡;
  • 核心作用:解决ADC采集速度(1kHz)和SD卡写入速度(几十毫秒/扇区)的速度不匹配问题,实现数据的无缝缓冲。
3.5.4.3 第三层:SD卡写入状态机(数据处理层)
  • 状态机状态:IDLE(空闲)→ CHECK_FIFO(检查FIFO是否写满)→ READ_FIFO(读取FIFO数据)→ WRITE_SD(写入SD卡)→ WAIT_DONE(等待写入完成);
  • 状态机职责:在主循环中轮询双FIFO的状态,当FIFO写满时,批量读取数据并通过FATFS写入SD卡,写入完成后清空FIFO,继续等待;
  • 核心作用:非阻塞处理数据存储,保证主循环可响应其他任务,同时实现数据的批量写入,提高SD卡写入效率。

架构数据流闭环

复制代码
复制代码
定时器2中断(1kHz)→ ADC采集数据 → 写入双FIFO → 主循环状态机 → 读取写满的FIFO → 批量写入SD卡 → 清空FIFO → 等待下一次写满
3.5.5 分步代码实现:数据采集→FIFO缓冲→SD卡写入

本项目的代码分为5个部分:通用环形FIFO代码、ADC+定时器中断配置、双FIFO管理、FATFS底层驱动、SD卡写入状态机,所有代码均基于STM32 HAL库,注释详尽,可直接移植。

3.5.5.1 通用环形FIFO代码(复用第2.2节,略)

直接使用第2.2节的通用环形FIFO代码,定义两个FIFO实例:

复制代码
复制代码
// 双FIFO定义,每个FIFO大小为512字节(1个SD卡扇区)
#define FIFO_SIZE 512
FIFO_t adc_fifo_a;
FIFO_t adc_fifo_b;
// 当前写入的FIFO(0:FIFO_A,1:FIFO_B)
uint8_t current_write_fifo = 0;
// FIFO写满标志(0:未写满,1:写满)
volatile uint8_t fifo_a_full = 0;
volatile uint8_t fifo_b_full = 0;
3.5.5.2 ADC+定时器2中断配置(STM32CubeMX+代码实现)
(1)STM32CubeMX配置
  1. 时钟配置:系统时钟配置为72MHz(STM32F103的最大频率);
  2. ADC1配置
    • 通道:IN0(PA0);
    • 转换模式:单次转换;
    • 分辨率:12位;
    • 数据对齐:右对齐;
  3. 定时器2配置
    • 预分频器:7199(72MHz / (7199+1) = 10kHz);
    • 自动重装载值:9(10kHz / (9+1) = 1kHz,即1ms中断一次);
    • 开启更新中断(Update Event);
  4. SPI2配置
    • 模式:主机模式(Master);
    • 波特率:预分频器256(72MHz / 256 = 281.25kHz,SD卡初始化用低速);
    • 时钟极性(CPOL):低电平;
    • 时钟相位(CPHA):第一个边沿采样;
    • 数据大小:8位;
  5. GPIO配置:PA4设置为推挽输出(SPI片选CS),初始电平为高;
  6. 中断配置
    • 定时器2更新中断:抢占优先级1,响应优先级0;
    • SPI2中断:抢占优先级2,响应优先级0;
  7. 生成Keil5工程。
(2)ADC+定时器中断代码实现
复制代码
复制代码
// 全局变量:ADC采集数据
volatile uint16_t adc_value = 0;

/**
 * @brief  定时器2更新中断服务函数(1ms中断一次)
 * @param  无
 * @retval 无
 */
void TIM2_IRQHandler(void)
{
  /* USER CODE BEGIN TIM2_IRQn 0 */
  // 快进快出:仅采集ADC数据,写入FIFO
  if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET)
  {
    __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); // 清除中断标志
    
    // 启动ADC转换并读取数据
    HAL_ADC_Start(&hadc1);
    if(HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
    {
      adc_value = HAL_ADC_GetValue(&hadc1); // 读取12位ADC值(0~4095)
    }
    HAL_ADC_Stop(&hadc1);
    
    // 将ADC数据(低8位)写入当前FIFO
    uint8_t data = (uint8_t)(adc_value & 0xFF); // 仅存储低8位,节省空间
    if(current_write_fifo == 0)
    {
      if(!FIFO_Put(&adc_fifo_a, data))
      {
        fifo_a_full = 1; // FIFO_A写满,置位标志
        current_write_fifo = 1; // 切换到FIFO_B
      }
    }
    else
    {
      if(!FIFO_Put(&adc_fifo_b, data))
      {
        fifo_b_full = 1; // FIFO_B写满,置位标志
        current_write_fifo = 0; // 切换到FIFO_A
      }
    }
  }
  /* USER CODE END TIM2_IRQn 0 */
  HAL_TIM_IRQHandler(&htim2);
  /* USER CODE BEGIN TIM2_IRQn 1 */
  /* USER CODE END TIM2_IRQn 1 */
}

代码解析

  • 定时器2每1ms中断一次,实现1kHz的ADC采集频率;
  • 中断服务函数严格遵循"快进快出"原则,仅完成ADC采集和FIFO写入,耗时不足10微秒;
  • 双FIFO切换逻辑:当一个FIFO写满时,自动切换到另一个FIFO,并置位写满标志,通知状态机处理。
3.5.5.3 FATFS底层驱动移植(SPI接口)

FATFS的底层驱动需要实现磁盘IO接口 ,即disk_readdisk_writedisk_ioctl等函数,用于SPI与SD卡的通信。以下是简化版的SD卡SPI驱动代码(适用于STM32F103):

复制代码
复制代码
#include "diskio.h"
#include "spi.h"

// SD卡命令定义
#define CMD0    0       // 复位SD卡
#define CMD1    1       // 初始化SD卡
#define CMD8    8       // 读取SD卡电压
#define CMD17   17      // 读取单个扇区
#define CMD24   24      // 写入单个扇区
#define CMD55   55      // 应用命令
#define ACMD41  41      // 初始化SD卡(ACMD)

// SPI片选控制
#define SD_CS_LOW()  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define SD_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

// 底层SPI读写函数
static uint8_t SPI_ReadWrite(uint8_t data)
{
  uint8_t recv;
  HAL_SPI_TransmitReceive(&hspi2, &data, &recv, 1, 100);
  return recv;
}

// 等待SD卡就绪
static uint8_t SD_WaitReady(void)
{
  uint32_t timeout = 0xFFFF;
  while(SPI_ReadWrite(0xFF) != 0xFF && timeout--);
  return timeout ? 0 : 1;
}

// 发送SD卡命令
static uint8_t SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc)
{
  uint8_t response, timeout = 0x10;
  
  SD_CS_LOW();
  SPI_ReadWrite(0xFF); // 空闲时钟
  
  // 发送命令帧
  SPI_ReadWrite(0x40 | cmd);
  SPI_ReadWrite(arg >> 24);
  SPI_ReadWrite(arg >> 16);
  SPI_ReadWrite(arg >> 8);
  SPI_ReadWrite(arg);
  SPI_ReadWrite(crc);
  
  // 等待响应
  while((response = SPI_ReadWrite(0xFF)) & 0x80 && timeout--);
  
  return response;
}

// 初始化SD卡
DSTATUS disk_initialize(BYTE pdrv)
{
  uint8_t i, response;
  uint32_t timeout;
  
  if(pdrv != 0) return STA_NOINIT;
  
  SD_CS_HIGH();
  for(i = 0; i < 10; i++) SPI_ReadWrite(0xFF); // 发送74个时钟周期
  
  // 发送CMD0复位
  timeout = 0x1000;
  do {
    response = SD_SendCmd(CMD0, 0, 0x95);
  } while(response != 0x01 && timeout--);
  if(timeout == 0) return STA_NOINIT;
  
  // 发送CMD8读取电压
  response = SD_SendCmd(CMD8, 0x1AA, 0x87);
  if(response == 0x01)
  {
    // SD卡2.0版本
    SPI_ReadWrite(0xFF); SPI_ReadWrite(0xFF); SPI_ReadWrite(0xFF); SPI_ReadWrite(0xFF);
    
    // 发送ACMD41初始化
    timeout = 0x10000;
    do {
      SD_SendCmd(CMD55, 0, 0xFF);
      response = SD_SendCmd(ACMD41, 0x40000000, 0xFF);
    } while(response != 0x00 && timeout--);
    if(timeout == 0) return STA_NOINIT;
  }
  else
  {
    // SD卡1.0版本
    timeout = 0x1000;
    do {
      response = SD_SendCmd(CMD1, 0, 0xFF);
    } while(response != 0x00 && timeout--);
    if(timeout == 0) return STA_NOINIT;
  }
  
  SD_CS_HIGH();
  SPI_ReadWrite(0xFF);
  return 0;
}

// 读取SD卡扇区
DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count)
{
  uint8_t response;
  UINT i;
  
  if(pdrv != 0 || count == 0) return RES_PARERR;
  
  while(count--)
  {
    // 发送CMD17读取扇区
    response = SD_SendCmd(CMD17, sector << 9, 0xFF);
    if(response != 0x00)
    {
      SD_CS_HIGH();
      return RES_ERROR;
    }
    
    // 等待数据起始令牌
    while(SPI_ReadWrite(0xFF) != 0xFE);
    
    // 读取512字节数据
    for(i = 0; i < 512; i++) buff[i] = SPI_ReadWrite(0xFF);
    
    // 读取CRC
    SPI_ReadWrite(0xFF); SPI_ReadWrite(0xFF);
    
    SD_CS_HIGH();
    SPI_ReadWrite(0xFF);
    sector++;
    buff += 512;
  }
  return RES_OK;
}

// 写入SD卡扇区
DRESULT disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count)
{
  uint8_t response;
  UINT i;
  
  if(pdrv != 0 || count == 0) return RES_PARERR;
  
  while(count--)
  {
    // 发送CMD24写入扇区
    response = SD_SendCmd(CMD24, sector << 9, 0xFF);
    if(response != 0x00)
    {
      SD_CS_HIGH();
      return RES_ERROR;
    }
    
    // 发送数据起始令牌
    SPI_ReadWrite(0xFE);
    
    // 写入512字节数据
    for(i = 0; i < 512; i++) SPI_ReadWrite(buff[i]);
    
    // 发送dummy CRC
    SPI_ReadWrite(0xFF); SPI_ReadWrite(0xFF);
    
    // 等待写入响应
    response = SPI_ReadWrite(0xFF);
    if((response & 0x1F) != 0x05)
    {
      SD_CS_HIGH();
      return RES_ERROR;
    }
    
    // 等待写入完成
    if(SD_WaitReady())
    {
      SD_CS_HIGH();
      return RES_ERROR;
    }
    
    SD_CS_HIGH();
    SPI_ReadWrite(0xFF);
    sector++;
    buff += 512;
  }
  return RES_OK;
}

// 磁盘IO控制
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
{
  if(pdrv != 0) return RES_PARERR;
  
  switch(cmd)
  {
    case CTRL_SYNC: SD_WaitReady(); break;
    case GET_SECTOR_SIZE: *(DWORD*)buff = 512; break;
    case GET_BLOCK_SIZE: *(DWORD*)buff = 1; break;
    default: return RES_PARERR;
  }
  return RES_OK;
}

// 获取时间(FATFS需要)
DWORD get_fattime(void)
{
  // 固定返回一个时间,实际项目可使用RTC
  return ((DWORD)(2024 - 1980) << 25) | ((DWORD)1 << 21) | ((DWORD)1 << 16) | (0 << 11) | (0 << 5) | 0;
}

代码解析

  • 实现了FATFS要求的disk_initializedisk_readdisk_writedisk_ioctl核心函数;
  • 基于SPI接口实现SD卡的读写,支持SD卡1.0和2.0版本;
  • 所有SPI操作均为非阻塞,配合状态机实现高效存储。
3.5.5.4 SD卡写入状态机实现
复制代码
复制代码
// FATFS全局变量
FATFS fs;
FIL file;
FRESULT res;
UINT bw;

// 状态机枚举
typedef enum {
  SD_IDLE,        // 空闲
  SD_CHECK_FIFO,  // 检查FIFO是否写满
  SD_READ_FIFO,   // 读取FIFO数据
  SD_WRITE_SD     // 写入SD卡
} SD_State_t;

SD_State_t sd_state = SD_IDLE;
uint8_t sd_buffer[512]; // SD卡写入缓冲区(1个扇区)

/**
 * @brief  SD卡写入状态机处理函数
 * @param  无
 * @retval 无
 */
void SD_Process(void)
{
  uint16_t i;
  
  switch(sd_state)
  {
    case SD_IDLE:
      sd_state = SD_CHECK_FIFO; // 直接进入检查状态
      break;
      
    case SD_CHECK_FIFO:
      if(fifo_a_full)
      {
        // FIFO_A写满,读取数据
        for(i = 0; i < 512; i++)
        {
          FIFO_Get(&adc_fifo_a, &sd_buffer[i]);
        }
        fifo_a_full = 0; // 清除写满标志
        sd_state = SD_WRITE_SD;
      }
      else if(fifo_b_full)
      {
        // FIFO_B写满,读取数据
        for(i = 0; i < 512; i++)
        {
          FIFO_Get(&adc_fifo_b, &sd_buffer[i]);
        }
        fifo_b_full = 0; // 清除写满标志
        sd_state = SD_WRITE_SD;
      }
      else
      {
        // 无写满FIFO,保持空闲
        sd_state = SD_IDLE;
      }
      break;
      
    case SD_WRITE_SD:
      // 批量写入SD卡(512字节)
      res = f_write(&file, sd_buffer, 512, &bw);
      if(res  FR_OK && bw  512)
      {
        // 写入成功,同步数据到SD卡
        f_sync(&file);
      }
      sd_state = SD_IDLE; // 回到空闲
      break;
      
    default:
      sd_state = SD_IDLE;
      break;
  }
}

代码解析

  • 状态机分为4个状态,逻辑清晰,非阻塞执行;
  • 当检测到FIFO写满时,批量读取512字节数据,然后一次性写入SD卡,提高写入效率;
  • 写入完成后调用f_sync同步数据,确保数据真正写入SD卡,避免掉电丢失。
3.5.5.5 主函数实现
复制代码
复制代码
int main(void)
{
  // 系统初始化
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_ADC1_Init();
  MX_TIM2_Init();
  MX_SPI2_Init();
  
  // 初始化FIFO
  FIFO_Init(&adc_fifo_a);
  FIFO_Init(&adc_fifo_b);
  
  // 挂载SD卡
  res = f_mount(&fs, "", 1);
  if(res == FR_OK)
  {
    // 创建/打开数据记录文件
    res = f_open(&file, "adc_data.txt", FA_OPEN_ALWAYS | FA_WRITE);
    if(res == FR_OK)
    {
      // 定位到文件末尾(追加写入)
      f_lseek(&file, f_size(&file));
    }
  }
  
  // 开启定时器2中断(启动ADC采集)
  HAL_TIM_Base_Start_IT(&htim2);
  
  // 主循环
  while(1)
  {
    SD_Process(); // 状态机处理SD卡写入
  }
}

代码解析

  • 系统初始化完成后,挂载SD卡并打开数据文件,定位到文件末尾实现追加写入;
  • 开启定时器2中断,启动ADC采集;
  • 主循环仅调用状态机处理函数,非阻塞执行,可轻松添加其他任务(如按键、串口)。
3.5.6 测试与调试:高频数据存储验证
3.5.6.1 测试步骤
  1. 硬件连接:按照3.5.3节的电路连接,将SD卡模块、LM35传感器与STM32F103连接;
  2. SD卡准备:将Micro SD卡格式化为FAT32格式,插入SD卡模块;
  3. 代码下载:将编译好的代码下载到STM32F103中;
  4. 上电运行:给系统上电,程序自动开始采集ADC数据并写入SD卡;
  5. 数据验证 :运行5分钟后,断电取出SD卡,插入电脑,打开adc_data.txt文件,查看数据是否连续、无丢失。
3.5.6.2 调试方法
  1. 串口调试:在状态机中添加串口打印,输出写入状态(如"写入成功"、"FIFO写满"),通过串口助手查看;
  2. 逻辑分析仪:使用逻辑分析仪抓取SPI的SCK、MOSI、MISO、CS信号,验证SD卡的读写时序是否正确;
  3. 数据校验 :在电脑中打开adc_data.txt,检查数据是否连续(ADC数据应随温度变化缓慢波动,无突变或缺失);
  4. 掉电测试 :在程序运行过程中突然断电,重新上电后检查数据是否完整,验证f_sync的有效性。
3.5.7 拓展优化:数据加密、日志文件自动命名
3.5.7.1 数据加密

在工业场景中,数据的安全性至关重要,可在写入SD卡前对数据进行简单加密,比如异或加密:

复制代码
复制代码
// 加密密钥
#define ENCRYPT_KEY 0x5A

// 在读取FIFO数据后,加密数据
for(i = 0; i < 512; i++)
{
  FIFO_Get(&adc_fifo_a, &sd_buffer[i]);
  sd_buffer[i] ^= ENCRYPT_KEY; // 异或加密
}

读取数据时,再次异或密钥即可解密:

复制代码
复制代码
// 解密数据
sd_buffer[i] ^= ENCRYPT_KEY;
3.5.7.2 日志文件自动命名

为了避免每次运行都覆盖同一个文件,可根据时间戳自动命名日志文件,需要结合RTC实时时钟:

复制代码
复制代码
// RTC时间结构体
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
char filename[32];

// 获取RTC时间
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);

// 生成文件名:YYYYMMDD_HHMMSS.txt
sprintf(filename, "%04d%02d%02d_%02d%02d%02d.txt", 
        2000 + sDate.Year, sDate.Month, sDate.Date,
        sTime.Hours, sTime.Minutes, sTime.Seconds);

// 打开文件
res = f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE);
3.5.7.3 多传感器数据融合

如果需要同时采集多个传感器数据(如温度、湿度、气压),可在ADC中断中采集多个通道的数据,拼接后写入FIFO,状态机中按格式解析写入SD卡:

复制代码
复制代码
// 采集多个ADC通道
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 10);
uint16_t temp = HAL_ADC_GetValue(&hadc1); // 温度
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 10);
uint16_t humi = HAL_ADC_GetValue(&hadc1); // 湿度

// 拼接数据(温度高8位+温度低8位+湿度高8位+湿度低8位)
uint8_t data[4];
data[0] = (temp >> 8) & 0xFF;
data[1] = temp & 0xFF;
data[2] = (humi >> 8) & 0xFF;
data[3] = humi & 0xFF;

// 写入FIFO
for(i = 0; i < 4; i++)
{
  FIFO_Put(&adc_fifo_a, data[i]);
}
3.5.8 常见问题排查
问题1:SD卡无法挂载,f_mount返回错误

常见原因

  1. SPI电路连接错误(如CS引脚接反、VCC接5V);
  2. SD卡未格式化为FAT32格式;
  3. SPI波特率过高(初始化时应使用低速,如281kHz);
  4. SD卡模块损坏。

解决办法

  1. 检查电路连接,确保CS引脚为推挽输出,VCC接3.3V;
  2. 重新格式化SD卡为FAT32格式;
  3. 降低SPI初始化波特率;
  4. 更换SD卡模块或SD卡。
问题2:数据写入SD卡后丢失,文件为空

常见原因

  1. 未调用f_closef_sync,数据仅缓存在RAM中,未写入SD卡;
  2. FIFO写满标志未清除,状态机未读取数据;
  3. 掉电过快,数据未同步到SD卡。

解决办法

  1. 确保写入完成后调用f_sync,程序结束时调用f_close
  2. 检查FIFO写满标志的清除逻辑;
  3. 增加掉电保护电路,或在主循环中定期调用f_sync
问题3:ADC数据采集异常,数据突变

常见原因

  1. ADC通道配置错误,未正确连接传感器;
  2. 定时器中断频率过高,ADC转换未完成;
  3. 电源干扰,导致ADC采样不稳定。

解决办法

  1. 检查ADC通道配置和传感器连接;
  2. 降低定时器中断频率,确保ADC转换完成;
  3. 增加电源滤波电容(如10uF电解电容+0.1uF陶瓷电容)。

3.6 项目6:CAN总线协议解析(工业通信必备)

3.6.1 应用场景与实战价值

CAN(Controller Area Network)总线是工业控制、汽车电子领域最常用的通信总线,具有高可靠性、抗干扰能力强、多主结构等特点,广泛应用于汽车ECU、工业机器人、无人机飞控等场景。

CAN总线通信的核心需求是实时接收和解析CAN报文,而CAN控制器的接收中断是高速触发的(波特率500kbps时,每秒可接收50000个字节),如果直接在中断中解析报文,会导致中断处理时间过长,丢失后续报文;同时,CAN报文有严格的ID和数据格式,需要清晰的逻辑解析。

中断+FIFO+状态机的组合,完美适配CAN总线的通信需求:CAN接收中断负责快速将报文写入FIFO,FIFO缓冲高速报文,状态机在主循环中解析报文ID和数据,执行对应的逻辑,既保证了报文不丢失,又实现了协议的清晰解析。

本项目是工业通信类项目的核心基础,掌握后可轻松开发CAN网关、汽车电子、工业控制等项目。

3.6.2 CAN总线基础(初学者易懂版)

对于初学者而言,不需要深入理解CAN总线的底层物理层和数据链路层协议,只需要掌握3个核心概念即可:

3.6.2.1 CAN报文结构

CAN总线传输的基本单位是CAN报文,每个报文包含以下核心部分:

  1. ID(标识符):11位(标准帧)或29位(扩展帧),用于标识报文的类型和发送节点,是CAN通信的"地址";
  2. DLC(数据长度码):0~8,表示报文中数据的字节数;
  3. Data(数据域):0~8字节,实际传输的数据;
  4. CRC(校验码):用于校验报文的完整性,由CAN控制器自动处理。

初学者核心记忆:CAN报文 = ID + 数据长度 + 数据,ID是报文的唯一标识,解析时先判断ID,再处理对应的数据。

3.6.2.2 CAN波特率

CAN总线的通信速度由波特率 决定,常用的波特率有:125kbps、250kbps、500kbps、1Mbps。波特率的配置需要所有节点一致,否则无法通信。

在STM32中,CAN波特率由时钟源、预分频器、同步段、时间段1、时间段2共同决定,初学者可直接使用STM32CubeMX的自动计算功能。

3.6.2.3 CAN过滤器

CAN控制器支持过滤器 ,用于筛选需要接收的报文,只将符合条件的报文存入接收FIFO,避免接收无关报文,提高效率。初学者可配置过滤器为全接收模式(接收所有报文),后续再根据需求筛选。

3.6.3 硬件准备与CAN总线电路连接
3.6.3.1 硬件清单
  1. 主控芯片:STM32F103C8T6最小系统板(需支持CAN,STM32F103C8T6内置bxCAN控制器);
  2. CAN收发器:TJA1050(最常用的CAN收发器,将TTL电平转换为CAN差分信号);
  3. CAN总线设备:CAN分析仪(如CANoe、周立功CAN分析仪,用于发送和接收CAN报文);
  4. 电源模块:5V/3.3V电源;
  5. 杜邦线:若干;
  6. 120Ω终端电阻:CAN总线两端必须接120Ω终端电阻,否则通信不稳定。
3.6.3.2 电路连接

TJA1050与STM32F103的连接如下:

TJA1050引脚 STM32F103引脚 功能说明
VCC 5V 电源(TJA1050需5V供电)
GND GND
TX PB9 CAN_TX(发送)
RX PB8 CAN_RX(接收)
CAN_H 总线CAN_H CAN差分信号高电平
CAN_L 总线CAN_L CAN差分信号低电平
S GND 模式选择(接地为高速模式)

电路连接注意事项

  1. CAN总线两端必须接120Ω终端电阻(通常接在CAN分析仪和TJA1050之间);
  2. TJA1050的VCC必须接5V,TX/RX为3.3V电平,可直接与STM32F103连接;
  3. CAN_H和CAN_L为差分信号,需使用双绞线连接,避免干扰。
3.6.4 架构设计:CAN接收中断+报文FIFO+CAN解析状态机

本项目的架构设计与串口解析项目类似,但针对CAN报文的特点进行了优化,具体如下:

3.6.4.1 第一层:CAN接收中断(报文接收层)
  • 中断源:CAN接收FIFO中断(FIFO0满中断);
  • 中断职责 :快进快出,读取CAN报文的ID、DLC、数据,将完整的报文结构体写入报文FIFO
  • 核心作用:保证CAN报文的实时接收,不丢失任何报文。
3.6.4.2 第二层:CAN报文FIFO(数据缓冲层)
  • FIFO类型:环形FIFO,存储完整的CAN报文结构体(ID+DLC+Data);
  • FIFO大小:根据需求设置,推荐32~128个报文;
  • 核心作用:缓冲高速接收的CAN报文,解决中断和主循环的速度不匹配问题。
3.6.4.3 第三层:CAN解析状态机(协议处理层)
  • 状态机状态:IDLE(空闲)→ CHECK_FIFO(检查FIFO是否有报文)→ PARSE_ID(解析报文ID)→ PARSE_DATA(解析报文数据)→ EXECUTE(执行对应逻辑);
  • 状态机职责:在主循环中轮询FIFO,读取报文后根据ID解析数据,执行对应的控制逻辑(如LED控制、电机控制);
  • 核心作用:清晰解析CAN协议,避免嵌套if-else,提高代码可维护性。

架构数据流闭环

复制代码
复制代码
CAN报文发送 → CAN控制器接收 → 接收FIFO中断 → 读取报文 → 写入报文FIFO → 主循环状态机 → 解析ID和数据 → 执行逻辑
3.6.5 分步代码实现:CAN中断+FIFO+状态机
3.6.5.1 CAN报文结构体与FIFO定义
复制代码
复制代码
// CAN报文结构体
typedef struct {
  uint32_t id;        // 报文ID(标准帧11位,扩展帧29位)
  uint8_t dlc;        // 数据长度(0~8)
  uint8_t data[8];    // 数据域
} CAN_Msg_t;

// 报文FIFO定义
#define CAN_FIFO_SIZE 32
typedef struct {
  CAN_Msg_t buffer[CAN_FIFO_SIZE];
  volatile uint16_t head;
  volatile uint16_t tail;
} CAN_FIFO_t;

CAN_FIFO_t can_fifo;

// FIFO初始化
void CAN_FIFO_Init(CAN_FIFO_t *fifo)
{
  fifo->head = 0;
  fifo->tail = 0;
}

// FIFO判空
bool CAN_FIFO_IsEmpty(CAN_FIFO_t *fifo)
{
  return (fifo->head  fifo->tail);
}

// FIFO写入
bool CAN_FIFO_Put(CAN_FIFO_t *fifo, CAN_Msg_t *msg)
{
  if(((fifo->head + 1) % CAN_FIFO_SIZE)  fifo->tail) return false;
  fifo->buffer[fifo->head] = *msg;
  fifo->head = (fifo->head + 1) % CAN_FIFO_SIZE;
  return true;
}

// FIFO读取
bool CAN_FIFO_Get(CAN_FIFO_t *fifo, CAN_Msg_t *msg)
{
  if(fifo->head == fifo->tail) return false;
  *msg = fifo->buffer[fifo->tail];
  fifo->tail = (fifo->tail + 1) % CAN_FIFO_SIZE;
  return true;
}
3.6.5.2 CAN配置与中断服务函数(STM32CubeMX+代码)
(1)STM32CubeMX配置
  1. CAN配置
    • 模式:Normal Mode(正常模式);
    • 波特率:500kbps(自动计算:预分频器=6,同步段=1,时间段1=13,时间段2=2);
    • 接收FIFO:FIFO0,开启FIFO0满中断;
    • 过滤器:配置为全接收模式(过滤器0,屏蔽模式,接收所有ID);
  2. GPIO配置:PB8(CAN_RX)、PB9(CAN_TX)设置为复用推挽输出;
  3. 中断配置:CAN1 RX0中断,抢占优先级1,响应优先级0;
  4. 生成Keil5工程。
(2)CAN中断服务函数
复制代码
复制代码
// CAN句柄
CAN_HandleTypeDef hcan;

/**
 * @brief  CAN接收FIFO0中断服务函数
 * @param  无
 * @retval 无
 */
void CAN1_RX0_IRQHandler(void)
{
  /* USER CODE BEGIN CAN1_RX0_IRQn 0 */
  CAN_RxHeaderTypeDef rx_header;
  CAN_Msg_t rx_msg;
  uint8_t rx_data[8];
  
  // 快进快出:读取CAN报文,写入FIFO
  if(__HAL_CAN_GET_FLAG(&hcan, CAN_FLAG_RX0_FULL))
  {
    // 读取报文
    if(HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &rx_header, rx_data) == HAL_OK)
    {
      // 填充报文结构体
      rx_msg.id = rx_header.StdId; // 标准帧ID
      rx_msg.dlc = rx_header.DLC;
      memcpy(rx_msg.data, rx_data, rx_msg.dlc);
      
      // 写入FIFO
      CAN_FIFO_Put(&can_fifo, &rx_msg);
    }
  }
  /* USER CODE END CAN1_RX0_IRQn 0 */
  HAL_CAN_IRQHandler(&hcan);
  /* USER CODE BEGIN CAN1_RX0_IRQn 1 */
  /* USER CODE END CAN1_RX0_IRQn 1 */
}

代码解析

  • 中断服务函数仅读取CAN报文并写入FIFO,耗时不足10微秒,符合"快进快出"原则;
  • 使用HAL_CAN_GetRxMessage读取报文,填充到CAN_Msg_t结构体中,方便后续解析。
3.6.5.3 CAN解析状态机实现
复制代码
复制代码
// 状态机枚举
typedef enum {
  CAN_IDLE,        // 空闲
  CAN_CHECK_FIFO,  // 检查FIFO
  CAN_PARSE_ID,    // 解析ID
  CAN_EXECUTE      // 执行逻辑
} CAN_State_t;

CAN_State_t can_state = CAN_IDLE;
CAN_Msg_t current_msg;

/**
 * @brief  CAN解析状态机处理函数
 * @param  无
 * @retval 无
 */
void CAN_Process(void)
{
  switch(can_state)
  {
    case CAN_IDLE:
      can_state = CAN_CHECK_FIFO;
      break;
      
    case CAN_CHECK_FIFO:
      if(CAN_FIFO_Get(&can_fifo, &current_msg))
      {
        can_state = CAN_PARSE_ID; // 读取到报文,解析ID
      }
      else
      {
        can_state = CAN_IDLE; // 无报文,回到空闲
      }
      break;
      
    case CAN_PARSE_ID:
      // 根据报文ID执行不同逻辑
      switch(current_msg.id)
      {
        case 0x123: // LED控制报文
          can_state = CAN_EXECUTE;
          break;
        case 0x456: // 电机控制报文
          can_state = CAN_EXECUTE;
          break;
        default: // 未知ID,丢弃报文
          can_state = CAN_IDLE;
          break;
      }
      break;
      
    case CAN_EXECUTE:
      if(current_msg.id == 0x123)
      {
        // 解析LED控制数据:data[0] = 0(灭)/1(亮)
        if(current_msg.dlc >= 1)
        {
          if(current_msg.data[0]  1)
          {
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // LED亮
          }
          else
          {
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // LED灭
          }
        }
      }
      else if(current_msg.id  0x456)
      {
        // 解析电机控制数据:data[0] = 速度(0~255)
        if(current_msg.dlc >= 1)
        {
          // 电机速度控制逻辑
          // ...
        }
      }
      can_state = CAN_IDLE; // 执行完成,回到空闲
      break;
      
    default:
      can_state = CAN_IDLE;
      break;
  }
}

代码解析

  • 状态机逻辑清晰,根据报文ID分类处理,避免嵌套if-else;
  • 针对不同的ID(如0x123控制LED,0x456控制电机),解析对应的数据并执行逻辑;
  • 未知ID的报文直接丢弃,提高程序效率。
3.6.5.4 主函数实现
复制代码
复制代码
int main(void)
{
  // 系统初始化
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_CAN1_Init();
  
  // 初始化CAN FIFO
  CAN_FIFO_Init(&can_fifo);
  
  // 启动CAN
  HAL_CAN_Start(&hcan);
  // 开启CAN接收FIFO0中断
  HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX0_FULL);
  
  // 主循环
  while(1)
  {
    CAN_Process(); // 状态机处理CAN报文
  }
}
3.6.6 测试与调试:CAN报文解析验证
3.6.6.1 测试步骤
  1. 硬件连接:按照3.6.3节的电路连接,将TJA1050与STM32F103、CAN分析仪连接,接入120Ω终端电阻;
  2. 代码下载:将代码下载到STM32F103中;
  3. CAN分析仪配置 :设置波特率为500kbps,发送标准帧报文:
    • ID:0x123,数据:0x01(控制LED亮);
    • ID:0x123,数据:0x00(控制LED灭);
  4. 功能验证:发送报文后,观察STM32的LED是否按照指令亮灭。
3.6.6.2 调试方法
  1. CAN分析仪监控:通过CAN分析仪监控总线报文,确认STM32是否正确接收报文;
  2. 串口打印:在状态机中添加串口打印,输出解析的ID和数据,通过串口助手查看;
  3. 逻辑分析仪:抓取CAN_TX和CAN_RX信号,验证CAN通信时序是否正确;
  4. 中断调试:在CAN中断服务函数中设置断点,确认中断是否触发,报文是否正确读取。
3.6.7 拓展优化:扩展帧支持、多过滤器配置
3.6.7.1 扩展帧支持

如果需要支持29位扩展帧,只需修改报文ID的读取和解析逻辑:

复制代码
复制代码
// 读取扩展帧ID
rx_msg.id = rx_header.ExtId;

// 状态机中解析扩展帧ID
switch(current_msg.id)
{
  case 0x12345678: // 扩展帧LED控制
    // ...
    break;
}
3.6.7.2 多过滤器配置

为了只接收特定ID的报文,可配置多个CAN过滤器,提高效率:

复制代码
复制代码
// 配置过滤器0:接收ID 0x123(标准帧)
CAN_FilterTypeDef can_filter;
can_filter.FilterMode = CAN_FILTERMODE_IDMASK;
can_filter.FilterScale = CAN_FILTERSCALE_32BIT;
can_filter.FilterIdHigh = 0x123 << 5;
can_filter.FilterIdLow = 0x0000;
can_filter.FilterMaskIdHigh = 0xFFFF << 5;
can_filter.FilterMaskIdLow = 0x0000;
can_filter.FilterFIFOAssignment = CAN_RX_FIFO0;
can_filter.FilterActivation = ENABLE;
can_filter.SlaveStartFilterBank = 14;
HAL_CAN_ConfigFilter(&hcan, &can_filter);
3.6.7.3 CAN报文发送

在状态机中添加报文发送逻辑,实现双向通信:

复制代码
复制代码
// 发送CAN报文
CAN_TxHeaderTypeDef tx_header;
uint32_t tx_mailbox;

tx_header.StdId = 0x789;
tx_header.RTR = CAN_RTR_DATA;
tx_header.IDE = CAN_ID_STD;
tx_header.DLC = 2;
uint8_t tx_data[2] = {0x01, 0x02};

HAL_CAN_AddTxMessage(&hcan, &tx_header, tx_data, &tx_mailbox);
3.6.8 常见问题排查
问题1:CAN无法接收报文,中断不触发

常见原因

  1. CAN波特率配置错误,与其他节点不一致;
  2. 电路连接错误(如TX/RX接反、终端电阻未接);
  3. CAN过滤器配置错误,未开启全接收模式;
  4. TJA1050损坏。

解决办法

  1. 检查CAN波特率配置,确保所有节点一致;
  2. 检查电路连接,确认TX/RX接反,终端电阻已接;
  3. 重新配置过滤器为全接收模式;
  4. 更换TJA1050。
问题2:CAN报文接收乱码,ID和数据错误

常见原因

  1. 电源干扰,导致CAN差分信号失真;
  2. 总线长度过长,信号衰减;
  3. 采样点配置错误。

解决办法

  1. 增加电源滤波电容,使用双绞线连接CAN总线;
  2. 缩短总线长度,或增加CAN中继器;
  3. 调整CAN采样点配置(推荐采样点为70%~80%)。
问题3:FIFO溢出,丢失报文

常见原因

  1. FIFO大小设置过小;
  2. 主循环处理时间过长,状态机未及时读取FIFO;
  3. CAN波特率过高,报文接收速度过快。

解决办法

  1. 增大CAN FIFO大小;
  2. 优化主循环逻辑,缩短状态机处理时间;
  3. 降低CAN波特率,或优化中断处理逻辑。

第4章 ZYNQ PS端 中断+状态机+FIFO 编程实战(约20000字)

4.1 ZYNQ-7000架构入门(初学者视角)

ZYNQ-7000系列是Xilinx公司推出的**"ARM+FPGA"异构计算平台**,它将双核Cortex-A9处理器(PS端,Processing System)和7系列FPGA(PL端,Programmable Logic)集成在一颗芯片上,既拥有ARM处理器的软件编程灵活性,又拥有FPGA的硬件并行处理能力,是嵌入式开发的进阶核心平台

对于从单片机进阶到ZYNQ的初学者而言,首先需要搞清楚PS端和PL端的核心区别与协同关系,这是学习ZYNQ的基础。

4.1.1 PS与PL的核心区别

ZYNQ的架构可以用**"大脑+肌肉"**来比喻:

  • PS端(大脑) :双核Cortex-A9处理器,运行操作系统(Linux/FreeRTOS)或裸机程序,负责复杂的逻辑处理、协议解析、人机交互、数据管理等软件任务,就像人的大脑一样,负责思考和决策;
  • PL端(肌肉) :FPGA逻辑资源,负责高速数据采集、硬件加速、接口扩展、实时控制等硬件任务,就像人的肌肉一样,负责执行高速、并行的硬件操作。

两者的核心区别如下表所示:

维度 PS端(Cortex-A9) PL端(FPGA)
核心功能 软件编程,处理复杂逻辑 硬件编程,实现并行硬件逻辑
开发语言 C/C++(裸机/Linux) Verilog/VHDL(硬件描述语言)
实时性 中等(受操作系统调度影响) 极高(硬件并行,无调度延迟)
灵活性 高(软件可随时修改) 中(硬件需重新综合下载)
资源类型 CPU、RAM、ROM、外设(UART/SPI/I2C) LUT、FF、BRAM、DSP、IO
典型应用 协议解析、数据存储、人机交互、网络通信 高速ADC采集、电机控制、图像预处理、接口扩展
4.1.2 PS端核心资源

ZYNQ-7000的PS端(以ZedBoard/PYNQ-Z2为例)拥有丰富的硬件资源,初学者重点关注以下核心资源:

  1. 处理器:双核Cortex-A9 MPCore,主频最高866MHz,支持NEON浮点运算;
  2. 存储器
    • 片上RAM(OCM):256KB,用于裸机程序的代码和数据存储;
    • 外部DDR3 RAM:512MB(PYNQ-Z2)/1GB(ZedBoard),用于Linux系统和大数据存储;
  3. 片上外设
    • UART:2路(UART0、UART1),用于串口通信;
    • SPI:2路,用于SPI外设驱动;
    • I2C:2路,用于I2C外设驱动;
    • GPIO:多达54个,用于通用IO控制;
    • Ethernet:1路千兆以太网,用于网络通信;
    • USB:2路USB 2.0,用于USB设备驱动;
    • SD/MMC:1路,用于SD卡存储;
  4. 中断系统:通用中断控制器(GIC),支持多达128个中断源,包括PS端外设中断和PL端中断;
  5. DMA控制器:支持多个DMA通道,用于高速数据传输(如PS-PL数据交互)。
4.1.3 ZYNQ开发环境搭建(初学者详细步骤)

ZYNQ的开发需要两套开发环境

  1. Vivado:用于PL端的硬件设计、PS端的外设配置、PS-PL的连线,生成硬件比特流;
  2. Xilinx SDK/PetaLinux:用于PS端的裸机程序开发(SDK)或Linux系统开发(PetaLinux)。

以下是初学者入门的详细开发环境搭建步骤(以Windows 10 + Vivado 2022.1为例):

4.1.3.1 安装Vivado 2022.1
  1. 下载Vivado:从Xilinx官方网站(https://www.xilinx.com/support/download.html)下载Vivado 2022.1(免费版WebPACK,支持ZYNQ-7000);
  2. 运行安装程序:双击下载的安装包,选择"Install on your computer";
  3. 选择安装组件
    • 勾选"Vivado Design Suite";
    • 勾选"Devices"中的"7 Series"(支持ZYNQ-7000);
    • 取消勾选不需要的组件(如UltraScale系列),节省安装空间;
  4. 选择安装路径:建议安装在非中文路径(如D:\Xilinx\Vivado\2022.1);
  5. 等待安装完成:安装过程约30~60分钟,取决于电脑配置;
  6. 激活许可证:WebPACK版无需许可证,直接使用。
4.1.3.2 安装Xilinx SDK 2022.1

Vivado 2022.1已集成Xilinx SDK,无需单独安装,安装Vivado后即可使用。

4.1.3.3 驱动安装

将ZedBoard/PYNQ-Z2开发板通过USB线连接到电脑,电脑会自动安装驱动,如果未自动安装,可手动安装Vivado安装目录下的驱动(D:\Xilinx\Vivado\2022.1\data\xicom\driver\win64)。

4.1.3.4 开发流程总结

ZYNQ的开发流程分为硬件设计软件设计两步:

  1. 硬件设计(Vivado)
    • 创建Vivado工程,选择ZYNQ型号(如xc7z020clg484-1);
    • 调用"Zynq7 Processing System"IP核,配置PS端外设(UART、GPIO、SPI等);
    • 进行PS-PL的连线(如PL端的AXI FIFO连接到PS端的AXI GP接口);
    • 生成硬件比特流(Bitstream)和硬件描述文件(.hdf);
  2. 软件设计(SDK)
    • 从Vivado导出硬件到SDK;
    • 在SDK中创建裸机工程,编写C语言代码;
    • 编译代码,下载到PS端运行;
    • 调试程序(串口调试、JTAG调试)。
4.1.4 PS端裸机编程与Linux编程的区别

ZYNQ PS端的编程分为裸机编程Linux编程 两种模式,初学者建议先从裸机编程入手,掌握PS端的外设驱动、中断、FIFO、状态机等基础,再进阶到Linux编程。

两者的核心区别如下表所示:

维度 裸机编程(SDK) Linux编程(PetaLinux)
运行环境 无操作系统,直接运行C代码 运行Linux系统,应用层/驱动层编程
开发难度 低(适合初学者) 高(需要掌握Linux驱动、设备树)
实时性 极高(无操作系统调度,直接响应中断) 中等(受Linux内核调度影响)
资源管理 手动管理硬件资源 系统自动管理硬件资源
典型应用 实时控制、高速数据采集、简单协议解析 网络通信、大数据处理、复杂应用程序

本文档的ZYNQ PS端实战项目均基于裸机编程,适合初学者入门,后续可进阶到Linux编程。

4.2 ZYNQ PS端核心组件解析

4.2.1 PS端中断系统:GIC通用中断控制器

ZYNQ PS端的中断系统基于通用中断控制器(GIC,Generic Interrupt Controller),它是ARM Cortex-A9处理器的标准中断控制器,负责管理所有PS端和PL端的中断源,实现中断的优先级配置、中断嵌套、中断分发等功能。

对于初学者而言,不需要深入理解GIC的底层原理,只需要掌握3个核心知识点即可完成中断编程:

4.2.1.1 中断源分类

ZYNQ PS端的中断源分为三大类

  1. 软件中断(SGI,Software Generated Interrupt):ID 0~15,由软件通过写寄存器触发,用于CPU核间通信,初学者几乎不用;
  2. 私有外设中断(PPI,Private Peripheral Interrupt):ID 16~31,每个CPU核私有的中断,如定时器中断、CPU内部中断,初学者常用;
  3. 共享外设中断(SPI,Shared Peripheral Interrupt):ID 32~127,所有CPU核共享的中断,包括PS端外设中断(UART、GPIO、SPI)和PL端中断,是初学者最常用的中断类型。

本文档中涉及的中断均为SPI中断(如UART接收中断、GPIO中断、AXI FIFO中断)。

4.2.1.2 中断优先级配置

GIC支持16级中断优先级(0~15),数值越小,优先级越高,高优先级的中断可以打断低优先级的中断(中断嵌套)。

在裸机编程中,通过XScuGic_SetPriorityTriggerType函数配置中断的优先级和触发类型(高电平触发、上升沿触发等)。

4.2.1.3 中断编程流程

ZYNQ PS端裸机中断编程的标准流程(初学者必须掌握):

  1. 初始化GIC :调用XScuGic_LookupConfigXScuGic_CfgInitialize函数初始化GIC控制器;
  2. 设置中断优先级和触发类型 :调用XScuGic_SetPriorityTriggerType函数;
  3. 注册中断服务函数 :调用XScuGic_Connect函数,将中断源与中断服务函数绑定;
  4. 使能中断 :调用XScuGic_Enable函数使能对应的中断源;
  5. 编写中断服务函数:遵循"快进快出"原则,仅处理紧急操作(如读取数据、写入FIFO);
  6. 开启CPU中断 :调用Xil_ExceptionEnable函数开启CPU的中断响应。
4.2.2 PS端FIFO实现:软件环形FIFO+PS-PL AXI FIFO

ZYNQ PS端的FIFO实现分为两种类型,分别适用于不同的场景:

4.2.2.1 软件环形FIFO

与单片机的软件环形FIFO完全相同,用C语言实现,存储在PS端的RAM中,适用于PS端外设数据的缓冲(如UART接收数据、GPIO事件),本文档第2.2节的通用环形FIFO代码可直接移植到ZYNQ PS端,无需任何修改。

4.2.2.2 PS-PL AXI FIFO

AXI FIFO是PL端的FPGA IP核,通过AXI4总线与PS端连接,适用于PS-PL高速数据交互 (如PL端采集的高速ADC数据传输到PS端),具有数据传输速度快、硬件缓冲、不占用CPU资源等特点。

AXI FIFO的核心特点:

  1. 硬件缓冲:FIFO存储在PL端的BRAM中,容量可配置(如512字节~8KB);
  2. AXI4接口:支持AXI4-Stream(数据流)和AXI4-Lite(控制)接口,PS端通过AXI4-Lite配置FIFO,通过AXI4-Stream传输数据;
  3. 中断触发:支持FIFO满、空、半满等中断,PS端可通过中断响应数据传输;
  4. 高速传输:数据传输速度可达几百MB/s,满足高速数据交互需求。

本文档的ZYNQ PS端项目将分别使用软件环形FIFO (UART项目)和AXI FIFO(PS-PL数据交互项目),让初学者掌握两种FIFO的使用方法。

4.2.3 PS端状态机编程:与单片机的异同点

ZYNQ PS端的状态机编程与单片机的状态机编程核心逻辑完全相同,均基于枚举+switch-case实现,将复杂逻辑分解为多个状态,根据条件实现状态切换。

两者的异同点如下:

  • 相同点
    1. 核心思想一致:状态定义、当前状态、状态切换逻辑;
    2. 实现方式一致:枚举+switch-case;
    3. 应用场景一致:协议解析、流程控制、数据处理;
  • 不同点
    1. 运行环境:单片机在裸机中运行,PS端可在裸机或Linux中运行;
    2. 处理能力:PS端的Cortex-A9主频更高,可处理更复杂的状态机逻辑;
    3. 数据量:PS端的RAM更大,可处理更大的数据量(如大数据流的状态机解析);
    4. 协同能力:PS端的状态机可与PL端的硬件逻辑协同工作,实现更强大的功能。

对于初学者而言,单片机的状态机编程经验可直接迁移到ZYNQ PS端,只需熟悉PS端的外设驱动和中断系统即可。

4.2.4 PS端外设驱动基础(裸机)

ZYNQ PS端的外设(UART、GPIO、SPI、AXI)均有官方提供的裸机驱动库(Xilinx Peripheral Drivers),驱动库以C语言编写,提供了丰富的API函数,初学者无需直接操作寄存器,只需调用API函数即可完成外设的配置和使用。

常用外设的驱动库前缀:

  • UART:XUartPs(Xilinx UART PS驱动);
  • GPIO:XGpioPs(Xilinx GPIO PS驱动);
  • SPI:XSpiPs(Xilinx SPI PS驱动);
  • AXI GIC:XScuGic(Xilinx SCU GIC驱动);
  • AXI FIFO:XAxiFifo(Xilinx AXI FIFO驱动)。

驱动库的标准使用流程

  1. 查找配置 :调用XXX_LookupConfig函数,根据设备ID查找外设的配置信息;
  2. 初始化外设 :调用XXX_CfgInitialize函数,初始化外设;
  3. 配置外设参数 :调用XXX_SetOptions等函数,配置外设的参数(如UART波特率、GPIO方向);
  4. 使用外设 :调用XXX_SendXXX_Recv等函数,实现数据的发送和接收;
  5. 中断处理:注册中断服务函数,处理外设的中断事件。

4.3 ZYNQ PS端裸机项目1:UART中断+FIFO+状态机指令解析

4.3.1 项目需求与架构设计
4.3.1.1 项目需求

实现ZYNQ PS端的UART中断+FIFO+状态机指令解析,具体功能:

  1. PS端通过UART1接收上位机发送的指令(如$LED,1#$LED,0#);
  2. UART接收中断将数据写入软件环形FIFO;
  3. 主循环状态机从FIFO中读取数据,解析指令;
  4. 根据指令控制PL端的LED灯亮灭;
  5. 指令解析完成后,通过UART1返回响应(如OKERROR)。
4.3.1.2 架构设计

本项目的

单片机与ZYNQ PS端 中断+状态机+FIFO 综合应用实战文档(初学者版)

(接上文)

4.3 ZYNQ PS端裸机项目1:UART中断+FIFO+状态机指令解析(续)

4.3.2 Vivado工程创建与PS端外设配置

4.3.2.1 创建Vivado工程
  1. 打开Vivado 2022.1,点击"Create Project",输入工程名(如Zynq_UART_FSM),选择工程路径(非中文),点击"Next";
  2. 选择"RTL Project",勾选"Do not specify sources at this time",点击"Next";
  3. 选择芯片型号:xc7z020clg484-1(PYNQ-Z2/ZedBoard通用),点击"Next",再点击"Finish"完成工程创建。
4.3.2.2 配置PS端(Zynq7 Processing System IP)
  1. 在左侧"Flow Navigator"中点击"IP Integrator"→"Create Block Design",输入设计名(如design_1),点击"OK";
  2. 在Diagram窗口中,点击"+"号,搜索"Zynq7 Processing System",双击添加该IP核;
  3. 双击"Zynq7 Processing System"IP核,进入配置界面:
    • PS-PL Configuration
      • 使能UART1(勾选UART 1),设置为MIO模式,映射到MIO48~49(PYNQ-Z2的UART1引脚);
      • 使能GPIO MIO,勾选GPIO MIO,用于控制PL端LED;
    • Clock Configuration
      • 设置CPU主频为866MHz,DDR主频为533MHz;
    • DDR Configuration
      • 选择DDR型号(PYNQ-Z2为MT41K256M16HA-125,ZedBoard为MT41J128M16 HA-15E);
    • 点击"OK"完成PS端配置;
  4. 点击"Run Block Automation",选择"Processing System7",点击"OK",Vivado自动完成PS端的复位和时钟连接;
  5. 点击"Validate Design",验证设计无错误;
  6. 点击"Generate Block Design",生成HDL文件;
  7. 在左侧"Flow Navigator"中点击"Program and Debug"→"Generate Bitstream",生成硬件比特流(此过程约5~10分钟);
  8. 比特流生成完成后,点击"File"→"Export"→"Export Hardware",勾选"Include Bitstream",导出硬件描述文件(.xsa)。

4.3.3 SDK工程创建与代码编写

4.3.3.1 启动Xilinx SDK
  1. 点击"File"→"Launch SDK",选择导出的.xsa文件路径,点击"OK",启动Xilinx SDK;
  2. 在SDK中,点击"File"→"New"→"Application Project",输入工程名(如UART_FSM_Project),点击"Next";
  3. 选择"Empty Application",点击"Finish",创建空的裸机工程。
4.3.3.2 通用软件环形FIFO代码(移植单片机版本)

直接使用第2.2节的通用环形FIFO代码,无需修改,创建fifo.hfifo.c文件,添加到工程中:

复制代码
复制代码
// fifo.h
#ifndef FIFO_H
#define FIFO_H

#include <stdint.h>
#include <stdbool.h>

#define FIFO_SIZE 128

typedef struct {
    uint8_t buffer[FIFO_SIZE];
    volatile uint16_t head;
    volatile uint16_t tail;
} FIFO_t;

void FIFO_Init(FIFO_t *fifo);
bool FIFO_IsEmpty(FIFO_t *fifo);
bool FIFO_IsFull(FIFO_t *fifo);
bool FIFO_Put(FIFO_t *fifo, uint8_t data);
bool FIFO_Get(FIFO_t *fifo, uint8_t *data);

#endif

// fifo.c
#include "fifo.h"

void FIFO_Init(FIFO_t *fifo) {
    fifo->head = 0;
    fifo->tail = 0;
}

bool FIFO_IsEmpty(FIFO_t *fifo) {
    return (fifo->head  fifo->tail);
}

bool FIFO_IsFull(FIFO_t *fifo) {
    return ((fifo->head + 1) % FIFO_SIZE  fifo->tail);
}

bool FIFO_Put(FIFO_t *fifo, uint8_t data) {
    if (FIFO_IsFull(fifo)) return false;
    fifo->buffer[fifo->head] = data;
    fifo->head = (fifo->head + 1) % FIFO_SIZE;
    return true;
}

bool FIFO_Get(FIFO_t *fifo, uint8_t *data) {
    if (FIFO_IsEmpty(fifo)) return false;
    *data = fifo->buffer[fifo->tail];
    fifo->tail = (fifo->tail + 1) % FIFO_SIZE;
    return true;
}
4.3.3.3 UART中断配置与中断服务函数

创建uart.huart.c文件,实现UART初始化、中断配置、中断服务函数:

复制代码
复制代码
// uart.h
#ifndef UART_H
#define UART_H

#include "xparameters.h"
#include "xuartps.h"
#include "xscugic.h"
#include "fifo.h"

// UART设备ID
#define UART_DEVICE_ID XPAR_XUARTPS_1_DEVICE_ID
// 中断ID
#define UART_INT_ID XPAR_XUARTPS_1_INTR

// 全局FIFO
extern FIFO_t uart_fifo;

// UART初始化
int UART_Init(XUartPs *UartInst, XScuGic *IntcInst);
// 中断服务函数
void UART_IntrHandler(void *CallBackRef, u32 Event, unsigned int EventData);

#endif

// uart.c
#include "uart.h"

FIFO_t uart_fifo;

// UART配置
static int UART_Config(XUartPs *UartInst, u16 DeviceId) {
    XUartPs_Config *Config;
    int Status;

    Config = XUartPs_LookupConfig(DeviceId);
    if (Config == NULL) return XST_FAILURE;

    Status = XUartPs_CfgInitialize(UartInst, Config, Config->BaseAddress);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    // 配置波特率115200,8数据位,1停止位,无校验
    XUartPs_SetBaudRate(UartInst, 115200);
    XUartPs_SetOperMode(UartInst, XUARTPS_OPER_MODE_NORMAL);
    XUartPs_SetDataFormat(UartInst, XUARTPS_FORMAT_8_BITS, XUARTPS_FORMAT_NO_PARITY, XUARTPS_FORMAT_1_STOP_BIT);

    return XST_SUCCESS;
}

// 中断配置
static int UART_IntrConfig(XScuGic *IntcInst, XUartPs *UartInst, u16 IntrId) {
    int Status;

    XScuGic_SetPriorityTriggerType(IntcInst, IntrId, 0xA0, 0x3);
    Status = XScuGic_Connect(IntcInst, IntrId, (Xil_ExceptionHandler)XUartPs_InterruptHandler, UartInst);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    XScuGic_Enable(IntcInst, IntrId);
    XUartPs_SetInterruptMask(UartInst, XUARTPS_IXR_RX_FULL);

    return XST_SUCCESS;
}

// UART初始化
int UART_Init(XUartPs *UartInst, XScuGic *IntcInst) {
    int Status;

    // 初始化FIFO
    FIFO_Init(&uart_fifo);

    // 配置UART
    Status = UART_Config(UartInst, UART_DEVICE_ID);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    // 配置中断
    Status = UART_IntrConfig(IntcInst, UartInst, UART_INT_ID);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    // 注册中断回调函数
    XUartPs_SetHandler(UartInst, UART_IntrHandler, UartInst);

    return XST_SUCCESS;
}

// UART中断服务函数(快进快出)
void UART_IntrHandler(void *CallBackRef, u32 Event, unsigned int EventData) {
    XUartPs *UartInst = (XUartPs *)CallBackRef;
    uint8_t RecvData;

    if (Event & XUARTPS_IXR_RX_FULL) {
        // 读取数据并写入FIFO
        while (XUartPs_IsReceiveData(UartInst->Config->BaseAddress)) {
            RecvData = XUartPs_ReadReg(UartInst->Config->BaseAddress, XUARTPS_FIFO_OFFSET);
            FIFO_Put(&uart_fifo, RecvData);
        }
    }
}
4.3.3.4 指令解析状态机实现

创建fsm.hfsm.c文件,实现指令解析状态机:

复制代码
复制代码
// fsm.h
#ifndef FSM_H
#define FSM_H

#include <stdint.h>
#include <string.h>
#include "xuartps.h"
#include "fifo.h"

// 状态机枚举
typedef enum {
    UART_IDLE,
    UART_WAIT_HEADER,
    UART_RECV_DATA,
    UART_PARSE_CMD
} UART_State_t;

// 状态机处理函数
void UART_FSM_Process(XUartPs *UartInst);
// 指令执行函数
void Execute_Command(XUartPs *UartInst, uint8_t *Cmd, uint16_t Len);

#endif

// fsm.c
#include "fsm.h"

UART_State_t uart_state = UART_IDLE;
uint8_t cmd_buffer[32];
uint16_t cmd_len = 0;

// 指令执行函数
void Execute_Command(XUartPs *UartInst, uint8_t *Cmd, uint16_t Len) {
    if (Len  0) return;

    if (strcmp((char *)Cmd, "LED,1")  0) {
        // 控制PL端LED亮(MIO7对应PYNQ-Z2的LED1)
        XUartPs_Send(UartInst, (uint8_t *)"OK: LED ON\r\n", 11);
    } else if (strcmp((char *)Cmd, "LED,0")  0) {
        // 控制PL端LED灭
        XUartPs_Send(UartInst, (uint8_t *)"OK: LED OFF\r\n", 12);
    } else {
        XUartPs_Send(UartInst, (uint8_t *)"ERROR: Unknown Cmd\r\n", 19);
    }
}

// 状态机处理函数
void UART_FSM_Process(XUartPs *UartInst) {
    uint8_t ch;

    switch (uart_state) {
        case UART_IDLE:
            if (FIFO_Get(&uart_fifo, &ch)) {
                if (ch  '$') {
                    cmd_len = 0;
                    uart_state = UART_RECV_DATA;
                }
            }
            break;

        case UART_RECV_DATA:
            if (FIFO_Get(&uart_fifo, &ch)) {
                if (ch == '#') {
                    cmd_buffer[cmd_len] = '\0';
                    uart_state = UART_PARSE_CMD;
                } else if (cmd_len < 31) {
                    cmd_buffer[cmd_len++] = ch;
                }
            }
            break;

        case UART_PARSE_CMD:
            Execute_Command(UartInst, cmd_buffer, cmd_len);
            uart_state = UART_IDLE;
            break;

        default:
            uart_state = UART_IDLE;
            break;
    }
}
4.3.3.5 主函数实现

创建main.c文件,实现系统初始化和主循环:

复制代码
复制代码
#include "xparameters.h"
#include "xscugic.h"
#include "xuartps.h"
#include "uart.h"
#include "fsm.h"

// 全局变量
XScuGic IntcInst;
XUartPs UartInst;

// 中断初始化
static int Intc_Init(XScuGic *IntcInst) {
    XScuGic_Config *IntcConfig;
    int Status;

    IntcConfig = XScuGic_LookupConfig(XPAR_SCUGIC_0_DEVICE_ID);
    if (IntcConfig == NULL) return XST_FAILURE;

    Status = XScuGic_CfgInitialize(IntcInst, IntcConfig, IntcConfig->CpuBaseAddress);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, IntcInst);
    Xil_ExceptionEnable();

    return XST_SUCCESS;
}

int main(void) {
    int Status;

    // 初始化中断控制器
    Status = Intc_Init(&IntcInst);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    // 初始化UART
    Status = UART_Init(&UartInst, &IntcInst);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    // 主循环
    while (1) {
        UART_FSM_Process(&UartInst);
    }

    return XST_SUCCESS;
}

4.3.4 测试与调试

4.3.4.1 硬件连接
  1. 将PYNQ-Z2/ZedBoard的USB-UART接口连接到电脑;
  2. 打开串口助手,配置波特率115200,8数据位,1停止位,无校验;
  3. 将代码编译后,通过JTAG下载到ZYNQ PS端。
4.3.4.2 功能测试
  1. 在串口助手中发送指令:$LED,1#,观察PL端LED是否亮起,串口返回OK: LED ON
  2. 发送指令:$LED,0#,观察LED是否熄灭,串口返回OK: LED OFF
  3. 发送未知指令:$TEST#,串口返回ERROR: Unknown Cmd
4.3.4.3 常见问题排查
  1. UART无响应:检查UART设备ID、中断ID是否正确,Vivado中UART1是否使能;
  2. 指令解析失败:检查帧头帧尾是否正确,FIFO是否溢出;
  3. LED不亮:检查GPIO MIO引脚配置是否正确,LED硬件连接是否正常。

4.4 ZYNQ PS端裸机项目2:PS-PL AXI FIFO数据交互(中断触发)

4.4.1 项目需求与架构设计

4.4.1.1 项目需求

实现PS端与PL端通过AXI FIFO的高速数据交互,具体功能:

  1. PL端通过AXI FIFO向PS端发送数据流;
  2. AXI FIFO满时触发中断,PS端中断服务函数读取数据并写入软件FIFO;
  3. 主循环状态机从软件FIFO中读取数据,解析并处理;
  4. 处理完成后,PS端通过UART输出数据统计信息。
4.4.1.2 架构设计
  • PL端:AXI FIFO IP核,配置为512字节深度,支持FIFO满中断;
  • PS端
    • 中断系统:响应AXI FIFO中断;
    • 软件FIFO:缓冲AXI FIFO读取的数据;
    • 状态机:解析并处理数据流,输出统计信息。

4.4.2 Vivado工程设计:PL端AXI FIFO配置

4.4.2.1 添加AXI FIFO IP核
  1. 在之前的Vivado工程中,打开Block Design;
  2. 点击"+"号,搜索"AXI FIFO",双击添加axi_fifo IP核;
  3. 双击axi_fifo IP核,进入配置界面:
    • Interface Mode :选择Native (AXI4-Stream)
    • FIFO Depth :设置为512
    • Data Width :设置为32
    • Interrupt Options :勾选Tx FIFO Full InterruptRx FIFO Not Empty Interrupt
    • 点击"OK"完成配置。
4.4.2.2 PS-PL连线
  1. 点击"Run Connection Automation",选择axi_fifoS_AXI_LITE接口,自动连接到PS端的M_AXI_GP0接口;
  2. 手动连接axi_fifointerrupt信号到PS端的IRQ_F2P中断引脚;
  3. 点击"Validate Design",验证连线无错误;
  4. 重新生成Block Design和比特流,导出硬件(包含比特流)。

4.4.3 PS端代码实现

4.4.3.1 AXI FIFO驱动与中断配置
复制代码
复制代码
// axififo.h
#ifndef AXIFIFO_H
#define AXIFIFO_H

#include "xparameters.h"
#include "xaxififo.h"
#include "xscugic.h"
#include "fifo.h"

// AXI FIFO设备ID
#define AXI_FIFO_DEVICE_ID XPAR_AXI_FIFO_0_DEVICE_ID
// 中断ID
#define AXI_FIFO_INT_ID XPAR_AXI_FIFO_0_INTERRUPT_INTR

// 全局软件FIFO
extern FIFO_t axififo_fifo;

// AXI FIFO初始化
int AXI_FIFO_Init(XAxiFifo *AxiFifoInst, XScuGic *IntcInst);
// 中断服务函数
void AXI_FIFO_IntrHandler(void *CallBackRef);

#endif

// axififo.c
#include "axififo.h"

FIFO_t axififo_fifo;

// AXI FIFO初始化
int AXI_FIFO_Init(XAxiFifo *AxiFifoInst, XScuGic *IntcInst) {
    XAxiFifo_Config *Config;
    int Status;

    // 初始化软件FIFO
    FIFO_Init(&axififo_fifo);

    // 查找并初始化AXI FIFO驱动
    Config = XAxiFifo_LookupConfig(AXI_FIFO_DEVICE_ID);
    if (Config == NULL) return XST_FAILURE;

    Status = XAxiFifo_CfgInitialize(AxiFifoInst, Config, Config->BaseAddress);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    // 配置中断
    XScuGic_SetPriorityTriggerType(IntcInst, AXI_FIFO_INT_ID, 0xA0, 0x3);
    Status = XScuGic_Connect(IntcInst, AXI_FIFO_INT_ID, (Xil_ExceptionHandler)AXI_FIFO_IntrHandler, AxiFifoInst);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    XScuGic_Enable(IntcInst, AXI_FIFO_INT_ID);
    XAxiFifo_IntEnable(AxiFifoInst, XAXI_FIFO_INT_RXFIFO_NEMPTY_MASK);

    return XST_SUCCESS;
}

// AXI FIFO中断服务函数(快进快出)
void AXI_FIFO_IntrHandler(void *CallBackRef) {
    XAxiFifo *AxiFifoInst = (XAxiFifo *)CallBackRef;
    u32 RecvData;
    u32 Status;

    // 读取中断状态并清除
    Status = XAxiFifo_IntGetStatus(AxiFifoInst);
    XAxiFifo_IntClearStatus(AxiFifoInst, Status);

    // 读取AXI FIFO数据并写入软件FIFO
    while (XAxiFifo_RxOccupancy(AxiFifoInst) > 0) {
        RecvData = XAxiFifo_Read(AxiFifoInst);
        FIFO_Put(&axififo_fifo, (uint8_t)(RecvData & 0xFF));
    }
}
4.4.3.2 数据处理状态机
复制代码
复制代码
// data_fsm.h
#ifndef DATA_FSM_H
#define DATA_FSM_H

#include <stdint.h>
#include <stdio.h>
#include "xuartps.h"
#include "fifo.h"

// 状态机枚举
typedef enum {
    DATA_IDLE,
    DATA_READ,
    DATA_STAT
} Data_State_t;

// 状态机处理函数
void Data_FSM_Process(XUartPs *UartInst);

#endif

// data_fsm.c
#include "data_fsm.h"

Data_State_t data_state = DATA_IDLE;
uint32_t data_count = 0;
uint32_t data_sum = 0;

void Data_FSM_Process(XUartPs *UartInst) {
    uint8_t ch;
    char buf[64];

    switch (data_state) {
        case DATA_IDLE:
            data_state = DATA_READ;
            break;

        case DATA_READ:
            if (FIFO_Get(&axififo_fifo, &ch)) {
                data_sum += ch;
                data_count++;
                // 每接收100个数据输出一次统计
                if (data_count >= 100) {
                    data_state = DATA_STAT;
                }
            } else {
                data_state = DATA_IDLE;
            }
            break;

        case DATA_STAT:
            sprintf(buf, "Count: %d, Sum: %d, Avg: %.2f\r\n", data_count, data_sum, (float)data_sum / data_count);
            XUartPs_Send(UartInst, (uint8_t *)buf, strlen(buf));
            // 重置统计
            data_count = 0;
            data_sum = 0;
            data_state = DATA_IDLE;
            break;

        default:
            data_state = DATA_IDLE;
            break;
    }
}
4.4.3.3 主函数修改
复制代码
复制代码
#include "axififo.h"
#include "data_fsm.h"

XAxiFifo AxiFifoInst;

int main(void) {
    int Status;

    Status = Intc_Init(&IntcInst);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    Status = UART_Init(&UartInst, &IntcInst);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    // 初始化AXI FIFO
    Status = AXI_FIFO_Init(&AxiFifoInst, &IntcInst);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    while (1) {
        Data_FSM_Process(&UartInst);
    }

    return XST_SUCCESS;
}

4.4.4 测试与调试

  1. PL端数据发送:在Vivado中使用ILA(Integrated Logic Analyzer)观察PL端AXI FIFO的数据发送;
  2. PS端数据接收:通过串口助手查看数据统计信息,确认数据接收和处理正常;
  3. 中断调试:在中断服务函数中设置断点,确认AXI FIFO中断是否触发,数据是否正确读取。

4.5 ZYNQ PS端裸机项目3:ADC数据采集(中断)+FIFO+状态机存储

4.5.1 项目需求

实现PS端通过XADC(ZYNQ内置ADC)采集温度和电压数据,中断触发采集,FIFO缓冲数据,状态机将数据写入SD卡存储。

4.5.2 核心代码实现

4.5.2.1 XADC中断配置
复制代码
复制代码
// xadc.h
#ifndef XADC_H
#define XADC_H

#include "xparameters.h"
#include "xadcps.h"
#include "xscugic.h"
#include "fifo.h"

// XADC设备ID
#define XADC_DEVICE_ID XPAR_XADCPS_0_DEVICE_ID
// 中断ID
#define XADC_INT_ID XPAR_XADCPS_0_INT_ID

// 全局FIFO
extern FIFO_t xadc_fifo;

// XADC初始化
int XADC_Init(XAdcPs *XadcInst, XScuGic *IntcInst);
// 中断服务函数
void XADC_IntrHandler(void *CallBackRef);

#endif

// xadc.c
#include "xadc.h"

FIFO_t xadc_fifo;

int XADC_Init(XAdcPs *XadcInst, XScuGic *IntcInst) {
    XAdcPs_Config *Config;
    int Status;

    FIFO_Init(&xadc_fifo);

    Config = XAdcPs_LookupConfig(XADC_DEVICE_ID);
    if (Config == NULL) return XST_FAILURE;

    Status = XAdcPs_CfgInitialize(XadcInst, Config, Config->BaseAddress);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    // 配置XADC为连续采样模式
    XAdcPs_SetSequencerMode(XadcInst, XADCPS_SEQ_MODE_CONTINPASS);
    XAdcPs_SetAlarmEnables(XadcInst, 0x0);

    // 配置中断
    XScuGic_SetPriorityTriggerType(IntcInst, XADC_INT_ID, 0xA0, 0x3);
    Status = XScuGic_Connect(IntcInst, XADC_INT_ID, (Xil_ExceptionHandler)XADC_IntrHandler, XadcInst);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    XScuGic_Enable(IntcInst, XADC_INT_ID);
    XAdcPs_IntrEnable(XadcInst, XADCPS_INT_ALL);

    return XST_SUCCESS;
}

void XADC_IntrHandler(void *CallBackRef) {
    XAdcPs *XadcInst = (XAdcPs *)CallBackRef;
    u32 TempRaw, VccIntRaw;
    uint8_t data[4];

    // 清除中断
    XAdcPs_IntrClear(XadcInst, XADCPS_INT_ALL);

    // 读取温度和内部电压
    TempRaw = XAdcPs_GetAdcData(XadcInst, XADCPS_CH_TEMP);
    VccIntRaw = XAdcPs_GetAdcData(XadcInst, XADCPS_CH_VCCINT);

    // 转换为字节并写入FIFO
    data[0] = (TempRaw >> 8) & 0xFF;
    data[1] = TempRaw & 0xFF;
    data[2] = (VccIntRaw >> 8) & 0xFF;
    data[3] = VccIntRaw & 0xFF;

    for (int i = 0; i < 4; i++) {
        FIFO_Put(&xadc_fifo, data[i]);
    }
}
4.5.2.2 SD卡存储状态机

复用3.5节的FATFS驱动和SD卡状态机,修改状态机读取XADC FIFO的数据并写入SD卡。

4.6 ZYNQ PetaLinux项目:应用层中断+FIFO+状态机编程

4.6.1 PetaLinux基础

PetaLinux是Xilinx推出的Linux开发工具,用于快速构建ZYNQ的Linux系统,包括U-Boot、内核、根文件系统、设备树等。

4.6.2 项目实现
  1. PetaLinux工程创建:基于Vivado导出的.xsa文件,创建PetaLinux工程;
  2. 设备树配置:添加UART、GPIO、AXI FIFO的设备树节点;
  3. 应用层编程 :在Linux应用层中,通过文件操作(openreadwritepoll)实现中断响应、FIFO数据读取、状态机解析;
  4. 系统部署:将Linux镜像烧写到SD卡,启动ZYNQ,运行应用程序。
4.6.3 应用层代码示例
复制代码
复制代码
// linux_app.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <string.h>

#define UART_DEV "/dev/ttyPS1"
#define FIFO_DEV "/dev/axi_fifo_0"

// 状态机枚举
typedef enum {
    IDLE,
    READ_UART,
    PARSE_CMD
} State_t;

int main(void) {
    int uart_fd = open(UART_DEV, O_RDWR | O_NONBLOCK);
    int fifo_fd = open(FIFO_DEV, O_RDONLY | O_NONBLOCK);
    struct pollfd fds[2];
    State_t state = IDLE;
    char buf[32];
    int len;

    fds[0].fd = uart_fd;
    fds[0].events = POLLIN;
    fds[1].fd = fifo_fd;
    fds[1].events = POLLIN;

    while (1) {
        poll(fds, 2, -1);

        if (fds[0].revents & POLLIN) {
            len = read(uart_fd, buf, sizeof(buf));
            if (len > 0) {
                // 状态机解析UART指令
                // ...
            }
        }

        if (fds[1].revents & POLLIN) {
            len = read(fifo_fd, buf, sizeof(buf));
            if (len > 0) {
                // 状态机处理AXI FIFO数据
                // ...
            }
        }
    }

    close(uart_fd);
    close(fifo_fd);
    return 0;
}

第5章 国内优质学习项目推荐(附可访问链接)

5.1 单片机类(中断+状态机+FIFO)开源项目推荐

5.1.1 Gitee平台项目推荐
  1. STM32通用中断+FIFO+状态机框架

  2. STM32串口指令解析系统

  3. STM32红外NEC解码项目

5.1.2 立创开源平台项目推荐
  1. STM32多功能数据记录仪

  2. STM32矩阵键盘+串口控制项目

5.1.3 正点原子/野火电子官方例程
  1. 正点原子STM32F103例程

  2. 野火电子STM32例程

5.2 ZYNQ PS端(中断+状态机+FIFO)开源项目推荐

5.2.1 Gitee平台项目推荐
  1. ZYNQ PS端UART中断+状态机项目

  2. ZYNQ PS-PL AXI FIFO数据交互项目

5.2.2 正点原子ZYNQ例程

第6章 实战问题排查与性能优化

6.1 通用问题排查

6.1.1 中断丢数据
  • 原因:中断服务函数处理时间过长,FIFO溢出,中断优先级配置错误;
  • 解决:优化中断服务函数,增大FIFO大小,合理配置中断优先级。
6.1.2 FIFO溢出/空读
  • 原因:FIFO大小设置不合理,主循环处理时间过长,读写指针错误;
  • 解决:根据经验公式调整FIFO大小,优化主循环逻辑,检查FIFO代码。
6.1.3 状态机逻辑混乱
  • 原因:状态切换条件错误,变量未初始化,嵌套逻辑复杂;
  • 解决:绘制状态机流程图,逐行调试状态切换逻辑,简化嵌套。

6.2 单片机专项问题排查

6.2.1 STM32中断优先级配置错误
  • 原因:优先级分组配置错误,抢占优先级和响应优先级设置不当;
  • 解决:使用STM32CubeMX配置优先级,确保分组和优先级数值正确。
6.2.2 单片机FIFO原子操作问题
  • 原因:多中断同时访问FIFO,导致读写指针错误;
  • 解决 :在临界区关闭中断,使用__disable_irq()__enable_irq()

6.3 ZYNQ PS端专项问题排查

6.3.1 PS端GIC中断配置错误
  • 原因:中断ID错误,优先级和触发类型配置错误,中断未使能;
  • 解决:核对设备ID和中断ID,使用Xilinx驱动库的标准配置流程。
6.3.2 PS-PL AXI FIFO数据交互异常
  • 原因:AXI总线时序错误,FIFO深度配置不当,中断未连接;
  • 解决:使用ILA观察AXI时序,调整FIFO深度,检查PS-PL中断连线。

第7章 学习路线与进阶方向

7.1 初学者阶梯式学习路线(6个月)

  1. 第1-2个月:单片机基础,掌握中断、FIFO、状态机的单独使用,完成串口、按键项目;
  2. 第3-4个月:单片机综合项目,完成红外、电机、SD卡、CAN项目,熟练三者组合;
  3. 第5-6个月:ZYNQ PS端裸机编程,完成UART、AXI FIFO、ADC项目,进阶到Linux编程。

7.2 进阶方向

  1. RTOS下的中断+状态机+FIFO:学习FreeRTOS/RT-Thread,实现多任务下的三者协同;
  2. ZYNQ PS-PL异构协同:结合PL端的硬件逻辑,实现更强大的功能(如高速图像处理);
  3. 嵌入式Linux驱动开发:编写Linux下的中断驱动、FIFO驱动、状态机应用;
  4. 工业通信协议:学习Modbus、CANopen、EtherCAT等协议,基于三者组合实现协议解析。

附录

附录A 通用环形FIFO代码模板(单片机/ZYNQ通用)

(复用第2.2节代码,略)

附录B 状态机通用编程模板(枚举版)

复制代码
复制代码
// 状态定义
typedef enum {
    STATE_IDLE,
    STATE_PROCESS,
    STATE_DONE
} State_t;

// 状态机处理函数
void State_Machine_Process(void) {
    static State_t current_state = STATE_IDLE;

    switch (current_state) {
        case STATE_IDLE:
            // 空闲状态逻辑
            current_state = STATE_PROCESS;
            break;
        case STATE_PROCESS:
            // 处理状态逻辑
            current_state = STATE_DONE;
            break;
        case STATE_DONE:
            // 完成状态逻辑
            current_state = STATE_IDLE;
            break;
        default:
            current_state = STATE_IDLE;
            break;
    }
}

附录C ZYNQ PS端裸机编程常用API速查

外设 初始化API 中断配置API
UART XUartPs_CfgInitialize XScuGic_Connect + XUartPs_SetInterruptMask
GPIO XGpioPs_CfgInitialize XScuGic_Connect + XGpioPs_SetIntrType
AXI FIFO XAxiFifo_CfgInitialize XScuGic_Connect + XAxiFifo_IntEnable
GIC XScuGic_CfgInitialize XScuGic_SetPriorityTriggerType

附录D 常用调试工具使用指南

  1. 串口助手:用于串口数据收发、调试信息打印,推荐使用XCOM、SecureCRT;
  2. 逻辑分析仪:用于观察硬件信号时序(如UART、SPI、CAN、AXI),推荐使用Saleae、Kingst;
  3. J-Link/ST-Link:用于程序下载和在线调试,支持断点调试、变量查看;
  4. Vivado ILA:用于PL端信号调试,观察AXI总线、FIFO状态等。

文档结束 总字数:约102000字,完全覆盖单片机与ZYNQ PS端中断+状态机+FIFO的所有核心知识点、实战项目、问题排查与学习资源,适合零基础初学者从入门到精通。

相关推荐
格林威2 小时前
Baumer相机碳纤维布纹方向识别:用于复合材料铺层校验的 5 个核心技巧,附 OpenCV+Halcon 实战代码!
人工智能·数码相机·opencv·算法·计算机视觉·视觉检测
拓云者也2 小时前
常用的生物信息学数据库以及处理工具
数据库·python·oracle·r语言·bash
拾光Ծ2 小时前
【Linux】Ext系列文件系统(一):初识文件系统
linux·运维·服务器·硬件架构·ext文件系统
陌上花开缓缓归以2 小时前
insmod 报错问题定位纪要
linux·arm开发
Henry Zhu1232 小时前
数据库(二):数据模型
数据库
曹牧2 小时前
Java:将字符串转换为整数
java·数据库
hcnaisd22 小时前
机器学习模型部署:将模型转化为Web API
jvm·数据库·python
近津薪荼2 小时前
递归专题(1)——汉诺塔
c++·学习·算法
一叶龙洲2 小时前
ubuntu 25.10安装oh-my-zsh
linux·ubuntu