Adding a Radio
A new radio is a new boards/<b>/ directory, drivers for any chip the tree doesn't have yet, and a platform if its SoC
is new. The core doesn't change. If it seems to need a change, the difference belongs in board_t or a hal_*
interface instead, used through a field that other radios leave at its default.
Before writing code, the facts go into a hardware spec, docs/spec/hardware-<b>.md, written by the spec side under
the clean-room rules. Use the hardware spec of a radio on the same platform (hardware-<id>.md, radios.md) as the model: MCU and memory map, pins, chips, keys, display, power,
bootloader and image format, calibration, codec location. Start from the radio's entry in
radios.md: its chip list and which RadioFoundry drivers it can reuse.
The files
A board's files list only how it differs from its platform. The platform's defaults are in platform/<p>/:
platform.mk (compiler, part defines, HAL modules), config.mk (feature defaults), common.ld (the sections) and
FreeRTOSConfig.h. Glue that every radio with the same part on that SoC shares is in a subdirectory the board lists
in BOARD_DIRS. The board then keeps only its wiring and what its chips leave to it.
| Subdirectory | For |
|---|---|
platform/stm32f4/hrc |
an HR_C6000 or HR_C5000: DMR glue, I2S audio, the chip's ports, the stock codec host |
platform/stm32f4/spi_flash |
the data flash on SPI NOR, with its security registers. A flash that shares its bus with another device takes the board's bus lock (bus_take, bus_give) instead of the driver's own mutex |
platform/stm32f4/fsmc_keypad |
a key matrix on the LCD data lines |
platform/stm32f4/gps_uart |
a GPS module on a USART |
The board's own files:
| File | Contents |
|---|---|
boards/<b>/board.mk |
PLATFORM, APP_BASE (where the image starts: the first byte the bootloader runs), BOARD_DIRS (the board directory and each driver it uses) and any MCU_DEFS of its own. STM32: MCU (the part, which picks the CMSIS define and start-up file) and HSE_VALUE (the crystal in Hz); optional BOARD_HAL_MODULES (HAL modules beyond the platform's, such as hal_i2c) |
boards/<b>/board.toml |
The board registry entry: identity, status, flashing, codec route, the site's data. tools/boards.py check validates it against board.mk, the linker script and board_t. Every key is in The board registry. Copy a neighbor on the same platform |
boards/<b>/config.mk |
Feature defaults where this radio differs from platform/<p>/config.mk, with ?= (for example CONFIG_USB ?= n on a radio without USB, CONFIG_DISPLAY_INSET), each with the reason. Also BOARD_UNSUPPORTED (switches its hardware can't do, one a line with the reason: BOARD_UNSUPPORTED += AM_RX # no air band receiver), BOARD_NOT_WRITTEN (the same form: the hardware can, the driver or glue isn't written yet, with CONFIG_X ?= n; ci.sh's maximal build leaves both lists off, the rule), BOARD_REQUIRED (switches it can't build without; ci.sh skips their no- build) and MAXIMAL_VARS (make variables for that build, e.g. LTO=1) |
boards/<b>/memory.ld |
The memory map: the codec symbols, then MEMORY (the application's FLASH region from __app_start, which the Makefile defines from APP_BASE, and the RAM regions), then INCLUDE common.ld, the platform's sections. Keep the region's end below the codec. Keep the application out of every flash sector the codec touches (not only the codec bytes), so rfoundry can update the firmware without erasing the codec. The flashing tools read the region and the codec symbols from it, and derive the erasable sectors and the protected range (release.md) |
boards/<b>/freertos_board.h |
Optional: RTOS settings that differ from platform/<p>/FreeRTOSConfig.h (the heap size, for example), read before its defaults |
boards/<b>/board.c |
The board_t descriptor, the key map, bands, power level names |
boards/<b>/pins.h, <b>.h |
Pin assignments and the board's private declarations. The registry reads the pins the flash stub uses from pins.h ([flash.stub] in The board registry) |
boards/<b>/*_<b>.c |
One file per interface: radio, DMR, audio, vocoder, display, input, flash, power, calibration, diagnostics, USB |
Look at the boards on your platform for a model (tools/boards.py list --platform <p>; their chips are in
radios.md).
board_t
hal/board.h defines it. The fields:
| Field | Fill with |
|---|---|
name |
The model as shown in About and the hotspot's description |
caps |
CAP_KEYPAD, CAP_ENCODER, CAP_GPS, CAP_DMR, CAP_VOLUME_POT, CAP_AM_RX, CAP_BCAST_FM, CAP_NO_BATTERY (a mobile on a fixed supply: no battery UI, no battery monitor; leave battery_mv and battery_pct NULL, and set hal_power_t.supply_mv if the radio measures its DC input: radio-features.md, battery) |
bands, band_count |
Receive ranges, each with tx true where transmit is allowed |
power_count, power_names, power_default |
Power levels as short labels, lowest first, and the default index |
display, input, keymap, keymap_len |
Display and key interfaces, and the key map (below) |
radio, dmr, audio, vocoder |
RF chip, DMR modem (NULL on an FM-only radio), audio path, codec hosting |
power, flash |
Battery and power switch, external flash |
host_port, cps, netif |
The link to a computer (NULL if none), CPS and USB network hooks |
system, diag_line, diag_reset |
Optional: chip identity and reboot modes, a diagnostics page |
battery_low_mv, battery_critical_mv |
Battery warning levels; 0 for none |
audio_tap, bcast, afsk |
Optional: audio path taps, a broadcast FM receiver, APRS AFSK transmit and receive audio |
gps_present |
Optional, with CAP_GPS: whether the GPS module is fitted (radios sold with and without one) |
Optional hooks may be NULL; the core checks them.
The key map
Each keymap_entry_t (hal/hal_input.h) maps one bit of the board's key scan to logical actions:
| Field | Meaning |
|---|---|
bit |
The key's bit in hal_input_t.scan() |
short_action, long_action |
input_action_t for a press and a hold |
raw_edges |
Report down and up instead of press and hold (PTT, monitor) |
repeat |
Repeat while held (Up, Down) |
Map keys to the logical actions of input_action_t (ACT_SELECT, ACT_BACK, ACT_QUICK, ACT_CONTEXT, ACT_USER_1 to ACT_USER_4, ACT_MONITOR, ACT_TALK_TARGET, ...)
so the five key rules of docs/ux/principles.md hold on the new radio. A radio without a keypad
or with fewer keys must still reach every task; docs/spec/ui.md section 3 says how. Give some key
ACT_EMERGENCY if the radio has one.
The display
The UI draws for a display class (size, color depth, insets). If the new display matches an existing class, nothing changes in the UI. A new class needs a gallery entry and a key map pair in the UI host tests (see Testing); ask before adding one, since every class multiplies the screens to review.
The frame buffer is sized to the panel in board.mk MCU_DEFS: GFX_FB_BYTES (width times height times bits per pixel,
over 8), GFX_MAX_ROWS (the height) and GFX_INDEXED=1 for a 16-color panel. Without them the build reserves the
40 KB RGB565 default. boards.py check requires the buffer to hold the board's hardware.gallery class.
Bring-up order
- Platform: start-up, clocks, a time base, a UART or USB for logging. Blink something.
board.cwith the display and input interfaces. The UI comes up with the built-in channels.- The debug link on the host port, so screenshots and key injection work from here on (Debug link).
If the host port is a two-pin K-type programming jack, check whether PTT shares the line that carries the host's
data: send back-to-back 0x00 bytes and watch the raw PTT pin. If it goes low, gate PTT on UART traffic
(
drivers/input/sharedptt, gotchas.md 16.2) before anything transmits. Then runtests/hil/test_ptt_host_traffic.py: host polling must make no PTT press. - Flash, settings and calibration reads. Check the foreign-region guard on a radio that still runs the vendor firmware (settings-store.md 5).
- RF chip: FM receive, then FM transmit into a dummy load.
- DMR modem and the codec: receive, then transmit, checked on the SDR bench.
- CPS, so qdmr can program it.
A bring-up page under docs/testing/ is this list made concrete for one radio.
Bring-up records
What the docs claim for a radio comes from its records, so each step above ends with a recorded run.
- Add
boards/<b>/board.tomlwith[status] level = "bring-up", asummaryand[docs] hardware. A bring-up page (docs/testing/<b>-bringup.md,[docs] bringup) is optional; with one, the README links the radio to it. tools/bringup.py list --radio <b>shows the checklist: every feature of tests/hil/features.toml the board builds,n/afor itsBOARD_UNSUPPORTEDswitches andnot writtenfor itsBOARD_NOT_WRITTENones. Each needs a reason inconfig.mk; a feature built but known not to work yet gets a[features]entry inboard.toml.- As a step works,
tools/bringup.py run --radio <b> --feature F -- <pytest options>runs its tests under the radio lock and writesdocs/testing/results/<b>/<date>-<commit>.json. Commit the record with the change it checks; a record with failures is the to-do list (HIL bring-up mode). - A check made before the radio had records (a bench log, a hand test written up in the hardware page) can go in
the board's seed record,
docs/testing/results/<b>/seed.json(format). A run replaces a seed row. - Raise
[status] leveltotesting, thensupported, as the records cover the radio, updatesummary, and runtools/boards.py docs.--floorin ci.sh fails a change that makes any radio read lower than onmain.
Tests and CI
- Add host tests for each new driver against a simulated chip (
tests/host/*Test.c; see the existing driver tests). Tests of the board's own code go inboards/<b>/tests/with ahost.mk(Testing). - HIL tests (tests/hil): the health checks' numbers go in
board.toml[hil], the CPS radio types in[cps], and a check only this radio can use inboards/<b>/hil/test_*.py. tools/ci.shfinds the board by itsboard.mkand builds its feature matrix when the compiler is installed. A board without its compiler is skipped with a notice.- Run
tools/boards.py docs: it writes the radio into the README's radio table, the user manual's radio index (23-radios.md), the feature matrix's "Built by default" table and the Port cell of the radio's row in the radio survey.tools/ci.shfails when that wasn't run. Remove the radio's planned row from the survey, or make it the board's row: a planned row that names a board failstools/boards.py check. Add the radio to the user manual's introduction. - The web site lists the radio from
board.toml:orderplaces it,[status] levelandsummarygive its badge and its line for visitors. A survey row that names a board is the board's row, so the site doesn't list it as planned. - Before it can be released: its
board.toml[flash]and[codec]tables,[status] release = "stable"and the regeneratedtools/rfoundry/rfoundry/registry.json(tools/boards.py json --for rfoundry --out ...). The release workflow's job for its toolchain then builds and packages it, with each build option value (release.md); CI and the nightly pick it up from the registry too (ci.md). The jobs are grouped by toolchain (platform.toml[toolchain] id), so neither a new radio nor a new platform on a known toolchain (another Arm SoC, say) needs a workflow edit. Only a new toolchain does.