type: readme status: current summary: The JSON-customized board generation tools: input format, LSE bypass, standby masks, standalone fmpp workflow and CMake integration.
Board configuration tools
These tools support a generated-board workflow for ChibiOS boards. Instead of
copying board.chcfg, board.c.ftl, board.h.ftl, board.mk.ftl, and
board.fmpp into every board directory, a board can provide:
- one JSON file containing board-specific XML and pin customizations;
- the ChibiOS processor template family, such as
stm32l4xx; - the generated board directory name used by firmware makefiles.
generate_board_chcfg.py patches a ChibiOS board XML template with the JSON
customizations and writes board.chcfg. It can also emit board_standby.h,
which contains project-level STM32 standby pull masks for firmware that wants
them. The script imports only the Python standard library and does not depend
on CMake or on a particular source tree layout.
The Python script does not produce board.c, board.h, or board.mk by
itself. Run fmpp afterwards against ChibiOS' matching board templates to
generate the complete board directory.
Input Files
The standalone workflow has one project-owned input and two ChibiOS inputs.
Customization JSON
The project-owned input is the JSON customization file passed with
--customizations. It describes the board identity, ChibiOS XML edits, and pin
attribute changes.
When generating from a stock ChibiOS processor template, the JSON should normally set:
board_idto the generated board directory name used by firmware makefiles;elements.configuration_settings/board_files_pathto$(BOARDDIR), so the generatedboard.mkstays relocatable;- processor-specific metadata required by the ChibiOS XML template, such as
subtypeand clock attributes; pins, either as a compact location map, grouped-by-port map, or list.
board_name is human-readable metadata. board_id is also used by ChibiOS'
stock board.mk.ftl when emitting BOARDSRC and BOARDINC, so it should
match the output BOARD_TYPE directory unless the consuming firmware makefiles
intentionally use a different layout.
A compact JSON file can name pins by location:
{
"board_name": "My Board",
"board_id": "MyBoard",
"pins": {
"PA5": {
"ID": "ACCEL_SCK",
"Mode": "Alternate",
"Alternate": "5",
"Standby": "PULLDOWN"
},
"GPIOB.pin6": {
"ID": "RTC_SDA",
"Type": "OpenDrain",
"Resistor": "PullUp",
"Mode": "Alternate",
"Alternate": "4"
}
}
}
When starting from a stock ChibiOS processor XML file, the same JSON can also customize non-pin XML:
{
"board_name": "My Board",
"board_id": "MyBoard",
"remove_elements": ["ethernet_phy"],
"elements": {
"subtype": "STM32L432xx",
"configuration_settings/board_files_path": "$(BOARDDIR)"
},
"attributes": {
"clocks": {
"HSEFrequency": "0",
"LSEFrequency": "32768"
}
},
"pins": {
"PA5": {
"ID": "ACCEL_SCK",
"Mode": "Alternate",
"Alternate": "5"
}
}
}
LSEBypass is the board's answer to "crystal or driven clock?"
Set "LSEBypass": "true" whenever the 32 kHz reference is driven into
OSC32_IN by another part rather than generated by a crystal across
OSC32_IN/OSC32_OUT. On the tags this means any board that clocks LSE from an
external RTC's CLKOUT pin.
Getting it wrong does not produce a diagnostic. With bypass off, ChibiOS
enables the crystal oscillator amplifier and then busy-waits on LSERDY with
no timeout, inside __early_init() — before chSysInit(), while crt0's
interrupt mask is still in force. A board that fails to latch LSERDY there
hangs forever, the DebugMonitor handler never comes up, and the only visible
symptom is that the monitor times out attaching. Flashing itself looks fine.
Only a fresh tag is exposed. bd_reset() resets the backup domain just when
RTCSEL does not already select LSE, so after one successful boot LSE keeps
running and the wait is a no-op forever after. That makes the failure look
intermittent and unit-specific when it is neither.
The template emits a bare #define STM32_LSE_BYPASS when this is "true" and
nothing at all when it is "false", which matters because stm32_lse.inc
tests #if defined(STM32_LSE_BYPASS) — a #define STM32_LSE_BYPASS FALSE
written by hand elsewhere still selects bypass.
Pins can also be grouped by port:
{
"pins": {
"GPIOA": {
"pin5": {
"ID": "ACCEL_SCK",
"Mode": "Alternate",
"Alternate": "5"
}
}
}
}
A list form is accepted for generated data:
{
"pins": [
{
"port": "GPIOA",
"pin": 5,
"ID": "ACCEL_SCK",
"Mode": "Alternate",
"Alternate": "5"
}
]
}
Pin attributes are matched case-insensitively against the attributes already
present on the template pin, so misspelled fields fail instead of silently
creating invalid configuration. Use --allow-new-attributes only when a new
ChibiOS board schema attribute is intentionally being introduced.
Standby is project metadata, not a ChibiOS XML attribute. It may be placed in
a pin definition after PinLock and accepts FLOAT, PULLUP, or PULLDOWN;
omitting it is the same as FLOAT. The generator strips Standby from
board.chcfg and uses it only for board_standby.h. The generated pull masks
use numeric pin positions, so firmware can include board_standby.h from
mcuconf.h and use the masks as ChibiOS PWR startup defaults.
For example, a board with PA4/PA9 pull-ups and PB3 pull-down generates:
#define BOARD_STANDBY_HAS_CONFIG 1
#define HAS_PULLUPA 1
#define HAS_PULLDWNA 0
#define PULLUPA ((1U << 4U) | \
(1U << 9U))
#define PULLDWNA 0U
#define HAS_PULLUPB 0
#define HAS_PULLDWNB 1
#define PULLUPB 0U
#define PULLDWNB ((1U << 3U))
Firmware can assign these generated PULLUPx and PULLDWNx macros to the
matching STM32_PWR_PUCRx and STM32_PWR_PDCRx definitions in mcuconf.h;
ChibiOS then applies the standby pull register values during HAL startup.
The script validates requested XML paths and pin names with ElementTree, then
patches the original XML text. This keeps generated board.chcfg files close
to the source template formatting, which makes generated-output diffs easier to
review.
ChibiOS Templates
The first ChibiOS input is the processor-family XML template passed to
generate_board_chcfg.py --template. The second is the matching board
FreeMarker template directory passed to fmpp as sourceRoot. Their names are
related but not identical:
| Processor family | XML template | FTL template directory |
|---|---|---|
stm32l4xx |
tools/ftl/xml/stm32l4board.xml |
tools/ftl/processors/boards/stm32l4xx/templates |
stm32u3xx |
tools/ftl/xml/stm32u3board.xml |
tools/ftl/processors/boards/stm32u3xx/templates |
stm32c0xx |
tools/ftl/xml/stm32c0board.xml |
tools/ftl/processors/boards/stm32c0xx/templates |
stm32f0xx |
tools/ftl/xml/stm32f0board.xml |
tools/ftl/processors/boards/stm32f0xx/templates |
Standalone Workflow
Copy generate_board_chcfg.py into any project that can provide the inputs
above. board.fmpp.in is optional, but it shows the minimal fmpp config
shape needed to turn the generated board.chcfg into board.c, board.h, and
board.mk.
The example/ directory is a complete standalone CMake project for a minimal
STM32L432 board. It demonstrates the JSON customization shape, the generated
standby pull masks, and configure-time diagnostics for missing CHIBIOS_DIR
and FMPP_EXECUTABLE paths.
The workflow has two phases:
- generate the
fmppinputs:board.chcfg, optionalboard_standby.h, and a smallboard.fmppconfig; - run
fmppto generate the board directory consumed by firmware makefiles.
Generate fmpp Inputs
Example using generic paths:
CHIBIOS_DIR=/path/to/ChibiOS
BOARD_JSON=/path/to/MyBoard/board-customizations.json
OUT_DIR=/path/to/generated-boards
BOARD_TYPE=MyBoard
PROCESSOR=stm32l4xx
XML_TEMPLATE=$CHIBIOS_DIR/tools/ftl/xml/stm32l4board.xml
FTL_TEMPLATE_DIR=$CHIBIOS_DIR/tools/ftl/processors/boards/$PROCESSOR/templates
mkdir -p "$OUT_DIR/cfg" "$OUT_DIR/$BOARD_TYPE"
python3 /path/to/generate_board_chcfg.py \
--template "$XML_TEMPLATE" \
--customizations "$BOARD_JSON" \
--output "$OUT_DIR/cfg/board.chcfg" \
--standby-header "$OUT_DIR/$BOARD_TYPE/board_standby.h"
Then create an fmpp configuration that points at the ChibiOS board templates,
the generated board.chcfg, and the output board directory:
cat > "$OUT_DIR/cfg/board.fmpp" <<EOF
sourceRoot: $FTL_TEMPLATE_DIR
outputRoot: $OUT_DIR/$BOARD_TYPE
dataRoot: $OUT_DIR/cfg
data : {
doc1:xml (
board.chcfg
{
}
)
}
EOF
Run fmpp
Run fmpp with ChibiOS' shared FreeMarker library path:
fmpp --freemarker-links "{lib : $CHIBIOS_DIR/tools/ftl/libs}" \
-C "$OUT_DIR/cfg/board.fmpp" \
-v \
-O "$OUT_DIR/$BOARD_TYPE"
The result is a complete generated board directory:
/path/to/generated-boards/MyBoard/board.c
/path/to/generated-boards/MyBoard/board.h
/path/to/generated-boards/MyBoard/board.mk
/path/to/generated-boards/MyBoard/board_standby.h
The generated board.mk normally expects firmware builds to set BOARDDIR to
the parent of BOARD_TYPE; for the example above that parent is
/path/to/generated-boards.
Firmware makefiles consume that directory through BOARDDIR. For example, a
firmware project.mk containing include $(BOARDDIR)/MyBoard/board.mk can be
built manually only if the surrounding firmware make scaffold receives the
variables it expects. Typical ChibiOS projects need at least BOARDDIR and
CHIBIOS; larger projects may also require build, dependency, project-name, or
generated-code paths such as BUILDDIR, DEPDIR, PROJECT, SOURCEDIR, and
protobuf or nanopb include directories.
CMake Integration Pattern
A CMake project can wrap the standalone tool with a custom target. The wrapper
needs to derive the ChibiOS XML template, configure an fmpp file, and run the
two generation commands.
The example below uses BOARD_TOOLS_DIR for the directory containing
generate_board_chcfg.py and board.fmpp.in. It assumes the helper is called
from a board source directory containing cfg/board-customizations.json.
It is deliberately self-contained, and deliberately simpler than what this repository does: it derives the template paths inline and has no notion of committing generated files, recording their inputs, or checking them for staleness. Copy it as a starting point, not as a description of the local integration -- This Repository covers that.
set(BOARD_TOOLS_DIR
""
CACHE PATH "Directory containing generate_board_chcfg.py and board.fmpp.in")
set(CHIBIOS_DIR "" CACHE PATH "ChibiOS source directory")
find_program(PYTHON_EXECUTABLE NAMES python3 python REQUIRED)
find_program(FMPP_EXECUTABLE NAMES fmpp fmpp.bat REQUIRED)
if(NOT EXISTS "${CHIBIOS_DIR}/os")
message(FATAL_ERROR "Set CHIBIOS_DIR to a ChibiOS source tree")
endif()
if(NOT EXISTS "${BOARD_TOOLS_DIR}/generate_board_chcfg.py")
message(FATAL_ERROR "Set BOARD_TOOLS_DIR to the board configuration tools directory")
endif()
if(NOT EXISTS "${BOARD_TOOLS_DIR}/board.fmpp.in")
message(FATAL_ERROR "BOARD_TOOLS_DIR must contain board.fmpp.in")
endif()
function(generate_configured_board_files target_name)
set(one_value_args PROCESSOR BOARD_TYPE CUSTOMIZATIONS OUTPUT_PARENT)
cmake_parse_arguments(GCB "" "${one_value_args}" "" ${ARGN})
if(NOT GCB_PROCESSOR)
message(FATAL_ERROR "generate_configured_board_files requires PROCESSOR")
endif()
if(NOT GCB_BOARD_TYPE)
message(FATAL_ERROR "generate_configured_board_files requires BOARD_TYPE")
endif()
if(NOT GCB_CUSTOMIZATIONS)
message(FATAL_ERROR "generate_configured_board_files requires CUSTOMIZATIONS")
endif()
if(NOT GCB_OUTPUT_PARENT)
set(GCB_OUTPUT_PARENT "${CMAKE_CURRENT_BINARY_DIR}/boards")
endif()
string(TOLOWER "${GCB_PROCESSOR}" processor)
string(REGEX REPLACE "xx$" "" processor_xml "${processor}")
set(template_xml
"${CHIBIOS_DIR}/tools/ftl/xml/${processor_xml}board.xml")
set(template_dir
"${CHIBIOS_DIR}/tools/ftl/processors/boards/${processor}/templates")
set(customizations
"${CMAKE_CURRENT_SOURCE_DIR}/${GCB_CUSTOMIZATIONS}")
set(generated_cfg_dir "${CMAKE_CURRENT_BINARY_DIR}/${GCB_BOARD_TYPE}-cfg")
set(generated_chcfg "${generated_cfg_dir}/board.chcfg")
set(generated_fmpp "${generated_cfg_dir}/board.fmpp")
set(board_output_dir "${GCB_OUTPUT_PARENT}/${GCB_BOARD_TYPE}")
set(standby_header "${board_output_dir}/board_standby.h")
if(NOT EXISTS "${template_xml}")
message(FATAL_ERROR "Missing ChibiOS board XML template: ${template_xml}")
endif()
if(NOT EXISTS "${template_dir}/board.c.ftl")
message(FATAL_ERROR "Missing ChibiOS board template directory: ${template_dir}")
endif()
file(MAKE_DIRECTORY "${generated_cfg_dir}")
set(BOARD_FMPP_SOURCE_ROOT "${template_dir}")
set(BOARD_FMPP_OUTPUT_ROOT "${board_output_dir}")
set(BOARD_FMPP_DATA_ROOT "${generated_cfg_dir}")
configure_file(
"${BOARD_TOOLS_DIR}/board.fmpp.in"
"${generated_fmpp}"
@ONLY)
add_custom_command(
OUTPUT "${generated_chcfg}" "${standby_header}"
COMMAND "${PYTHON_EXECUTABLE}"
"${BOARD_TOOLS_DIR}/generate_board_chcfg.py"
--template "${template_xml}"
--customizations "${customizations}"
--output "${generated_chcfg}"
--standby-header "${standby_header}"
DEPENDS
"${BOARD_TOOLS_DIR}/generate_board_chcfg.py"
"${customizations}"
"${template_xml}"
VERBATIM)
set(board_files
"${board_output_dir}/board.c"
"${board_output_dir}/board.h"
"${board_output_dir}/board.mk")
add_custom_command(
OUTPUT ${board_files}
COMMAND "${CMAKE_COMMAND}" -E make_directory "${board_output_dir}"
COMMAND "${FMPP_EXECUTABLE}"
--freemarker-links "{lib : ${CHIBIOS_DIR}/tools/ftl/libs}"
-C "${generated_fmpp}"
-v
-O "${board_output_dir}"
DEPENDS
"${generated_chcfg}"
"${standby_header}"
"${generated_fmpp}"
"${template_dir}/board.c.ftl"
"${template_dir}/board.h.ftl"
"${template_dir}/board.mk.ftl"
VERBATIM)
add_custom_target("${target_name}"
DEPENDS ${board_files} "${standby_header}")
endfunction()
Use the helper from a board directory like this:
generate_configured_board_files(board-myboard
PROCESSOR stm32l4xx
BOARD_TYPE MyBoard
CUSTOMIZATIONS cfg/board-customizations.json)
Configure and build it with the paths for your checkout:
cmake -S . -B build \
-DCHIBIOS_DIR=/path/to/ChibiOS \
-DBOARD_TOOLS_DIR=/path/to/board-tools
cmake --build build --target board-myboard
Arguments:
PROCESSOR: ChibiOS processor template family. For example,stm32l4xxuses<ChibiOS>/tools/ftl/xml/stm32l4board.xmland<ChibiOS>/tools/ftl/processors/boards/stm32l4xx/templates. A wrapper can derive the XML name by lowercasingPROCESSORand dropping a trailingxx; for examplestm32u3xxbecomesstm32u3board.xml.BOARD_TYPE: build-tree board directory and firmware include name. Firmware should include$(BOARDDIR)/<BOARD_TYPE>/board.mk. This should align with the customization JSON'sboard_id, as described in Input Files.CUSTOMIZATIONS: JSON file relative to the board source directory.OUTPUT_PARENT: optional parent directory for generated board directories. Firmware makefiles should receive this value asBOARDDIR.
A generated board target should produce:
<build>/boards/<BOARD_TYPE>/board.c
<build>/boards/<BOARD_TYPE>/board.h
<build>/boards/<BOARD_TYPE>/board.mk
<build>/boards/<BOARD_TYPE>/board_standby.h
Firmware targets should depend on the board target, set BOARDDIR to the
parent output directory, and include $(BOARDDIR)/<BOARD_TYPE>/board.mk.
For example:
add_dependencies(MyFirmware board-myboard)
The firmware build command or wrapper would then pass something equivalent to:
BOARDDIR=<build>/boards
This Repository
This final section documents the local integration in this source tree. It can be deleted or replaced when copying the tools into another project.
In this repository, CMake supplies the script's --template path from
CHIBIOS_DIR inside generate_configured_board_files() in
embedded/boards/CMakeLists.txt.
Two things the example above does inline are shared here instead, because the
freshness check needs them as well and the two disagreeing is a real failure
mode rather than a hypothetical one. cmake/GeneratedSourceInputs.cmake
supplies board_template_paths(), which turns a PROCESSOR into the template
directory and XML path, and board_manifest_inputs(), which lists every input
a board's inputs.sha256 records. generate_configured_board_files() and
cmake/CheckGeneratedSourcesFresh.cmake both call them, so a board cannot be
rendered from one template directory and checked against another.
The fmpp environment is pinned rather than inherited: board.fmpp.in sets
the locale, number format and encodings, because FreeMarker otherwise takes
them from the machine and the templates interpolate numbers in many places.
FMPP_VERSION pins the renderer itself and configure checks it. board.fmpp.in
is one of the hashed inputs, so changing those settings correctly makes every
board stale.
embedded/CMakeLists.txt resolves CHIBIOS_DIR in this order:
- the repository
ChibiOS/submodule, whenChibiOS/osexists; - the
CHIBIOS_DIRenvironment variable, when it points at a ChibiOS source tree; - the CMake cache value passed with
-DCHIBIOS_DIR=/path/to/ChibiOS.
The usual reproducible setup is:
git submodule update --init --recursive ChibiOS
cmake -S . -B build-embedded \
-DBUILD_HOST=OFF \
-DBUILD_QT_APPS=OFF \
-DBUILD_EMBEDDED=ON
For an out-of-tree ChibiOS checkout, configure with:
cmake -S . -B build-embedded \
-DBUILD_HOST=OFF \
-DBUILD_QT_APPS=OFF \
-DBUILD_EMBEDDED=ON \
-DCHIBIOS_DIR=/path/to/ChibiOS
CMake prints the resolved path as CHIBIOS_DIR is ... during configuration.
The embedded configure step also requires arm-none-eabi-gcc, make, Python,
and fmpp on PATH. Because the top-level embedded configuration also adds embedded/proto-c,
configure can require more than a board-generation target needs. It no longer
requires the C++ Protobuf library -- config-gen is a Python script now, so
that is looked for only when BUILD_HOST is on -- but unless
REGENERATE_SOURCES=OFF, configure will build a virtual environment from
embedded/proto-c/requirements.txt and look for a protoc, which normally
comes from the nanopb distribution.
The configure command above assumes only CMake cache variables. It does not
require users to predefine Make variables such as CHIBIOS, BOARDDIR,
BUILDDIR, or PROJECT; add_embedded_target() supplies those later when a
tag or base firmware target invokes make.
Build a generated board directly when checking the board files only:
cmake --build build-embedded --target board-bitprestag
That produces:
build-embedded/embedded/boards/BitPresTagv1/board.c
build-embedded/embedded/boards/BitPresTagv1/board.h
build-embedded/embedded/boards/BitPresTagv1/board.mk
build-embedded/embedded/boards/BitPresTagv1/board_standby.h
To build firmware that consumes the generated board, build the tag or base target instead:
cmake --build build-embedded --target BitPresTag
The firmware target's CMakeLists.txt should depend on the board target, for
example add_dependencies(BitPresTag board-bitprestag), and its project.mk
should include the generated fragment:
include $(BOARDDIR)/BitPresTagv1/board.mk