嵌入式通信核心实战:状态机与环形FIFO(UART/CAN/IIC/SPI全解析)
写给嵌入式新手的前言
如果你是刚接触嵌入式的新手,大概率会遇到这些问题:用UART接收数据时一着急就丢字节,解析自定义协议时逻辑写得像“意大利面”,CAN总线报文来了不知道怎么高效缓存,IIC/SPI通信时序稍不注意就卡死……这些问题的核心,其实都指向两个嵌入式通信的“底层法宝”——环形FIFO(环形队列) 和状态机(FSM)。
本文从新手视角出发,不堆砌晦涩的理论,只讲“能落地、能跑通”的知识:先拆解环形FIFO和状态机的核心原理,再结合嵌入式最常用的4种通信接口(UART、CAN、IIC、SPI),从硬件配置、代码实现、调试排错全流程讲解,每个案例都基于STM32(新手最易上手的MCU),代码逐行注释,问题逐个拆解,总字数超10万字,足够你从“零基础”到“能独立实现通信数据转发”。
阅读建议:不用追求一次看完,按章节循序渐进,每学完一个模块就动手写代码、烧录测试,遇到问题先翻“常见排错”章节,嵌入式的核心是“动手”,看懂100遍不如亲手跑通1遍。
第一章 嵌入式通信入门:为什么需要状态机和环形FIFO?
1.1 嵌入式系统的“通信本质”(新手必懂)
嵌入式系统不是孤立的“单机”,而是需要和传感器、执行器、上位机、其他MCU交互的“节点”——比如智能手环需要通过IIC读取心率传感器数据,汽车ECU需要通过CAN总线和车灯、刹车模块通信,智能家居模块需要通过UART和Wi-Fi模块交互,显示屏需要通过SPI接收主控的显示数据。
这些交互的核心是“数据传输”,而嵌入式通信的最大特点是:
- 异步性:数据什么时候来、来多少,完全由外部设备决定(比如UART中断随机触发,CAN报文随时可能到达);
- 实时性:数据必须及时处理,丢一个字节可能导致整个协议解析失败;
- 可靠性:工业/汽车场景下,哪怕有电磁干扰,数据也不能错、不能丢。
新手最开始的错误做法是:
// 新手踩坑示例:裸机直接处理UART接收(错误示范)
uint8_t uart_data;
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uart_data = USART_ReceiveData(USART1); // 直接读寄存器
// 这里如果在中断里做复杂解析,会导致中断阻塞,后续数据丢失
parse_data(uart_data); // 中断里解析数据,极容易丢字节
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
这段代码的问题显而易见:中断里做解析会占用大量时间,后续数据到来时中断还没退出,就会丢失;没有缓冲,一旦主循环没及时处理uart_data,新数据会直接覆盖旧数据。
而解决这些问题的核心,就是环形FIFO(缓冲数据)+ 状态机(解析数据):
- 环形FIFO:把“异步到来的数据”先存起来,让“慢的主循环”可以慢慢处理,避免丢失;
- 状态机:把“复杂的协议解析逻辑”拆成一个个简单的“状态”,逐个处理,避免逻辑混乱。
1.2 嵌入式4大通信接口:新手的“认知地图”
先花半小时搞懂UART、CAN、IIC、SPI的核心区别,避免后续混淆。我们用“生活场景”类比,新手一看就懂:
| 通信接口 | 生活类比 | 核心特点 | 典型应用 | 新手易踩坑 |
|---|---|---|---|---|
| UART | 两个人打电话(一对一) | 双线(TX/RX)、异步、串行、波特率决定速度 | 上位机调试、模块通信(如Wi-Fi模块) | 波特率不匹配、TX/RX接反、没加校验 |
| CAN | 公交车(多节点共享总线) | 双线(CAN_H/CAN_L)、差分信号、多主、抗干扰强 | 汽车ECU、工业控制 | 没接120Ω终端电阻、波特率错误、ID冲突 |
| IIC | 快递员送快递(一对多) | 双线(SDA/SCL)、同步、主从、地址寻址 | 传感器(温湿度)、存储(AT24C02) | 没加上拉电阻、时序错误、ACK没检测 |
| SPI | 高速传送带(一对一/一对多) | 四线(MOSI/MISO/SCK/CS)、同步、全双工、高速 | 显示屏(LCD)、Flash(W25Q64) | 片选(CS)没拉低、时钟极性/相位错误 |
新手记住:不管哪种接口,数据处理的逻辑都相通——先缓冲(环形FIFO),再解析(状态机),最后转发/执行。
第二章 环形FIFO(环形队列):嵌入式通信的“数据缓冲池”
2.1 什么是环形FIFO?(新手能懂的解释)
先想一个生活场景:你去食堂打饭,窗口是环形的,第一个人打完到最后一个位置,下一个人直接到第一个位置,不用所有人往后挪——这就是“环形”的核心:循环利用空间,不用频繁移动数据。
嵌入式里的环形FIFO,本质是一段连续的数组,加上两个指针:
- 写指针(wr_ptr):记录下一个要写入数据的位置;
- 读指针(rd_ptr):记录下一个要读取数据的位置。
数据写入时,写指针往后走;数据读取时,读指针往后走;当指针走到数组末尾,就回到开头(用“取模运算”实现)。
对比新手常用的“普通数组缓冲”:
- 普通数组:写满后需要把未处理的数据往前挪,效率低,还容易溢出;
- 环形FIFO:指针循环移动,不用挪数据,空间利用率100%(或接近100%),处理异步数据时几乎不会丢字节。
2.2 环形FIFO的核心原理(从0拆解)
2.2.1 基本结构(新手必记)
环形FIFO的最小结构包含4个要素:
// 新手友好的环形FIFO结构体定义
typedef struct
{
uint8_t *buf; // 存储数据的数组(缓冲池)
uint16_t size; // 数组长度(FIFO的最大容量)
uint16_t rd_ptr; // 读指针:下一个要读取的位置
uint16_t wr_ptr; // 写指针:下一个要写入的位置
uint16_t cnt; // 当前存储的数据个数(新手首选:计数法判空满)
} RingFifo_t;
为什么用cnt(计数)?新手最容易搞混“空/满判断”,用计数法是最简单的:
- 空:
cnt == 0; - 满:
cnt == size; - 写入:
cnt++; - 读取:
cnt--。
另一种方法是“预留空位法”(wr_ptr + 1 == rd_ptr为满),新手暂时不用学,先掌握计数法,避免踩坑。
2.2.2 核心运算:取模(%)实现“环形”
取模运算(%)是环形FIFO的“灵魂”,新手一定要理解: 比如FIFO的大小是8(size=8),写指针当前在7(wr_ptr=7),再写入一个数据: wr_ptr = (wr_ptr + 1) % size → (7+1)%8=0,写指针回到开头,实现“环形”。
举个直观例子:
| 操作 | wr_ptr | rd_ptr | cnt | 说明 |
|---|---|---|---|---|
| 初始化 | 0 | 0 | 0 | 空FIFO |
| 写入1字节 | 1 | 0 | 1 | 写指针+1,计数+1 |
| 写入7字节 | 0 | 0 | 8 | 写指针到8%8=0,计数满(size=8) |
| 读取1字节 | 0 | 1 | 7 | 读指针+1,计数-1 |
2.2.3 环形FIFO的基本操作(新手必背)
所有操作围绕“初始化、写入、读取、判空、判满”展开,逻辑简单到新手能默写:
- 初始化:给数组分配空间,指针和计数归0;
- 写入:先判满→写入数据→移动写指针→计数+1;
- 读取:先判空→读取数据→移动读指针→计数-1;
- 判空/判满:直接看计数
cnt。
2.3 新手友好的环形FIFO C语言实现(逐行解释)
我们实现一个通用的环形FIFO模块(ring_fifo.c + ring_fifo.h),可以直接移植到UART/CAN/IIC/SPI,代码逐行注释,新手能看懂每一步。
2.3.1 头文件(ring_fifo.h)
#ifndef __RING_FIFO_H
#define __RING_FIFO_H
#include "stdint.h" // 新手注意:必须包含标准整数类型头文件
#include "string.h" // 用于memset等函数
// 定义环形FIFO结构体(新手重点:记住每个成员的意义)
typedef struct
{
uint8_t *buf; // 数据缓冲区指针
uint16_t size; // FIFO最大容量(数组长度)
uint16_t rd_ptr; // 读指针
uint16_t wr_ptr; // 写指针
uint16_t cnt; // 当前数据个数(计数法)
} RingFifo_t;
// 函数声明(新手:先记函数名和功能,再记参数)
/**
* @brief 初始化环形FIFO
* @param fifo:FIFO结构体指针
* @param buf:外部数组指针(新手:数组需要提前定义,比如uint8_t uart_fifo_buf[128];)
* @param size:数组长度(FIFO容量)
* @retval 0:成功,1:失败(参数错误)
*/
uint8_t RingFifo_Init(RingFifo_t *fifo, uint8_t *buf, uint16_t size);
/**
* @brief 写入1字节到FIFO
* @param fifo:FIFO结构体指针
* @param data:要写入的字节
* @retval 0:成功,1:FIFO满
*/
uint8_t RingFifo_Write_Byte(RingFifo_t *fifo, uint8_t data);
/**
* @brief 从FIFO读取1字节
* @param fifo:FIFO结构体指针
* @param data:存储读取数据的指针(新手:传变量地址,比如&data)
* @retval 0:成功,1:FIFO空
*/
uint8_t RingFifo_Read_Byte(RingFifo_t *fifo, uint8_t *data);
/**
* @brief 写入多个字节到FIFO
* @param fifo:FIFO结构体指针
* @param data:要写入的数组指针
* @param len:要写入的长度
* @retval 实际写入的字节数(新手:可能小于len,因为FIFO可能满)
*/
uint16_t RingFifo_Write_Multi(RingFifo_t *fifo, uint8_t *data, uint16_t len);
/**
* @brief 从FIFO读取多个字节
* @param fifo:FIFO结构体指针
* @param data:存储读取数据的数组指针
* @param len:要读取的长度
* @retval 实际读取的字节数(新手:可能小于len,因为FIFO可能没这么多数据)
*/
uint16_t RingFifo_Read_Multi(RingFifo_t *fifo, uint8_t *data, uint16_t len);
/**
* @brief 判断FIFO是否为空
* @param fifo:FIFO结构体指针
* @retval 0:非空,1:空
*/
uint8_t RingFifo_Is_Empty(RingFifo_t *fifo);
/**
* @brief 判断FIFO是否为满
* @param fifo:FIFO结构体指针
* @retval 0:未满,1:满
*/
uint8_t RingFifo_Is_Full(RingFifo_t *fifo);
/**
* @brief 获取FIFO当前数据个数
* @param fifo:FIFO结构体指针
* @retval 当前数据个数
*/
uint16_t RingFifo_Get_Len(RingFifo_t *fifo);
/**
* @brief 清空FIFO
* @param fifo:FIFO结构体指针
* @retval 0:成功
*/
uint8_t RingFifo_Clear(RingFifo_t *fifo);
#endif
2.3.2 源文件(ring_fifo.c)
#include "ring_fifo.h"
// 初始化FIFO(新手逐行解释)
uint8_t RingFifo_Init(RingFifo_t *fifo, uint8_t *buf, uint16_t size)
{
// 第一步:检查参数是否合法(新手:空指针是嵌入式常见错误)
if(fifo NULL || buf NULL || size == 0)
{
return 1; // 参数错误,返回失败
}
// 第二步:给FIFO成员赋值
fifo->buf = buf; // 绑定数据缓冲区
fifo->size = size; // 设置FIFO容量
fifo->rd_ptr = 0; // 读指针归0
fifo->wr_ptr = 0; // 写指针归0
fifo->cnt = 0; // 计数归0(空FIFO)
// 第三步:清空缓冲区(新手:避免数组初始值干扰)
memset(fifo->buf, 0, size);
return 0; // 初始化成功
}
// 写入1字节(新手核心函数)
uint8_t RingFifo_Write_Byte(RingFifo_t *fifo, uint8_t data)
{
// 第一步:检查参数和FIFO状态(满了就写不进去)
if(fifo == NULL || RingFifo_Is_Full(fifo))
{
return 1; // 失败
}
// 第二步:关中断(新手重点:临界区保护,避免中断和主循环同时写)
__disable_irq(); // STM32关全局中断(简单粗暴,新手首选)
// 第三步:写入数据到当前写指针位置
fifo->buf[fifo->wr_ptr] = data;
// 第四步:移动写指针(取模实现环形)
fifo->wr_ptr = (fifo->wr_ptr + 1) % fifo->size;
// 第五步:计数+1(表示FIFO里多了1个字节)
fifo->cnt++;
// 第六步:开中断
__enable_irq();
return 0; // 写入成功
}
// 读取1字节(新手核心函数)
uint8_t RingFifo_Read_Byte(RingFifo_t *fifo, uint8_t *data)
{
// 第一步:检查参数和FIFO状态(空的就读不出来)
if(fifo NULL || data NULL || RingFifo_Is_Empty(fifo))
{
return 1; // 失败
}
// 第二步:关中断(临界区保护)
__disable_irq();
// 第三步:从当前读指针位置读取数据
*data = fifo->buf[fifo->rd_ptr];
// 第四步:移动读指针(取模实现环形)
fifo->rd_ptr = (fifo->rd_ptr + 1) % fifo->size;
// 第五步:计数-1(表示FIFO里少了1个字节)
fifo->cnt--;
// 第六步:开中断
__enable_irq();
return 0; // 读取成功
}
// 写入多个字节(新手拓展函数)
uint16_t RingFifo_Write_Multi(RingFifo_t *fifo, uint8_t *data, uint16_t len)
{
// 第一步:检查参数
if(fifo NULL || data NULL || len == 0)
{
return 0; // 写入0字节
}
uint16_t write_len = 0; // 记录实际写入的长度
// 第二步:循环写入,直到写满或写完
for(uint16_t i=0; i<len; i++)
{
if(RingFifo_Write_Byte(fifo, data[i]) 0)
{
write_len++; // 写入成功,计数+1
}
else
{
break; // FIFO满了,退出循环
}
}
return write_len; // 返回实际写入长度
}
// 读取多个字节(新手拓展函数)
uint16_t RingFifo_Read_Multi(RingFifo_t *fifo, uint8_t *data, uint16_t len)
{
// 第一步:检查参数
if(fifo NULL || data NULL || len 0)
{
return 0; // 读取0字节
}
uint16_t read_len = 0; // 记录实际读取的长度
// 第二步:循环读取,直到读完或空了
for(uint16_t i=0; i<len; i++)
{
if(RingFifo_Read_Byte(fifo, &data[i]) 0)
{
read_len++; // 读取成功,计数+1
}
else
{
break; // FIFO空了,退出循环
}
}
return read_len; // 返回实际读取长度
}
// 判断FIFO是否为空(新手辅助函数)
uint8_t RingFifo_Is_Empty(RingFifo_t *fifo)
{
if(fifo NULL)
{
return 1; // 非法参数,视为空
}
return (fifo->cnt 0) ? 1 : 0; // 计数为0就是空
}
// 判断FIFO是否为满(新手辅助函数)
uint8_t RingFifo_Is_Full(RingFifo_t *fifo)
{
if(fifo NULL)
{
return 1; // 非法参数,视为满
}
return (fifo->cnt fifo->size) ? 1 : 0; // 计数等于容量就是满
}
// 获取当前数据长度(新手辅助函数)
uint16_t RingFifo_Get_Len(RingFifo_t *fifo)
{
if(fifo NULL)
{
return 0; // 非法参数,返回0
}
return fifo->cnt; // 直接返回计数
}
// 清空FIFO(新手辅助函数)
uint8_t RingFifo_Clear(RingFifo_t *fifo)
{
if(fifo == NULL)
{
return 1; // 失败
}
__disable_irq(); // 关中断
fifo->rd_ptr = 0; // 读指针归0
fifo->wr_ptr = 0; // 写指针归0
fifo->cnt = 0; // 计数归0
__enable_irq(); // 开中断
return 0; // 成功
}
2.3.3 代码关键注释(新手必看)
-
临界区保护(关/开中断): 嵌入式中,FIFO通常会被“中断”(比如UART接收中断)和“主循环”同时访问——如果中断正在写FIFO,主循环同时读,可能导致指针/计数错乱(比如写指针刚移动,计数还没加,主循环就读了,导致数据错误)。 新手先用
__disable_irq()/__enable_irq()(STM32专用),简单有效;进阶后可以用“局部关中断”(只关对应中断),减少对系统的影响。 -
volatile关键字(新手易错点): 如果FIFO结构体成员被中断和主循环同时访问,需要加
volatile修饰,防止编译器优化导致值错误。比如:typedef struct { uint8_t *buf; uint16_t size; volatile uint16_t rd_ptr; // 加volatile volatile uint16_t wr_ptr; // 加volatile volatile uint16_t cnt; // 加volatile } RingFifo_t;新手记住:被中断修改的变量,都要加volatile。
-
FIFO容量选择(新手经验):
- UART:选64/128字节(波特率9600时,1秒传约1000字节,128字节足够缓冲);
- CAN:选16/32个报文结构体(CAN报文通常8字节,32个足够);
- IIC/SPI:选32/64字节(低速通信,不用太大)。
2.4 环形FIFO适配不同通信接口(新手实战)
2.4.1 UART + 环形FIFO(最常用,新手先学)
需求:STM32的UART1用DMA接收数据,写入环形FIFO,主循环读取并打印。
步骤1:CubeMX配置(新手图文步骤)
- 打开STM32CubeMX,选择你的MCU(比如STM32F103C8T6);
- 配置时钟:HSE=8MHz,SYSCLK=72MHz;
- 配置UART1:
- Mode:Asynchronous(异步);
- Baud Rate:9600;
- Word Length:8 Bits;
- Parity:None;
- Stop Bits:1;
- DMA Settings:添加RX DMA(Stream0,Channel4,Circular模式);
- NVIC Settings:开启UART1全局中断(优先级设为2,比主循环高);
- 生成代码(MDK-ARM,V5)。
步骤2:代码集成(新手逐行加)
// 第一步:在main.c中包含头文件
#include "ring_fifo.h"
// 第二步:定义FIFO缓冲区和结构体(新手:全局变量,方便中断访问)
#define UART1_FIFO_SIZE 128 // FIFO容量128字节
uint8_t uart1_fifo_buf[UART1_FIFO_SIZE]; // 数据缓冲区
RingFifo_t uart1_fifo; // FIFO结构体
// 第三步:DMA接收缓冲区(1字节,循环模式)
uint8_t uart1_dma_buf[1];
// 第四步:初始化FIFO(在main函数的MX_USART1_UART_Init后)
RingFifo_Init(&uart1_fifo, uart1_fifo_buf, UART1_FIFO_SIZE);
// 第五步:启动UART1 DMA接收(在main函数中)
HAL_UART_Receive_DMA(&huart1, uart1_dma_buf, 1);
// 第六步:重写DMA接收完成回调函数(在main.c末尾)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance USART1)
{
// DMA接收1字节,写入环形FIFO
RingFifo_Write_Byte(&uart1_fifo, uart1_dma_buf[0]);
// 重新启动DMA接收(循环接收)
HAL_UART_Receive_DMA(&huart1, uart1_dma_buf, 1);
}
}
// 第七步:主循环读取FIFO(在while(1)中)
uint8_t uart_data;
while(1)
{
// 如果FIFO非空,读取1字节并打印
if(RingFifo_Read_Byte(&uart1_fifo, &uart_data) 0)
{
HAL_UART_Transmit(&huart1, &uart_data, 1, 100); // 回显
}
}
新手测试:
- 编译代码,烧录到STM32;
- 用串口助手(比如SSCOM)连接UART1(TX=PA9,RX=PA10),波特率9600;
- 发送任意字符,串口助手会收到回显——说明FIFO工作正常,没有丢字节。
2.4.2 CAN + 环形FIFO(汽车/工业场景)
需求:STM32的CAN1接收报文,写入环形FIFO,主循环解析。
步骤1:CubeMX配置
- 配置CAN1:
- Mode:Normal(正常模式);
- Prescaler:18(72MHz/18/4=1MHz,波特率500k);
- SJW:1;
- BS1:13;
- BS2:2;
- NVIC Settings:开启CAN1 RX0中断(优先级2);
- 配置过滤器:接收所有ID的报文(新手先简单配)。
步骤2:代码集成
// 第一步:定义CAN报文FIFO(结构体版)
#include "ring_fifo.h"
#define CAN1_FIFO_SIZE 32 // 32个报文
// CAN报文结构体(和STM32 HAL库一致)
typedef struct
{
uint32_t id; // CAN ID
uint8_t data[8]; // 数据域
uint8_t len; // 数据长度
} CAN_Msg_t;
CAN_Msg_t can1_fifo_buf[CAN1_FIFO_SIZE]; // 报文缓冲区
RingFifo_t can1_fifo; // FIFO结构体(注意:FIFO存的是CAN_Msg_t,不是uint8_t)
// 第二步:重写CAN接收回调函数
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
if(hcan->Instance == CAN1)
{
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[8];
// 读取CAN报文
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data);
// 封装成自定义报文结构体
CAN_Msg_t can_msg;
can_msg.id = rx_header.StdId; // 标准ID
can_msg.len = rx_header.DLC; // 数据长度
memcpy(can_msg.data, rx_data, rx_header.DLC);
// 写入环形FIFO(注意:这里要修改RingFifo_Write_Byte为写结构体,新手拓展)
// 拓展后的RingFifo_Write_Struct函数(原理和写字节一致,只是数据类型变了)
RingFifo_Write_Struct(&can1_fifo, &can_msg);
}
}
// 第三步:主循环读取CAN FIFO
CAN_Msg_t can_msg;
while(1)
{
if(RingFifo_Read_Struct(&can1_fifo, &can_msg) == 0)
{
// 解析CAN报文(比如ID=0x123的报文,打印数据)
if(can_msg.id == 0x123)
{
printf("CAN ID:0x%03X, Data:", can_msg.id);
for(uint8_t i=0; i<can_msg.len; i++)
{
printf("%02X ", can_msg.data[i]);
}
printf("\r\n");
}
}
}
新手注意:
环形FIFO不仅能存uint8_t,还能存任意结构体(比如CAN报文),只需要把写入/读取函数的参数类型改成对应结构体即可——核心逻辑不变,只是数据单元变大了。
2.4.3 IIC + 环形FIFO(传感器通信)
需求:STM32的IIC1读取AT24C02(EEPROM)的数据,写入环形FIFO,主循环打印。
核心代码(新手版)
// 定义IIC FIFO
#define I2C1_FIFO_SIZE 64
uint8_t i2c1_fifo_buf[I2C1_FIFO_SIZE];
RingFifo_t i2c1_fifo;
// IIC读取AT24C02的函数(新手简化版)
uint8_t AT24C02_Read(uint8_t addr, uint8_t *data)
{
HAL_I2C_Mem_Read(&hi2c1, 0xA0, addr, I2C_MEMADD_SIZE_8BIT, data, 1, 100);
return 0;
}
// 主循环读取传感器数据并写入FIFO
uint8_t sensor_data;
while(1)
{
// 每100ms读取一次AT24C02的0x00地址数据
AT24C02_Read(0x00, &sensor_data);
RingFifo_Write_Byte(&i2c1_fifo, sensor_data);
// 读取FIFO并打印
if(RingFifo_Read_Byte(&i2c1_fifo, &sensor_data) == 0)
{
printf("AT24C02 Data:0x%02X\r\n", sensor_data);
}
HAL_Delay(100);
}
2.4.4 SPI + 环形FIFO(高速数据)
需求:STM32的SPI1读取W25Q64(Flash)的ID,写入环形FIFO,主循环验证。
核心代码(新手版)
// 定义SPI FIFO
#define SPI1_FIFO_SIZE 32
uint8_t spi1_fifo_buf[SPI1_FIFO_SIZE];
RingFifo_t spi1_fifo;
// SPI读取W25Q64 ID的函数
void W25Q64_Read_ID(uint8_t *id)
{
uint8_t cmd = 0x90; // 读ID命令
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // CS拉低
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); // 发送命令
HAL_SPI_Transmit(&hspi1, id, 3, 100); // 读取3字节ID
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // CS拉高
}
// 主循环
uint8_t flash_id[3];
while(1)
{
// 读取Flash ID
W25Q64_Read_ID(flash_id);
// 写入FIFO
RingFifo_Write_Multi(&spi1_fifo, flash_id, 3);
// 读取FIFO并验证(W25Q64的ID是0xEF 0x40 0x17)
uint8_t read_id[3];
if(RingFifo_Read_Multi(&spi1_fifo, read_id, 3) 3)
{
printf("Flash ID:0x%02X 0x%02X 0x%02X\r\n", read_id[0], read_id[1], read_id[2]);
if(read_id[0] 0xEF && read_id[1] 0x40 && read_id[2] 0x17)
{
printf("Flash ID Verify OK!\r\n");
}
else
{
printf("Flash ID Verify Error!\r\n");
}
}
HAL_Delay(1000);
}
2.5 环形FIFO新手常见问题与排错
| 问题现象 | 常见原因 | 解决方法 |
|---|---|---|
| 数据丢失 | 1. FIFO容量太小;2. 没加临界区保护;3. DMA模式错误(不是Circular) | 1. 增大FIFO容量;2. 写入/读取时关中断;3. CubeMX中DMA设为Circular模式 |
| 数据乱码 | 1. 波特率不匹配(UART);2. volatile漏加;3. 指针取模错误 | 1. 核对UART波特率;2. 给指针/计数加volatile;3. 检查取模运算((ptr+1)%size) |
| FIFO判满错误 | 1. 计数法没同步;2. 预留空位法指针处理错误 | 1. 优先用计数法;2. 写入时先判满,再操作指针 |
| 中断不触发 | 1. NVIC没开启中断;2. DMA通道配置错误 | 1. CubeMX中开启对应中断;2. 核对DMA通道(比如UART1 RX是DMA1 Channel4) |
第三章 嵌入式状态机(FSM):通信协议解析的“大脑”
3.1 什么是状态机?(新手能懂的生活类比)
如果说环形FIFO是“数据仓库”,那状态机就是“仓库管理员”——管理员不会一下子处理所有货物,而是按“步骤”来:先验货(看是不是自己要的货),再点数(确认数量),再入库(处理数据),最后记账(完成解析)。
生活中的状态机例子(新手一看就懂):
- 红绿灯:状态(红灯→绿灯→黄灯),事件(时间到),动作(亮对应灯);
- 自动售货机:状态(待机→投币→选货→出货),事件(投币、选货按钮),动作(收钱、出货);
- 你早上起床:状态(睡觉→醒→穿衣→洗漱→吃饭),事件(闹钟响、完成穿衣),动作(起床、刷牙)。
嵌入式中的状态机(FSM,有限状态机),核心是把复杂的协议解析逻辑拆成一个个有限的、简单的状态,每个状态只处理一件事,通过“事件”(比如收到一个字节、时间到)触发“状态转移”(比如从“等待帧头”转到“接收长度”)。
新手最开始的错误解析逻辑(以UART自定义协议为例):
// 错误示范:线性解析,一旦丢字节就全错
uint8_t buf[100];
uint16_t idx = 0;
void parse_uart_data(uint8_t data)
{
buf[idx++] = data;
// 协议:帧头0xAA,长度1字节,数据N字节,帧尾0x55
if(buf[0] 0xAA && buf[2] 0x55) // 线性判断,丢字节就错
{
// 解析数据
}
}
这段代码的问题:如果帧头0xAA丢了,后续所有判断都错;如果中间少一个字节,帧尾判断也错——而状态机的核心优势是:哪怕中间有干扰字节,只要没触发状态转移,就不会影响解析。
3.2 状态机的核心要素(新手必记)
不管多复杂的状态机,都包含4个核心要素,用“自动售货机”类比:
| 要素 | 解释 | 自动售货机例子 | 通信解析例子 |
|---|---|---|---|
| 状态(State) | 当前所处的阶段 | 待机、投币、选货、出货 | 等待帧头、接收长度、接收数据、校验、完成 |
| 事件(Event) | 触发状态转移的条件 | 投币、按选货按钮、出货完成 | 收到一个字节、字节等于帧头、校验通过 |
| 动作(Action) | 处于某个状态时要做的事 | 待机:亮屏;投币:计数 | 等待帧头:判断字节是否为0xAA;接收长度:记录数据长度 |
| 转移(Transition) | 事件发生后,从一个状态转到另一个状态 | 投币(事件)→待机→投币状态 | 收到0xAA(事件)→等待帧头→接收长度状态 |
新手记住:一个状态机,只需要定义“状态枚举”+“状态处理逻辑”+“状态转移规则”,就能跑起来。
3.3 状态机的分类(新手先学实用的)
嵌入式中常用两种状态机,新手先学“米利机”(Mealy),足够应对90%的通信场景:
- 摩尔机(Moore):输出只依赖当前状态(比如红绿灯,状态决定灯的颜色,和时间无关);
- 米利机(Mealy):输出依赖当前状态+输入事件(比如售货机,投币(事件)+待机(状态)→投币状态)——通信解析几乎全用这种。
3.4 新手友好的状态机C语言实现(两种方式)
3.4.1 方式1:switch-case实现(新手首选,易理解)
这是新手最容易上手的方式,核心是“定义状态枚举→用switch-case处理每个状态→根据事件转移状态”。
以“UART自定义协议解析”为例,协议定义:
- 帧头:0xAA(1字节);
- 长度:1字节(表示后续数据的长度);
- 数据:N字节(N等于长度字段的值);
- 校验和:1字节(帧头+长度+数据的累加和,取低8位);
- 帧尾:0x55(1字节)。
步骤1:定义状态枚举(新手先写死,后续再拓展)
// UART协议解析状态枚举(新手:用typedef简化)
typedef enum
{
STATE_IDLE = 0, // 空闲状态(初始状态)
STATE_WAIT_HEAD, // 等待帧头(0xAA)
STATE_RECV_LEN, // 接收长度字节
STATE_RECV_DATA, // 接收数据字节
STATE_RECV_CHECK, // 接收校验和
STATE_RECV_TAIL, // 接收帧尾(0x55)
STATE_PARSE_DONE, // 解析完成
STATE_PARSE_ERROR // 解析错误(超时/校验错)
} UartParseState_t;
步骤2:定义全局变量(新手:方便中断/主循环访问)
// 协议解析相关变量
UartParseState_t uart_parse_state = STATE_IDLE; // 当前状态(初始为空闲)
uint8_t uart_protocol_buf[64]; // 协议帧缓冲区
uint16_t uart_protocol_idx = 0; // 缓冲区索引
uint8_t uart_protocol_len = 0; // 协议帧长度(从长度字段读取)
uint8_t uart_protocol_check = 0; // 校验和
uint32_t uart_parse_timeout = 0; // 解析超时(防止卡死)
#define UART_PARSE_TIMEOUT 1000 // 超时时间1000ms
步骤3:状态机处理函数(核心,逐行解释)
/**
* @brief UART协议解析状态机
* @param data:收到的1字节数据(从环形FIFO读取)
* @retval 0:正常,1:解析完成,2:解析错误
*/
uint8_t Uart_Parse_State_Machine(uint8_t data)
{
// 重置超时计数器(只要有数据来,就说明没超时)
uart_parse_timeout = 0;
// 核心:switch-case处理每个状态
switch(uart_parse_state)
{
case STATE_IDLE: // 空闲状态:等待帧头,清空缓冲区
{
uart_protocol_idx = 0; // 索引归0
uart_protocol_check = 0; // 校验和归0
// 事件:收到帧头0xAA
if(data == 0xAA)
{
// 动作:保存帧头,更新校验和
uart_protocol_buf[uart_protocol_idx++] = data;
uart_protocol_check += data;
// 转移:转到等待长度状态
uart_parse_state = STATE_RECV_LEN;
}
// 其他字节:忽略,保持空闲状态
break;
}
case STATE_RECV_LEN: // 接收长度状态
{
// 动作:保存长度字节,更新校验和,记录长度
uart_protocol_buf[uart_protocol_idx++] = data;
uart_protocol_check += data;
uart_protocol_len = data; // 记录数据长度
// 检查长度是否合法(防止缓冲区溢出)
if(uart_protocol_len > 60) // 缓冲区总长度64,留4字节给头/尾/校验
{
uart_parse_state = STATE_PARSE_ERROR; // 长度非法,转到错误状态
break;
}
// 转移:转到接收数据状态
uart_parse_state = STATE_RECV_DATA;
break;
}
case STATE_RECV_DATA: // 接收数据状态
{
// 动作:保存数据字节,更新校验和
uart_protocol_buf[uart_protocol_idx++] = data;
uart_protocol_check += data;
// 事件:数据接收完成(索引=帧头+长度=1+uart_protocol_len)
if(uart_protocol_idx == (1 + uart_protocol_len))
{
// 转移:转到接收校验和状态
uart_parse_state = STATE_RECV_CHECK;
}
break;
}
case STATE_RECV_CHECK: // 接收校验和状态
{
// 动作:保存校验和字节
uart_protocol_buf[uart_protocol_idx++] = data;
// 事件:校验和是否正确
if(data == (uart_protocol_check & 0xFF)) // 累加和取低8位
{
// 转移:转到接收帧尾状态
uart_parse_state = STATE_RECV_TAIL;
}
else
{
// 校验错误,转到错误状态
uart_parse_state = STATE_PARSE_ERROR;
}
break;
}
case STATE_RECV_TAIL: // 接收帧尾状态
{
// 动作:保存帧尾字节
uart_protocol_buf[uart_protocol_idx++] = data;
// 事件:帧尾是否为0x55
if(data == 0x55)
{
// 转移:转到解析完成状态
uart_parse_state = STATE_PARSE_DONE;
}
else
{
// 帧尾错误,转到错误状态
uart_parse_state = STATE_PARSE_ERROR;
}
break;
}
case STATE_PARSE_DONE: // 解析完成状态
{
// 动作:重置状态机,返回解析完成
uart_parse_state = STATE_IDLE; // 回到空闲状态,准备下一次解析
return 1; // 解析完成
}
case STATE_PARSE_ERROR: // 解析错误状态
{
// 动作:重置状态机,返回解析错误
uart_parse_state = STATE_IDLE; // 回到空闲状态
return 2; // 解析错误
}
default: // 未知状态,重置
{
uart_parse_state = STATE_IDLE;
break;
}
}
return 0; // 正常,未完成
}
步骤4:主循环集成状态机(新手关键)
// 主循环while(1)中
uint8_t uart_data;
uint8_t parse_ret;
while(1)
{
// 第一步:处理超时(防止状态机卡死)
uart_parse_timeout++;
if(uart_parse_timeout > UART_PARSE_TIMEOUT)
{
uart_parse_state = STATE_PARSE_ERROR; // 超时,转到错误状态
uart_parse_timeout = 0; // 重置超时
}
// 第二步:从环形FIFO读取1字节
if(RingFifo_Read_Byte(&uart1_fifo, &uart_data) == 0)
{
// 第三步:喂给状态机解析
parse_ret = Uart_Parse_State_Machine(uart_data);
// 第四步:处理解析结果
if(parse_ret == 1)
{
// 解析完成,处理数据(比如转发到CAN)
printf("UART Parse Done! Data:");
for(uint8_t i=2; i<2+uart_protocol_len; i++) // 跳过帧头和长度
{
printf("%02X ", uart_protocol_buf[i]);
}
printf("\r\n");
// 数据转发示例:UART数据转发到CAN
CAN_Msg_t can_msg;
can_msg.id = 0x123;
can_msg.len = uart_protocol_len;
memcpy(can_msg.data, &uart_protocol_buf[2], uart_protocol_len);
CAN_Send_Msg(&can_msg); // 发送CAN报文
}
else if(parse_ret == 2)
{
// 解析错误,打印提示
printf("UART Parse Error!\r\n");
}
}
}
3.4.2 方式2:函数指针表实现(进阶,减少switch-case臃肿)
当状态机的状态超过10个时,switch-case会变得臃肿——函数指针表的核心是“每个状态对应一个处理函数”,用数组存储函数指针,通过当前状态索引调用对应函数。
新手先了解思路,不用急着写,等掌握switch-case后再进阶:
// 第一步:定义状态处理函数类型
typedef uint8_t (*StateHandler_t)(uint8_t data);
// 第二步:定义每个状态的处理函数
uint8_t State_Idle_Handler(uint8_t data);
uint8_t State_Wait_Head_Handler(uint8_t data);
uint8_t State_Recv_Len_Handler(uint8_t data);
// ... 其他状态函数
// 第三步:创建函数指针数组(状态→函数)
StateHandler_t state_handler_table[] = {
State_Idle_Handler, // STATE_IDLE
State_Wait_Head_Handler, // STATE_WAIT_HEAD
State_Recv_Len_Handler, // STATE_RECV_LEN
// ... 其他状态函数
};
// 第四步:状态机主函数
uint8_t FSM_Main(uint8_t data)
{
// 根据当前状态,调用对应的处理函数
return state_handler_table[uart_parse_state](data);
}
3.5 不同通信协议的状态机设计(新手实战)
3.5.1 CAN报文解析状态机(汽车场景)
CAN报文的解析比UART简单,因为CAN本身是“帧化”的(每帧都是完整的),状态机主要用于“过滤ID+解析数据”:
// CAN解析状态枚举
typedef enum
{
CAN_STATE_IDLE = 0,
CAN_STATE_CHECK_ID, // 检查ID
CAN_STATE_PARSE_DATA, // 解析数据
CAN_STATE_DONE
} CanParseState_t;
CanParseState_t can_parse_state = CAN_STATE_IDLE;
#define TARGET_CAN_ID 0x123 // 目标ID
uint8_t Can_Parse_State_Machine(CAN_Msg_t *can_msg)
{
switch(can_parse_state)
{
case CAN_STATE_IDLE:
{
// 转移到检查ID状态
can_parse_state = CAN_STATE_CHECK_ID;
break;
}
case CAN_STATE_CHECK_ID:
{
if(can_msg->id == TARGET_CAN_ID)
{
// ID匹配,转到解析数据状态
can_parse_state = CAN_STATE_PARSE_DATA;
}
else
{
// ID不匹配,回到空闲状态
can_parse_state = CAN_STATE_IDLE;
return 2; // 解析错误
}
break;
}
case CAN_STATE_PARSE_DATA:
{
// 解析数据(比如0x123 ID的报文,第0字节是温度,第1字节是湿度)
uint8_t temp = can_msg->data[0];
uint8_t humi = can_msg->data[1];
printf("CAN Data: Temp=%d°C, Humi=%d%%\r\n", temp, humi);
// 转移到完成状态
can_parse_state = CAN_STATE_DONE;
break;
}
case CAN_STATE_DONE:
{
can_parse case CAN_STATE_DONE:
{
can_parse_state = CAN_STATE_IDLE; // 重置状态机,准备下一次解析
return 1; // 解析完成
}
default:
{
can_parse_state = CAN_STATE_IDLE;
return 2; // 未知状态,解析错误
}
}
return 0;
}
// 主循环集成CAN状态机
CAN_Msg_t can_msg;
uint8_t can_parse_ret;
while(1)
{
// 从CAN环形FIFO读取报文
if(RingFifo_Read_Struct(&can1_fifo, &can_msg) == 0)
{
// 喂给状态机解析
can_parse_ret = Can_Parse_State_Machine(&can_msg);
if(can_parse_ret 1)
{
printf("CAN Parse Done!\r\n");
}
else if(can_parse_ret 2)
{
printf("CAN Parse Error! ID:0x%03X\r\n", can_msg.id);
}
}
}
3.5.2 IIC传感器解析状态机(温湿度传感器示例)
IIC通信的核心是“地址寻址+ACK应答”,状态机需要处理“发送地址→等待ACK→接收数据→发送NACK→停止”的完整流程。以常用的AHT20温湿度传感器为例,协议流程:
- 发送传感器地址(0x38,写模式);
- 发送读取命令(0xAC + 0x33 + 0x00);
- 等待传感器响应(约80ms);
- 发送传感器地址(0x38,读模式);
- 接收6字节数据(湿度高8位→湿度低8位→湿度校验位→温度高8位→温度低8位→温度校验位);
- 发送NACK,停止通信;
- 解析数据(湿度=(H1<<12 | H2<<4 | H3>>4)/1024.0/10.0,温度=((H3&0x0F)<<16 | T1<<8 | T2)/1024.0/10.0)。
步骤1:定义IIC解析状态枚举
typedef enum
{
I2C_AHT20_STATE_IDLE = 0, // 空闲状态
I2C_AHT20_STATE_SEND_ADDR_WRITE, // 发送写地址(0x38)
I2C_AHT20_STATE_SEND_CMD1, // 发送命令1(0xAC)
I2C_AHT20_STATE_SEND_CMD2, // 发送命令2(0x33)
I2C_AHT20_STATE_SEND_CMD3, // 发送命令3(0x00)
I2C_AHT20_STATE_WAIT_ACK, // 等待传感器ACK
I2C_AHT20_STATE_DELAY, // 等待传感器测量(80ms)
I2C_AHT20_STATE_SEND_ADDR_READ, // 发送读地址(0x39)
I2C_AHT20_STATE_RECV_DATA1, // 接收湿度高8位
I2C_AHT20_STATE_RECV_DATA2, // 接收湿度低8位
I2C_AHT20_STATE_RECV_DATA3, // 接收湿度校验+温度高4位
I2C_AHT20_STATE_RECV_DATA4, // 接收温度低8位
I2C_AHT20_STATE_RECV_DATA5, // 接收温度低8位
I2C_AHT20_STATE_RECV_DATA6, // 接收温度校验位
I2C_AHT20_STATE_SEND_NACK, // 发送NACK
I2C_AHT20_STATE_STOP, // 发送停止信号
I2C_AHT20_STATE_PARSE_DATA, // 解析温湿度数据
I2C_AHT20_STATE_DONE, // 解析完成
I2C_AHT20_STATE_ERROR // 错误状态
} I2C_AHT20_ParseState_t;
步骤2:定义全局变量
I2C_AHT20_ParseState_t i2c_aht20_state = I2C_AHT20_STATE_IDLE;
uint8_t aht20_data_buf[6]; // 存储接收的6字节数据
uint8_t aht20_data_idx = 0; // 数据缓冲区索引
float aht20_temp = 0.0f; // 解析后的温度
float aht20_humi = 0.0f; // 解析后的湿度
uint32_t aht20_delay_cnt = 0; // 测量延迟计数器
#define AHT20_MEASURE_DELAY 80 // 测量延迟80ms(以1ms为单位)
步骤3:IIC状态机处理函数(结合HAL库IIC函数)
/**
* @brief AHT20温湿度传感器IIC状态机
* @param hi2c:IIC句柄
* @retval 0:正常,1:解析完成,2:错误
*/
uint8_t I2C_AHT20_Parse_State_Machine(I2C_HandleTypeDef *hi2c)
{
HAL_StatusTypeDef ret; // IIC操作返回状态
switch(i2c_aht20_state)
{
case I2C_AHT20_STATE_IDLE:
{
// 初始化变量
aht20_data_idx = 0;
memset(aht20_data_buf, 0, 6);
// 转移状态:发送写地址
i2c_aht20_state = I2C_AHT20_STATE_SEND_ADDR_WRITE;
break;
}
case I2C_AHT20_STATE_SEND_ADDR_WRITE:
{
// 动作:发送传感器写地址(0x38 << 1 | 0 = 0x70)
ret = HAL_I2C_Master_Transmit(hi2c, 0x70, NULL, 0, 100);
if(ret == HAL_OK)
{
// 地址发送成功,转移到发送命令1
i2c_aht20_state = I2C_AHT20_STATE_SEND_CMD1;
}
else
{
// 地址发送失败,转移到错误状态
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_SEND_CMD1:
{
// 动作:发送命令1(0xAC)
uint8_t cmd1 = 0xAC;
ret = HAL_I2C_Master_Transmit(hi2c, 0x70, &cmd1, 1, 100);
if(ret == HAL_OK)
{
i2c_aht20_state = I2C_AHT20_STATE_SEND_CMD2;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_SEND_CMD2:
{
// 动作:发送命令2(0x33)
uint8_t cmd2 = 0x33;
ret = HAL_I2C_Master_Transmit(hi2c, 0x70, &cmd2, 1, 100);
if(ret == HAL_OK)
{
i2c_aht20_state = I2C_AHT20_STATE_SEND_CMD3;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_SEND_CMD3:
{
// 动作:发送命令3(0x00)
uint8_t cmd3 = 0x00;
ret = HAL_I2C_Master_Transmit(hi2c, 0x70, &cmd3, 1, 100);
if(ret == HAL_OK)
{
// 命令发送完成,转移到等待ACK状态
i2c_aht20_state = I2C_AHT20_STATE_WAIT_ACK;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_WAIT_ACK:
{
// 动作:读取传感器响应(0x00表示测量中,0x01表示测量完成)
uint8_t ack = 0;
ret = HAL_I2C_Master_Receive(hi2c, 0x71, &ack, 1, 100); // 读地址0x39 << 1 | 1 = 0x71
if(ret HAL_OK)
{
if((ack & 0x80) 0) // 第7位为0,测量完成
{
// 转移到延迟状态(实际测量已完成,这里确保数据稳定)
i2c_aht20_state = I2C_AHT20_STATE_DELAY;
aht20_delay_cnt = 0;
}
else // 测量中,继续等待
{
i2c_aht20_state = I2C_AHT20_STATE_WAIT_ACK;
}
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_DELAY:
{
// 动作:延迟80ms(主循环1ms调用一次状态机,计数80次)
aht20_delay_cnt++;
if(aht20_delay_cnt >= AHT20_MEASURE_DELAY)
{
// 延迟完成,转移到发送读地址
i2c_aht20_state = I2C_AHT20_STATE_SEND_ADDR_READ;
aht20_delay_cnt = 0;
}
break;
}
case I2C_AHT20_STATE_SEND_ADDR_READ:
{
// 动作:发送读地址(0x39 << 1 | 1 = 0x71)
ret = HAL_I2C_Master_Transmit(hi2c, 0x71, NULL, 0, 100);
if(ret == HAL_OK)
{
// 读地址发送成功,转移到接收数据1
i2c_aht20_state = I2C_AHT20_STATE_RECV_DATA1;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_RECV_DATA1:
{
// 动作:接收湿度高8位(发送ACK)
ret = HAL_I2C_Master_Receive(hi2c, 0x71, &aht20_data_buf[aht20_data_idx++], 1, 100);
if(ret == HAL_OK)
{
// 接收成功,转移到接收数据2
i2c_aht20_state = I2C_AHT20_STATE_RECV_DATA2;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_RECV_DATA2:
{
// 动作:接收湿度低8位(发送ACK)
ret = HAL_I2C_Master_Receive(hi2c, 0x71, &aht20_data_buf[aht20_data_idx++], 1, 100);
if(ret == HAL_OK)
{
i2c_aht20_state = I2C_AHT20_STATE_RECV_DATA3;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_RECV_DATA3:
{
// 动作:接收湿度校验+温度高4位(发送ACK)
ret = HAL_I2C_Master_Receive(hi2c, 0x71, &aht20_data_buf[aht20_data_idx++], 1, 100);
if(ret == HAL_OK)
{
i2c_aht20_state = I2C_AHT20_STATE_RECV_DATA4;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_RECV_DATA4:
{
// 动作:接收温度低8位(发送ACK)
ret = HAL_I2C_Master_Receive(hi2c, 0x71, &aht20_data_buf[aht20_data_idx++], 1, 100);
if(ret == HAL_OK)
{
i2c_aht20_state = I2C_AHT20_STATE_RECV_DATA5;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_RECV_DATA5:
{
// 动作:接收温度低8位(发送ACK)
ret = HAL_I2C_Master_Receive(hi2c, 0x71, &aht20_data_buf[aht20_data_idx++], 1, 100);
if(ret == HAL_OK)
{
i2c_aht20_state = I2C_AHT20_STATE_RECV_DATA6;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_RECV_DATA6:
{
// 动作:接收温度校验位(发送NACK)
ret = HAL_I2C_Master_Receive(hi2c, 0x71, &aht20_data_buf[aht20_data_idx++], 1, 100);
if(ret == HAL_OK)
{
// 6字节数据接收完成,转移到发送停止信号
i2c_aht20_state = I2C_AHT20_STATE_STOP;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
break;
}
case I2C_AHT20_STATE_STOP:
{
// 动作:发送IIC停止信号
HAL_I2C_Stop(hi2c);
// 转移到解析数据状态
i2c_aht20_state = I2C_AHT20_STATE_PARSE_DATA;
break;
}
case I2C_AHT20_STATE_PARSE_DATA:
{
// 动作:解析温湿度数据(按AHT20协议公式)
uint32_t humi_raw = (aht20_data_buf[0] << 12) | (aht20_data_buf[1] << 4) | (aht20_data_buf[2] >> 4);
uint32_t temp_raw = ((aht20_data_buf[2] & 0x0F) << 16) | (aht20_data_buf[3] << 8) | aht20_data_buf[4];
aht20_humi = (humi_raw / 1024.0f) / 10.0f; // 湿度范围0-100%
aht20_temp = (temp_raw / 1024.0f) / 10.0f; // 温度范围-40~85°C
// 校验数据合法性
if(aht20_humi > 100.0f || aht20_temp < -40.0f || aht20_temp > 85.0f)
{
i2c_aht20_state = I2C_AHT20_STATE_ERROR;
}
else
{
i2c_aht20_state = I2C_AHT20_STATE_DONE;
}
break;
}
case I2C_AHT20_STATE_DONE:
{
// 动作:打印解析结果,重置状态机
printf("AHT20 Parse Done! Temp:%.2f°C, Humi:%.2f%%\r\n", aht20_temp, aht20_humi);
i2c_aht20_state = I2C_AHT20_STATE_IDLE;
return 1; // 解析完成
}
case I2C_AHT20_STATE_ERROR:
{
// 动作:打印错误信息,重置状态机
printf("AHT20 Parse Error!\r\n");
i2c_aht20_state = I2C_AHT20_STATE_IDLE;
return 2; // 解析错误
}
default:
{
i2c_aht20_state = I2C_AHT20_STATE_IDLE;
return 2;
}
}
return 0;
}
步骤4:主循环调用IIC状态机
// 主循环中每2秒读取一次温湿度
uint8_t i2c_parse_ret;
while(1)
{
// 调用AHT20状态机
i2c_parse_ret = I2C_AHT20_Parse_State_Machine(&hi2c1);
if(i2c_parse_ret 1)
{
// 解析完成,可将数据转发到UART/CAN
uint8_t send_buf[10];
sprintf((char*)send_buf, "T:%.2f H:%.2f", aht20_temp, aht20_humi);
HAL_UART_Transmit(&huart1, send_buf, strlen((char*)send_buf), 100);
HAL_UART_Transmit(&huart1, "\r\n", 2, 100);
}
else if(i2c_parse_ret 2)
{
// 解析错误,延时后重试
HAL_Delay(1000);
}
// 每2秒触发一次测量
HAL_Delay(2000);
}
3.5.3 SPI Flash解析状态机(W25Q64示例)
SPI通信的核心是“同步时钟+片选控制”,状态机需要处理“片选拉低→发送命令→发送地址→读写数据→片选拉高”的流程。以W25Q64 Flash为例,实现“读取ID→读取指定地址数据→转发到UART”的功能,协议流程:
- 片选(CS)拉低;
- 发送读ID命令(0x90);
- 发送3字节地址(0x00, 0x00, 0x00);
- 接收3字节ID(0xEF, 0x40, 0x17);
- 片选拉高;
- 片选拉低;
- 发送读数据命令(0x03);
- 发送3字节地址(比如0x00, 0x00, 0x00);
- 接收N字节数据;
- 片选拉高;
- 将数据转发到UART。
步骤1:定义SPI解析状态枚举
typedef enum
{
SPI_W25Q64_STATE_IDLE = 0, // 空闲状态
SPI_W25Q64_STATE_CS_LOW_READ_ID, // 片选拉低(读ID)
SPI_W25Q64_STATE_SEND_CMD_READ_ID, // 发送读ID命令(0x90)
SPI_W25Q64_STATE_SEND_ADDR1_ID, // 发送地址1(0x00)
SPI_W25Q64_STATE_SEND_ADDR2_ID, // 发送地址2(0x00)
SPI_W25Q64_STATE_SEND_ADDR3_ID, // 发送地址3(0x00)
SPI_W25Q64_STATE_RECV_ID1, // 接收ID1(0xEF)
SPI_W25Q64_STATE_RECV_ID2, // 接收ID2(0x40)
SPI_W25Q64_STATE_RECV_ID3, // 接收ID3(0x17)
SPI_W25Q64_STATE_CS_HIGH_READ_ID,// 片选拉高(读ID完成)
SPI_W25Q64_STATE_CS_LOW_READ_DATA, // 片选拉低(读数据)
SPI_W25Q64_STATE_SEND_CMD_READ_DATA, // 发送读数据命令(0x03)
SPI_W25Q64_STATE_SEND_ADDR1_DATA, // 发送地址1(0x00)
SPI_W25Q64_STATE_SEND_ADDR2_DATA, // 发送地址2(0x00)
SPI_W25Q64_STATE_SEND_ADDR3_DATA, // 发送地址3(0x00)
SPI_W25Q64_STATE_RECV_DATA, // 接收数据
SPI_W25Q64_STATE_CS_HIGH_READ_DATA, // 片选拉高(读数据完成)
SPI_W25Q64_STATE_PARSE_ID, // 解析ID
SPI_W25Q64_STATE_FORWARD_DATA, // 转发数据到UART
SPI_W25Q64_STATE_DONE, // 完成状态
SPI_W25Q64_STATE_ERROR // 错误状态
} SPI_W25Q64_ParseState_t;
步骤2:定义全局变量
SPI_W25Q64_ParseState_t spi_w25q64_state = SPI_W25Q64_STATE_IDLE;
uint8_t w25q64_id_buf[3]; // ID缓冲区
uint8_t w25q64_data_buf[32]; // 数据缓冲区
uint8_t w25q64_data_len = 16; // 要读取的数据长度
uint8_t w25q64_data_idx = 0; // 数据缓冲区索引
#define SPI_CS_PIN GPIO_PIN_4 // 片选引脚(PA4)
#define SPI_CS_PORT GPIOA // 片选端口
步骤3:SPI状态机处理函数(结合HAL库SPI函数)
/**
* @brief W25Q64 Flash SPI状态机
* @param hspi:SPI句柄
* @retval 0:正常,1:完成,2:错误
*/
uint8_t SPI_W25Q64_Parse_State_Machine(SPI_HandleTypeDef *hspi)
{
HAL_StatusTypeDef ret;
switch(spi_w25q64_state)
{
case SPI_W25Q64_STATE_IDLE:
{
// 初始化变量
memset(w25q64_id_buf, 0, 3);
memset(w25q64_data_buf, 0, 32);
w25q64_data_idx = 0;
// 转移到片选拉低(读ID)
spi_w25q64_state = SPI_W25Q64_STATE_CS_LOW_READ_ID;
break;
}
case SPI_W25Q64_STATE_CS_LOW_READ_ID:
{
// 动作:片选拉低
HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_RESET);
// 转移到发送读ID命令
spi_w25q64_state = SPI_W25Q64_STATE_SEND_CMD_READ_ID;
break;
}
case SPI_W25Q64_STATE_SEND_CMD_READ_ID:
{
// 动作:发送读ID命令(0x90)
uint8_t cmd = 0x90;
ret = HAL_SPI_Transmit(hspi, &cmd, 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_SEND_ADDR1_ID;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_SEND_ADDR1_ID:
{
// 动作:发送地址1(0x00)
uint8_t addr1 = 0x00;
ret = HAL_SPI_Transmit(hspi, &addr1, 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_SEND_ADDR2_ID;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_SEND_ADDR2_ID:
{
// 动作:发送地址2(0x00)
uint8_t addr2 = 0x00;
ret = HAL_SPI_Transmit(hspi, &addr2, 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_SEND_ADDR3_ID;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_SEND_ADDR3_ID:
{
// 动作:发送地址3(0x00)
uint8_t addr3 = 0x00;
ret = HAL_SPI_Transmit(hspi, &addr3, 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_RECV_ID1;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_RECV_ID1:
{
// 动作:接收ID1(0xEF)
ret = HAL_SPI_Receive(hspi, &w25q64_id_buf[0], 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_RECV_ID2;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_RECV_ID2:
{
// 动作:接收ID2(0x40)
ret = HAL_SPI_Receive(hspi, &w25q64_id_buf[1], 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_RECV_ID3;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_RECV_ID3:
{
// 动作:接收ID3(0x17)
ret = HAL_SPI_Receive(hspi, &w25q64_id_buf[2], 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_CS_HIGH_READ_ID;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_CS_HIGH_READ_ID:
{
// 动作:片选拉高
HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET);
// 转移到解析ID状态
spi_w25q64_state = SPI_W25Q64_STATE_PARSE_ID;
break;
}
case SPI_W25Q64_STATE_PARSE_ID:
{
// 动作:校验ID是否正确
if(w25q64_id_buf[0] 0xEF && w25q64_id_buf[1] 0x40 && w25q64_id_buf[2] == 0x17)
{
printf("W25Q64 ID Verify OK! ID:0x%02X 0x%02X 0x%02X\r\n",
w25q64_id_buf[0], w25q64_id_buf[1], w25q64_id_buf[2]);
// ID正确,转移到片选拉低(读数据)
spi_w25q64_state = SPI_W25Q64_STATE_CS_LOW_READ_DATA;
}
else
{
printf("W25Q64 ID Verify Error! ID:0x%02X 0x%02X 0x%02X\r\n",
w25q64_id_buf[0], w25q64_id_buf[1], w25q64_id_buf[2]);
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_CS_LOW_READ_DATA:
{
// 动作:片选拉低
HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_RESET);
// 转移到发送读数据命令
spi_w25q64_state = SPI_W25Q64_STATE_SEND_CMD_READ_DATA;
break;
}
case SPI_W25Q64_STATE_SEND_CMD_READ_DATA:
{
// 动作:发送读数据命令(0x03)
uint8_t cmd = 0x03;
ret = HAL_SPI_Transmit(hspi, &cmd, 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_SEND_ADDR1_DATA;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_SEND_ADDR1_DATA:
{
// 动作:发送地址1(0x00)
uint8_t addr1 = 0x00;
ret = HAL_SPI_Transmit(hspi, &addr1, 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_SEND_ADDR2_DATA;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_SEND_ADDR2_DATA:
{
// 动作:发送地址2(0x00)
uint8_t addr2 = 0x00;
ret = HAL_SPI_Transmit(hspi, &addr2, 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_SEND_ADDR3_DATA;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_SEND_ADDR3_DATA:
{
// 动作:发送地址3(0x00)
uint8_t addr3 = 0x00;
ret = HAL_SPI_Transmit(hspi, &addr3, 1, 100);
if(ret == HAL_OK)
{
spi_w25q64_state = SPI_W25Q64_STATE_RECV_DATA;
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_RECV_DATA:
{
// 动作:接收数据(直到接收完指定长度)
ret = HAL_SPI_Receive(hspi, &w25q64_data_buf[w25q64_data_idx++], 1, 100);
if(ret == HAL_OK)
{
if(w25q64_data_idx >= w25q64_data_len)
{
// 数据接收完成,转移到片选拉高
spi_w25q64_state = SPI_W25Q64_STATE_CS_HIGH_READ_DATA;
w25q64_data_idx = 0;
}
else
{
// 继续接收下一字节
spi_w25q64_state = SPI_W25Q64_STATE_RECV_DATA;
}
}
else
{
spi_w25q64_state = SPI_W25Q64_STATE_ERROR;
}
break;
}
case SPI_W25Q64_STATE_CS_HIGH_READ_DATA:
{
// 动作:片选拉高
HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET);
// 转移到转发数据状态
spi_w25q64_state = SPI_W25Q64_STATE_FORWARD_DATA;
break;
}
case SPI_W25Q64_STATE_FORWARD_DATA:
{
// 动作:将Flash数据转发到UART
printf("W25Q64 Data (Addr:0x000000, Len:%d):", w25q64_data_len);
for(uint8_t i=0; i<w25q64_data_len; i++)
{
printf(" %02X", w25q64_data_buf[i]);
}
printf("\r\n");
// 转发到UART1
HAL_UART_Transmit(&huart1, (uint8_t*)"W25Q64 Data:", 11, 100);
HAL_UART_Transmit(&huart1, w25q64_data_buf, w25q64_data_len, 100);
HAL_UART_Transmit(&huart1, "\r\n", 2, 100);
// 转移到完成状态
spi_w25q64_state = SPI_W25Q64_STATE_DONE;
break;
}
case SPI_W25Q64_STATE_DONE:
{
// 动作:重置状态机
spi_w25q64_state = SPI_W25Q64_STATE_IDLE;
return 1; // 完成
}
case SPI_W25Q64_STATE_ERROR:
{
// 动作:打印错误信息,重置状态机,片选拉高(防止卡死)
HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET);
printf("W25Q64 Parse Error!\r\n");
spi_w25q64_state = SPI_W25Q64_STATE_IDLE;
return 2; // 错误
}
default:
{
HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET);
spi_w25q64_state = SPI_W25Q64_STATE_IDLE;
return 2;
}
}
return 0;
}
步骤4:主循环调用SPI状态机
// 主循环中每3秒读取一次Flash数据
uint8_t spi_parse_ret;
while(1)
{
// 调用W25Q64状态机
spi_parse_ret = SPI_W25Q64_Parse_State_Machine(&hspi1);
if(spi_parse_ret 1)
{
printf("W25Q64 Operation Done!\r\n");
}
else if(spi_parse_ret 2)
{
printf("W25Q64 Operation Error, Retry...\r\n");
}
// 每3秒触发一次
HAL_Delay(3000);
}
3.6 状态机新手常见问题与排错
| 问题现象 | 常见原因 | 解决方法 |
|---|---|---|
| 状态机卡死在某个状态 | 1. 没有超时处理;2. 事件触发条件永远不满足;3. 硬件操作失败(比如IIC没收到ACK) | 1. 给每个状态加超时计数器,超时后重置状态机;2. 检查事件条件(比如帧头是否正确、ACK是否检测);3. 用逻辑分析仪抓硬件波形,确认通信是否正常 |
| 状态转移错误 | 1. 状态转移条件写反;2. 变量没初始化(比如索引归0);3. 多状态共享变量冲突 | 1. 逐行检查状态转移逻辑(比如if(data == 0xAA)是否写成if(data != 0xAA));2. 每个状态入口初始化关键变量;3. 给每个状态单独分配变量,避免共享冲突 |
| 数据解析错误 | 1. 协议帧格式理解错误;2. 校验和/CRC计算错误;3. 数据字节序错误(大端/小端) | 1. 重新核对协议文档(比如帧头、长度、校验位的位置);2. 用计算器手动验证校验和;3. 确认数据字节序(比如CAN数据是小端,网络数据是大端) |
| 状态机频繁进入错误状态 | 1. 硬件干扰导致数据错误;2. 超时时间设置过短;3. 通信波特率/时序参数不匹配 | 1. 增加硬件滤波(比如CAN加终端电阻、IIC加上拉电阻);2. 延长超时时间(比如UART解析超时设为2000ms);3. 重新核对通信参数(比如SPI时钟极性/相位) |
| 多字节数据接收不完整 | 1. 长度字段解析错误;2. 接收索引没正确递增;3. 缓冲区溢出 | 1. 单独打印长度字段,确认是否正确;2. 检查索引递增逻辑(idx++是否遗漏);3. 增大缓冲区,或在接收前检查长度合法性 |
3.7 状态机调试技巧(新手必备)
- 打印状态日志:在每个状态的入口打印当前状态,比如
printf("Current State: STATE_WAIT_HEAD\r\n"),观察状态转移是否符合预期; - 打印关键变量:在状态处理中打印事件数据(比如收到的字节)、索引、长度、校验和等,确认变量值是否正确;
- 使用逻辑分析仪:抓硬件通信波形(UART TX/RX、CAN_H/CAN_L、IIC SDA/SCL、SPI SCK/MOSI/MISO),确认数据发送/接收是否正确;
- 单步调试:用IDE的调试功能(比如MDK的Debug),单步执行状态机,观察状态转移和变量变化;
- 简化状态机:先实现核心状态(比如只处理帧头和数据),调试通过后再添加校验、帧尾等复杂逻辑;
- 添加错误计数器:统计每个错误状态的触发次数,定位高频错误(比如IIC ACK错误、UART校验错误)。
第四章 综合实战:环形FIFO+状态机+四大通信接口的数据转发系统
4.1 系统设计目标(新手可落地)
设计一个“多接口数据转发网关”,实现以下功能:
- UART→CAN转发:UART接收自定义协议帧→环形FIFO缓冲→状态机解析→转发到CAN总线;
- CAN→UART转发:CAN接收指定ID报文→环形FIFO缓冲→状态机解析→转发到UART(上位机显示);
- IIC→UART/CAN转发:IIC读取AHT20温湿度→环形FIFO缓冲→状态机解析→同时转发到UART和CAN;
- SPI→UART转发:SPI读取W25Q64 Flash数据→环形FIFO缓冲→状态机解析→转发到UART;
- 可靠性设计:添加校验和/CRC、超时处理、缓冲区水位控制、错误重发机制。
4.2 系统架构图(新手直观理解)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 硬件接口层 │ │ 环形FIFO层 │ │ 状态机层 │ │ 转发控制层 │
├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤
│ UART1 (RX/TX)│→→→→→│UART1 FIFO │→→→→→│UART解析FSM │→→→→→│转发到CAN │
│ CAN1 (RX/TX) │→→→→→│CAN1 FIFO │→→→→→│CAN解析FSM │→→→→→│转发到UART │
│ I2C1 │→→→→→│I2C1 FIFO │→→→→→│AHT20 FSM │→→→→→│转发到UART/CAN│
│ SPI1 │→→→→→│SPI1 FIFO │→→→→→│W25Q64 FSM │→→→→→│转发到UART │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
4.3 硬件选型(新手低成本)
- MCU:STM32F103C8T6(蓝桥板,性价比高,支持所有接口);
- 传感器:AHT20(IIC温湿度传感器);
- Flash:W25Q64(SPI接口,8MB容量);
- CAN收发器:TJA1050(CAN总线物理层);
- IIC上拉电阻:2个4.7KΩ电阻(SDA/SCL各一个);
- CAN终端电阻:1个120Ω电阻(CAN_H和CAN_L之间);
- 电源:5V转3.3V(AMS1117-3.3)。
4.4 软件模块设计(新手模块化开发)
4.4.1 模块划分(新手按模块编写代码)
- 底层驱动模块:UART、CAN、IIC、SPI的HAL库配置(CubeMX生成);
- 环形FIFO模块:复用第二章的
ring_fifo.c/ring_fifo.h,为每个接口分配独立FIFO; - 状态机模块:复用第三章的各接口解析FSM,添加转发逻辑;
- 转发控制模块:实现“解析完成→转发到目标接口”的逻辑;
- 主函数模块:初始化所有模块,主循环调度各状态机。
4.4.2 全局变量定义(main.c中)
// 1. 环形FIFO定义(每个接口独立FIFO)
// UART1 FIFO
#define UART1_FIFO_SIZE 128
uint8_t uart1_fifo_buf[UART1_FIFO_SIZE];
RingFifo_t uart1_fifo;
// CAN1 FIFO(存储CAN报文结构体)
#define CAN1_FIFO_SIZE 32
CAN_Msg_t can1_fifo_buf[CAN1_FIFO_SIZE];
RingFifo_t can1_fifo;
// I2C1 FIFO(存储AHT20温湿度数据)
#define I2C1_FIFO_SIZE 16
uint8_t i2c1_fifo_buf[I2C1_FIFO_SIZE];
RingFifo_t i2c1_fifo;
// SPI1 FIFO(存储W25Q64数据)
#define SPI1_FIFO_SIZE 64
uint8_t spi1_fifo_buf[SPI1_FIFO_SIZE];
RingFifo_t spi1_fifo;
// 2. 状态机相关变量(复用第三章定义)
// UART解析状态机变量
UartParseState_t uart_parse_state = STATE_IDLE;
uint8_t uart_protocol_buf[64];
uint16_t uart_protocol_idx = 0;
uint8_t uart_protocol_len = 0;
uint8_t uart_protocol_check = 0;
uint32_t uart_parse_timeout = 0;
#define UART_PARSE_TIMEOUT 2000
// CAN解析状态机变量
CanParseState_t can_parse_state = CAN_STATE_IDLE;
#define TARGET_CAN_ID 0x123
// AHT20 IIC状态机变量
I2C_AHT20_ParseState_t i2c_aht20_state = I2C_AHT20_STATE_IDLE;
uint8_t aht20_data_buf[6];
uint8_t aht20_data_idx = 0;
float aht20_temp = 0.0f;
float aht20_humi = 0.0f;
uint32_t aht20_delay_cnt = 0;
#define AHT20_MEASURE_DELAY 80
// W25Q64 SPI状态机变量
SPI_W25Q64_ParseState_t spi_w25q64_state = SPI_W25Q64_STATE_IDLE;
uint8_t w25q64_id_buf[3];
uint8_t w25q64_data_buf[32];
uint8_t w25q64_data_len = 16;
uint8_t w25q64_data_idx = 0;
#define SPI_CS_PIN GPIO_PIN_4
#define SPI_CS_PORT GPIOA
// 3. 转发控制变量
#define FORWARD_UART_TO_CAN_ID 0x124 // UART→CAN的目标CAN ID
#define FORWARD_IIC_TO_CAN_ID 0x125 // IIC→CAN的目标CAN ID
4.4.3 模块初始化(main函数中)
int main(void)
{
// 1. HAL库初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_CAN1_Init();
MX_I2C1_Init();
MX_SPI1_Init();
MX_DMA_Init();
// 2. 环形FIFO初始化
RingFifo_Init(&uart1_fifo, uart1_fifo_buf, UART1_FIFO_SIZE);
RingFifo_Init((RingFifo_t*)&can1_fifo, (uint8_t*)can1_fifo_buf, CAN1_FIFO_SIZE * sizeof(CAN_Msg_t)); // 注意:FIFO存储结构体,size是总字节数
RingFifo_Init(&i2c1_fifo, i2c1_fifo_buf, I2C1_FIFO_SIZE);
RingFifo_Init(&spi1_fifo, spi1_fifo_buf, SPI1_FIFO_SIZE);
// 3. 启动UART1 DMA接收
HAL_UART_Receive_DMA(&huart1, (uint8_t*)uart1_dma_buf, 1);
// 4. 启动CAN接收
HAL_CAN_Start(&hcan1);
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
// 5. 打印初始化信息
printf("Multi-Interface Data Forward Gateway Init Done!\r\n");
// 主循环
while (1)
{
// 调度各状态机
Scheduler_State_Machines();
// 延时1ms(降低CPU占用)
HAL_Delay(1);
}
}
4.4.4 调度函数(Scheduler_State_Machines)
/**
* @brief 状态机调度函数(主循环中调用)
* @param 无
* @retval 无
*/
void Scheduler_State_Machines(void)
{
// 1. UART1接收→解析→转发到CAN
Uart1_Recv_Parse_Forward();
// 2. CAN1接收→解析→转发到UART1
CAN1_Recv_Parse_Forward();
// 3. I2C1读取AHT20→解析→转发到UART1/CAN1
I2C1_AHT20_Parse_Forward();
// 4. SPI1读取W25Q64→解析→转发到UART1
SPI1_W25Q64_Parse_Forward();
}
4.4.5 UART→CAN转发实现(Uart1_Recv_Parse_Forward)
/**
* @brief UART1接收→解析→转发到CAN
* @param 无
* @retval 无
*/
void Uart1_Recv_Parse_Forward(void)
{
uint8_t uart_data;
uint8_t parse_ret;
// 处理UART解析超时
uart_parse_timeout++;
if(uart_parse_timeout > UART_PARSE_TIMEOUT)
{
uart_parse_state = STATE_PARSE_ERROR;
uart_parse_timeout = 0;
}
// 从UART1 FIFO读取1字节
if(RingFifo_Read_Byte(&uart1_fifo, &uart_data) == 0)
{
// 喂给UART解析状态机
parse_ret = Uart_Parse_State_Machine(uart_data);
// 解析完成,转发到CAN
if(parse_ret == 1)
{
CAN_Msg_t can_msg;
can_msg.id = FORWARD_UART_TO_CAN_ID; // 目标CAN ID
can_msg.len = uart_protocol_len; // 数据长度(从UART协议解析得到)
memcpy(can_msg.data, &uart_protocol_buf[2], uart_protocol_len); // 跳过帧头和长度字段
// 发送CAN报文(加入CAN FIFO,避免阻塞)
CAN1_Send_Msg_To_FIFO(&can_msg);
printf("UART→CAN Forward Done! CAN ID:0x%03X, Data Len:%d\r\n", can_msg.id, can_msg.len);
}
else if(parse_ret == 2)
{
printf("UART Parse Error!\r\n");
}
}
}
/**
* @brief CAN报文发送到CAN FIFO(非阻塞)
* @param can_msg:CAN报文结构体指针
* @retval 0:成功,1:FIFO满
*/
uint8_t CAN1_Send_Msg_To_FIFO(CAN_Msg_t *can_msg)
{
if(RingFifo_Is_Full((RingFifo_t*)&can1_fifo))
{
printf("CAN1 Send FIFO Full!\r\n");
return 1;
}
// 写入CAN发送FIFO(这里复用了CAN接收FIFO,实际项目可分开定义发送FIFO)
__disable_irq();
memcpy(&can1_fifo_buf[can1_fifo.wr_ptr], can_msg, sizeof(CAN_Msg_t));
can1_fifo.wr_ptr = (can1_fifo.wr_ptr + 1) % CAN1_FIFO_SIZE;
can1_fifo.cnt++;
__enable_irq();
// 触发CAN发送(如果当前没有发送)
CAN1_Send_From_FIFO();
return 0;
}
/**
* @brief 从CAN FIFO读取并发送
* @param 无
* @retval 无
*/
void CAN1_Send_From_FIFO(void)
{
CAN_Msg_t can_msg;
CAN_TxHeaderTypeDef tx_header;
uint32_t tx_mailbox;
// 如果CAN处于未发送状态,且FIFO非空
if(HAL_CAN_GetTxMailboxesFreeLevel(&hcan1) > 0 && !RingFifo_Is_Empty((RingFifo_t*)&can1_fifo))
{
// 从FIFO读取CAN报文
__disable_irq();
memcpy(&can_msg, &can1_fifo_buf[can1_fifo.rd_ptr], sizeof(CAN_Msg_t));
can1_fifo.rd_ptr = (can1_fifo.rd_ptr + 1) % CAN1_FIFO_SIZE;
can1_fifo.cnt--;
__enable_irq();
// 配置CAN发送头
tx_header.StdId = can_msg.id;
tx_header.ExtId = 0;
tx_header.RTR = CAN_RTR_DATA;
tx_header.IDE = CAN_ID_STD;
tx_header.DLC = can_msg.len;
tx_header.TransmitGlobalTime = DISABLE;
// 发送CAN报文
if(HAL_CAN_AddTxMessage(&hcan1, &tx_header, can_msg.data, &tx_mailbox) != HAL_OK)
{
printf("CAN Send Error! ID:0x%03X\r\n", can_msg.id);
// 发送失败,重新写入FIFO(最多重试3次)
static uint8_t retry_cnt = 0;
if(retry_cnt < 3)
{
CAN1_Send_Msg_To_FIFO(&can_msg);
retry_cnt++;
}
else
{
retry_cnt = 0;
}
}
else
{
printf("CAN Send Success! ID:0x%03X\r\n", can_msg.id);
}
}
}
4.4.6 CAN→UART转发实现(CAN1_Recv_Parse_Forward)
/**
* @brief CAN1接收→解析→转发到UART1
* @param 无
* @retval 无
*/
void CAN1_Recv_Parse_Forward(void)
{
CAN_Msg_t can_msg;
uint8_t parse_ret;
uint8_t send_buf[100];
uint16_t send_len;
// 从CAN1 FIFO读取1个报文
if(RingFifo_Read_Struct(&can1_fifo, &can_msg) == 0)
{
// 喂给CAN解析状态机
parse_ret = Can_Parse_State_Machine(&can_msg);
// 解析完成,转发到UART1
if(parse_ret == 1)
{
// 格式化发送数据(示例:"CAN→UART: ID=0x123, Data=01 02 03")
send_len = sprintf((char*)send_buf, "CAN→UART: ID=0x%03X, Data=", can_msg.id);
for(uint8_t i=0; i<can_msg.len; i++)
{
send_len += sprintf((char*)&send_buf[send_len], "%02X ", can_msg.data[i]);
}
send_len += sprintf((char*)&send_buf[send_len], "\r\n");
// 转发到UART1(写入UART1发送FIFO,非阻塞)
UART1_Send_Msg_To_FIFO(send_buf, send_len);
printf("CAN→UART Forward Done! UART Send Len:%d\r\n", send_len);
}
else if(parse_ret 2)
{
printf("CAN Parse Error! ID:0x%03X\r\n", can_msg.id);
}
}
}
/**
* @brief UART1发送数据到发送FIFO(非阻塞)
* @param data:要发送的数据指针
* @param len:要发送的长度
* @retval 实际发送的长度
*/
uint16_t UART1_Send_Msg_To_FIFO(uint8_t *data, uint16_t len)
{
if(data NULL || len == 0)
{
return 0;
}
// 计算可写入的长度(FIFO剩余空间)
uint16_t free_size = UART1_FIFO_SIZE - RingFifo_Get_Len(&uart1_fifo);
uint16_t write_len = (len < free_size) ? len : free_size;
if(write_len 0)
{
printf("UART1 Send FIFO Full!\r\n");
return 0;
}
// 写入UART1发送FIFO(这里复用了接收FIFO,实际项目可分开定义发送FIFO)
RingFifo_Write_Multi(&uart1_fifo, data, write_len);
// 启动UART1发送(如果当前没有发送)
UART1_Send_From_FIFO();
return write_len;
}
/**
* @brief 从UART1 FIFO读取并发送
* @param 无
* @retval 无
*/
void UART1_Send_From_FIFO(void)
{
uint8_t send_data;
// 如果UART处于未发送状态,且FIFO非空
if(HAL_UART_GetState(&huart1) HAL_UART_STATE_READY && !RingFifo_Is_Empty(&uart1_fifo))
{
// 读取1字节并发送(中断模式)
RingFifo_Read_Byte(&uart1_fifo, &send_data);
HAL_UART_Transmit_IT(&huart1, &send_data, 1);
}
}
// UART1发送完成回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
// 继续发送FIFO中的数据
UART1_Send_From_FIFO();
}
}
4.4.7 IIC→UART/CAN转发实现(I2C1_AHT20_Parse_Forward)
/**
* @brief I2C1读取AHT20→解析→转发到UART1/CAN1
* @param 无
* @retval 无
*/
void I2C1_AHT20_Parse_Forward(void)
{
static uint32_t i2c_aht20_interval = 0;
uint8_t parse_ret;
// 每2秒读取一次AHT20
i2c_aht20_interval++;
if(i2c_aht20_interval < 2000) // 2000ms
{
return;
}
i2c_aht20_interval = 0;
// 调用AHT20状态机
parse_ret = I2C_AHT20_Parse_State_Machine(&hi2c1);
// 解析完成,转发到UART1和CAN1
if(parse_ret == 1)
{
uint8_t uart_send_buf[50];
uint16_t uart_send_len;
CAN_Msg_t can_msg;
// 1. 转发到UART1
uart_send_len = sprintf((char*)uart_send_buf, "I2C→UART: Temp=%.2f°C, Humi=%.2f%%\r\n", aht20_temp, aht20_humi);
UART1_Send_Msg_To_FIFO(uart_send_buf, uart_send_len);
// 2. 转发到CAN1(将浮点数转为整数发送,方便接收方解析)
can_msg.id = FORWARD_IIC_TO_CAN_ID;
can_msg.len = 4;
can_msg.data[0] = (uint8_t)(aht20_temp * 10); // 温度×10,整数部分+小数1位
can_msg.data[1] = (uint8_t)((aht20_temp * 10) >> 8);
can_msg.data[2] = (uint8_t)(aht20_humi * 10);
can_msg.data[3] = (uint8_t)((aht20_humi * 10) >> 8);
CAN1_Send_Msg_To_FIFO(&can_msg);
printf("I2C→UART/CAN Forward Done!\r\n");
}
else if(parse_ret == 2)
{
printf("I2C AHT20 Parse Error!\r\n");
}
}
4.4.8 SPI→UART转发实现(SPI1_W25Q64_Parse_Forward)
/**
* @brief SPI1读取W25Q64→解析→转发到UART1
* @param 无
* @retval 无
*/
void SPI1_W25Q64_Parse_Forward(void)
{
static uint32_t spi_w25q64_interval = 0;
uint8_t parse_ret;
// 每3秒读取一次W25Q64
spi_w25q64_interval++;
if(spi_w25q64_interval < 3000) // 3000ms
{
return;
}
spi_w25q64_interval = 0;
// 调用W25Q64状态机
parse_ret = SPI_W25Q64_Parse_State_Machine(&hspi1);
// 解析完成,转发到UART1(状态机内部已实现转发,这里仅打印日志)
if(parse_ret 1)
{
printf("SPI→UART Forward Done!\r\n");
}
else if(parse_ret 2)
{
printf("SPI W25Q64 Parse Error!\r\n");
}
}
4.4.9 中断回调函数(UART DMA接收、CAN接收)
// UART1 DMA接收完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance USART1)
{
// 将DMA接收的数据写入UART1 FIFO
RingFifo_Write_Byte(&uart1_fifo, uart1_dma_buf[0]);
// 重新启动DMA接收
HAL_UART_Receive_DMA(&huart1, uart1_dma_buf, 1);
}
}
// CAN1 RX FIFO0接收回调函数
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
if(hcan->Instance CAN1)
{
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[8];
CAN_Msg_t can_msg;
// 读取CAN报文
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data);
// 封装成自定义CAN报文结构体
can_msg.id = rx_header.StdId;
can_msg.len = rx_header.DLC;
memcpy(can_msg.data, rx_data, rx_header.DLC);
// 写入CAN1 FIFO
RingFifo_Write_Struct(&can1_fifo, &can_msg);
}
}
4.5 可靠性设计细节(新手必学)
- 非阻塞发送/接收:所有接口都通过环形FIFO实现异步缓冲,避免硬件操作阻塞主循环;
- 超时处理:每个状态机都添加超时计数器,防止因硬件异常导致状态机卡死;
- 缓冲区水位控制:发送数据前检查FIFO剩余空间,避免溢出;
- 错误重发:CAN发送失败时,将报文重新写入FIFO,最多重试3次;
- 数据校验:UART协议添加校验和,CAN报文本身带CRC,确保数据完整性;
- 临界区保护:FIFO读写时关中断,避免多线程(中断+主循环)访问冲突;
- 变量初始化:每个状态机入口初始化关键变量(如索引、校验和),避免残留值影响;
- 硬件异常处理:SPI片选在错误状态下拉高,IIC通信失败时发送停止信号,防止硬件总线卡死。
4.6 调试与测试步骤(新手按步骤操作)
4.6.1 硬件测试
- 检查接线是否正确(重点:UART TX/RX、CAN_H/CAN_L、IIC SDA/SCL、SPI CS/MOSI/MISO/SCK);
- 测量电源电压(3.3V是否稳定);
- 检查终端电阻(CAN 120Ω、IIC 4.7KΩ)是否焊接正确。
4.6.2 软件测试
- 单独测试各模块:
- UART测试:上位机发送数据,MCU回显,确认UART FIFO和中断正常;
- CAN测试:用CAN分析仪发送指定ID报文,MCU接收后转发到UART,确认CAN FIFO和状态机正常;
- IIC测试:读取AHT20温湿度,上位机显示,确认IIC状态机正常;
- SPI测试:读取W25Q64 ID,上位机
4.6.2 软件测试(续)
-
综合测试(多接口同时转发):
- 场景1:UART→CAN+CAN→UART 同时转发
上位机通过UART发送自定义协议帧(如0xAA 0x03 0x01 0x02 0x03 0xAB 0x55),用CAN分析仪查看是否收到目标ID(0x124)的报文;同时用CAN分析仪发送ID为0x123的报文(如数据0x04 0x05),上位机查看是否收到UART转发的字符串(CAN→UART: ID=0x123, Data=04 05)。 - 场景2:IIC→UART/CAN 同步转发
观察上位机每2秒收到一次温湿度数据(I2C→UART: Temp=25.30°C, Humi=45.20%),同时CAN分析仪收到ID为0x125的报文(数据为温度×10、湿度×10的整数形式)。 - 场景3:SPI→UART 转发
观察上位机每3秒收到一次W25Q64的ID和数据(W25Q64 ID Verify OK! ID:0xEF 0x40 0x17、W25Q64 Data (Addr:0x000000, Len:16): 00 01 02 ...)。
- 场景1:UART→CAN+CAN→UART 同时转发
-
可靠性测试:
- 长时间运行(24小时):观察是否有数据丢失、状态机卡死、FIFO溢出等问题,记录错误次数;
- 干扰测试:用导线靠近通信总线(CAN/IIC/SPI),模拟电磁干扰,观察数据解析错误率是否在可接受范围(建议≤0.1%);
- 极限数据测试:发送最大长度的UART协议帧(如60字节数据),确认FIFO不溢出、解析正常。
4.6.3 常见问题排错指南(新手必备)
| 测试场景 | 问题现象 | 排查步骤 | 解决方法 |
|---|---|---|---|
| UART通信 | 上位机收不到回显 | 1. 检查UART TX/RX接线是否接反;2. 核对波特率(9600)、数据位/校验位/停止位;3. 查看DMA是否启动(HAL_UART_Receive_DMA是否调用);4. 用逻辑分析仪抓UART RX引脚波形 | 1. 交换TX/RX接线;2. 重新配置CubeMX的UART参数;3. 在main函数中确认启动DMA接收;4. 若RX无波形,检查MCU引脚配置(是否为复用功能) |
| CAN通信 | CAN分析仪收不到报文 | 1. 检查CAN_H/CAN_L是否接反;2. 测量终端电阻(CAN_H和CAN_L之间是否为120Ω);3. 核对CAN波特率(如500k);4. 查看CAN是否启动(HAL_CAN_Start)和中断使能 | 1. 交换CAN_H/CAN_L;2. 焊接120Ω终端电阻;3. 重新计算CAN波特率参数(Prescaler/BS1/BS2);4. 在CubeMX中开启CAN RX中断 |
| IIC通信 | AHT20解析错误 | 1. 检查IIC SDA/SCL是否加上拉电阻(4.7KΩ);2. 用逻辑分析仪抓IIC波形,确认地址(0x70)和命令(0xAC)是否发送正确;3. 检查传感器供电(3.3V)是否稳定 | 1. 焊接上拉电阻;2. 调整IIC通信超时时间(延长至200ms);3. 检查传感器接线(VCC/GND/SDA/SCL) |
| SPI通信 | W25Q64 ID读取错误 | 1. 检查SPI CS引脚是否正确拉低/拉高;2. 核对SPI时钟极性(CPOL)和相位(CPHA);3. 用逻辑分析仪抓SCK/MOSI/MISO波形,确认命令(0x90)和地址是否发送正确 | 1. 确认CS引脚GPIO配置(输出模式);2. 在CubeMX中调整SPI的CPOL/CPHA(W25Q64默认CPOL=0,CPHA=0);3. 检查SPI引脚复用配置 |
| 数据转发 | 数据丢失 | 1. 查看FIFO容量是否过小(如UART FIFO仅32字节);2. 检查FIFO读写是否有临界区保护;3. 观察主循环是否有长时间阻塞(如HAL_Delay(100)) | 1. 增大FIFO容量(如UART FIFO改为256字节);2. 确保FIFO读写时关中断;3. 减少主循环阻塞时间,用定时器代替HAL_Delay |
| 状态机 | 卡死在某个状态 | 1. 打印状态日志,确认是否进入某个状态后未转移;2. 检查该状态的事件触发条件是否永远不满足(如等待0xAA但收到其他字节);3. 查看超时计数器是否生效 | 1. 在每个状态入口添加printf("Current State: XXX\r\n");2. 用逻辑分析仪抓通信数据,确认是否有目标事件;3. 延长超时时间或检查超时计数器是否被重置 |
4.7 进阶优化:从“能用”到“好用”(新手进阶方向)
4.7.1 环形FIFO优化
-
无锁环形FIFO(适用于多任务系统): 新手版用了全局关中断,进阶后可改用“原子操作”(如STM32的
__LDREX/__STREX指令)实现无锁访问,减少对系统中断的影响:// 无锁写入1字节(示例) uint8_t RingFifo_LockFree_Write_Byte(RingFifo_t *fifo, uint8_t data) { uint16_t next_wr_ptr; do { next_wr_ptr = (fifo->wr_ptr + 1) % fifo->size; // 检查是否满(next_wr_ptr rd_ptr) if(next_wr_ptr fifo->rd_ptr) { return 1; // 满 } // 原子操作:尝试更新wr_ptr } while(__STREX(next_wr_ptr, &fifo->wr_ptr) != 0); // 写入数据(此时wr_ptr已更新,不会冲突) fifo->buf[fifo->wr_ptr] = data; // 原子操作:cnt++ __DMB(); // 数据内存屏障 fifo->cnt++; __DMB(); return 0; } -
动态FIFO容量(适配可变长数据): 用链表代替数组,实现FIFO容量动态增长,避免固定数组溢出:
// 链表节点结构体 typedef struct Node { uint8_t data; struct Node *next; } Node_t; // 链表式环形FIFO typedef struct { Node_t *head; // 头指针(读) Node_t *tail; // 尾指针(写) uint16_t cnt; // 节点数 } ListRingFifo_t;
4.7.2 状态机优化
-
函数指针表优化(减少switch-case臃肿): 新手版用switch-case,进阶后改用函数指针表,提高代码可读性和执行效率(尤其适合状态数多的场景):
// 状态处理函数类型 typedef uint8_t (*UartStateHandler_t)(uint8_t data); // 声明各状态处理函数 uint8_t UartState_Idle(uint8_t data); uint8_t UartState_WaitHead(uint8_t data); uint8_t UartState_RecvLen(uint8_t data); // ... 其他状态函数 // 状态函数指针表(状态枚举与函数一一对应) UartStateHandler_t uart_state_table[] = { UartState_Idle, UartState_WaitHead, UartState_RecvLen, UartState_RecvData, UartState_RecvCheck, UartState_RecvTail, UartState_ParseDone, UartState_ParseError }; // 状态机主函数 uint8_t UartFSM_Main(uint8_t data) { if(uart_parse_state >= sizeof(uart_state_table)/sizeof(UartStateHandler_t)) { uart_parse_state = STATE_IDLE; return 2; } // 调用当前状态对应的处理函数 return uart_state_table[uart_parse_state](data); } // 示例:空闲状态处理函数 uint8_t UartState_Idle(uint8_t data) { uart_protocol_idx = 0; uart_protocol_check = 0; if(data == 0xAA) { uart_protocol_buf[uart_protocol_idx++] = data; uart_protocol_check += data; uart_parse_state = STATE_RECV_LEN; // 状态转移 } return 0; } -
分层状态机(适用于复杂协议): 对于多协议、多帧类型的场景,将状态机分为“帧解析层”和“业务处理层”,降低耦合:
- 帧解析层:负责帧头、长度、校验等通用逻辑;
- 业务处理层:根据帧类型(如UART协议中的命令字)调用不同的业务函数。
4.7.3 通信协议优化
-
添加CRC校验(比校验和更可靠): 新手版用累加和校验,进阶后改用CRC-8/CRC-16,抗干扰能力更强:
// CRC-16计算函数(示例:多项式0x8005,初始值0xFFFF) uint16_t CRC16_Calculate(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for(uint16_t i=0; i<len; i++) { crc = (uint16_t)data[i] << 8; for(uint8_t j=0; j<8; j++) { if(crc & 0x8000) { crc = (crc << 1) 0x8005; } else { crc <<= 1; } } } return crc; } -
帧加密(适用于安全场景): 用简单的XOR加密(与密钥异或)保护数据,防止被篡改:
#define ENCRYPT_KEY 0x3A // 加密密钥 // 数据加密 void Data_Encrypt(uint8_t *data, uint16_t len) { for(uint16_t i=0; i<len; i++) { data[i] ^= ENCRYPT_KEY; } } // 数据解密 void Data_Decrypt(uint8_t *data, uint16_t len) { Data_Encrypt(data, len); // XOR加密和解密相同 }
4.7.4 多任务系统适配(FreeRTOS)
新手版是裸机程序,进阶后可移植到FreeRTOS,用任务管理各接口,提高系统实时性:
// UART处理任务(优先级3)
void UART_Process_Task(void *pvParameters)
{
uint8_t uart_data;
uint8_t parse_ret;
while(1)
{
// 从队列读取UART数据(用FreeRTOS队列替代环形FIFO)
if(xQueueReceive(uart_rx_queue, &uart_data, portMAX_DELAY) == pdPASS)
{
parse_ret = UartFSM_Main(uart_data);
if(parse_ret 1)
{
// 转发到CAN任务队列
xQueueSend(can_tx_queue, &can_msg, 100);
}
}
}
}
// CAN处理任务(优先级3)
void CAN_Process_Task(void *pvParameters)
{
CAN_Msg_t can_msg;
while(1)
{
// 从队列读取CAN数据
if(xQueueReceive(can_rx_queue, &can_msg, portMAX_DELAY) pdPASS)
{
// 解析并转发到UART
CanFSM_Main(&can_msg);
}
}
}
4.7.5 低功耗优化(适用于电池供电场景)
- 通信接口低功耗:闲置时关闭UART/CAN/IIC/SPI的时钟和电源;
- 状态机低功耗:无数据时让MCU进入休眠模式(如STM32的STOP模式),由通信中断唤醒;
- FIFO低功耗:减少FIFO读写频率,批量处理数据。
4.8 项目工程结构(新手规范示例)
Multi-Interface-Gateway/
├── Core/
│ ├── Inc/
│ │ ├── main.h
│ │ ├── ring_fifo.h // 环形FIFO头文件
│ │ ├── uart_fsm.h // UART状态机头文件
│ │ ├── can_fsm.h // CAN状态机头文件
│ │ ├── i2c_aht20_fsm.h // AHT20状态机头文件
│ │ ├── spi_w25q64_fsm.h // W25Q64状态机头文件
│ │ └── forward_ctrl.h // 转发控制头文件
│ └── Src/
│ ├── main.c
│ ├── ring_fifo.c // 环形FIFO实现
│ ├── uart_fsm.c // UART状态机实现
│ ├── can_fsm.c // CAN状态机实现
│ ├── i2c_aht20_fsm.c // AHT20状态机实现
│ ├── spi_w25q64_fsm.c // W25Q64状态机实现
│ └── forward_ctrl.c // 转发控制实现
├── Drivers/
│ ├── STM32F1xx_HAL_Driver/ // HAL库驱动(CubeMX生成)
│ └── BSP/ // 板级支持包
│ ├── inc/
│ │ ├── led.h
│ │ └── key.h
│ └── src/
│ ├── led.c
│ └── key.c
├── Middlewares/
│ └── FreeRTOS/ // 可选:FreeRTOS源码(进阶用)
├── Project/
│ └── MDK-ARM/ // MDK工程文件
└── Doc/
├── 硬件接线图.pdf
├── 协议文档.pdf
└── 测试报告.pdf
第五章 总结:嵌入式通信的“道”与“术”
5.1 核心知识点回顾(新手必背)
-
环形FIFO的“道”:
本质是“异步数据缓冲”,解决“中断/硬件快、主循环慢”的矛盾,核心是“指针循环+临界区保护”,适用于所有异步通信接口(UART/CAN/IIC/SPI)。 -
状态机的“道”:
本质是“复杂逻辑拆解”,将协议解析拆成“有限状态+事件触发”,核心是“状态枚举+转移规则”,避免逻辑混乱,提高代码可读性和可维护性。 -
四大通信接口的“术”:
- UART:异步、低速、点对点,适合调试和简单模块通信,重点是波特率和DMA中断;
- CAN:差分、抗干扰、多主,适合汽车/工业场景,重点是终端电阻和波特率;
- IIC:双线、主从、地址寻址,适合传感器,重点是上拉电阻和ACK应答;
- SPI:高速、同步、全双工,适合Flash/显示屏,重点是片选和时钟极性/相位。
-
数据转发的“术”:
核心架构是“接收→缓冲→解析→转发”,可靠性关键是“非阻塞+超时+校验+错误重发”。
5.2 新手学习路径建议(从入门到精通)
-
基础阶段(1-2个月):
- 掌握STM32 HAL库基础(GPIO、中断、DMA);
- 实现环形FIFO(字节型+结构体型);
- 用switch-case实现UART简单协议解析(如帧头+数据+帧尾)。
-
实战阶段(2-3个月):
- 完成本章的多接口转发项目(裸机版);
- 熟练使用逻辑分析仪和串口助手调试;
- 解决常见问题(数据丢失、状态机卡死、通信错误)。
-
进阶阶段(3-6个月):
- 移植到FreeRTOS,用任务和队列管理接口;
- 优化FIFO(无锁、动态容量)和状态机(函数指针表、分层);
- 学习复杂协议(如Modbus、MQTT),实现更灵活的转发逻辑。
-
精通阶段(6-12个月):
- 结合FPGA/Zynq实现高速通信(如SPI DMA+FPGA缓存);
- 学习低功耗、安全加密、远程升级(OTA)等高级功能;
- 参与实际项目(如工业网关、汽车电子模块),积累工程经验。
5.3 新手避坑忠告(来自工程实践)
- 先懂原理,再写代码:不要直接复制粘贴代码,先理解环形FIFO的指针操作、状态机的状态转移,否则遇到问题无法排查;
- 重视硬件基础:通信问题80%来自硬件(接线、电阻、电源),遇到通信失败先查硬件,再查软件;
- 调试工具是神器:逻辑分析仪(如Saleae Logic)、串口助手、CAN分析仪是嵌入式通信调试的必备工具,学会看波形比盲目改代码更高效;
- 代码模块化:将FIFO、状态机、转发控制分开编写,便于复用和维护,避免写“流水账代码”;
- 多动手,多踩坑:嵌入式是“实践学科”,只有亲手实现项目、解决问题,才能真正掌握知识,看懂100遍不如跑通1遍。
5.4 最终寄语
环形FIFO和状态机是嵌入式通信的“底层基石”,无论你后续从事工业控制、汽车电子、智能家居还是物联网,这两个技术都能用到。本章的多接口转发项目是一个“通用模板”,你可以根据实际需求修改协议帧格式、通信接口、转发逻辑,快速适配不同场景。
记住:嵌入式开发的核心是“稳定、可靠、高效”,从简单项目开始,逐步积累经验,你终将能独立设计复杂的通信系统。祝你在嵌入式的道路上越走越远!
全文结束(总字数:108,632字)

被折叠的 条评论
为什么被折叠?



