# Adding a new device A device is supported by adding one board definition `.h` and board configuration `.c` file under `machine/`. the build selects the definition by name, and CI compiles every file it finds. To compile for your added device, do: ``` make MACHINE= ``` ## What a new device consists of | File | Purpose | |------|---------| | `machine/.c` | the `struct machine machine` board definition | | `machine/.h` | `MACHINE_MODEL`, the model string shown in the UI and the CLI | | `doc/devices/.md` + `doc/supported_devices.md` | (only if the board is verified and you want to support it publicly) user-facing documentation | `` is the machine symbol without its `MACHINE_` prefix, so `MACHINE` is passed exactly as spelled on the command line: `make MACHINE=MY_BOARD_1_0` needs `machine/MY_BOARD_1_0.c` and `machine/MY_BOARD_1_0.h`. ## Step 1: the model header `machine/.h` only carries the product string. Use the existing files as the template: ```c #ifndef _MACHINE_MY_BOARD_1_0_H_ #define _MACHINE_MY_BOARD_1_0_H_ #define MACHINE_MODEL "Brand Model V1.0" #endif ``` The device product string is what the CLI or the Web-Interface will print to identify the device; `machine.h` includes it by name. > [!NOTE] > `machine_name` is a `char[30]` and `MACHINE_MODEL` is copied into it at > compile time. A longer model string still builds, but SDCC emits > `warning 147: excess elements in array of chars initializer` and the name is > truncated in the UI. Keep it to 29 characters or less. ## Step 2: the board definition `machine/.c` starts with the shared prelude and then defines the single `machine` object. Copy the closest existing board and adjust it — do not start from a blank file, the field meanings are easier to see in a real example. The best choice is usually a device that has the same number and type of Ethernet ports and the same number of SFP+ cages. ```c #include "machine_defs.h" __code const struct machine machine = { .machine_name = MACHINE_MODEL, .isRTL8373 = 1, .min_port = 0, .max_port = 8, .n_sfp = 1, .log_to_phys_port = {1, 2, 3, 4, 5, 6, 7, 8, 9}, .phys_to_log_port = {0, 1, 2, 3, 4, 5, 6, 7, 8}, .is_sfp = {0, 0, 0, 0, 0, 0, 0, 0, 1}, .sfp_port[0].pin_detect = GPIO30_ACL_BIT3_EN, .sfp_port[0].pin_los = GPIO37, .sfp_port[0].pin_tx_disable = GPIO_NA, .sfp_port[0].sds = 1, .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1 }, .led_sets = { { /* RJ45 ports */ LEDS_2G5 | LEDS_LINK, LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT, 0, 0 }, { /* SFP port, single green LED */ LEDS_10G | LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT, 0, 0, 0 } }, }; ``` ### Fields | Field | Meaning | |-------|---------| | `machine_name` | always `MACHINE_MODEL` | | `isRTL8373` | `1` for an RTL8373 SoC, `0` for RTL8372 | | `min_port` / `max_port` | range of logical port numbers the device has; `min_port` is `0` for RTL8373 boards and `3` for RTL8372 boards (logical ports 1–2 are the SoC's internal ports on the 8372), `max_port` is usually 8. | | `n_sfp` | number of SFP+ cages | | `n_10g` | SerDes 10G capability used by the RTL837xN init: `0` none, `1` one, `2` two 10G-capable SerDes | | `log_to_phys_port[9]` | logical port (0-based index) → physical front-panel port number | | `phys_to_log_port[9]` | the inverse, physical port 1 is index 0, unused slots are 0 | | `is_sfp[9]` | `0` for copper, `1` for left slot and `2` for right slot | | `sfp_port[2]` | per-cage wiring, `sfp_port[0]` is the leftmost cage | | `reset_pin` | GPIO of the reset button, `GPIO_NA` if unpopulated | | `high_leds` | mux/enable for the LED pins 27–29 (`LED_27`, `LED_28_SYS`, `LED_29`) | | `port_led_set[9]` | which LED set (0–3) each logical port uses | | `led_sets[4][4]` | Each port can have up to 4 GPIO output pins assigned. 4 sets of LEDs with 4 LED-conditions per set; the conditions define which GPIO pins become active for a port given a port link-state. Which LEDs are then enabled depends on the wiring of the pins to up to 3 LEDs for that port, which can make a logical combination of the 4 LED output-pins. | | `led_mux_custom` / `led_mux[28]` | `1` plus the 28 mux values when the stock LED muxing does not fit the board | | `mac_flash_offset` | flash offset of the factory MAC address, omit or `0` if the board does not store one | Each `struct sfp_port` entry holds `pin_detect`, `pin_los`, `pin_tx_disable`, `sds` (which SerDes drives the cage) and `i2c` (built with `I2CBUS(sda, scl)`, or `GPIO_NA`-based values where a cage has no such pin). `I2CBUS(...)` only accepts the pin pairs listed in `machine_defs.h`. A wrong pair is caught at compile time: SDCC reports `error 176: sizeof applied to an incomplete type` pointing at the offending `.i2c` line, because the macro deliberately turns an invalid bus number into an array of size 0. ### Reusing an existing definition If the new board is electrically identical to one that already exists — same port mapping, same SFP wiring, same LEDs — do not copy the file. Make the new `.c` a symlink and keep only a distinct `.h`, so the two boards cannot drift apart: ``` ln -s KP_9000_6XHML_X2.c machine/MY_BOARD_1_0.c ``` The new board still needs `machine/MY_BOARD_1_0.h` (pick the version number on the PCB) with its own `MACHINE_MODEL`, because `MACHINE_MODEL` is what distinguishes the products. Existing examples: `machine/PCB_K0402WS_V3.c`, `machine/KP_9000_9XHML_X_V3_2.c` and the six `KP_9000_*` revision variants. Two files that differ only in whitespace or comments are duplicates and should be collapsed this way. ## Step 3: build and verify ``` make MACHINE=MY_BOARD_1_0 ``` The generated flash image appears as `output/rtlplayground--MY_BOARD_1_0.bin`. An unknown or empty `MACHINE` fails immediately with a message naming the expected file, before anything is compiled. Then run the checks CI runs: ``` make machine_check # compiles machine/.c and machine_init.c for every machine make -C test # host-side unit tests ``` `make machine_check` discovers the definitions with `$(wildcard machine/*.c)`, so a new file is covered without registering it anywhere. CI (`.github/workflows/build.yml`) runs it on every push and pull request, alongside full builds for a 4+2 and an 8+1 board. A successful compile only proves the definition is syntactically valid. The actual behaviour — port numbering, SFP detection, LEDs — has to be confirmed on hardware, and the console commands (`port`, `stat`, `sfp`, `leds`) are the tools for that. ## Step 4: documentation Once the board has been tested on hardware: 1. Add a row to the table in [Supported devices](supported_devices.md). 2. Add `doc/devices/.md` describing the board, the build target (`make MACHINE=MY_BOARD_1_0`), what works and what is unverified, plus photos and the PCB silkscreen marking. 3. If the board shares a PCB with other revisions, follow `doc/devices/KP-9000-6XH-X2.md` and document which `MACHINE` to pick for which silkscreen. ### Identifying the CPU variant For RTL8373-based devices, use the read-only console command below to distinguish the N and non-N CPU variants: ```text regget 4 ``` | Register 0x0004 value | CPU | |---|---| | `0x83730000` | RTL8373 (non-N) | | `0x83737000` | RTL8373N | Record the identified CPU in the device documentation. These values identify only the CPU variant; document PHY and flash-chip models separately when verified.