单片机基础核心知识点汇总(二十九)

目录

前言

一、串口通信最大痛点:粘包与分包

1、什么是分包

2、什么是粘包

二、工业通用串口帧协议格式

三、串口底层架构:环形缓冲区

四、状态机帧解析(解决粘包分包核心)

五、串口空闲中断(判定帧结束)

[六、CRC16 校验代码(工业标准)](#六、CRC16 校验代码(工业标准))

七、业务解耦:协议层与业务层分离

八、串口工程九大踩坑总结

[九、RTOS 架构升级版](#九、RTOS 架构升级版)


前言

在所有嵌入式通信中,串口 USART 是使用最多、BUG 最多、最需要工程架构的通信方式

新手串口写法:

  • 收到数据立刻处理
  • 裸字节判断、无帧结构
  • 遇到粘包、断包直接解析错乱
  • 无校验、无超时、无容错
  • 协议和业务耦合,完全无法迭代

量产设备必须拥有标准帧协议 + 环形缓冲区 + 状态机解析 + 超时容错架构。

本篇手把手搭建工业级串口协议栈,解决:粘包、分包、干扰乱码、超时断帧、CRC 校验、自动容错,裸机 / RTOS 通用。

一、串口通信最大痛点:粘包与分包

1、什么是分包

一帧数据被分多次接收 比如一帧 100 字节,分 3 次进串口缓冲区

2、什么是粘包

多帧数据粘在一起一次性收到 两三条指令粘成一坨,直接解析错误

解决核心:必须基于「完整数据帧」解析,不能单字节处理业务

二、工业通用串口帧协议格式

复制代码
帧头2字节 + 长度1字节 + 命令1字节 + 数据域 + CRC16校验2字节 + 帧尾

常用标准格式:

  • 帧头:0xAA 0x55
  • 数据长度:有效数据长度
  • 命令码:区分不同功能
  • 数据域:自定义参数
  • CRC16:校验整条数据
  • 帧尾:可选

优势:

  • 不怕乱码、不怕丢字节
  • 精准区分每一帧
  • 错误帧直接丢弃、自动容错

三、串口底层架构:环形缓冲区

固定数组容易溢出、丢数据,量产全部用环形 FIFO

复制代码
#define USART_BUF_LEN 256
typedef struct
{
    uint8_t buf[USART_BUF_LEN];
    uint16_t w_index;
    uint16_t r_index;
}usart_fifo_t;

usart_fifo_t g_usart_fifo;

//入队
void usart_fifo_push(uint8_t dat)
{
    uint16_t next = (g_usart_fifo.w_index + 1) % USART_BUF_LEN;
    if(next != g_usart_fifo.r_index)
    {
        g_usart_fifo.buf[g_usart_fifo.w_index] = dat;
        g_usart_fifo.w_index = next;
    }
}

//出队
uint8_t usart_fifo_pop(uint8_t *dat)
{
    if(g_usart_fifo.r_index == g_usart_fifo.w_index)
        return 0;
        
    *dat = g_usart_fifo.buf[g_usart_fifo.r_index];
    g_usart_fifo.r_index = (g_usart_fifo.r_index + 1) % USART_BUF_LEN;
    return 1;
}

//获取缓存数量
uint16_t usart_fifo_cnt(void)
{
    return (g_usart_fifo.w_index - g_usart_fifo.r_index + USART_BUF_LEN) % USART_BUF_LEN;
}

中断里只做一件事:压入环形缓冲区

复制代码
void USART1_IRQHandler(void)
{
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        uint8_t dat = USART_ReceiveData(USART1);
        usart_fifo_push(dat);
    }
}

四、状态机帧解析

采用 空闲中断 + 状态机解析 串口超过 20ms 无新数据判定一帧接收完毕

复制代码
typedef enum
{
    USART_IDLE,
    USART_GET_HEAD,
    USART_GET_LEN,
    USART_GET_DATA,
    USART_GET_CRC
}usart_state_t;

void usart_parse_process(void)
{
    static usart_state_t state = USART_IDLE;
    static uint8_t tmp_buf[128];
    static uint16_t idx = 0;
    static uint16_t crc = 0;
    uint8_t dat;

    while(usart_fifo_pop(&dat))
    {
        switch(state)
        {
            case USART_IDLE:
                idx = 0;
                //匹配帧头 0xAA 0x55
                if(dat == 0xAA)
                {
                    tmp_buf[idx++] = dat;
                    state = USART_GET_HEAD;
                }
                break;

            case USART_GET_HEAD:
                if(dat == 0x55)
                {
                    tmp_buf[idx++] = dat;
                    state = USART_GET_LEN;
                }
                else
                {
                    state = USART_IDLE;
                }
                break;

            case USART_GET_LEN:
                tmp_buf[idx++] = dat;
                state = USART_GET_DATA;
                break;

            case USART_GET_DATA:
                //根据长度接收剩余数据
                if(idx < tmp_buf[2] + 3)
                {
                    tmp_buf[idx++] = dat;
                }
                else
                {
                    //最后两字节CRC
                    tmp_buf[idx++] = dat;
                    state = USART_GET_CRC;
                }
                break;

            case USART_GET_CRC:
                tmp_buf[idx++] = dat;
                
                //CRC校验判断
                //crc_check(tmp_buf, idx-2) == 正确
                if(1)
                {
                    //完整合法帧,执行业务处理
                    usart_data_handle(tmp_buf, idx);
                }

                state = USART_IDLE;
                idx = 0;
                break;
            default:
                state = USART_IDLE;
                break;
        }
    }
}

五、串口空闲中断

裸机唯一精准判帧方式: 一帧数据传输完成后总线空闲,触发空闲中断,标志本次帧接收完成

复制代码
void USART_Idle_IRQ(void)
{
    if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
    {
        USART_ClearITPendingBit(USART1, USART_IT_IDLE);
        //触发一次解析
        usart_parse_process();
    }
}

六、CRC16 校验代码

复制代码
uint16_t crc16_calc(uint8_t *buf,uint16_t len)
{
    uint16_t crc = 0xFFFF;
    uint16_t i,j;
    for(i=0;i<len;i++)
    {
        crc ^= buf[i];
        for(j=0;j<8;j++)
        {
            if(crc & 1)
                crc = (crc >> 1) ^ 0xA001;
            else
                crc >>= 1;
        }
    }
    return crc;
}

校验作用:杜绝干扰乱码、字节错误,量产设备必须带 CRC。

七、业务解耦:协议层与业务层分离

复制代码
void usart_data_handle(uint8_t *buf,uint16_t len)
{
    uint8_t cmd = buf[3];
    switch(cmd)
    {
        case 0x01:
            //设备查询指令
            break;
        case 0x02:
            //参数设置指令
            break;
        case 0x03:
            //控制指令
            break;
        default:
            //非法指令直接丢弃
            break;
    }
}

八、串口工程九大踩坑总结

  1. 不用环形缓冲区:数据溢出、丢字节
  2. 无帧头帧尾:无法区分粘包
  3. 无超时判断:残留半截脏数据卡死解析
  4. 无 CRC 校验:干扰直接乱执行
  5. 中断里处理业务:导致中断过长、卡死、优先级抢占
  6. 单字节判断指令:完全不适合量产
  7. 不做容错复位:一次错误后续全部解析失败
  8. 串口发送不屏蔽中断:并发错乱
  9. 大数据不做分包发送:缓冲区溢出

九、RTOS 架构升级版

RTOS 中直接将接收数据放入队列:

复制代码
xQueueSend(uart_queue,&dat,0);

任务中统一解析,实时性更强、完全无阻塞。

相关推荐
Rsingstarzengjx1 小时前
第二十六章 Linux 蜂鸣器实验笔记
linux·驱动开发·笔记·stm32·嵌入式硬件·stm32mp157
一路往蓝-Anbo1 小时前
第七篇:以太网设计 —— 从 RMII 时序到变压器隔离的深度拆解
stm32·单片机·嵌入式硬件·嵌入式·stm32硬件设计
jianqiang.xue3 小时前
ESP-IDF保姆级入门22|WiFi联网与网络编程全解:STA/AP双模式/TCP-UDP Socket/HTTP服务端/自动重连,掌握工业级可靠网络通信
人工智能·stm32·单片机·mcu·物联网·51单片机·iot
恒锐丰科技林技术员7 小时前
EG2302 半桥驱动芯片详解:高压半桥高性价比栅极驱动方案
经验分享·嵌入式硬件·硬件工程
LCG元11 小时前
STM32 定时器编码器接口模式电机测速实战:从 CubeMX 配置到 M 法转速计算与溢出处理
stm32·单片机·嵌入式硬件
星河单片机14 小时前
STM32驱动WaterSensor水位传感器+OLED显示完整原理与实验
stm32·单片机·嵌入式硬件
子文雨15 小时前
基于 uC3845 与 ATtiny13 双芯片架构的 8.4V/2.5Ah 锂电池智能充电器设计
单片机·嵌入式硬件·算法
快秃头的码农15 小时前
esp32-s3移植lvgl总结
嵌入式硬件·esp32-s3
恒锐丰科技林技术员16 小时前
EG1191 宽压降压 DC‑DC 芯片解析:高性价比工业电源方案
经验分享·嵌入式硬件·硬件工程