找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1042

积分

0

好友

152

主题
发表于 昨天 01:44 | 查看: 5| 回复: 0

在嵌入式开发中,我们经常需要处理不同类型数据的传输(如通过UART、SPI、I2C)与存储(如写入EEPROM或SPI Flash)。一个常见的挑战是:这些外设通常以字节为单位进行操作,而我们需要读写的可能是uint16_tfloatuint32_t等复合数据类型。手动拆分和组装这些数据不仅代码冗长,还容易出错。

本文将介绍一种基于C语言结构体的简易方法,可以极大地简化这一过程,并附带讨论与之相关的字节序(大小端)问题

关于字节序(大小端)

在进行跨设备或跨协议的数据交换时,字节序是一个必须考虑的因素。本文主要讨论的Cortex-M内核同时支持大端和小端模式。

但在实际应用中,绝大多数Cortex-M芯片(包括ST的STM32全系列)在出厂时就被固化为小端(Little-Endian)模式,且不可更改。 其他厂商的芯片也大多如此。因此,在单一小端设备内部进行数据打包与解析,或在小端设备之间通信时,我们可以暂时忽略字节序转换问题。

以下是STM32各系列参考手册的佐证:

  • F1系列:明确说明数据以小端格式存储。
    F1编程手册关于小端的说明
  • F3/F4系列:同样指出存储器系统采用小端格式。
    F3和F4编程手册关于小端的说明
  • F7/H7系列:延续了这一设计。
    F7和H7编程手册关于小端的说明

了解这一背景后,我们就可以专注于如何高效地组织数据。

方法核心:使用结构体封装数据

本方法的核心思想是:定义一个结构体,将需要传输或存储的所有数据成员封装在一起。这样,整个结构体变量在内存中就是一段连续的字节序列,可以直接用于字节读写操作。

1. 应用于数据存储(如EEPROM、Flash)

假设我们需要向SPI Flash存储一个配置参数集合,包含一个字节、一个短整型和一个浮点数。

首先,定义结构体:

typedef struct {
    uint8_t  status;
    uint16_t counter;
    float    voltage;
} Config_T;

Config_T g_tConfig;

写入时,可以直接将结构体变量的地址强制转换为uint8_t*指针,并以其大小为长度进行写入:

// 假设 spiFlash_Write 函数原型为: spiFlash_Write(uint32_t addr, uint8_t *pBuf, uint32_t len)
spiFlash_Write(SAVE_ADDR, (uint8_t*)&g_tConfig, sizeof(Config_T));

读取时,采用类似的方式:

// 假设 spiFlash_Read 函数原型为: spiFlash_Read(uint32_t addr, uint8_t *pBuf, uint32_t len)
spiFlash_Read(SAVE_ADDR, (uint8_t*)&g_tConfig, sizeof(Config_T));
// 之后即可直接访问成员
// uint16_t cnt = g_tConfig.counter;
2. 应用于数据通信(以UART为例)
在通信协议设计中,这种方法尤为高效。例如,主机需要向从机发送一帧包含多种数据类型的数据,格式如下: 起始符 CO2值 PM2.5值 湿度值 温度值 参数 结束符1 结束符2
1字节 2字节 2字节 2字节 4字节 4字节 1字节 1字节

我们可以定义一个与之完全对应的结构体:

typedef struct {
    uint8_t  ucStart;        // 起始符,例如 ‘$'
    uint16_t usCO2;
    uint16_t usPM25;
    uint16_t usHumidity;
    float    fTemperature;   // 温度值
    uint32_t ulParam;
    uint8_t  ucEnd1;         // 结束符1,例如 ‘\r’
    uint8_t  ucEnd2;         // 结束符2,例如 ‘\n’
} UART_FRAME_T;

UART_FRAME_T g_tTxFrame; // 发送帧

主机发送时,直接将结构体作为字节流发出:

// 假设 comSendBuf 函数原型为: comSendBuf(uint8_t com, uint8_t *pBuf, uint16_t len)
comSendBuf(COM1, (uint8_t*)&g_tTxFrame, sizeof(UART_FRAME_T));

从机接收时,在接收缓冲区上定义同样的结构体指针,即可直接访问各个字段:

uint8_t uartRxBuf[128];
UART_FRAME_T *pRxFrame;

// 将接收缓冲区的地址强制转换为结构体指针
pRxFrame = (UART_FRAME_T *)uartRxBuf;

// 现在可以直接像访问结构体一样访问协议数据
uint16_t co2_value = pRxFrame->usCO2;
float temp_value   = pRxFrame->fTemperature;

这种方法避免了手动解析每一个字段的繁琐操作,极大地提高了代码的简洁性和可维护性,是进行嵌入式通信协议处理的实用技巧。

完整代码示例

以下是一个模拟通过两个串口(COM1发送,COM2接收)进行数据帧收发的简单例程,演示了上述结构体方法的完整应用。

#include <stdint.h>
#include <string.h>
// 假设必要的硬件驱动头文件已包含

/* 定义通信帧结构体 */
typedef struct {
    uint8_t  ucStart;
    uint16_t usCO2;
    uint16_t usPM25;
    uint16_t usHumidity;
    float    fTemperature;
    uint32_t ulParam;
    uint8_t  ucEnd1;
    uint8_t  ucEnd2;
} UART_FRAME_T;

/* 全局变量定义 */
UART_FRAME_T g_tTxFrame;          // 发送帧(用于COM1)
UART_FRAME_T *pRxFrame;           // 接收帧指针(用于COM2)
uint8_t      rxBuffer[128];       // 原始接收缓冲区
uint8_t      rxStatus = 0;        // 接收状态机状态
uint8_t      rxIndex = 0;         // 缓冲区索引

int main(void) {
    uint8_t key;
    uint8_t uartByte;
    float testTemp = 0.11f;

    /* 硬件初始化 */
    bsp_Init();
    pRxFrame = (UART_FRAME_T *)rxBuffer; // 将缓冲区映射为结构体

    /* 主循环 */
    while (1) {
        bsp_Idle(); // 处理休眠、看门狗等

        /* 按键K1触发发送 */
        key = bsp_GetKey();
        if (key == KEY_DOWN_K1) {
            // 填充发送帧数据
            g_tTxFrame.ucStart = ‘$’;
            g_tTxFrame.usCO2 = 1;
            g_tTxFrame.usPM25 = 2;
            g_tTxFrame.usHumidity = 3;
            g_tTxFrame.fTemperature = testTemp++;
            g_tTxFrame.ulParam = 5;
            g_tTxFrame.ucEnd1 = ‘\r’;
            g_tTxFrame.ucEnd2 = ‘\n’;

            // 以字节流形式发送整个结构体
            comSendBuf(COM1, (uint8_t*)&g_tTxFrame, sizeof(UART_FRAME_T));
            printf(">> 数据帧发送完成。\r\n");
        }

        /* 从COM2接收并解析数据 */
        if (comGetChar(COM2, &uartByte)) {
            switch (rxStatus) {
                case 0: // 等待起始符
                    if (uartByte == ‘$’) {
                        memset(rxBuffer, 0, sizeof(rxBuffer));
                        rxBuffer[rxIndex++] = uartByte;
                        rxStatus = 1;
                    }
                    break;

                case 1: // 接收数据体
                    rxBuffer[rxIndex] = uartByte;
                    // 检查是否接收到完整的帧(以\r\n结尾)
                    if (rxIndex >= 1 && rxBuffer[rxIndex-1] == ‘\r’ && rxBuffer[rxIndex] == ‘\n’) {
                        // 接收完成,通过结构体指针直接访问数据
                        printf("<< 接收到数据帧:\r\n");
                        printf("   CO2: %d\r\n", pRxFrame->usCO2);
                        printf("   PM2.5: %d\r\n", pRxFrame->usPM25);
                        printf("   湿度: %d\r\n", pRxFrame->usHumidity);
                        printf("   温度: %.2f\r\n", pRxFrame->fTemperature);
                        printf("   参数: %lu\r\n", pRxFrame->ulParam);
                        printf("\r\n");

                        // 重置状态,准备接收下一帧
                        rxStatus = 0;
                        rxIndex = 0;
                    } else {
                        rxIndex++;
                        // 简单防止缓冲区溢出
                        if (rxIndex >= sizeof(rxBuffer)) {
                            rxStatus = 0;
                            rxIndex = 0;
                        }
                    }
                    break;

                default:
                    rxStatus = 0;
                    break;
            }
        }
    }
    return 0;
}

总结

通过使用C语言结构体来映射通信协议或存储格式,我们可以将一段内存字节流直接转化为有组织的、易于访问的强类型数据,反之亦然。这种方法在嵌入式系统编程中具有显著优势:

  1. 代码简洁:省去了大量的移位、掩码和拼接操作。
  2. 易于维护:协议格式或存储结构的变更,只需修改结构体定义即可。
  3. 逻辑清晰:数据访问方式直观(frame->sensorValue)。

需要注意的要点

  • 字节对齐:编译器可能会在结构体成员间插入填充字节以保证对齐。在跨平台或严格遵循字节协议的场景下,需要使用编译器指令(如GCC的__attribute__((packed)))来强制结构体紧凑排列。
  • 字节序:如前所述,本文方法在双方均为小端设备时可直接使用。若与大端设备网络协议(如TCP/IP)通信,则必须在打包或解包前进行必要的字节序转换(如使用htonlntohl等函数)。
  • 数据填充:确保接收缓冲区或存储区的大小至少等于结构体的大小。

掌握这一技巧,能让你在处理嵌入式系统的数据交换与持久化时更加得心应手。




上一篇:提升Java开发效率的10个必备IDEA插件:代码生成与调试利器
下一篇:基于Playwright与LLM的智能Web自动化:Stagehand框架解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 16:41 , Processed in 0.128543 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表