1. 嵌入式通信核心实战:状态机与环形FIFO(UART/CAN/IIC/SPI全解析)
1.1 写给嵌入式新手的前言
如果你是刚接触嵌入式开发的新手,大概率会遇到这些问题:用UART接收数据时一着急就丢字节,解析自定义协议时逻辑写得像"意大利面",CAN总线报文来了不知道怎么高效缓存,IIC/SPI通信时序稍不注意就卡死。这些问题的核心,其实都指向两个嵌入式通信的"底层法宝"——环形FIFO(环形队列)和状态机(FSM)。
本文将从新手视角出发,不堆砌晦涩的理论,只讲"能落地、能跑通"的知识:先拆解环形FIFO和状态机的核心原理,再结合嵌入式最常用的4种通信接口(UART、CAN、IIC、SPI),从硬件配置、代码实现、调试排错全流程讲解。每个案例都基于STM32(新手最易上手的MCU),代码逐行注释,问题逐个拆解。
1.2 为什么需要状态机和环形FIFO?
1.2.1 嵌入式系统的通信本质
嵌入式系统不是孤立的"单机",而是需要和传感器、执行器、上位机、其他MCU交互的"节点"。这些交互的核心是"数据传输",而嵌入式通信的最大特点是:
- 异步性:数据什么时候来、来多少,完全由外部设备决定
- 实时性:数据必须及时处理,丢一个字节可能导致整个协议解析失败
- 可靠性:工业/汽车场景下,哪怕有电磁干扰,数据也不能错、不能丢
新手最开始的错误做法是直接在中断里处理数据,这样会导致中断阻塞,后续数据丢失。而解决这些问题的核心,就是环形FIFO(缓冲数据)+状态机(解析数据)。
1.2.2 嵌入式4大通信接口对比
| 通信接口 | 特点 | 典型应用 | 新手易踩坑 |
|---|---|---|---|
| UART | 双线(TX/RX)、异步、串行 | 上位机调试、模块通信 | 波特率不匹配、TX/RX接反 |
| CAN | 双线(CAN_H/CAN_L)、差分信号、多主 | 汽车ECU、工业控制 | 没接120Ω终端电阻、波特率错误 |
| IIC | 双线(SDA/SCL)、同步、主从 | 传感器、存储芯片 | 没加上拉电阻、时序错误 |
| SPI | 四线(MOSI/MISO/SCK/CS)、同步、高速 | 显示屏、Flash芯片 | 片选(CS)没拉低、时钟极性错误 |
2. 环形FIFO:嵌入式通信的数据缓冲池
2.1 环形FIFO的核心原理
环形FIFO的本质是一段连续的数组,加上两个指针:
- 写指针(wr_ptr):记录下一个要写入数据的位置
- 读指针(rd_ptr):记录下一个要读取数据的位置
数据写入时,写指针往后走;数据读取时,读指针往后走;当指针走到数组末尾,就回到开头(用"取模运算"实现)。
2.2 环形FIFO的C语言实现
2.2.1 数据结构定义
c复制typedef struct {
uint8_t *buf; // 存储数据的数组
uint16_t size; // 数组长度
uint16_t rd_ptr; // 读指针
uint16_t wr_ptr; // 写指针
uint16_t cnt; // 当前数据个数
} RingFifo_t;
2.2.2 核心操作函数
c复制// 初始化FIFO
uint8_t RingFifo_Init(RingFifo_t *fifo, uint8_t *buf, uint16_t size) {
if(fifo == NULL || buf == NULL || size == 0) return 1;
fifo->buf = buf;
fifo->size = size;
fifo->rd_ptr = 0;
fifo->wr_ptr = 0;
fifo->cnt = 0;
memset(fifo->buf, 0, size);
return 0;
}
// 写入1字节
uint8_t RingFifo_Write_Byte(RingFifo_t *fifo, uint8_t data) {
if(fifo == NULL || RingFifo_Is_Full(fifo)) return 1;
__disable_irq(); // 关中断保护
fifo->buf[fifo->wr_ptr] = data;
fifo->wr_ptr = (fifo->wr_ptr + 1) % fifo->size;
fifo->cnt++;
__enable_irq();
return 0;
}
// 读取1字节
uint8_t RingFifo_Read_Byte(RingFifo_t *fifo, uint8_t *data) {
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;
fifo->cnt--;
__enable_irq();
return 0;
}
2.3 环形FIFO的实战应用
2.3.1 UART接收数据示例
c复制#define UART1_FIFO_SIZE 128
uint8_t uart1_fifo_buf[UART1_FIFO_SIZE];
RingFifo_t uart1_fifo;
// 在UART中断中写入数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart->Instance == USART1) {
RingFifo_Write_Byte(&uart1_fifo, uart1_dma_buf[0]);
HAL_UART_Receive_DMA(&huart1, uart1_dma_buf, 1);
}
}
// 主循环中读取处理
uint8_t uart_data;
while(1) {
if(RingFifo_Read_Byte(&uart1_fifo, &uart_data) == 0) {
// 处理接收到的数据
}
}
2.3.2 CAN报文接收示例
c复制typedef struct {
uint32_t id;
uint8_t data[8];
uint8_t len;
} CAN_Msg_t;
#define CAN1_FIFO_SIZE 32
CAN_Msg_t can1_fifo_buf[CAN1_FIFO_SIZE];
RingFifo_t can1_fifo;
// CAN接收回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[8];
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data);
CAN_Msg_t can_msg;
can_msg.id = rx_header.StdId;
can_msg.len = rx_header.DLC;
memcpy(can_msg.data, rx_data, rx_header.DLC);
RingFifo_Write_Struct(&can1_fifo, &can_msg);
}
3. 状态机:通信协议解析的大脑
3.1 状态机的核心概念
状态机(FSM)由以下几个核心要素组成:
- 状态(State):当前所处的阶段
- 事件(Event):触发状态转移的条件
- 动作(Action):处于某个状态时要做的事
- 转移(Transition):事件发生后,从一个状态转到另一个状态
3.2 状态机的C语言实现
3.2.1 基于switch-case的实现
c复制// UART协议解析状态枚举
typedef enum {
STATE_IDLE = 0, // 空闲状态
STATE_WAIT_HEAD, // 等待帧头
STATE_RECV_LEN, // 接收长度
STATE_RECV_DATA, // 接收数据
STATE_RECV_CHECK, // 接收校验和
STATE_RECV_TAIL, // 接收帧尾
STATE_PARSE_DONE, // 解析完成
STATE_PARSE_ERROR // 解析错误
} UartParseState_t;
// 状态机处理函数
uint8_t Uart_Parse_State_Machine(uint8_t data) {
switch(uart_parse_state) {
case STATE_IDLE:
if(data == 0xAA) { // 帧头
uart_parse_state = STATE_RECV_LEN;
}
break;
case STATE_RECV_LEN:
uart_protocol_len = data;
uart_parse_state = STATE_RECV_DATA;
break;
// 其他状态处理...
}
return 0;
}
3.2.2 基于函数指针表的实现(进阶)
c复制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);
// ...
// 函数指针表
StateHandler_t state_handler_table[] = {
State_Idle_Handler,
State_Wait_Head_Handler,
// ...
};
// 状态机主函数
uint8_t FSM_Main(uint8_t data) {
return state_handler_table[uart_parse_state](data);
}
3.3 状态机的实战应用
3.3.1 UART自定义协议解析
c复制// 主循环中集成状态机
uint8_t uart_data;
uint8_t parse_ret;
while(1) {
if(RingFifo_Read_Byte(&uart1_fifo, &uart_data) == 0) {
parse_ret = Uart_Parse_State_Machine(uart_data);
if(parse_ret == 1) {
// 解析完成,处理数据
} else if(parse_ret == 2) {
// 解析错误
}
}
}
3.3.2 CAN报文解析
c复制typedef enum {
CAN_STATE_IDLE = 0,
CAN_STATE_CHECK_ID,
CAN_STATE_PARSE_DATA,
CAN_STATE_DONE
} CanParseState_t;
uint8_t Can_Parse_State_Machine(CAN_Msg_t *can_msg) {
switch(can_parse_state) {
case CAN_STATE_CHECK_ID:
if(can_msg->id == TARGET_CAN_ID) {
can_parse_state = CAN_STATE_PARSE_DATA;
}
break;
// 其他状态处理...
}
return 0;
}
4. 常见问题与解决方案
4.1 环形FIFO常见问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 数据丢失 | FIFO容量太小 | 增大FIFO容量 |
| 数据乱码 | 没加volatile修饰 | 给指针/计数加volatile |
| 判满错误 | 临界区未保护 | 写入/读取时关中断 |
4.2 状态机常见问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 状态卡死 | 缺少超时处理 | 添加超时机制 |
| 解析错误 | 状态转移条件不完整 | 检查所有可能的转移路径 |
| 内存溢出 | 缓冲区太小 | 增大协议缓冲区 |
5. 性能优化建议
- DMA+环形FIFO组合:对于高速通信接口(如SPI),使用DMA将数据直接写入环形FIFO,减少CPU开销
- 双缓冲技术:对于大数据量传输,可以使用双缓冲机制,一个缓冲处理数据时,另一个缓冲接收新数据
- 状态机分层设计:复杂协议可以分层设计状态机,上层状态机处理协议帧,下层状态机处理字节流
6. 实际项目经验分享
在实际项目中,我发现以下几个经验特别重要:
- FIFO容量选择:UART通常需要64-128字节,CAN需要16-32个报文结构体,IIC/SPI需要32-64字节
- 状态机设计原则:每个状态只做一件事,状态转移条件要明确,避免模糊逻辑
- 调试技巧:在状态机每个状态转移点添加调试打印,可以快速定位问题
- 临界区保护:不仅是中断和主循环之间,多任务系统中任务间共享FIFO也需要保护
最后,嵌入式通信的核心是"稳定可靠",环形FIFO和状态机只是工具,真正重要的是理解通信协议的本质和嵌入式系统的特点。建议新手从简单的UART通信开始,逐步过渡到更复杂的CAN、IIC、SPI协议,在实践中积累经验。