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

目录

前言

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

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);

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

相关推荐
巴巴媛66625 分钟前
【Debug1】程序死机,while(1)循环,怎么办?
c语言·笔记·stm32·嵌入式硬件·bug
Lxx_Zw26 分钟前
PID 用顺了,然后呢:增强手术与前馈
c语言·stm32·单片机·嵌入式硬件
张怀民31642 分钟前
PFC_CCM_NCP1654详讲
单片机·嵌入式硬件·硬件工程·pfc
linweidong1 小时前
RTC 走偏、时间跳变?嵌入式时间管理的工程避坑
嵌入式硬件·面试·实时音视频·rtc·嵌入式面经·嵌入式ntp·嵌入式对时
Yiran_G2 小时前
ZigBee单播、广播与组播详解:从寻址机制到批量控制设计
嵌入式硬件·物联网·智能家居
youcans_2 小时前
【STM32C5教程】(9)DAC 模拟量信号输出实验
stm32·单片机·嵌入式硬件·stm32c5·cubemx2
智购科技自动售货机工厂2 小时前
省电40%的底层逻辑:冷热双温售货机基于SaaS的智能温控算法~YH
单片机·嵌入式硬件·百度·架构·emacs
深圳市恒锐丰科技杨生2 小时前
SS6203D 200V 单路半桥栅极驱动芯片|LeadPower 率能半导体
嵌入式硬件·硬件工程
深圳老胡2 小时前
STM32C5 系列 MCU 的 FreeRTOS 软件架构搭建指南:外设配置与应用实践
笔记·stm32·单片机·代码规范
霖霖7143 小时前
【ESP32笔记】I2S的音频录制与播放
笔记·单片机·mcu·音视频