Building - subtilitas/rcbench GitHub Wiki

Building

English · Deutsch

Three builds read one source tree: the host test suite, the panel firmware (ESP-IDF (Espressif Internet-of-Things Development Framework)) and the coprocessor firmware (pico-sdk).

The tree

rcbench/
  docs/                   wiki source, English and German
  tools/                  the checks, generators and measurements below
  test/tools/             pytest cases for the tools
  shared/                 pure C: no ESP-IDF, no pico-sdk, no FreeRTOS types
    gfx/                  rasteriser and three fonts
    touch/                coordinate and event mapping
    ui/                   theme · widgets · icons · router · screens
    settings/             typed schema and values
    logfile/              number and CSV parsing
    link/                 page protocol · CAN framing · watchdogs · diagnosis
    artwork/              the panel's cache of board drawings
    bench/                bench_state · telemetry simulator · log writer · rotary knob decode
    outputs/              channels · driver table · the ARMED state, slew and staleness
    safety/               heartbeat generator (panel) and monitor (coprocessor)
    servo/                limit and synchronisation searches · servo model
    can/                  bit timing for both controllers · MCP2515 registers · echo self-test
    esc/                  ESC programming profiles, their JSON reader and registry
    sense/                INA228 and INA3221 current monitor drivers
    fwupdate/             panel firmware image check · upload staging · installer
    webaccess/            Wi-Fi session · card listing and ZIP stream · the page · mDNS
  protocols/              one module per external interface: a pure C core, its RP2350 pin driver in rp2350/, a README.md
    dshot/                DShot frames · GCR decode · eRPM · PIO driver, plain and bidirectional
    ppm/                  PPM frame layout · PIO and DMA driver
    sbus/                 S.BUS decoder; no pin driver
    openyge/              OpenYGE framing, CRC, status and parameter cache; no pin driver
    pdmini/               PD mini driver · PIO UART
    phase_tap/            ESC tone detector · edge ring reader · PIO capture
    kst/                  programming of KST and Chaservo servos: frames, registers, limits, write plans, session · rp2350: kst_line.pio · kst_line
  firmware/
    panel/                ESP-IDF project (ESP32-S3)
    iomcu/                pico-sdk project (RP2350)
  test/host/              the host suite, one binary per module or screen
  hardware/               board design record; no board exists

Modules under shared/ contain the logic and have no hardware dependency. A module under protocols/ is one interface to external hardware, built to be copied into another pico-sdk project: its core includes C standard headers only, and its rp2350/ folder includes the core and the pico-sdk only. Everything else that touches hardware is under firmware/. tools/check_protocols.py holds each module to that (CONTRIBUTING).

Two directories, three builds

Each module under shared/ and protocols/ carries a CMakeLists.txt that registers an IDF component under ESP_PLATFORM and a plain static library otherwise:

if(ESP_PLATFORM)
    idf_component_register(SRCS ${SRCS} INCLUDE_DIRS include)
else()
    add_library(rcbench_gfx STATIC ${SRCS})
    target_include_directories(rcbench_gfx PUBLIC include)
endif()

The panel sets EXTRA_COMPONENT_DIRS to shared/ and to each module under protocols/ it names; the coprocessor and the host suite use add_subdirectory() for the modules they need. A module with an rp2350/ folder adds its pin driver as the INTERFACE library rcbench_<module>_rp2350 in a pico-sdk build only. Includes are flat: #include "gfx.h".

Module panel iomcu host
gfx · touch · ui · settings · logfile · sbus ✔ ✔
link · bench · outputs · servo · safety · can ✔ ✔ ✔
artwork · esc · fwupdate · webaccess ✔ ✔
openyge · ppm ✔ ✔
sense · pdmini · phase_tap · kst · dshot ✔ ✔ ✔

Toolchains

Version Notes
ESP-IDF v5.4 or newer the RGB (parallel red-green-blue) panel's on_frame_buf_complete event, which the framebuffer swap waits on, exists from v5.4. Neither v5.5.0 nor v5.5.1 compiles: their TWAI (two-wire automotive interface) driver sends a CAN frame with DLC (data length code) 0 as DLC 8, and the coprocessor refuses every link read sent that way. CI (continuous integration) builds v5.4 and v5.5.3
pico-sdk 2.0 or newer RP2350 support; CI builds 2.3.0
ARM GNU toolchain 14.2 any arm-none-eabi release targeting Cortex-M33

firmware/panel/sdkconfig.defaults sets octal PSRAM (pseudo-static random-access memory) at 80 MHz, the 64 KB data cache with 64-byte lines, code and constants in flash rather than PSRAM, the zero-initialised data that main/psram_bss.lf names in PSRAM (Internal RAM), the RGB LCD (liquid-crystal display) interrupt not IRAM-safe (IRAM: instruction random-access memory), so ESP-IDF masks it during a flash operation, -O2, and the console on UART0 with USB-Serial-JTAG (the ESP32-S3's built-in USB (Universal Serial Bus) serial and debug bridge) as secondary. Start from it rather than from menuconfig.

ESP-IDF reads sdkconfig.defaults only when it generates sdkconfig. A tree that has been built before already has firmware/panel/sdkconfig, and that file wins: delete it after changing the defaults, or the change has no effect on the image.

Rotary knob wiring

The rotary knob is an AS5600 magnetic angle sensor on the panel's I2C (Inter-Integrated Circuit) terminal, the 4-pin PH2.0 connector on the Waveshare ESP32-S3-Touch-LCD-7. The terminal is GPIO8 (SDA) and GPIO9 (SCL), the bus the touch controller and the CH422G expander use, through the board's level translator. Pin order from the board schematic's text, not measured: 1 I2C_VCC, 2 GND, 3 SDA, 4 SCL. Check it with a meter before connecting.

AS5600 pin Connect to
VDD3V3 and VDD5V 3.3 V from I2C_VCC, both pins tied together, which is the datasheet's 3.3 V supply. The board's solder jumper feeds I2C_VCC from 3V3 as shipped; a breakout with its own regulator takes its supply pin from I2C_VCC at 3.3 V
GND GND
SDA, SCL SDA, SCL
DIR GND or VCC, never open. GND: the angle rises when the magnet turns clockwise seen from the chip's top (datasheet; not checked here). VCC reverses it
PGO GND
OUT not connected; the panel reads the angle over I2C

The magnet is a diametrically magnetised disc centred over the chip. The AS5600 reports a field too weak or too strong in its STATUS register, and the panel treats either as no answer. The sensor reads at 0x36, which is fixed. On the Waveshare ESP32-S3 Touch LCD 7 the CH422G I/O expander answers at I2C addresses 0x20 to 0x27 and 0x30 to 0x3F, which includes 0x36, so an AS5600 cannot be read on that bus. The knob is off until SETUP, APPLICATION, Rotary knob is switched on. Screens describes what it does.

Commands

# host suite
cmake -S test/host -B test/host/build -DCMAKE_BUILD_TYPE=Debug
cmake --build test/host/build
ctest --test-dir test/host/build --output-on-failure

# panel
. $IDF_PATH/export.sh
idf.py -C firmware/panel set-target esp32s3
idf.py -C firmware/panel build
idf.py -C firmware/panel -p /dev/ttyACM0 flash monitor    # COMx on Windows

# panel carrying the coprocessor image it offers over CAN; build the
# coprocessor first
idf.py -C firmware/panel -D RCBENCH_IOMCU_BIN=$PWD/firmware/iomcu/build/rcbench-iomcu.bin build

# panel for the measurement bench: also reads a touch injector at 0x14
# (testbench/touch-injector); never a release image
idf.py -C firmware/panel -B firmware/panel/build-inject -D RCBENCH_TOUCH_INJECTOR=ON build

# panel, as one image at offset 0; it erases the settings in nvs
idf.py -C firmware/panel merge-bin -o rcbench-panel-merged.bin
esptool.py -p /dev/ttyACM0 write_flash 0x0 \
    firmware/panel/build/rcbench-panel-merged.bin

# coprocessor
export PICO_SDK_PATH=/path/to/pico-sdk
cmake -S firmware/iomcu -B firmware/iomcu/build
cmake --build firmware/iomcu/build

# coprocessor, debug build that prints INA3221 CH1's 1 ms samples
cmake -S firmware/iomcu -B firmware/iomcu/build-trace -DSENSE_TRACE=ON
cmake --build firmware/iomcu/build-trace

The coprocessor build produces three UF2 (USB Flashing Format) files:

File Holds For
rcbench-iomcu-install.uf2 the partition table at 0x0 and the image in slot A a module without the table: the one USB load of First run
rcbench-iomcu.uf2 the image alone a module with the table; the boot ROM writes it into the slot that is not running
rcbench-iomcu-pt.uf2 the partition table alone picotool

Copy one to the module's mass-storage drive while the module is held in BOOTSEL. rcbench-iomcu.bin is the image the panel carries: the panel build option RCBENCH_IOMCU_BIN takes its path, and the panel then offers it over CAN (Link). A panel built without the option offers no coprocessor update.

PICO_BOARD defaults to pimoroni_pico_plus2_rp2350. The module used for bring-up is a Waveshare RP2350-CAN (RP2350A, 4 MB flash), which has no board file in the SDK (software development kit); the default builds and runs on it. Override with -DPICO_BOARD=. The final board needs an RP2350B: the planned pin budget is 27 to 32 GPIO (general-purpose input/output).

Tools

Tool Purpose
tools/coverage.py measures host-suite line coverage, enforces the floors (94% total, 85% per file) and writes the table in STATUS.md and the figure in README.md and README-de.md; --check fails on drift, and on a C file under shared/ or a core file under protocols/ that is not in the measurement or has no counters
tools/mutate.py changes one line of shared/ or of a core under protocols/ at a time in a copy of the tree (a comparison flipped, a bound plus or minus 1, a stored assignment dropped), builds and runs the host suite and reports the changes the suite passes with; by default the lines changed since origin/main, at most 60 mutants and 2700 s; CI runs it on pull requests and does not fail on a survivor
tools/check_sanitizers.py configures the sanitizer build and fails unless every compile command under shared/, protocols/ and test/host/ carries -fsanitize=address,undefined, -fno-sanitize-recover=all and -fno-omit-frame-pointer, and no other flag of that family
tools/check_protocols.py holds every module under protocols/ to the rule that lets it be built in another pico-sdk project: a core file includes C standard headers and the core's own headers only, a file under rp2350/ also the module's driver headers, its generated .pio.h and pico-sdk headers, a driver's header includes no pico-sdk header, every public header carries extern "C" and compiles by itself as C11 and as C++17 with -Wall -Wextra -Werror, no include of a module names a path with .. or an absolute path or a macro, or has a token after its operand, no #include_next names a header other than a C standard header or, in a C file under rp2350/, a pico-sdk header, the name __has_include stands outside a literal only as the operand of #ifdef, #ifndef, #elifdef or #elifndef, or of a defined term on an #if or #elif line that holds nothing but such terms, white space, (, ), !, & and `
tools/security_parsers.py lists the modules under shared/ and protocols/ whose public headers declare a parse, decode, unpack, _rx, _feed, _write, _read, _done or _utf8_next function, plus the forced modules in the tool, which take outside input under other names, each with a reason (shared/settings, shared/touch); --check fails when the parser table in SECURITY.md and that list differ, or when a forced entry names a module that does not exist or that the name rule finds, a module being excused only by an entry with a reason in the tool
tools/check_docs.py holds the pages to the tree: links and anchors resolve, every image is used, the sidebar is complete, every page has a German counterpart, the suite list in STATUS.md matches CMake, the screenshot counts in STATUS.md match docs/img, the stick programming pages' red light table matches esc_stick_reason_is_fault(), the tree above lists every module under shared/ and protocols/, every source file carries an SPDX (Software Package Data Exchange) line, the protocol version, the heartbeat and link timings, the heartbeat and CAN pins, the coverage floors and the stack margin a page states are the constants in the headers and tools (FACTS in the tool lists each sentence), a table row that names a C constant gives its value, the ceiling table in Performance matches the --max-lines arguments in ci.yml, the pin counts in hardware/docs/Pins.md match pinmap.json, a German page quotes in backticks the German the screen shows: interface strings and formats, setting labels, help, options and categories, and the servo test's words
tools/wiki_links.py rewrites the links of a copy of docs/ for the wiki, where pages are addressed by title: Page.md becomes Page, a target above docs/ a repository URL (blob, tree, raw for an image); compares each page with its rewrite through markdown-it-py 3.0.0 and exits 1 on any other difference, with page and line; --check changes nothing; --verify fails on a converted page whose links a wiki cannot open
tools/check_formats.py compiles shared/ with every TR() lookup and report word replaced by its English literal, under -Wformat=2 -Wformat-nonliteral -Wformat-signedness, and fails on any warning: each English format against the arguments of its call (Language)
tools/gen_font.py regenerates the three embedded fonts from DejaVu Sans Mono, the two text faces with the German letters; --check fails if the committed tables differ
tools/render_ui.py renders every screen to PNG (Portable Network Graphics) with the code the panel runs, in English into docs/img/ and in German into docs/img/de/; --check compares with the committed images; --fit fails when a string overflows where it is drawn, in either language, except the English overflows the tool lists as known (Language)
tools/frame_cost.py measures cache-line fills per frame under cachegrind; --check-doc holds the table in Performance
tools/stack_check.py reads every panel task's deepest call chain out of the built ELF (Executable and Linkable Format) file and fails when one exceeds its stack less 1024 bytes; takes the build directory, default firmware/panel/build; -v prints each deepest chain and every call it cannot follow; --check-doc holds the task table in Performance to the build; --iomcu reads the coprocessor image instead and fails when a core's deepest chain, one interrupt and 256 bytes exceed its stack; with --check-doc it holds core 0's row of the coprocessor table; the ELF reader and the call graph are in tools/stack_elf.py, the coprocessor's half in tools/stack_iomcu.py
tools/ram_budget.py reads the internal heap a panel build leaves, subtracts every task stack, queue and buffer the panel allocates at run time and what each Wi-Fi mode allocates on top, and fails when the largest mode leaves under 32,768 bytes; takes the build directory, default firmware/panel/build; -v lists every allocation; --check-doc holds the internal RAM (random-access memory) table in Performance to the build
tools/sense_trace.py reads a captured console of a coprocessor built with -DSENSE_TRACE=ON: reads each line against the line format and each trace of INA3221 CH1's 1 ms samples against its end line, writes one CSV (comma-separated values) file per trace, and replays every move through shared/servo/servo_move.c with the filter at 1, 4 and 8 samples and the band at 0.02, 0.05 and 0.10 A; --servo-csv adds the arrival less the AS5600's travel time, --loaded-channel names the output channels with a servo connected and leaves the commands of the others out (First run)
tools/pinmap_check.py checks the IO board's draft pin map, hardware/docs/ioboard-pinmap-draft.json, against the pin functions in the pico-sdk's io_bank0.h; needs a pico-sdk checkout
tools/gen_board_drawing.py, tools/gen_esc_profiles.py generate the checked-in board drawings and ESC profile tables from their JSON sources (the board drawing from the configurator's configurator/drawings/panel.json); --check fails if the committed tables differ
tools/ci_gate.py waits for the CI run on a commit and fails unless one passed; the first step of docs.yml and release.yml
tools/patch_rgb_driver.py runs in the panel's configure step: writes a copy of ESP-IDF's RGB panel driver (components/esp_lcd/rgb/esp_lcd_panel_rgb.c) into the build directory in which every restart of the scan is the clean start of boot, and fails the build when one of its 3 patches does not match the driver exactly once (Constraints)
test/tools/ pytest cases for the tools above: python3 -m pytest test/tools
.clang-tidy, .cppcheck-suppress, ruff.toml static analysis and lint configuration; every finding is an error

gen_font.py looks for the font in RCBENCH_FONT_DIR, then ~/.local/share/fonts, then the system font directories. frame_cost.py needs valgrind; the other tools need a C compiler and Pillow. mutate.py needs git and CMake as well, and ci_gate.py the gh command. sense_trace.py needs CMake and a C compiler: it builds test/host/sense_trace_replay.c into build-trace/ unless --replay names a built one.

CI

Workflow Trigger Jobs
ci.yml push, pull request, tag v*, manual host suite; the same suite under AddressSanitizer and UBSan (UndefinedBehaviorSanitizer); coverage floors and Codecov upload, which fails the job when it fails; font, docs, web help, wiki-link, frame-cost, screenshot and research-script checks; the protocol module check; clang-tidy and cppcheck over shared/, the cores under protocols/ and test/host/, cppcheck's warning, performance and portability classes over firmware/ and the pin drivers under protocols/, and ruff; the tools' pytest cases; on a pull request the mutation check of the changed lines, which reports and does not fail on a survivor; panel build on ESP-IDF v5.4 and v5.5.3, each with the task stack check and the internal RAM budget, v5.4 with the stack table and the internal RAM table of this wiki; coprocessor build on pico-sdk 2.3.0 with the pin map check and the stack check of its two cores, and once more with -DSENSE_TRACE=ON, with the same stack check, failing when the default image holds a trace symbol; on v5.4 a panel build with RCBENCH_TOUCH_INJECTOR=ON, with the stack check and the internal RAM budget, failing when the default image holds an injector symbol; the touch injector build on pico-sdk 2.3.0 for waveshare_rp2350_zero, not uploaded; firmware artifacts including a merged panel image for offset 0
docs.yml push to main touching docs/, manual waits for the CI run on the same commit and, when it passed, mirrors docs/ to the GitHub wiki
mutation.yml Sunday 03:17 UTC, manual every mutant of the lines of shared/safety and shared/outputs (3032 mutants) in 6 parallel shards of files, no mutant limit, no mutant started after 18000 s in a shard; the mutants left then are reported as not run; does not fail on a survivor; the result is the artifacts mutation-full-0 to mutation-full-5 and the job summaries
release.yml tag v*, manual for a tag waits for the CI run on the tagged commit and, when it passed, builds both images, packages them with checksums and creates a release; beside that, without holding the release back, runs the configurator data check (configurator/core/tools/check-annotate.sh --warn-only) and shows each warning, such as a release tag newer than the configurator data, as an annotation and in the run summary
pages.yml pull request and push to main touching configurator/, help/, docs/img/ or the help generator type check, tests, data check, size budget and browser tests of the configurator; the web help build of tools/help_site.py; on a push to main it publishes the built page to GitHub Pages, with the web help under help/

clang-tidy does not run over firmware/: it compiles each file and needs the ESP-IDF and pico-sdk headers, which the analysis job does not have. cppcheck's style class does not run over firmware/ either. The host tests are built with the library flags -Wall -Wextra -Werror -Wshadow -Wconversion -Wno-sign-conversion; test/host/.clang-tidy lists the checks that are off for them, each with its reason.

CI installs pillow 12.3.0, gcovr 8.6, ruff 0.16.5 and pytest 9.1.1 at these exact versions. Every job has a timeout-minutes limit: 100 min for the jobs that wait for CI with tools/ci_gate.py (up to 5400 s, 90 min), 10 to 60 min for the others. ctest stops a test after 120 s. With the environment variable CI set, a missing C++ compiler fails the CMake configure step of the host suite, and a missing git, cmake or cc fails the end-to-end cases of tools/mutate.py. tools/check_docs.py holds the pico-sdk and ESP-IDF tags of this page to those in ci.yml and release.yml, and fails when a build page has no ESP-IDF tag statement.

Every check runs locally; CONTRIBUTING.md lists the commands.

The wiki must contain at least one page before the first docs.yml run; otherwise the wiki repository does not exist and the clone fails.

⚠️ **GitHub.com Fallback** ⚠️