On STM32, you can use Protocol Buffers, but you almost never want Google’s full C++ runtime. The practical embedded approach is nanopb (C) or Embedded Proto (C++).
Pick an implementation
- nanopb (C): tiny footprint, designed for microcontrollers.
- Embedded Proto (C++): MCU-focused C++ codegen + STM32CubeIDE examples.
- Google protobuf-lite (C++): still very large for many MCUs (hundreds of KB even before your messages).
Below is the nanopb workflow (most common on STM32).
1) Define your .proto
Example (proto3):
syntax = "proto3";
message Telemetry {
uint32 seq = 1;
float temperature = 2;
bytes payload = 3; // be careful: variable length on MCU
}
Important for embedded (strings/bytes/repeated)
In nanopb you typically set max sizes in options so it can generate fixed-size buffers instead of callbacks. Nanopb supports .proto options like (nanopb).max_size for strings/bytes.
2) Generate C code on your PC (not on STM32)
Nanopb generates message.pb.c/.h from .proto using its generator script (which uses protoc under the hood).
Typical command:
# inside nanopb repo
python3 generator/nanopb_generator.py telemetry.proto
# -> telemetry.pb.h + telemetry.pb.c
Nanopb docs note you need a sufficiently new protoc; they mention installing grpcio-tools via pip as one way.
3) Add nanopb to your STM32 project (CubeIDE / CubeMX)
In your STM32 project, add:
- nanopb core sources: pb_encode.c, pb_decode.c, pb_common.c
- your generated: telemetry.pb.c
- include paths for nanopb headers + generated headers
Nanopb’s README explicitly calls out the “compile protos + include pb_encode/pb_decode/pb_common” steps.
4) Encode on STM32 and send
#include "pb_encode.h"
#include "telemetry.pb.h"
uint8_t txbuf[128];
Telemetry msg = Telemetry_init_zero;
msg.seq = 123;
msg.temperature = 36.5f;
pb_ostream_t s = pb_ostream_from_buffer(txbuf, sizeof(txbuf));
bool ok = pb_encode(&s, Telemetry_fields, &msg);
if (ok) {
size_t n = s.bytes_written;
// send txbuf[0..n-1] over UART/SPI/USB/etc.
}
Tip: you can compute encoded size first using nanopb’s sizing stream approach (useful when you want to allocate the exact frame length).
5) Frame messages (UART needs this!)
Protobuf itself does not define framing, so over UART/SPI streams you must add message boundaries.
Easiest: length-delimited protobuf framing
Nanopb supports PB_ENCODE_DELIMITED / PB_DECODE_DELIMITED to prefix a varint length (compatible with Google’s delimited parsing).
Encode (delimited):
pb_ostream_t s = pb_ostream_from_buffer(txbuf, sizeof(txbuf));
pb_encode_ex(&s, Telemetry_fields, &msg, PB_ENCODE_DELIMITED);
size_t n = s.bytes_written; // includes length prefix
Decode (delimited):
pb_istream_t is = pb_istream_from_buffer(rxbuf, rxlen);
Telemetry msg = Telemetry_init_zero;
pb_decode_ex(&is, Telemetry_fields, &msg, PB_DECODE_DELIMITED);
In real UART RX, you still typically:
- read enough bytes to decode the varint length,
- wait until you have length bytes of payload,
- then call pb_decode_ex.
6) Practical STM32 tips (so it doesn’t hurt)
- Prefer fixed-size fields (avoid unbounded string/bytes/repeated), or use nanopb callbacks when you truly need streaming/large data. Nanopb documents callback handling for variable-length/repeated fields.
- If you’re tight on flash, you can disable error strings with PB_NO_ERRMSG=1 (saves code space).
- For most STM32 projects, don’t use Google protobuf-lite unless you’re on a big MCU / STM32MP1-class Linux system—size can be very large.

Top comments (0)