Skip to content

type: readme status: current summary: Orientation to the embedded tree: boards, proto-c, bases, tags and loaders, and how CMake ties firmware targets to them.


Embedded Source Layout

The embedded tree is organized around five major sections:

  • bases: firmware images for programmer/base boards.
  • boards: generated ChibiOS board descriptions for physical hardware.
  • proto-c: nanopb C bindings and default configuration data for tag protocols.
  • tags: firmware images that run on the tags themselves.
  • loaders: SRAM-resident external-flash loaders for reading a tag's external flash over SWD without touching its internal flash.

At a high level, boards describes hardware pins and signals, proto-c describes the compact protocol structures used by tag firmware, and bases and tags consume those descriptions to build firmware images.

Directory Relationship

embedded/
       boards/      Hardware signal and pin definitions
          |       Generates board.h, board.c, and board.mk
          |
       bases/    Programmer/base-board firmware targets
          |
        tags/     Tag firmware targets
          |
     loaders/     External flash loaders (per board + flash part)
          |
     proto-c/     nanopb protocol bindings and default tag configuration
          |
          +-----> various tags/     Tag firmware communication structures

The CMake build wires these relationships together. A firmware target usually depends on one generated board target and, for tag firmware, one protocol target. For example:

add_embedded_target(BitTag bittag_proto)
add_dependencies(BitTag board-bittag-v6)

This means the BitTag firmware uses the bittag_proto nanopb target from proto-c and the board-bittag-v6 generated board files from boards.

boards

boards contains the physical board descriptions used by both tag and base firmware. The current preferred workflow is generated from a ChibiOS processor template plus a small project JSON file:

embedded/boards/<board>/
  CMakeLists.txt
  cfg/board-customizations.json

The board CMakeLists.txt calls generate_configured_board_files() from embedded/boards/CMakeLists.txt:

generate_configured_board_files(board-bitprestag
  PROCESSOR stm32l4xx
  BOARD_TYPE BitPresTagv1
  CUSTOMIZATIONS cfg/board-customizations.json)

The arguments define the board contract:

  • PROCESSOR: ChibiOS processor-template family. For example, stm32l4xx uses the ChibiOS stm32l4board.xml template and the matching STM32L4 FreeMarker board templates.
  • BOARD_TYPE: output board directory name in the CMake build tree and the name firmware makefiles include as $(BOARDDIR)/<BOARD_TYPE>/board.mk. The JSON board_id should normally match this value because ChibiOS' stock board.mk.ftl emits BOARDSRC and BOARDINC from board_id.
  • CUSTOMIZATIONS: the board-specific JSON file relative to the board source directory.

The generated-board build has two steps:

  1. embedded/boards/tools/generate_board_chcfg.py starts from the ChibiOS processor XML template and applies board-customizations.json to board metadata, clocks, optional XML elements, and GPIO pin attributes.
  2. CMake runs fmpp with the generated board.chcfg and ChibiOS' processor-specific board.c.ftl, board.h.ftl, and board.mk.ftl templates.

The outputs are board.h, board.c, board.mk and board_standby.h. For a board used by a distributed tag they are committed in embedded/boards/<board>/generated/ and regenerated only when their recorded inputs change. Either way, firmware reads them through BOARDDIR from the build tree: a board that does not commit its files generates them there, and a committed board's files are copied there:

<build>/embedded/boards/<BOARD_TYPE>/board.h
<build>/embedded/boards/<BOARD_TYPE>/board.c
<build>/embedded/boards/<BOARD_TYPE>/board.mk
<build>/embedded/boards/<BOARD_TYPE>/board_standby.h

See Tag Firmware Build Reproducibility for which boards commit their files and how staleness is detected.

board.h defines the board-level LINE_xxx names consumed by tag and base code. Keep physical signal names in the board customizations whenever possible; avoid creating tag-level aliases in custom.h just to compensate for a board name mismatch. For example, BitPresTag uses the board-generated LINE_LPS_CS and LINE_WKUP1 names directly in its family code.

board.c contains the low-level board initialization code, and board.mk is included by firmware project.mk files:

include $(BOARDDIR)/BitPresTagv1/board.mk

The firmware CMake target also declares an explicit dependency on the generated board target:

add_embedded_target(BitPresTag bitprestag_proto)
add_dependencies(BitPresTag board-bitprestag)

This dependency ensures the board files exist before the ChibiOS make-based firmware build starts. common/make.mk receives BOARDDIR from CMake and uses the board make fragment selected by project.mk.

Some older board directories still use the static-board helper generate_board_files(). Those boards keep cfg/board.chcfg, cfg/board.fmpp, and local FreeMarker templates in the source tree:

generate_board_files(board-older-board)

That path still renders board.h, board.c, and board.mk into the build tree, but new boards should prefer generate_configured_board_files() so the repository stores only project-specific customizations while ChibiOS owns the processor XML and template defaults.

Board directories are shared resources. They do not normally define a complete application on their own; instead, a tag or base firmware target declares which board target it depends on.

proto-c

proto-c builds the nanopb C view of the shared protocol definitions. It uses the top-level .proto files from proto, combines shared nanopb options with tag-specific overrides, generates .pb.c and .pb.h files, and converts tag-specific default configuration JSON into C data. For the protocol variants of distributed tags the generated files are committed in <variant>-proto-c/generated/; the others are generated in the build tree.

Each protocol subdirectory provides:

  • default-config.json: the default protobuf configuration message for that tag family, written as JSON.
  • tag.override.options: nanopb overrides for tag.proto.
  • tagdata.override.options: nanopb overrides for tagdata.proto.
  • CMakeLists.txt: a call to add_nanopb_target(<name>).

The resulting protocol target is an interface library such as bittag_proto or prestag_proto. Tag firmware links to one of these targets so it can encode and decode compact messages for host communication and stored tag configuration.

default-config.json is intentionally tag-specific. The shared protobuf configuration message can describe features used by many tag families, but a particular firmware image should only initialize the portions that apply to that tag. embedded/proto-c/config-gen.py converts this JSON file into default_config.c, which is compiled into the firmware through the generated protocol target. It runs in a virtual environment the build creates from embedded/proto-c/requirements.txt (decision 0013).

Nanopb options also have two layers:

File Purpose
proto-c/default-options/tag.options Shared defaults for tag.proto
proto-c/default-options/tagdata.options Shared defaults for tagdata.proto
proto-c/<tag>-proto-c/tag.override.options Tag-specific overrides for tag.proto
proto-c/<tag>-proto-c/tagdata.override.options Tag-specific overrides for tagdata.proto

At build time, CombineFiles.cmake combines the shared default and the tag-specific override into the conventional nanopb file names in the build tree, <build>/embedded/proto-c/<tag>-proto-c/tag.options and tagdata.options, which nanopb_generator reads. This lets most protobuf fields share one memory/layout policy while still allowing a tag family to tighten array sizes, string sizes, field allocation, or other nanopb details where the default would be too broad.

To add a protocol target:

  1. Create embedded/proto-c/<tag>-proto-c/CMakeLists.txt calling add_nanopb_target(<tag>).
  2. Add tag.override.options, tagdata.override.options, and default-config.json.
  3. Add the subdirectory from embedded/proto-c/CMakeLists.txt.
  4. Reference the generated interface target as <tag>_proto from the firmware target.

If the tag will be distributed, create generated/ in the new directory and regenerate; see Promoting a tag to distributed.

tags

tags contains the firmware applications that run on tag hardware. A tag target combines:

  • tag-specific application code, usually under inc, src, and cfg;
  • shared tag support code from tags/common;
  • one board target from boards;
  • one nanopb protocol target from proto-c;
  • ChibiOS sources from the repository ChibiOS submodule.

The target's CMakeLists.txt declares both sides of that relationship. A typical tag uses:

add_embedded_target(PresTag prestag_proto DISTRIBUTE)
add_dependencies(PresTag board-prestag)

DISTRIBUTE marks a tag that ships: it installs its firmware, is built by distributed_firmware, and is held to the reproducibility checks. Prototypes omit it.

Generated build products are placed in the CMake build tree, not in the source tree. The important final artifacts are the .elf, .hex, .bin, .dmp, and .list files under the tag target's build directory. When installed, those same artifacts are copied to share/UltralightTags/firmware/<target>/.

Firmware targets also get STM32CubeProgrammer helper targets such as BitTag-download and BitTag-erase when STM32_Programmer_CLI is available. These targets select an ST-LINK probe before invoking CubeProgrammer. By default, CMake sets STM32_PROGRAMMER_PROBE=pid:0483:3748, which prefers the tag programmer base. If that PID is not present, the selector falls back to the available ST-LINK probes, using the only attached probe automatically or prompting when multiple probes are attached. Override this at configure time or in the build environment when you need a specific probe such as an STLINK-V3PWR (0483:3757):

cmake -S . -B build -DSTM32_PROGRAMMER_PROBE=serial:<stlink-serial>
cmake -S . -B build -DSTM32_PROGRAMMER_PROBE=index:<cube-index>
cmake -S . -B build -DSTM32_PROGRAMMER_PROBE=pid:0483:3757
cmake -S . -B build -DSTM32_PROGRAMMER_PROBE=prompt

The selector uses USB VID:PID discovery on Linux, macOS, and Windows when available, then passes sn=<serial> to CubeProgrammer. index:<n> is still available for setups where the OS does not expose a usable serial number.

bases

bases contains firmware applications for base or programmer boards. These targets use the same board-generation mechanism as tags, but they generally do not use a nanopb protocol target because they are not tag data-log firmware.

A base target typically looks like:

add_embedded_target(bittag-base-v7 0)
add_dependencies(bittag-base-v7 board-bittag-base-v7)

The 0 protocol argument means the firmware target does not consume generated nanopb sources.

Adding or Updating Firmware

When adding a new embedded target, decide which layer is changing:

  • Add or update a boards entry when the physical pinout, signal names, or ChibiOS board configuration changes. For new boards, add a cfg/board-customizations.json file and call generate_configured_board_files() from the board CMakeLists.txt.
  • Add or update a proto-c entry when the tag family needs a new nanopb option set or default configuration.
  • Add or update a tags entry when creating tag firmware for a tag family or sensor combination.
  • Add or update a bases entry when creating firmware for a programmer or base board.

Most bugs should be fixed at the lowest layer that owns the behavior. Pin and signal mistakes belong in boards; protocol encoding and default configuration belong in proto-c; sensor behavior and runtime state belong in tags; base board USB/SWD/programmer behavior belongs in bases.

loaders

loaders contains STM32CubeProgrammer-compatible external loaders (.stldr), one per board and flash part, and RV3028 RTC register probes (RV3028_*), which use the same framework but are not flash loaders. A loader is laid out like a tag target -- a Makefile including ../common/make.mk (../common/make-u375.mk for STM32U375 targets), a project.mk, and local overrides of ../common -- but it has no startup code, no RTOS kernel and no protocol target: it uses the committed board.h, ChibiOS PAL and the os-less OSAL, and shares the flash part's command set with the firmware driver through a dependency-free <part>_commands.h. Each AT25XE source directory builds two images, read-only and read-write; GD5F2GM7RE_IMUTagNandv2 and the probes build one:

add_embedded_loader(AT25XE_PresTagv3 DISTRIBUTE)
add_embedded_loader(AT25XE_PresTagv3-RW
  DIRECTORY AT25XE_PresTagv3 ALLOW_WRITE DISTRIBUTE)

Distributed loaders are built by distributed_firmware and installed to share/<package>/loaders/<image>/. See External Flash Loaders.