還在用數組一個個填數據?高手都在用結構體“降維打擊”。
今天聊一個讓STM32代碼從“能跑”到“飛起”的硬核技巧:結構體在通信協議中的實戰應用。
如果你還在用 sendBuf[0] = addr; sendBuf[1] = cmd; 這種“原始人”寫法,這篇文章將徹底改變你的編程習慣。
假設我們要發送一個溫濕度傳感器的數據包,協議如下:
| 字段 | 長度 | 說明 |
|---|---|---|
傳統數組寫法(新手版):
uint8_t buf[8];buf[0] = 0xAA;buf[1] = 0x55;buf[2] = (temp >> 8) & 0xFF; // 高字節buf[3] = temp & 0xFF; // 低字節buf[4] = (humi >> 8) & 0xFF;buf[5] = humi & 0xFF;uint16_t crc = CalcCRC(buf, 6);buf[6] = crc & 0xFF;buf[7] = (crc >> 8) & 0xFF;HAL_UART_Transmit(&huart1, buf, 8, 100);
問題來了:
核心思想:讓結構體的內存布局,直接等于數據包的字節流。
定義協議結構體:
// 關鍵:必須用 __attribute__((packed)) 或 #pragma pack(1) 取消內存對齊!typedef struct __attribute__((packed)) {uint16_t header; // 0xAA55int16_t temp; // 溫度 * 10int16_t humi; // 濕度 * 10uint16_t crc; // 校驗} SensorPacket_t;
發送代碼(高手版):
SensorPacket_t pkt;pkt.header = 0xAA55;pkt.temp = (int16_t)(get_temperature() * 10); // 直接賦值!pkt.humi = (int16_t)(get_humidity() * 10);pkt.crc = 0; // 先清零// 計算CRC(跳過CRC字段本身)pkt.crc = CalcCRC((uint8_t*)&pkt, sizeof(pkt) - 2);// 直接強轉指針發送!sizeof就是包長HAL_UART_Transmit(&huart1, (uint8_t*)&pkt, sizeof(pkt), 100);
降維打擊效果:
代碼量減少70%:邏輯清晰,一眼看懂。
零拷貝:直接取結構體地址發送,CPU不用做 memcpy。
防錯:編譯器幫你檢查類型,不會把 int16_t 錯塞進 uint8_t。
直接強轉結構體指針很爽,但在STM32(ARM Cortex-M)上,有三個致命陷阱必須避開。
現象:代碼編譯通過,但一運行就進 HardFault(硬錯誤)。
原因:STM32是32位CPU,訪問 uint16_t 或 uint32_t 時,地址必須是2或4的倍數。如果你用 packed 取消了填充,很可能導致 temp 字段的地址是 0x00000003(奇數),CPU訪問就會崩潰。
解決方案:發送用DMA,接收用字節拷貝。
發送:結構體在棧上定義,地址是編譯器分配的,通常是對齊的,可以直接發。
接收:絕對不要直接把 UART_RX_Buffer 強轉成結構體指針!必須用 memcpy 把數據從緩沖區復制到結構體變量里。
// 錯誤!可能導致崩潰SensorPacket_t *pkt = (SensorPacket_t*)UART_RX_Buffer;printf("Temp: %d", pkt->temp); // HardFault!// 正確!安全拷貝SensorPacket_t pkt;memcpy(&pkt, UART_RX_Buffer, sizeof(pkt)); // CPU會處理非對齊訪問printf("Temp: %d", pkt.temp); // OK
現象:上位機收到的數據高低字節是反的。
原因:STM32是小端模式(Little Endian),低位字節在低地址。而網絡協議通常約定用大端模式(高位在前)。
解決方案:在賦值前用宏轉換。
// 定義轉換宏(STM32是小端,所以要轉成大端發送)pkt.header = HTONS(0xAA55); // 發送前轉一下pkt.temp = HTONS(get_temperature());
現象:DMA發送時,數據還沒發完,就被其他代碼改寫了。
原因:編譯器認為 pkt.crc 計算完后沒人會讀,可能把 pkt 優化到寄存器里,而DMA實際讀取的是內存地址,導致數據不同步。
解決方案:加 volatile 修飾符。
// 告訴編譯器:這個變量會被硬件(DMA)偷偷修改,別優化它!volatile SensorPacket_t pkt;
以最常用的Modbus讀保持寄存器(03功能)請求幀為例:
typedef struct __attribute__((packed)) {uint8_t addr; // 從站地址uint8_t func; // 0x03uint16_t reg_addr; // 寄存器地址uint16_t reg_num; // 數量uint16_t crc; // 校驗} ModbusReadReq_t;void Send_Read_Holding(uint8_t slave_addr, uint16_t start_reg, uint16_t num) {volatile ModbusReadReq_t req;req.addr = slave_addr;req.func = 0x03;req.reg_addr = HTONS(start_reg); // 大小端轉換req.reg_num = HTONS(num);req.crc = 0;req.crc = Modbus_CRC((uint8_t*)&req, 6); // 計算前6字節的CRCreq.crc = HTONS(req.crc); // CRC本身也要轉大小端HAL_UART_Transmit_DMA(&huart1, (uint8_t*)&req, sizeof(req));}
| 階段 | 操作 | 口訣 |
|---|---|---|
| 定義 | 布局即協議 | |
| 填充 | 賦值即組包 | |
| 收發 | 發送靠指針,接收靠拷貝 |
最后留個思考題:為什么接收數據時,結構體定義里通常會把 CRC 字段放在最后,而不是像協議文檔里那樣緊跟在 Data 后面?
(提示:想想變長數據包怎么用結構體頭 + 柔性數組實現)
點贊、轉發、關注,獲取更多嵌入式開發干貨! ??
