STM32之自定义协议帧格式

自定义协议就是自己指定一套通信的语言格式。

一、为什么用它(自定义协议)?

虽然目前有很多成熟的协议(如Modbus、MQTT、HTTP、CANopen...),但是嵌入式往往资源有限、功能定制化强,这时就需要自定义的协议,汇总一下原因主要有以下几种:

1、系统资源受限;(标准协议太复杂,占内存大,单片机难实现,如HTTP、MQTT)

2、功能需求独特;(需要传输特定数据,如传感器值、命令、状态等)

3、速度和效率要求高;(通信简洁、响应迅速)

4、硬件接口简单;(串口、485、IIC、SPI等通信接口不自带高级协议,需要自己封装)

5、多设备配合工作。(主控与多节点或模块通信时需要统一自定义格式)

先举个例子:

STM32通过串口与ESP8266通信,想上传温湿度和报警状态到上位机,可以定义一个数据格式,如:

复制代码
$TEMP,25.6,HUM,60.2,ALARM,0#

上位机根据$开头,#结束,逗号分隔来解析。这就是一个简单的自定义协议。

二、应用场景

1、单片机与传感器节点通信;(如主控STM32与多个从设备通信IIC或SPI或UART)

2、主机与上位机通信;(如STM32通过串口或WIFI发送数据给Qt或PC端软件)

3、多机协同系统;(例如路灯控制系统中主控与多个节点通信)

4、无线通信模块中数据封装;(如LoRa、nRF24L01、蓝牙模块,自己规定帧结构)

5、网关或云端上传前的数据整合;(物联网系统中,网关在上传数据前封装成统一协议格式)

三、怎么设计一个合理的通信协议?

1、定义帧格式

举个例子说明:

复制代码
AA 55 05 01 19 32 2A 0D 0A

AA 55:帧头 05:数据长度 01:命令字(表示上传温度) 19 32:温度数据(25.62℃)

2A:校验 0D 0A:帧尾

关于命令字:

相当于是一个函数的编号,如:0x01 表示上传温湿度数据;0x02表示上传报警状态;0x10表示设置报警阈值;0x11表示查询设备状态。

关于校验:

这是防止传输数据错误的关键,常用的方法有:

1.累加和校验。这种方法简单快速,即将所有字节求和取低8位

2.CRC16:工业级常用(例如Modbus CRC16)

解析和打包代码:

发送端需要有打包函数,将数据组装成协议的格式后发送;

接收端,解析函数,将接收缓存中提取完整的帧并进行校验。

四、示例:STM32和上位机使用自定义串口协议通信

案例背景:

在智能控制或检测类系统中,例如实现以下功能:

STM32负责采集温湿度、气体浓度、火焰等数据;

上位机(Qt编写)负责实时显示数据、发送控制命令(如控制继电器、蜂鸣器、设置阈值等);

通信方式:串口(UART)双向通信。

我们思考,怎么实现它?怎么保证STM32和上位机可靠通信?自定义协议?

由于串口传输的只是原始的数据流,没有边界和含义区分。如果不自定义协议(这是我们思考问题的一种方式,就是不使用它会出现什么问题),就会出现以下问题:

复制代码
1.收到数据无法判断哪个是数据头,哪个是数据尾;
2.多条命令放一块无法区分
3.无法判断指令的类型(几位传输)
4.校验也没有(无法判断数据是否传输正确)

综上所述,自定义协议是一个中肯的选择。

那么针对这个案例,怎么合理的自定义通信协议呢?别着急往下看...

将定义固定的帧格式,包括:帧头、设备ID、命令字、数据长度、数据区、校验、帧尾。

详细的定义如下:

命令字定义:

传输数据帧示例:

cpp 复制代码
AA 55  01  01   04         19 28 00 64   D1    0D 0A
帧头   ID  命令  数据区长度  温湿度数据    校验   帧尾

STM32端实现代码:

cpp 复制代码
//打包发送函数
#include "stdint.h"
#include "string.h"

#define FRAME_HEAD1  0xAA
#define FRAME_HEAD2  0x55
#define FRAME_TAIL1  0x0D
#define FRAME_TAIL2  0x0A

uint8_t tx_buf[64];

// 累加和校验函数
uint8_t Get_Sum(uint8_t *data, uint8_t len)
{
    uint8_t sum = 0;
    for (uint8_t i = 0; i < len; i++)
        sum += data[i];
    return sum;
}

// 打包上传温湿度数据
uint8_t Pack_Temp_Humi(uint8_t id, float temp, float hum)
{
    uint8_t len = 0;
    uint16_t t = (uint16_t)(temp * 10);
    uint16_t h = (uint16_t)(hum * 10);

    tx_buf[len++] = FRAME_HEAD1;
    tx_buf[len++] = FRAME_HEAD2;
    tx_buf[len++] = id;
    tx_buf[len++] = 0x01;    // 功能码:上传温湿度
    tx_buf[len++] = 0x04;    // 数据长度

    tx_buf[len++] = (t >> 8) & 0xFF;
    tx_buf[len++] = t & 0xFF;
    tx_buf[len++] = (h >> 8) & 0xFF;
    tx_buf[len++] = h & 0xFF;

    uint8_t sum = Get_Sum(tx_buf, len);
    tx_buf[len++] = sum;

    tx_buf[len++] = FRAME_TAIL1;
    tx_buf[len++] = FRAME_TAIL2;

    return len;
}

// 串口发送函数
void UART_Send(uint8_t *data, uint8_t len)
{
    for(uint8_t i=0; i<len; i++)
    {
        USART_SendData(USART1, data[i]);
        while(USART_GetFlagStatus(USART1, USART_FLAG_TC)==RESET);
    }
}

说明一下(思路理清它):将固定的帧头、帧尾使用宏定义。定义一个接收缓冲区。将检验和的校验函数封装起来,传参分别为数组指针和长度。编写温湿度数据打包函数,参数为设备ID,温度数据(浮点型数据)和湿度数据(也是浮点型数据),浮点型数据是32位的。先定义一个数组的下标值(len)为0,再将传入的参数1(温度数据)和参数2(湿度数据),扩大10倍,小数点消了,再强转为16位无符号整型数据,方便后面传输数据(将这16位无符号整型数据分结尾两个字节数据因为传输是按字节传输的),依次传输缓冲区数组第1、第2...一直传输到第12个字节数据,他们分别是:帧头(2字节)、id(1字节)、功能码(1字节)、数据区长度(4字节)、温度数据(2字节)、湿度数据(2字节)、帧校验(1字节)、帧尾(2字节),len的值到最后帧尾还要自增,自增后的值就是12,这个12也就是总共传输的字节数,当做返回值。最后就是串口发送函数,我们使用UART1,参数也是传入两个,参数1是数组指针(也就是缓冲区)和长度(也就是上面需要发送的字节数,因为UART一次传输一个字节),放一个循环到里面,因为要循环传输len次,所以条件是<len,变量每次循环自增,每次发送一个字节,发送的缓冲区是变量做数组下标的哪个数组内容,最后加一个判断也就是等待发送完成,才能进入下一次循环,再次进行发送。

那么怎么调用它?

cpp 复制代码
uint8_t len = Pack_Temp_Humi(1, 25.3, 60.2);
UART_Send(tx_buf, len);

接收区解析函数:

思路就是:STM32打开UART中断依次接收字节数据,并把它放到接收缓冲区里(这时我们就要定义一个接收缓冲区),这个里面要增加判断语句,就是判断一帧数据(自定义的协议里面的)是否发送完,可以判断标号是否大于1并且标号的前两个标号的数组内容是帧尾的两个帧尾字节数据。成立的话我们就给一个标志说明一帧数据已经接收完成了。(中断函数不易过长,所以帧解析函数就另封装),帧解析函数无参无返回值,先判断标志位是否是1(也就是一帧传输完成),先将标志位复位0,判断帧头,将接收缓冲区的数据赋给响应的变量(如命令字,数据区长度,校验),将校验函数的返回值赋给一个变量,将这个变量与传过来的校验值对应的变量比较对不,如果OK,则通过命令字的值执行相应的操作。最后将接收缓冲区的下标置0,并且清空接收缓冲区的值都为0.

cpp 复制代码
void TCP_Parse_Recv(uint8_t *buf, uint16_t len)
{
    if(len < 8) return;  // 最小长度检查

    if(buf[0]==0xAA && buf[1]==0x55)
    {
        uint8_t cmd = buf[4];
        uint8_t datalen = buf[5];
        uint8_t checksum = buf[6 + datalen];
        uint8_t sum = Get_Sum(buf, 6 + datalen);

        if(checksum == sum && buf[7 + datalen] == 0x0D && buf[8 + datalen] == 0x0A)
        {
            switch(cmd)
            {
                case 0x10:  // 设置阈值
                    // buf[6], buf[7]等解析
                    break;

                case 0x11:  // 控制继电器
                    if(buf[6] == 0x01)
                        Relay_On();
                    else
                        Relay_Off();
                    break;
            }
        }
    }
}

学到这里,应用起来。

实现:STM32使用串口1发送数据(以自定义的协议类型)给上位机,看看上位机收到了什么(这里前提是上位机自己已经做好了,目前还没做),也可以验证一下。所以真正需要验证的是:上位机根据这个格式发送给STM32特定格式的数据,随后下位机接收后根据协议格式,返回给上位机真实数据。

自定义的帧格式是:

cpp 复制代码
AA 55  01 02  00 02       01 02  00 00 00 00   07     0D 0A
帧头   命令    数据区长度  数据区  保留区         校验   帧尾
(2B)   (2B)   (2B)        (2B)   (4B)           (1B)  (2B)

验证现象一:STM32-->串口助手发送验证成功!

验证现象二:串口助手-->STM32发送验证成功!

发数据1:AA 55 01 02 00 02 01 02 00 00 00 00 07 0D 0A

发数据2:AA 55 02 03 00 02 01 02 00 00 00 00 09 0D 0A(记得校验码要正确,否则不会执行)

两个现象不一致!

测试PASS!说明我们写的程序没有问题!继续再接再厉💪

相关推荐
kaixin_啊啊1 小时前
Codex论文辅助全流程
人工智能·笔记·学习·ai·大模型
愚公移山填海1 小时前
【无标题】
笔记·python·学习·pycharm
云栖梦泽1 小时前
网络设备驱动(3)
linux·运维·服务器·网络·嵌入式硬件
蓝速科技1 小时前
蓝速科技 F100 双屏翻译机:中小企业跨国会议提效方案
大数据·网络·数据结构·人工智能·科技·运维开发
sugar__salt1 小时前
大模型流式输出完全指南(上):从 HTTP 长连接到手写 SSE
网络·网络协议·http
牛奶yu茶1 小时前
数据链路层的MTU
网络·网络通信·通信·通信协议·通信网络
江湖人称菠萝包2 小时前
【Windows】《深入浅出Windows API程序设计:核心编程篇》笔记-Chapter5-剪贴板
windows·笔记
志尊宝2 小时前
Vue3 零基础每日笔记(017):事件处理进阶——$event、多事件与事件修饰符全家桶
javascript·vue.js·笔记
洪核单片机2 小时前
DPJ-91基于32单片机的语音识别车语音播报车语音控制智能车
stm32·单片机·嵌入式硬件·智能车·语音识别车·语音播报车·语音控制智能车