TaterLi 个人博客

实现MCU和PC的通信 (Protobuf)

最近有一个产品规划,要用到上位机和下位机通信,为了长期演进不成屎山,所以我一开始考虑了各种TLV形式协议,然后突然想起这不protobuf就是最现成的东西.

首先确保你传输程序是正常的,比如用SPI/UART等,比如实现这两个函数,只给签名就明白.

/**
 * Send `size` bytes from `buf` over UART (blocking, polled).
 * Safe to call after USART hardware is initialised and enabled.
 */
void uart_send(const uint8_t *buf, size_t size);

/**
 * Read one byte from the receive ring buffer.
 * Returns 1 on success (byte written to *out), 0 if buffer empty.
 */
int uart_read_byte(uint8_t *out);

其中适用于MCU的库,我选择这个,把对应源码放到MCU工程内.

https://github.com/nanopb/nanopb

定义一个 chat.proto

syntax = "proto2";

message ChatMsg {
  required uint32 seq = 1;
  required string from = 2;
  required string text = 3;
  optional bool ack = 4 [default = false];
}

再写一个限制,限制最大长度,不过由于pb传输是压缩的,不是每次传输maxsize那么大,只是为了一些协议限制.

ChatMsg.from max_size:16
ChatMsg.text max_size:64

为了简单实现,先考虑实现一个帧头+数据,确定一次性接受完整的一个pb包,我这么准备数据包.

N的长度是payload_len指定,这样我只需要多2个字节,就可以指示长度,当然,当前的协议不健壮,不能解决各种粘包问题,但是先解决通信问题.

把proto转换成对应C代码.

protoc \
  --plugin=protoc-gen-nanopb=/usr/bin/protoc-gen-nanopb \
  --nanopb_out="<输出目录>" \
  --nanopb_opt="-f<chat.options路径>" \
  -I "<proto目录>" \
  "<chat.proto路径>"

如果没有对应工具,这样安装.

apt-get install -y protobuf-compiler nanopb libnanopb-dev

这样得到截图上四个文件,2个写的,2个是生成的.

看生成文件,是发现有发送结构体的

/* Struct definitions */
typedef struct _ChatMsg {
    uint32_t seq;
    char from[16];
    char text[64];
    bool has_ack;
    bool ack;
} ChatMsg;

我们可以写一个发包的函数,proto_send_chat后续实现,实现方法就是加包头发串口,等下实现.

static void proto_send_text(const char *from, const char *text, bool ack, uint32_t seq)
{
    ChatMsg msg = ChatMsg_init_zero;

    msg.seq = seq;
    (void)strncpy(msg.from, from, sizeof(msg.from) - 1U);
    (void)strncpy(msg.text, text, sizeof(msg.text) - 1U);
    msg.has_ack = true;
    msg.ack = ack;

    (void)proto_send_chat(&msg);
}

然后实现到UART的接口,这里注意一些类似固定范式的,ostream申请后,编码信息,最后发送.

static bool proto_send_chat(const ChatMsg *msg)
{
    uint8_t payload[PROTO_MAX_PAYLOAD];
    pb_ostream_t ostream = pb_ostream_from_buffer(payload, sizeof(payload));
    if (!pb_encode(&ostream, ChatMsg_fields, msg)) {
        return false;
    }

    if (ostream.bytes_written > 0xFFFFu) {
        return false;
    }

    uint16_t payload_len = (uint16_t)ostream.bytes_written;
    uint8_t header[PROTO_FRAME_HDR_SIZE] = {
        (uint8_t)(payload_len & 0xFFu),
        (uint8_t)((payload_len >> 8) & 0xFFu)
    };

    uart_send(header, sizeof(header));
    uart_send(payload, payload_len);
    return true;
}

至于回包就是反方向

void proto_link_poll(void)
{
    uint8_t byte = 0U;

    while (uart_read_byte(&byte)) {
        if (g_rx.received_len < PROTO_FRAME_HDR_SIZE) { // 接受头中
            g_rx.header_buf[g_rx.received_len++] = byte;

            if (g_rx.received_len == PROTO_FRAME_HDR_SIZE) {
                g_rx.expected_len = (uint16_t)g_rx.header_buf[0] |
                                    ((uint16_t)g_rx.header_buf[1] << 8);
                if ((g_rx.expected_len == 0U) || (g_rx.expected_len > PROTO_MAX_PAYLOAD)) {
                    proto_reset_rx(); // 不是正常的包,因为我这个协议非常脆,真实自己替换.
                }
            }
            continue; // 跳出当前循环,下面代码不执行(正在接受头)
        }

        // 接受数据
        g_rx.payload_buf[g_rx.received_len - PROTO_FRAME_HDR_SIZE] = byte;
        g_rx.received_len++;

        // 判断收完了没,收完去解码
        if (g_rx.received_len == (uint16_t)(PROTO_FRAME_HDR_SIZE + g_rx.expected_len)) {
            proto_try_decode_frame(g_rx.payload_buf, g_rx.expected_len); // 等下再介绍这个
            proto_reset_rx();
        }
    }
}

正常来说,你应该设计一个简单的包校验,因为我这里是纯演示,就没怎么做.

下面是处理代码

static void proto_handle_message(const ChatMsg *msg)
{
    if (msg->has_ack && msg->ack) {
        return;
    }

    proto_send_text("mcu", "ack from stm32", true, msg->seq);
}

static void proto_try_decode_frame(const uint8_t *payload, uint16_t len)
{
    ChatMsg msg = ChatMsg_init_zero;
    pb_istream_t istream = pb_istream_from_buffer(payload, len);

    if (pb_decode(&istream, ChatMsg_fields, &msg)) {
        proto_handle_message(&msg);
    }
}

另一个单片机或者PC,就同样方法处理,不过当前演示简单,建议实际工程中,要注意自己做包结构,pb只负责解释包有效数据本身.

具体Demo代码 https://github.com/nickfox-taterli/nanopb-mcu-demo

退出移动版