一个适用于嵌入式领域的轻量级协议库

一个适用于嵌入式领域的轻量级协议库

编码文章call10242025-09-11 15:46:193A+A-

在嵌入式开发中,设备间的可靠通信一直是个难题。

今天我们来分享一个优秀的开源项目——LwPKT,看看它如何用不到1000行代码解决复杂的通信协议问题。

1. 嵌入式通信的痛点

在嵌入式开发中,我们常常遇到这些问题:

  • 内存受限:MCU只有几KB RAM,复杂协议栈根本跑不起来
  • 实时性要求高:工业控制场景下,毫秒级延迟都不能容忍
  • 可靠性要求:数据传输错误可能导致设备故障甚至安全事故
  • 多设备组网:RS-485总线上挂载十几个设备,需要地址管理
  • 开发效率:从零写通信协议,调试周期长,bug多

今天介绍的LwPKT(Lightweight Packet Protocol)就是为了解决这些痛点而生的。

  • 工业自动化:RS-485网络设备通信
  • MCU间通信:STM32、ESP32等微控制器互联
  • 物联网设备:传感器数据采集与控制指令下发
  • 无线通信:LoRa、WiFi、蓝牙模块数据封装

2. LwPKT简介

LwPKT由知名嵌入式开发者Tilen MAJERLE开发,它是一个用C语言编写的轻量级数据包协议库,应用于嵌入式领域。目前已发布v1.4.1版本。

https://github.com/MaJerle/lwpkt

让我们先看看它的核心特性:

2.1 核心特性

  • 超轻量级:核心代码不到1000行,最小内存占用<100字节
  • 平台无关:纯C语言实现,支持所有主流MCU平台
  • 可配置:功能模块化,按需启用,避免资源浪费
  • 可靠传输:支持CRC校验,超时检测,多层错误处理
  • 变长编码:理论支持无限长数据包,传输效率高
  • 事件驱动:异步处理模式,不阻塞主程序执行

2.2 应用场景

  • 工业自动化:RS-485网络设备通信
  • MCU间通信:STM32、ESP32等微控制器互联
  • 物联网设备:传感器数据采集与控制指令下发
  • 无线通信:LoRa、WiFi、蓝牙模块数据封装

3. 核心原理学习

3.1 核心文件结构

lwpkt/
├── lwpkt/src/
│   ├── include/lwpkt/
│   │   ├── lwpkt.h          # 核心API接口
│   │   └── lwpkt_opt.h      # 配置选项
│   └── lwpkt/
│       └── lwpkt.c          # 核心实现
├── examples/
│   └── example_lwpkt.c      # 使用示例
├── tests/
│   └── test_main.c          # 测试用例
└── docs/                    # 文档

3.2 数据包格式

LwPKT的数据包格式经过精心设计,既保证了功能完整性,又兼顾了资源消耗:

字段说明

  • START (0xAA):起始标志,帮助接收端同步
  • FROM/TO(可选):发送方和接收方地址,支持8位或32位
  • FLAGS(可选):用户自定义标志位,可用于优先级、类型标识等
  • CMD(可选):命令字段,支持8位或多字节命令
  • LEN(变长编码):数据长度,采用变长编码,节省传输带宽
  • DATA:实际数据载荷
  • CRC(可选):校验码,支持CRC-8或CRC-32
  • STOP (0x55):结束标志

3.3 核心数据结构

LwPKT的核心是lwpkt_t结构体:

部分成员是可配置的,功能模块化,按需启用,避免资源浪费。

3.4 环形缓冲区

LwPKT依赖LwRB(Lightweight Ring Buffer)库管理数据缓冲,这是一个经典的生产者-消费者模式应用:

LwRB我们上一篇已经分享:适用于嵌入式的轻量级环缓冲区管理库!

数据发送流程:应用 -> LwPKT -> TX缓冲区 -> 硬件接口。

数据接收流程:硬件接口 -> RX缓冲区 -> LwPKT -> 应用。

3.5 协议解析

协议数据接受解析采用有限状态机(FSM)设计。

状态枚举:

状态转换函数:

状态转换函数,利用C语言的fallthrough特性,根据配置自动跳过禁用的字段。

在 C 语言中,fallthrough 指的是 switch 语句中,某个 case 分支执行完毕后,未使用break语句,导致程序流程自动 “穿透” 到下一个 case 分支继续执行 的特性。这是 C 语言中switch结构的默认行为,既是灵活特性,也可能因误用导致逻辑错误。

为了区分 “有意的 fallthrough” 和 “无意的遗漏”,C17 标准引入了[[fallthrough]]属性,用于显式标记 “此处的 fallthrough 是故意的”,消除编译器警告。

状态机的优势在于:

  • 逻辑清晰:每个状态职责单一,便于理解和调试
  • 处理高效:O(1)时间复杂度,适合实时系统
  • 错误恢复:任何状态下的异常都能快速恢复到初始状态

3.6 变长编、解码

LwPKT使用MSB(最高位)编码来实现数据编码。它的核心思想就是"按需分配"——小数字用少字节,大数字用多字节,从而在大多数情况下节省传输带宽。

在LwPKT协议中,协议中的关键字段都设计为支持32位(4字节)数据:

// 地址字段 - 支持扩展32位地址
#if LWPKT_CFG_ADDR_EXTENDED
typedef uint32_t lwpkt_addr_t;  // 32位地址
#else  
typedef uint8_t lwpkt_addr_t;   // 8位地址
#endif

// 命令字段
uint32_t cmd;

// 标志字段
uint32_t flags;

// 数据长度 - size_t类型(通常32位或64位)
size_t len;

使用变长编码可以显著减少协议开销,提高传输效率。

定长编码 vs 变长编码:

例如,uint32_t类型的命令字段,假如赋值为1。按照定长编码得传输4字节,按照MSB(最高位)变长编码只需传输1字节。

LwPKT编解码流程:

MSB变长编码要点:

  • MSB位控制:最高位=1表示继续,=0表示结束
  • 7位数据:每字节只用7位存储数据,1位用于控制
  • 小端序组装:低位字节在前,高位字节在后
  • 按需扩展:小数字用少字节,大数字用多字节

编码、解码相关代码:

3.7 可配置机制

LwPKT采用了三层配置系统,实现了从编译时到运行时的完整可配置性:

第一层:编译时全局配置

// lwpkt_opt.h - 默认配置
#define LWPKT_OFF        0    // 功能完全禁用
#define LWPKT_ON_STATIC  1    // 功能静态启用
#define LWPKT_ON_DYNAMIC 2    // 功能动态控制

// 用户可在 lwpkt_opts.h 中重写
#ifndef LWPKT_CFG_USE_CRC
#define LWPKT_CFG_USE_CRC LWPKT_ON_STATIC
#endif

第二层:条件编译控制

// 根据配置值决定代码是否编译
#if LWPKT_CFG_USE_CRC
    // CRC相关代码
    static void prv_crc_init(lwpkt_t* pkt, lwpkt_crc_t* crcobj);
    static uint32_t prv_crc_in(lwpkt_t* pkt, lwpkt_crc_t* crcobj, const void* inp, const size_t len);
#endif

// 函数参数也受条件编译影响
static uint8_t prv_write_bytes_var_encoded(
    lwpkt_t* pkt, 
    uint32_t var_num
#if LWPKT_CFG_USE_CRC
    , lwpkt_crc_t* crc    // 只有启用CRC时才有此参数
#endif
);

第三层:运行时动态控制

// 实例级别的标志位控制
typedef struct lwpkt {
    uint8_t flags;    // 运行时控制标志
    // ...
} lwpkt_t;

#define LWPKT_FLAG_USE_CRC       ((uint8_t)0x01)
#define LWPKT_FLAG_CRC32         ((uint8_t)0x02)
#define LWPKT_FLAG_USE_ADDR      ((uint8_t)0x04)
#define LWPKT_FLAG_ADDR_EXTENDED ((uint8_t)0x08)
#define LWPKT_FLAG_USE_CMD       ((uint8_t)0x10)
#define LWPKT_FLAG_CMD_EXTENDED  ((uint8_t)0x20)
#define LWPKT_FLAG_USE_FLAGS     ((uint8_t)0x40)

// 动态控制函数
void lwpkt_set_crc_enabled(lwpkt_t* pkt, uint8_t enable) {
    if (enable) {
        pkt->flags |= LWPKT_FLAG_USE_CRC;
    } else {
        pkt->flags &= ~LWPKT_FLAG_USE_CRC;
    }
}

3.8 事件机制

LwPKT的事件机制实现了一个轻量级的观察者模式,允许应用程序监听协议栈的各种状态变化和操作事件。

相关文章:嵌入式编程模型 | 观察者模式

3.8.1 核心事件类型

typedef enum {
    LWPKT_EVT_PKT,        // 数据包就绪事件
    LWPKT_EVT_TIMEOUT,    // 超时事件
    LWPKT_EVT_READ,       // 读操作事件
    LWPKT_EVT_WRITE,      // 写操作事件
    LWPKT_EVT_PRE_WRITE,  // 写前事件
    LWPKT_EVT_POST_WRITE, // 写后事件
    LWPKT_EVT_PRE_READ,   // 读前事件
    LWPKT_EVT_POST_READ,  // 读后事件
} lwpkt_evt_type_t;

3.8.2 事件机制及触发时机

// 回调函数定义
typedef void (*lwpkt_evt_fn)(struct lwpkt* pkt, lwpkt_evt_type_t evt_type);

// 事件注册接口
lwpktr_t lwpkt_set_evt_fn(lwpkt_t* pkt, lwpkt_evt_fn evt_fn) {
    pkt->evt_fn = evt_fn;
    return lwpktOK;
}

// 事件发送宏
#if LWPKT_CFG_USE_EVT
#define SEND_EVT(pkt, event) \
    do { \
        if ((pkt)->evt_fn != NULL) { \
            (pkt)->evt_fn((pkt), (event)); \
        } \
    } while (0)
#else
#define SEND_EVT(pkt, event)  // 空实现,零开销
#endif

触发时机:

// lwpkt_read接口触发
lwpktr_t lwpkt_read(lwpkt_t* pkt) {
    lwpktr_t res = lwpktOK;
    uint8_t b, e = 0;

    // 1. 读操作开始前事件
    SEND_EVT(pkt, LWPKT_EVT_PRE_READ);

    // 处理接收数据的主循环
    // ...

retpre:
    // 2. 读操作完成后事件
    SEND_EVT(pkt, LWPKT_EVT_POST_READ);
    
    // 3. 如果处理了数据,发送读事件
    if (e) {
        SEND_EVT(pkt, LWPKT_EVT_READ);
    }
    
    return res;
}

// lwpkt_write接口触发
lwpktr_t lwpkt_write(lwpkt_t* pkt, /* 参数列表 */) {
    lwpktr_t res = lwpktOK;

    // 1. 写操作开始前事件
    SEND_EVT(pkt, LWPKT_EVT_PRE_WRITE);

    // 数据包构建过程...
    // 写入START, 地址, 命令, 长度, 数据, CRC, STOP

fast_return:
    // 2. 写操作完成后事件
    SEND_EVT(pkt, LWPKT_EVT_POST_WRITE);
    
    // 3. 如果写入成功,发送写事件
    if (res == lwpktOK) {
        SEND_EVT(pkt, LWPKT_EVT_WRITE);
    }
    
    return res;
}

// lwpkt_process接口触发
lwpktr_t lwpkt_process(lwpkt_t* pkt, uint32_t time) {
    lwpktr_t pktres = lwpkt_read(pkt);
    
    if (pktres == lwpktVALID) {
        pkt->last_rx_time = time;
        // 1. 数据包就绪事件
        SEND_EVT(pkt, LWPKT_EVT_PKT);
    } else if (pktres == lwpktINPROG) {
        if ((time - pkt->last_rx_time) >= LWPKT_CFG_PROCESS_INPROG_TIMEOUT) {
            lwpkt_reset(pkt);
            pkt->last_rx_time = time;
            // 2. 超时事件
            SEND_EVT(pkt, LWPKT_EVT_TIMEOUT);
        }
    } else {
        pkt->last_rx_time = time;
    }
    
    return pktres;
}

3.9 代码实战

让我们实现一个LwPKT最小例子:

#include <stdio.h>
#include <string.h>
#include "lwpkt/lwpkt.h"

/* 定义缓冲区大小 */
#define BUFFER_SIZE           64

/* LwPKT实例和环形缓冲区 */
static lwpkt_t pkt;
static lwrb_t tx_rb, rx_rb;
static uint8_t tx_data[BUFFER_SIZE], rx_data[BUFFER_SIZE];

void simulate_transfer(void) {
    uint8_t byte = 0;

    while (lwrb_read(&tx_rb, &byte, 1) == 1) {
        lwrb_write(&rx_rb, &byte, 1);
    }
}

int main(void) {
    lwpktr_t result;
    const char* message = "Hello LwPKT!";
    
    printf("============ LwPKT Test ============\n");
    
    /* 初始化环形缓冲区 */
    lwrb_init(&tx_rb, tx_data, sizeof(tx_data));
    lwrb_init(&rx_rb, rx_data, sizeof(rx_data));
    
    /* 初始化LwPKT实例 */
    if (lwpkt_init(&pkt, &tx_rb, &rx_rb) != lwpktOK) {
        printf("LwPKT初始化失败!\n");
        return -1;
    }
    
    /* 发送数据包 */
    printf("\n====== 发送 ======\n");
    printf("发送数据包: %s\n", message);
    result = lwpkt_write(&pkt,
                         0x01,                      /* 目标地址 */
                         0x12345678,                /* 标志位 */
                         0x01,                      /* 命令 */
                         message, strlen(message)); /* 数据 */
    
    if (result != lwpktOK) {
        printf("发送失败: %d\n", result);
        return -1;
    }
    
    /* 模拟网络传输 */
    simulate_transfer();
    
    /* 接收并解析数据包 */
    result = lwpkt_read(&pkt);
    if (result == lwpktVALID) {
        printf("\n====== 接收 ======\n");
        /* 打印数据包信息 */
        printf("发送方地址: 0x%08X\n", (unsigned)lwpkt_get_from_addr(&pkt));
        printf("接收方地址: 0x%08X\n", (unsigned)lwpkt_get_to_addr(&pkt));
        printf("标志位: 0x%08X\n", (unsigned)lwpkt_get_flags(&pkt));
        printf("命令: 0x%02X\n", (unsigned)lwpkt_get_cmd(&pkt));
        
        size_t data_len = lwpkt_get_data_len(&pkt);
        printf("数据长度: %zu字节\n", data_len);
        
        if (data_len > 0) {
            uint8_t* data = lwpkt_get_data(&pkt);
            printf("数据内容: ");
            for (size_t i = 0; i < data_len; i++) {
                printf("%c", data[i]);
            }
            printf("\n");
        }
    }
    
    return 0;
}

3.10 与其他协议的对比

特性 LwPKT Modbus RTU JSON over UART 自定义协议 代码大小 <1KB ~10KB ~50KB 不定 RAM占用 <100B ~1KB ~5KB 不定 解析速度 极快 快 慢 不定 配置灵活性 极高 低 中 高 学习成本 低 中 低 高 生态支持 中 高 高 低

选择建议

  • 资源极度受限:选择LwPKT最小配置
  • 需要标准化:优先考虑Modbus RTU
  • 快速原型开发:JSON方案开发效率高
  • 特殊需求:基于LwPKT定制开发

4. 总结

通过学习LwPKT项目,我们可以看到一个优秀的嵌入式通信协议应该具备的特质:

  • 极简设计哲学:不到1000行代码实现完整协议栈
  • 高度可配置性:按需启用功能,避免资源浪费
  • 可靠性保证:多层错误检测,适应恶劣环境
  • 性能优化:状态机驱动,O(1)复杂度处理
  • 易于集成:纯C实现,无外部依赖
点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4