What Is Nanopb? Protocol Buffers for Embedded Systems Explained

What Is Nanopb? Protocol Buffers for Embedded Systems Explained

If you have ever tried to squeeze structured data out of a tiny microcontroller, you already know the pain. JSON is readable but bulky, hand rolled byte packing is compact but brittle, and a full protobuf library expects memory your chip simply does not have. That gap is exactly where nanopb lives. Nanopb is a small C library that brings Google’s Protocol Buffers to embedded systems, with a runtime measured in kilobytes and no dynamic memory allocation unless you ask for it. In this guide you will learn what nanopb is, how it works under the hood, and why so many firmware projects rely on it to move data between a chip and the outside world.

First, a quick word on Protocol Buffers

Protocol Buffers, usually shortened to protobuf, is a serialization format from Google. The idea is simple. You describe your data once, in a plain text file with a .proto extension, and a code generator turns that description into code that packs your data into a compact binary form and unpacks it again on the other side. Instead of sending field names and punctuation over the wire like JSON does, protobuf sends small field numbers and tightly encoded values. A temperature reading that takes thirty bytes as text can shrink to a handful of bytes in protobuf form.

The .proto file becomes the single source of truth for both ends of the connection. Your firmware, your phone app, and your cloud server all generate code from the same file, so they can never quietly drift apart on field names or types. You also get a wire format that tolerates change well: add a new field with a new number and old receivers simply ignore it, which matters a lot when devices stay in the field for years.

So what is nanopb, exactly?

Nanopb is an implementation of Protocol Buffers written in plain C and aimed squarely at embedded systems. It was created by Petteri Aimonen, who goes by jpa online, and it is open source under the zlib license. Development happens in the open on GitHub at github.com/nanopb/nanopb, and the current release line is the 0.4 series.

The full protobuf implementations from Google are excellent tools, but they were designed for PCs and servers. They lean on dynamic memory allocation, generous buffers, and runtime libraries that assume megabytes of RAM are free for the taking. A microcontroller might give you 32 kilobytes of RAM total, shared with your application, your radio stack, and everything else. Nanopb starts from the opposite assumption: memory is scarce, allocation is dangerous, and every byte of flash you spend on a serialization library is a byte you cannot spend on features.

How nanopb works, step by step

The workflow has three parts, and once you see them the whole system feels predictable.

  • You write a .proto file that describes your messages, for example a SensorMessage with a device id, a temperature, and a label.
  • The nanopb generator, a protoc plugin written in Python, reads that file and produces a matching pair of C files, a .pb.c and a .pb.h, with ready made structs and field descriptors.
  • You add those generated files plus the small nanopb runtime to your firmware project, then call pb_encode() to turn a struct into bytes and pb_decode() to turn bytes back into a struct.

The runtime itself is tiny: pb_encode.c, pb_decode.c, and pb_common.c, plus their headers. There are no external dependencies to drag in. The wire format nanopb produces is one hundred percent standard protobuf, so the other end of your cable or radio link can be ordinary protobuf code in Python, Go, C++, or anything else that speaks the format.

Why nanopb stays so small

Most of nanopb’s discipline comes from one decision: by default, nothing is allocated at runtime. Strings, byte arrays, and repeated fields get fixed sizes that you declare ahead of time in a small sidecar file called a .options file. A string field gets a max_size, a repeated field gets a max_count, and the generator then builds plain C structs with fixed arrays inside them. Encoding walks the struct, decoding fills it, and the memory footprint of a message is something you can read straight off the generated header.

That predictability is gold on a microcontroller. There is no heap to fragment, no allocation that can fail at 3 a.m. in a device bolted to a warehouse wall, and no surprise garbage collection pause because C does not have one. If you truly need dynamic fields, nanopb can do that too with pointer fields, but you opt into it deliberately rather than getting it by accident.

What nanopb looks like in real code

Here is the flavor of it. Your .proto file might define a small telemetry message:

message SensorMessage { uint32 device_id = 1; float temperature = 2; string label = 3; }

After generation, your firmware fills in a struct, wraps a byte buffer in an output stream, and encodes:

SensorMessage msg = SensorMessage_init_zero; msg.device_id = 7; msg.temperature = 21.5f; pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); pb_encode(&stream, SensorMessage_fields, &msg);

That is most of the story. The bytes in the buffer are standard protobuf, ready to push out over a UART, a BLE characteristic, or a TCP socket. On the receiving end, pb_decode() with the same SensorMessage_fields descriptor reverses the process.

Where people actually use nanopb

Nanopb shows up anywhere a small device needs to talk in a structured way. IoT sensor nodes use it to send compact telemetry readings without burning radio time and battery on verbose text formats. BLE devices use it because every byte in a payload costs power and airtime. Firmware teams store configuration blobs in nanopb format so settings can be versioned and extended safely. It is also a natural fit for RPC style communication between a microcontroller and a host program, where both sides share the same .proto contract.

Because the ecosystem matters to embedded developers, nanopb is packaged for the places they already work: there is an Arduino library in the Arduino Library Manager, a PlatformIO library, a Zephyr module, and an ESP-IDF component. Getting started rarely means fighting a build system.

How nanopb stacks up against hand rolled formats

Plenty of firmware teams start with hand rolled formats: a struct cast onto a byte buffer, or a sprintf into a text line. Those approaches work right up until the protocol changes, a second platform joins, or someone discovers the struct packing differs between compilers. Nanopb replaces that fragility with a declared contract. The .proto file says what the data is, generated code on both ends enforces it, and the wire format stays stable while implementations evolve. You also get forward and backward tolerance almost for free: receivers skip field numbers they do not know, so a newer sensor can talk to an older gateway without a flag day upgrade. For a single throwaway prototype, hand packing may be quicker. For anything that ships, updates, or talks to software written by other people, the contract wins, and nanopb is how you afford that contract on a small chip.

Conclusion

Nanopb takes the best idea in Protocol Buffers, a shared .proto contract with a compact binary wire format, and rebuilds it for machines with kilobytes instead of gigabytes. You define your messages once, generate plain C structs with fixed, predictable sizes, and encode or decode with two small functions and a runtime that fits almost anywhere. If your project moves structured data to or from a microcontroller, nanopb deserves a serious look. The guides below will take you from downloading it to generating your first message.

Read next:

1 thought on “What Is Nanopb? Protocol Buffers for Embedded Systems Explained”

  1. Pingback: Nanopb vs Protobuf: Which One Should You Use?

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top