0:事由
最近开发,上位机Qt与下位机通讯的时候发现通讯规则有些不一样,这里简单记录一下 。所有代码基于元宝生成,属于伪代码不保证真实可用,啊但是逻辑是这么个逻辑。
1:底层通讯规则
以STM32向上位机通讯通讯基础为例:
#include "main.h"
#include "usart.h"
#include "gpio.h"void SystemClock_Config(void);int main(void)
{HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USART1_UART_Init();uint8_t data_to_send[] = {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xFF}; // 8个字节数据uint16_t data_length = sizeof(data_to_send); // 数据长度=8while (1){// 发送多个字节HAL_UART_Transmit(&huart1, data_to_send, data_length, HAL_MAX_DELAY);// 延时1秒(可选)HAL_Delay(1000);}
}
还有一种发送的案例如下:
#include "main.h"
#include "usart.h"
#include "gpio.h"void SystemClock_Config(void);int main(void)
{HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USART1_UART_Init();uint16_t sensor_data_u16 = 0x1234; // 16位数据uint32_t sensor_data_u32 = 0x12345678; // 32位数据uint8_t u16_data_bytes[2];uint8_t u32_data_bytes[4];// 拆分u16数据(大端模式)u16_data_bytes[0] = (sensor_data_u16 >> 8) & 0xFF; // 高字节u16_data_bytes[1] = sensor_data_u16 & 0xFF; // 低字节// 拆分u32数据(大端模式)u32_data_bytes[0] = (sensor_data_u32 >> 24) & 0xFF; // 最高字节u32_data_bytes[1] = (sensor_data_u32 >> 16) & 0xFF;u32_data_bytes[2] = (sensor_data_u32 >> 8) & 0xFF;u32_data_bytes[3] = sensor_data_u32 & 0xFF; // 最低字节while (1){// 发送u16数据(2字节)HAL_UART_Transmit(&huart1, u16_data_bytes, 2, HAL_MAX_DELAY);// 发送u32数据(4字节)HAL_UART_Transmit(&huart1, u32_data_bytes, 4, HAL_MAX_DELAY);// 延时1秒HAL_Delay(1000);}
}
这里简单说一下他们通讯的原理:底层向上位机通讯的是以字节为单位发送的,一个字节(8位)可以用两个十六进制(Hex)字符表示。你想发送多少个字节就多少个字节乘以二发送多少个16进制的数。在底层有三个特别经典的数据类型:
uint8_t flag = 0x01; //两个16进制 8位数据 一个字节
uint16_t sensor_data_u16 = 0x1234; //四个16进制 16位数据 两个字节
uint32_t sensor_data_u32 = 0x12345678; //八个16进制 32位数据 四个字节
然后如果想发送多个字节的话,以下面的代码为例
#include "usart.h"void send_sensor_data() {uint8_t flag = 0x01; // 1字节uint16_t sensor_data_u16 = 0x1234; // 2字节uint32_t sensor_data_u32 = 0x12345678; // 4字节uint8_t tx_buffer[7]; // 总字节数: 1 + 2 + 4 = 7uint8_t index = 0;// 拼接数据(注意字节序!)tx_buffer[index++] = flag; // 直接拷贝1字节// 拆分uint16_t为2字节(假设使用大端序)tx_buffer[index++] = (sensor_data_u16 >> 8) & 0xFF; // 高字节tx_buffer[index++] = sensor_data_u16 & 0xFF; // 低字节// 拆分uint32_t为4字节(大端序)tx_buffer[index++] = (sensor_data_u32 >> 24) & 0xFF; // 最高字节tx_buffer[index++] = (sensor_data_u32 >> 16) & 0xFF;tx_buffer[index++] = (sensor_data_u32 >> 8) & 0xFF;tx_buffer[index++] = sensor_data_u32 & 0xFF; // 最低字节// 通过UART发送HAL_UART_Transmit(&huart1, tx_buffer, sizeof(tx_buffer), HAL_MAX_DELAY);
}
2:上位机处理数据
上位机通过串口接收数据的时候可能会有各方面的问题,比如说需要设置串口的波特率、串口、比校验码、当然最主要还是需要借助串口调试助手工具来进行调试。
但无论如何只要你能接收到的数据你就会发现你的数据其实是很乱的因为你不知道从哪个地方开始然后就需要下沉向上层传输数据时在头部引入两个没有任何意义的字节,即引入一个头部。根据这个头部来确定发送数据的长度。一般接受的逻辑是这样一个结构
[Header1][Header2][Length][Flag][u16_Data][u32_Data][CRC][Footer]
- Header1/Header2:
0xAA 0x55
(起始标志)- Length: 数据部分长度(固定为
7
字节,1+2+4
)- Flag: 用户标志(
uint8_t
)- u16_Data/u32_Data: 传感器数据
- CRC: 校验码(简单求和校验)
- Footer:
0xBB
(结束标志)
当然这个接收也是以字节为单位进行接收的(伪代码)。
while (1) {// 1. 从串口读取数据到缓存(伪代码,需替换为实际串口读取函数)uint8_t byte;if (serial_read_byte(&byte)) { // 假设serial_read_byte()从串口读取1字节serial_buf.buffer[serial_buf.index++] = byte;// 2. 检查缓存中是否有完整帧(从头部开始)while (serial_buf.index >= 2) { // 至少需要Header1/Header2// 查找头部起始位置(简化版:假设头部在缓存开头)if (serial_buf.buffer[0] == HEADER1 && serial_buf.buffer[1] == HEADER2) {// 3. 检查缓存是否足够容纳一帧uint8_t frame_length = 2 + 1 + EXPECTED_LEN + 1 + 1; // 头+长度+CRC+尾if (serial_buf.index >= frame_length) {// 4. 提取完整帧uint8_t frame[256] = {0};memcpy(frame, serial_buf.buffer, frame_length);// 5. 解析帧if (parse_frame(frame, &sensor_data) == 0) {// 6. 处理有效数据(示例:打印)printf("解析成功: flag=0x%02X, u16=0x%04X, u32=0x%08X\n",sensor_data.flag, sensor_data.u16_data, sensor_data.u32_data);}// 7. 移除已处理的数据(滑动窗口)serial_buf.index -= frame_length;memmove(serial_buf.buffer, &serial_buf.buffer[frame_length], serial_buf.index);} else {break; // 数据不足,等待更多字节}} else {// 头部不匹配,丢弃第一个字节(避免死循环)memmove(serial_buf.buffer, &serial_buf.buffer[1], --serial_buf.index);}}}
上面的代码其实还是比较难懂的主要就是说干了以下几个事情:
- 首先是根据下位机传送出来的数据,并放到一个数组中。这这主要是根据头部码以及长度来确定的
- 其次是取出其中的数据具体就是提取从哪一字节到哪一个字节再取出来,然后进行分析解码。
QByteArray pressureData = frame.mid(4, 4);
如果你有一个 frame 的16进制表示(比如 "01 02 03 04 05 06 07 08 09 0a 0b 0c"),那么:
第4个字节(索引4)是 05,
接下来的4个字节是 05 06 07 08。
所以 pressureData 就是 05 06 07 08(16进制)。
存在一个问题就是这个数据传输到底是第一个字节是高位还是后一个字节是高位的问题,也就是所谓的小端字节跟大端字节的一个问题
QByteArray frame = QByteArray::fromHex("0102030405060708090a0b0c"); // 示例数据
QByteArray pressureData = frame.mid(4, 4); // 提取第4~7字节(索引4开始)小端解析:0x08 0x07 0x06 0x05 → 0x08070605 → 84281096
大端解析:0x05 0x06 0x07 0x08 → 0x05060708 → 134810149// 打印提取的16进制数据
qDebug() << "Extracted data (hex):" << pressureData.toHex(); // 输出 "05060708"// 转换为十进制(假设是小端字节序)
quint32 pressureValue = static_cast<quint32>(pressureData[0]) |(static_cast<quint32>(pressureData[1]) << 8) |(static_cast<quint32>(pressureData[2]) << 16) |(static_cast<quint32>(pressureData[3]) << 24);
qDebug() << "Pressure value (decimal, little endian):" << pressureValue; // 输出 84281096// 如果是大端字节序
quint32 pressureValueBigEndian = static_cast<quint32>(pressureData[3]) |(static_cast<quint32>(pressureData[2]) << 8) |(static_cast<quint32>(pressureData[1]) << 16) |(static_cast<quint32>(pressureData[0]) << 24);
qDebug() << "Pressure value (decimal, big endian):" << pressureValueBigEndian; // 输出 134810149
当然一般来说他就是按照大端解析进行的,就是按照正常人的思维从左至右从大到小(当然了是按照正常人的思维进行的考虑到数据加密的话可能会有一点不一样)。