One of the quiet bits of magic in Betaflight is that the same firmware runs on hundreds of physically different boards. The generic flight code does not know or care which gyro you have or which pin a motor is on. All of that lives in one place: a board’s config – a header file that is, in effect, a wiring diagram written in C.
If you have ever wanted to read one (or write one for a new board), this is the tour. Our specimen is HELLBENDER_0001, an RP2350-based board – the same chip I have been poking at in recent posts.
Identity
#define FC_TARGET_MCU RP2350B #define BOARD_NAME HELLBENDER_0001 #define MANUFACTURER_ID RASP
The opening lines just say what this is: which MCU family the platform code should build for, the human-readable board name the configurator shows, and a manufacturer ID. Nothing flies yet – this is the label on the tin.
Buses
#define SPI0_SCK_PIN PA2 #define SPI0_SDI_PIN PA4 #define SPI0_SDO_PIN PA3 #define SPI1_SCK_PIN PA26 #define SPI1_SDI_PIN PA24 #define SPI1_SDO_PIN PA27 #define I2C0_SDA_PIN PA44 #define I2C0_SCL_PIN PA45
Almost everything else hangs off a shared bus, so the buses come first. Each SPI bus needs three pins – clock, data-in and data-out (SCK/SDI/SDO) – and each device on it gets its own chip-select later. I2C needs just two, data and clock. Define the bus once here, then point devices at it by name.
The most important sensor: the gyro
#define USE_GYRO #define USE_GYRO_SPI_ICM42688P #define USE_ACC #define USE_ACC_SPI_ICM42688P #define GYRO_1_SPI_INSTANCE SPI0 #define GYRO_1_CS_PIN PA1 #define GYRO_1_EXTI_PIN PA22
The gyro is the heart of a flight controller, and these lines wire it up: which driver to compile (an ICM42688P here), which bus it sits on (SPI0, defined above), its chip-select pin, and – crucially – its EXTI pin. That interrupt line is how the gyro tells the MCU “fresh sample ready,” and it is what lets the whole control loop run in lockstep with the sensor instead of guessing. Get the EXTI wrong and the board flies, badly; everything else is downhill from the gyro.
Motors
#define MOTOR1_PIN PA28 #define MOTOR2_PIN PA29 #define MOTOR3_PIN PA30 #define MOTOR4_PIN PA31
Four pins, four motors. On this chip these are not timer outputs in the STM32 sense – they become PIO-driven DShot outputs – but at the config level it is just “motor N lives on this pin.” The platform code works out how to actually generate the signal.
Storage and the OSD
#define USE_MAX7456 #define MAX7456_SPI_INSTANCE SPI1 #define MAX7456_SPI_CS_PIN PA17 #define USE_FLASH #define USE_FLASH_W25Q128FV #define FLASH_CS_PIN PA0 #define DEFAULT_BLACKBOX_DEVICE BLACKBOX_DEVICE_FLASH #define USE_SDCARD #define USE_SDCARD_SPI #define SDCARD_SPI_CS_PIN PA25
Here is where the shared-bus pattern pays off. The MAX7456 analogue OSD chip, the blackbox flash, and the SD card all sit on the same buses defined earlier – each just needs its own chip-select pin to be addressed independently. The board even nominates flash as the default blackbox device, so logging works out of the box.
Serial – and the PIO UART trick
#define UART0_TX_PIN PA12 // video TX #define UART0_RX_PIN PA13 #define UART1_TX_PIN PA8 // GPS #define UART1_RX_PIN PA9 #define PIOUART0_TX_PIN PA20 // CRSF receiver #define PIOUART0_RX_PIN PA21 #define USE_RX_CRSF
The RP2350 only has two hardware UARTs, which is nowhere near enough for a modern quad. So the config also declares PIO UARTs – serial ports synthesised out of the chip’s programmable I/O, which I went into in the PIO article. To the rest of the firmware a PIOUART is just another serial port; the board config is where the illusion is set up.
The rest of the cast
#define USE_BARO #define USE_BARO_DPS310 #define BARO_I2C_INSTANCE I2CDEV_0 #define USE_BEEPER #define BEEPER_PIN PA5 #define LED0_PIN PA6 #define LED_STRIP_PIN PA38 #define ADC_VBAT_PIN PA40 #define ADC_CURR_PIN PA41 #define ADC_RSSI_PIN PA42
The remainder fills out the board: a DPS310 barometer on the I2C bus for altitude, a beeper, status LEDs and an addressable LED-strip pin, and the ADC inputs that measure battery voltage, current and RSSI. There are a few board-specific extras too – this one has switchable 5V and 9V BEC enable pins – but the pattern is always the same: name the feature, point it at a pin.
How it all comes together
At build time you pass CONFIG=HELLBENDER_0001 and this header is pulled in. The platform code reads the defines, binds each peripheral to its pins and bus, and the otherwise-generic firmware suddenly knows how this board is wired. That is the entire trick behind “one firmware, hundreds of boards.”
Once you can read a config, every board becomes legible – you can tell at a glance what sensors it has, what it can log to, how many serial ports it really offers. And when a new board appears, supporting it is, at heart, writing one of these files carefully. It is the most quietly powerful file in the whole project.
Header photo: “Racing Drone” (Minion 220 by RotorGeeks) by Commanderbryce, CC BY-SA 4.0, via Wikimedia Commons (cropped).