The Lightweight Communication Protocol Weapon in Embedded Systems!

In embedded development, reliable communication between devices has always been a challenge.

Today, we will share an excellent open-source project—LwPKT, and see how it solves complex communication protocol issues with less than 1000 lines of code.

1. Pain Points of Embedded Communication

In embedded development, we often encounter the following issues:

  • Memory Constraints: MCUs have only a few KB of RAM, and complex protocol stacks cannot run at all.
  • High Real-Time Requirements: In industrial control scenarios, millisecond-level delays are intolerable.
  • Reliability Requirements: Data transmission errors can lead to device failures or even safety incidents.
  • Multi-Device Networking: Connecting dozens of devices on an RS-485 bus requires address management.
  • Development Efficiency: Writing communication protocols from scratch leads to long debugging cycles and many bugs.

The LwPKT (Lightweight Packet Protocol) introduced today is designed to address these pain points.

  • Industrial Automation: Communication between RS-485 network devices.
  • MCU Communication: Interconnection of microcontrollers such as STM32 and ESP32.
  • IoT Devices: Sensor data collection and control command issuance.
  • Wireless Communication: Data encapsulation for LoRa, WiFi, and Bluetooth modules.

2. Introduction to LwPKT

LwPKT was developed by renowned embedded developer Tilen MAJERLE. It is a lightweight packet protocol library written in C, applied in the embedded field. The current version is v1.4.1.

https://github.com/MaJerle/lwpkt

Let’s first look at its core features:

2.1 Core Features

  • Ultra Lightweight: Core code is less than 1000 lines, with a minimum memory footprint of <100 bytes.
  • Platform Independent: Pure C implementation, supports all mainstream MCU platforms.
  • Configurable: Functional modularization, enabling features as needed to avoid resource waste.
  • Reliable Transmission: Supports CRC checks, timeout detection, and multi-layer error handling.
  • Variable-Length Encoding: Theoretically supports unlimited-length packets, with high transmission efficiency.
  • Event-Driven: Asynchronous processing mode, does not block the main program execution.

2.2 Application Scenarios

  • Industrial Automation: Communication between RS-485 network devices.
  • MCU Communication: Interconnection of microcontrollers such as STM32 and ESP32.
  • IoT Devices: Sensor data collection and control command issuance.
  • Wireless Communication: Data encapsulation for LoRa, WiFi, and Bluetooth modules.

3. Learning Core Principles

3.1 Core File Structure

lwpkt/
├── lwpkt/src/
│   ├── include/lwpkt/
│   │   ├── lwpkt.h          # Core API interface
│   │   └── lwpkt_opt.h      # Configuration options
│   └── lwpkt/
│       └── lwpkt.c          # Core implementation
├── examples/
│   └── example_lwpkt.c      # Usage example
├── tests/
│   └── test_main.c          # Test cases
└── docs/                    # Documentation

3.2 Packet Format

The packet format of LwPKT is carefully designed to ensure both functional integrity and resource consumption:

The Lightweight Communication Protocol Weapon in Embedded Systems!

Field Description:

  • START (0xAA): Start flag, helps the receiver synchronize.
  • FROM/TO (optional): Sender and receiver addresses, supports 8-bit or 32-bit.
  • FLAGS (optional): User-defined flags, can be used for priority, type identification, etc.
  • CMD (optional): Command field, supports 8-bit or multi-byte commands.
  • LEN (variable-length encoding): Data length, using variable-length encoding to save transmission bandwidth.
  • DATA: Actual data payload.
  • CRC (optional): Checksum, supports CRC-8 or CRC-32.
  • STOP (0x55): End flag.

3.3 Core Data Structure

The core of LwPKT is the <span>lwpkt_t</span> structure:

The Lightweight Communication Protocol Weapon in Embedded Systems!

Some members are configurable, with functional modularization, enabling features as needed to avoid resource waste.

3.4 Circular Buffer

LwPKT relies on the LwRB (Lightweight Ring Buffer) library to manage data buffering, which is a classic producer-consumer pattern application:

We shared LwRB in our previous article: A lightweight ring buffer management library suitable for embedded systems!

Data sending process: Application -> LwPKT -> TX buffer -> Hardware interface. The Lightweight Communication Protocol Weapon in Embedded Systems!

Data receiving process: Hardware interface -> RX buffer -> LwPKT -> Application.

The Lightweight Communication Protocol Weapon in Embedded Systems!

3.5 Protocol Parsing

The protocol data reception and parsing use a finite state machine (FSM) design.

State enumeration:

The Lightweight Communication Protocol Weapon in Embedded Systems!

State transition function:

The Lightweight Communication Protocol Weapon in Embedded Systems!

The state transition function utilizes the fallthrough feature of C language to automatically skip disabled fields based on configuration.

In C language, <span>fallthrough</span><span> refers to the feature where, after a case branch in a </span><code><span>switch</span> statement is executed, the program flow automatically “falls through” to the next case branch without using a <span>break</span> statement. This is the default behavior of the <span>switch</span> structure in C, which is both a flexible feature and can lead to logical errors if misused.

To distinguish between “intentional fallthrough” and “unintentional omission”, the C17 standard introduced the <span>[[fallthrough]]</span> attribute to explicitly mark “this fallthrough is intentional”, eliminating compiler warnings.

The advantages of the state machine are:

  • Clear Logic: Each state has a single responsibility, making it easy to understand and debug.
  • Efficient Processing: O(1) time complexity, suitable for real-time systems.
  • Error Recovery: Any exception in any state can quickly recover to the initial state.

3.6 Variable-Length Encoding and Decoding

LwPKT uses MSB (Most Significant Bit) encoding to implement data encoding. Its core idea is “allocate as needed”—small numbers use fewer bytes, while large numbers use more bytes, thus saving transmission bandwidth in most cases.

In the LwPKT protocol, key fields in the protocol are designed to support 32-bit (4-byte) data:

// Address field - supports extended 32-bit address
#if LWPKT_CFG_ADDR_EXTENDED
typedef uint32_t lwpkt_addr_t;  // 32-bit address
#else  
typedef uint8_t lwpkt_addr_t;   // 8-bit address
#endif

// Command field
uint32_t cmd;

// Flag field
uint32_t flags;

// Data length - size_t type (usually 32-bit or 64-bit)
size_t len;

Using variable-length encoding can significantly reduce protocol overhead and improve transmission efficiency.

Fixed-Length Encoding vs Variable-Length Encoding:

For example, for a uint32_t type command field, if assigned the value 1, fixed-length encoding would require 4 bytes for transmission, while MSB (Most Significant Bit) variable-length encoding would only require 1 byte.

The Lightweight Communication Protocol Weapon in Embedded Systems!

LwPKT Encoding and Decoding Process:

The Lightweight Communication Protocol Weapon in Embedded Systems!

Key points of MSB variable-length encoding:

  • MSB Control: Highest bit = 1 indicates continue, = 0 indicates end.

  • 7-bit Data: Each byte uses only 7 bits to store data, 1 bit for control.

  • Little-endian Assembly: Low-order byte first, high-order byte last.

  • On-Demand Expansion: Small numbers use fewer bytes, large numbers use more bytes.

Encoding and decoding related code:

The Lightweight Communication Protocol Weapon in Embedded Systems!
The Lightweight Communication Protocol Weapon in Embedded Systems!
The Lightweight Communication Protocol Weapon in Embedded Systems!

3.7 Configuration Mechanism

LwPKT adopts a three-layer configuration system, achieving complete configurability from compile time to runtime:

The Lightweight Communication Protocol Weapon in Embedded Systems!
The Lightweight Communication Protocol Weapon in Embedded Systems!
The Lightweight Communication Protocol Weapon in Embedded Systems!

First Layer: Global Configuration at Compile Time

The Lightweight Communication Protocol Weapon in Embedded Systems!
// lwpkt_opt.h - Default configuration
#define LWPKT_OFF        0    // Function completely disabled
#define LWPKT_ON_STATIC  1    // Function statically enabled
#define LWPKT_ON_DYNAMIC 2    // Function dynamically controlled

// Users can override in lwpkt_opts.h
#ifndef LWPKT_CFG_USE_CRC
#define LWPKT_CFG_USE_CRC LWPKT_ON_STATIC
#endif

Second Layer: Conditional Compilation Control

// Decide whether to compile code based on configuration value
#if LWPKT_CFG_USE_CRC
    // CRC related code
    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

// Function parameters are also affected by conditional compilation
static uint8_t prv_write_bytes_var_encoded(
    lwpkt_t* pkt, 
    uint32_t var_num
#if LWPKT_CFG_USE_CRC
    , lwpkt_crc_t* crc    // This parameter only exists when CRC is enabled
#endif
);

Third Layer: Runtime Dynamic Control

// Instance-level flag control
typedef struct lwpkt {
    uint8_t flags;    // Runtime control 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)

// Dynamic control function
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 Event Mechanism

The event mechanism of LwPKT implements a lightweight observer pattern, allowing applications to listen to various state changes and operation events of the protocol stack.

Related Articles: Embedded Programming Models | Observer Pattern

3.8.1 Core Event Types

typedef enum {
    LWPKT_EVT_PKT,        // Packet ready event
    LWPKT_EVT_TIMEOUT,    // Timeout event
    LWPKT_EVT_READ,       // Read operation event
    LWPKT_EVT_WRITE,      // Write operation event
    LWPKT_EVT_PRE_WRITE,  // Pre-write event
    LWPKT_EVT_POST_WRITE, // Post-write event
    LWPKT_EVT_PRE_READ,   // Pre-read event
    LWPKT_EVT_POST_READ,  // Post-read event
} lwpkt_evt_type_t;
The Lightweight Communication Protocol Weapon in Embedded Systems!

3.8.2 Event Mechanism and Trigger Timing

// Callback function definition
typedef void (*lwpkt_evt_fn)(struct lwpkt* pkt, lwpkt_evt_type_t evt_type);

// Event registration interface
lwpkt_ret_t lwpkt_set_evt_fn(lwpkt_t* pkt, lwpkt_evt_fn evt_fn) {
    pkt->evt_fn = evt_fn;
    return lwpktOK;
}

// Event sending macro
#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)  // Empty implementation, zero overhead
#endif

Trigger timing:

// Triggered by lwpkt_read interface
lwpkt_ret_t lwpkt_read(lwpkt_t* pkt) {
    lwpkt_ret_t res = lwpktOK;
    uint8_t b, e = 0;

    // 1. Pre-read event before read operation
    SEND_EVT(pkt, LWPKT_EVT_PRE_READ);

    // Main loop for processing received data
    // ...

retpre:
    // 2. Post-read event after read operation
    SEND_EVT(pkt, LWPKT_EVT_POST_READ);
    
    // 3. If data was processed, send read event
    if (e) {
        SEND_EVT(pkt, LWPKT_EVT_READ);
    }
    
    return res;
}

// Triggered by lwpkt_write interface
lwpkt_ret_t lwpkt_write(lwpkt_t* pkt, /* parameter list */) {
    lwpkt_ret_t res = lwpktOK;

    // 1. Pre-write event before write operation
    SEND_EVT(pkt, LWPKT_EVT_PRE_WRITE);

    // Packet construction process...
    // Write START, address, command, length, data, CRC, STOP

fast_return:
    // 2. Post-write event after write operation
    SEND_EVT(pkt, LWPKT_EVT_POST_WRITE);
    
    // 3. If write is successful, send write event
    if (res == lwpktOK) {
        SEND_EVT(pkt, LWPKT_EVT_WRITE);
    }
    
    return res;
}

// Triggered by lwpkt_process interface
lwpkt_ret_t lwpkt_process(lwpkt_t* pkt, uint32_t time) {
    lwpkt_ret_t pktres = lwpkt_read(pkt);
    
    if (pktres == lwpktVALID) {
        pkt->last_rx_time = time;
        // 1. Packet ready event
        SEND_EVT(pkt, LWPKT_EVT_PKT);
    } elseif (pktres == lwpktINPROG) {
        if ((time - pkt->last_rx_time) >= LWPKT_CFG_PROCESS_INPROG_TIMEOUT) {
            lwpkt_reset(pkt);
            pkt->last_rx_time = time;
            // 2. Timeout event
            SEND_EVT(pkt, LWPKT_EVT_TIMEOUT);
        }
    } else {
        pkt->last_rx_time = time;
    }
    
    return pktres;
}

3.9 Code Practice

Let’s implement a minimal example of LwPKT:

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

/* Define buffer size */
#define BUFFER_SIZE           64

/* LwPKT instance and ring buffer */
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(&amp;tx_rb, &amp;byte, 1) == 1) {
        lwrb_write(&amp;rx_rb, &amp;byte, 1);
    }
}

int main(void) {
    lwpkt_ret_t result;
    const char* message = "Hello LwPKT!";
    
    printf("============ LwPKT Test ============\n");
    
    /* Initialize ring buffer */
    lwrb_init(&amp;tx_rb, tx_data, sizeof(tx_data));
    lwrb_init(&amp;rx_rb, rx_data, sizeof(rx_data));
    
    /* Initialize LwPKT instance */
    if (lwpkt_init(&amp;pkt, &amp;tx_rb, &amp;rx_rb) != lwpktOK) {
        printf("LwPKT initialization failed!\n");
        return -1;
    }
    
    /* Send data packet */
    printf("\n====== Sending ======\n");
    printf("Sending data packet: %s\n", message);
    result = lwpkt_write(&amp;pkt,
                         0x01,                      /* Target address */
                         0x12345678,                /* Flags */
                         0x01,                      /* Command */
                         message, strlen(message)); /* Data */
    
    if (result != lwpktOK) {
        printf("Sending failed: %d\n", result);
        return -1;
    }
    
    /* Simulate network transfer */
    simulate_transfer();
    
    /* Receive and parse data packet */
    result = lwpkt_read(&amp;pkt);
    if (result == lwpktVALID) {
        printf("\n====== Receiving ======\n");
        /* Print packet information */
        printf("Sender address: 0x%08X\n", (unsigned)lwpkt_get_from_addr(&amp;pkt));
        printf("Receiver address: 0x%08X\n", (unsigned)lwpkt_get_to_addr(&amp;pkt));
        printf("Flags: 0x%08X\n", (unsigned)lwpkt_get_flags(&amp;pkt));
        printf("Command: 0x%02X\n", (unsigned)lwpkt_get_cmd(&amp;pkt));
        
        size_t data_len = lwpkt_get_data_len(&amp;pkt);
        printf("Data length: %zu bytes\n", data_len);
        
        if (data_len > 0) {
            uint8_t* data = lwpkt_get_data(&amp;pkt);
            printf("Data content: ");
            for (size_t i = 0; i < data_len; i++) {
                printf("%c", data[i]);
            }
            printf("\n");
        }
    }
    
    return 0;
}
The Lightweight Communication Protocol Weapon in Embedded Systems!

3.10 Comparison with Other Protocols

Feature LwPKT Modbus RTU JSON over UART Custom Protocol
Code Size <1KB ~10KB ~50KB Variable
RAM Usage <100B ~1KB ~5KB Variable
Parsing Speed Very Fast Fast Slow Variable
Configuration Flexibility Very High Low Medium High
Learning Curve Low Medium Low High
Ecological Support Medium High High Low

Selection Recommendations:

  • Severely Resource-Constrained: Choose the minimal configuration of LwPKT.
  • Need Standardization: Prioritize Modbus RTU.
  • Rapid Prototyping: JSON solutions have high development efficiency.
  • Special Requirements: Custom development based on LwPKT.

4. Conclusion

By studying the LwPKT project, we can see the qualities that an excellent embedded communication protocol should possess:

  • Minimal Design Philosophy: A complete protocol stack implemented in less than 1000 lines of code.
  • High Configurability: Enable features as needed to avoid resource waste.
  • Reliability Assurance: Multi-layer error detection, suitable for harsh environments.
  • Performance Optimization: State machine driven, O(1) complexity processing.
  • Easy Integration: Pure C implementation, no external dependencies.

Leave a Comment