Sunday, August 16, 2020

Google Protocol Buffers for serial communication with a microcontroller


For a while now I was thinking about implementing a protobuf based binary protocol in the firmware of a microcontroller.

This would have the following advantages:

proto files as specification
parser generators for many languages (python, c)
versioning/backward compatibility
speed up transfer


As a proof of concept I implemented a single message that the microcontroller will send to the host computer using the following packet.
The packet starts with 5 "U" characters and ends with 5 "0xff" bytes:

0x55 0x55 0x55 0x55 0x55 <len_lsb> <len_msb> <payload_bytes...> 0xff 0xff 0xff 0xff 0xff


This preamble looks good in the logic analyzer and I think I can eventually use it for baudrate estimation.


A 16 bit packet length is included and the payload data is encoded using a google protocol buffer file:
https://github.com/plops/cl-cpp-generator2/blob/master/example/29_stm32nucleo/source/simple.proto


syntax = "proto2";
import "nanopb.proto";
message SimpleMessage {
required uint32 id = 1;
required uint32 timestamp = 2;
required uint32 phase = 3;
repeated int32 int32value = 4;
required int32 sample00 = 5;
...
required int32 sample59 = 64;
};





The packet encoder on the MCU is using https://github.com/nanopb/nanopb. It generates encoders and parsers that are compatible with google protocol buffers but the code is C instead of C++ and the code size is small.


https://github.com/plops/cl-cpp-generator2/blob/master/example/29_stm32nucleo/source/boilerplate/main.c



The packet is generated around line 524 (search for "SimpleMessage").


Initially I couldn't figure out how to fill a variable length array. That is why have samle00..sample59 variables. Later I learned how to store an array in int32value line 531, using callback the callback encode_int32.


The python code
https://github.com/plops/cl-cpp-generator2/blob/master/example/29_stm32nucleo/source2/run_00_uart.py

feeds all received bytes into a finite state machine that searches for the five "U" character preamble, parses the packet length and decodes the payload data with code that is autogenerated by google protobuf from simple.proto.

 

Received packets look like this after decoding:


id: 1431655765
timestamp: 4281899953 
phase: 16
int32value: 42
int32value: 43
int32value: 44
int32value: 45
int32value: 46
int32value: 47
int32value: 48
int32value: 49
int32value: 50
int32value: 51
sample00: 193
sample01: 193
...
sample52: 206
sample53: 1999
sample54: 1776
sample55: 300
sample56: 205
sample57: 196
sample58: 193
sample59: 190






The MCU code itself solves a toy problem. I generate output like this:
4095 0 0 4095 0 0 0 4095 0 0 0 ...
on the DAC and read it back with the ADC. Each sample of the ADC is integrated for 2.5cycles of the 80MHz system clock


I shift the ADC trigger relative to the DAC output clock with a PWM timer.


My goal was to investigate the influence of the DAC output buffer.



With buffer the ADC samples do not change much when the ADC acquires the data with a delay after the DAC has settled:

 
 
 
Without DAC buffer the ADC signal gets lower the longer the duration between DAC output instance and ADC acquisition trigger.