Skip to content

Flight controller firmware

Status: pre-hardware. th-eda’s pin map is frozen and the board is in layout (six layers, being routed). No board is built or flown yet, so this hwdef is not tested on hardware. It compiles; that is all it has proved.

The hwdef is generated, not hand-edited: make_hwdef.py reads ArduPilot’s Pixhawk6C/hwdef.dat and hwdef-bl.dat and applies th-eda’s pin map (docs/flight-controller-pinmap.md in th-eda’s terrahawk-eda repository, which is not published), so the board stays a short, reviewable diff of a design ArduPilot already flies. Every rule must match exactly once or the script stops.

Terminal window
make firmware ARDUPILOT=<private ArduPilot checkout with submodules> # never the SITL tree

Outputs in build/: TerraHawkFC_bl.bin, arduplane.apj, arduplane_with_bl.hex.

Item Value
Board id 20410, not registered with ArduPilot (the charge node, 20401, is not registered either). The bootloader accepts firmware by this id, so if someone else registers the same number later, their firmware can flash onto this board. Registration is a pull request to ArduPilot and Ryan’s decision
USB ids ArduPilot’s defaults, not Holybro’s
CAN CAN1 PD0/PD1, CAN2 PB5/PB13, two DroneCAN drivers (defaults.parm)
Outputs 1-2 TIM5 (pusher, spare motor; provisional, see below), 3-6 TIM1 (the 4-in-1’s lift motors with bidirectional DShot, or servos), 7-8 TIM4 (elevons alone), 9-10 TIM8 (camera, NODMA); with DroneCAN pod ESCs, the lift motors are virtual 11-14 on CAN bus 1
IMUs ICM-42688-P on SPI1 (with DMA), ST LSM6DSV16X on SPI6 (a second vendor; the BMI088 is out of stock; no DMA stream is available for SPI6, so it is polled)
Compass none on the board; the Q43’s on the GPS1 port
Peripheral 5 V Enables active-high (VDD_5V_PERIPH_EN, VDD_5V_HIPOWER_EN), for the TPS2553 family
Checked ArduPlane at 4c98c92 builds with -Werror, Lua scripting compiled in, 1,544,184 bytes, 421,892 free (2026-09-30, with the 2026-09-29 change: BIDIR on TIM1 and the DMA priority). The build’s hwdef.h gives SPI1 streams 2,7 and 2,3 and TIM1 stream 2,6, none shared

Known sharing: TIM5_UP shares a DMA stream with SPI1_TX. It is used only for DShot, and the pusher runs PWM. If the pusher moves to DShot, review the DMA allocation first.

DMA for the lift motors on outputs 3-6 (2026-09-29): DMA_PRIORITY puts SPI1 (the primary IMU) first and TIM1 second. chibios_hwdef.py at 4c98c92 then gives SPI1 streams 2,7 and 2,3 and TIM1 (update and both capture channels) stream 2,6, none shared. The sharing falls on the FRAM (SPI2), UART7 and USART3/USART2 TX, and the PWM-only TIM4 and TIM5. With the 6C’s own order, TIM1 shared the FRAM’s stream; with TIM1 first, SPI1 shared with the FRAM. No pin moves. For that reason, outputs 1 and 2 (PA0/PA1) are provisional: if Ryan chooses a bidirectional-DShot pusher ESC, th-eda moves them to another timer.

Rotations of both IMUs are the 6C’s until th-eda’s layout fixes the parts’ orientation. A third CAN bus (H723 in the same footprint, FDCAN3 on PF6/PF7 AF2 per ST DS13313) would need those two pins added to ArduPilot’s STM32H723xx.py and a 1 MB build with Lua forced on.

Bring-up check for the IMU heater: the IMU island joins the board through a thermal break of about 30 K/W (th-eda’s estimate, against about 8 K/W with the planes crossing; terrahawk-eda 0ad59bc), so some processor heat still reaches the IMUs. On the bench, log both IMUs’ temperatures against CPU load with the heater at its 45 °C target. If a temperature rises above 45 °C and follows the load, set BRD_HEAT_TARG 50 in defaults.parm and log again.

ArduPlane compiles QAUTOTUNE only for SITL (ArduPlane/qautotune.h), so the stock LUX F765 firmware has no QAUTOTUNE mode. The lift testbed (params/testbed-quad.parm) and the first build need it for the VTOL rate tune. lux-qautotune.hwdef adds define QAUTOTUNE_ENABLED 1 to ArduPilot’s own LumenierLUXF765-NDAA hwdef through waf’s --extra-hwdef; nothing else in the board changes.

Terminal window
make lux-firmware ARDUPILOT=<private ArduPilot checkout with submodules> # never the SITL tree

Output in build/lux/: arduplane.apj (load with Mission Planner’s Load custom firmware) and arduplane_with_bl.hex. Checked at 4c98c92 (2026-10-10): 1,515,676 bytes, 483,164 free; the define is in the build’s hwdef.h, and the binary carries the QAutoTune code (16 symbols). Not flown yet.

The source files this page describes, as they are in the repo.

hw/flight-controller/hwdef/TerraHawkFC/hwdef.dat · 259 lines · open · download
# TerraHawk flight controller, generated by hw/flight-controller/make_hwdef.py
# as a diff of Pixhawk6C/hwdef.dat at ArduPilot 4c98c92. Edit the script, not this file.
# default to all pins low to avoid ESD issues
#DEFAULTGPIO OUTPUT LOW PULLDOWN
# MCU class and specific type
MCU STM32H7xx STM32H743xx
# crystal frequency
OSCILLATOR_HZ 16000000
# ChibiOS system timer
STM32_ST_USE_TIMER 2
# USB setup
USB_STRING_MANUFACTURER "TerraHawk"
# board ID. See Tools/AP_Bootloader/board_types.txt
APJ_BOARD_ID 20410
FLASH_RESERVE_START_KB 128
# flash size
FLASH_SIZE_KB 2048
# order of UARTs (and USB)
SERIAL_ORDER OTG1 UART7 UART5 USART1 UART8 USART2 USART3 OTG2
# USB
PA11 OTG_FS_DM OTG1
PA12 OTG_FS_DP OTG1
PA9 VBUS INPUT OPENDRAIN
# pins for SWD debugging
PA13 JTMS-SWDIO SWD
PA14 JTCK-SWCLK SWD
# telem1
PE8 UART7_TX UART7
PE7 UART7_RX UART7
PE9 UART7_RTS UART7 LOW PULLDOWN
PE10 UART7_CTS UART7
# telem2
PC12 UART5_TX UART5
PD2 UART5_RX UART5
PC8 UART5_RTS UART5 LOW PULLDOWN
PC9 UART5_CTS UART5
# GPS1
PB6 USART1_TX USART1
PA10 USART1_RX USART1
# GPS2
PE1 UART8_TX UART8
PE0 UART8_RX UART8
# uart2 telem3
PD5 USART2_TX USART2
PA3 USART2_RX USART2
# debug uart
PD8 USART3_TX USART3
PD9 USART3_RX USART3
# outputs 9-10 (TIM8): camera trigger, exposure feedback (was USART6 to the IO coprocessor).
# NODMA: PWM needs none, and TIM8_UP would otherwise share a stream with SPI1_TX, the primary IMU
PC6 TIM8_CH1 TIM8 PWM(9) GPIO(58) NODMA
PC7 TIM8_CH2 TIM8 PWM(10) GPIO(59) NODMA
# ADC
PC5 BATT_VOLTAGE_SENS ADC1 SCALE(1)
PC4 BATT_CURRENT_SENS ADC1 SCALE(1)
PB1 BATT2_VOLTAGE_SENS ADC1 SCALE(1)
PA2 BATT2_CURRENT_SENS ADC1 SCALE(1)
define HAL_BATT_VOLT_PIN 8
define HAL_BATT_CURR_PIN 4
define HAL_BATT_VOLT_SCALE 18.18
define HAL_BATT_CURR_SCALE 36.36
PA4 VDD_5V_SENS ADC1 SCALE(2)
# SPI1 - IMUs
PA5 SPI1_SCK SPI1
PA6 SPI1_MISO SPI1
PA7 SPI1_MOSI SPI1
PC13 ICM42688_CS CS
PE6 ICM42688_DRDY INPUT
# SPI6 - second IMU on its own bus
PG13 SPI6_SCK SPI6
PG12 SPI6_MISO SPI6
PG14 SPI6_MOSI SPI6
PC15 LSM6DSV_CS CS
PE4 LSM6DSV_DRDY INPUT
# SPI2 - FRAM
PD3 SPI2_SCK SPI2
PC2 SPI2_MISO SPI2
PC3 SPI2_MOSI SPI2
PD4 FRAM_CS CS
# PWM output pins
PA0 TIM5_CH1 TIM5 PWM(1) GPIO(50)
PA1 TIM5_CH2 TIM5 PWM(2) GPIO(51)
PA8 TIM1_CH1 TIM1 PWM(3) GPIO(52) BIDIR
PE11 TIM1_CH2 TIM1 PWM(4) GPIO(53)
PE13 TIM1_CH3 TIM1 PWM(5) GPIO(54) BIDIR
PE14 TIM1_CH4 TIM1 PWM(6) GPIO(55)
PD14 TIM4_CH3 TIM4 PWM(7) GPIO(56)
PD15 TIM4_CH4 TIM4 PWM(8) GPIO(57)
# CAN bus
PD0 CAN1_RX CAN1
PD1 CAN1_TX CAN1
PB5 CAN2_RX CAN2
PB13 CAN2_TX CAN2
# I2C buses
# I2C1, GPS+MAG
PB7 I2C1_SDA I2C1
PB8 I2C1_SCL I2C1
# I2C2, GPS2+MAG
PB10 I2C2_SCL I2C2
PB11 I2C2_SDA I2C2
# I2C4 internal
PD12 I2C4_SCL I2C4
PD13 I2C4_SDA I2C4
# order of I2C buses
I2C_ORDER I2C4 I2C1 I2C2
define HAL_I2C_INTERNAL_MASK 1
# this board is tight on DMA channels. To allow for more UART DMA we
# disable DMA on I2C. This also prevents a problem with DMA on I2C
# interfering with IMUs
# heater
PB9 HEATER_EN OUTPUT LOW GPIO(80)
define HAL_HEATER_GPIO_PIN 80
# Setup the IMU heater
define HAL_HAVE_IMU_HEATER 1
define HAL_IMU_TEMP_DEFAULT 45
define HAL_IMUHEAT_P_DEFAULT 50
define HAL_IMUHEAT_I_DEFAULT 0.07
# power enable pins
PB2 VDD_3V3_SENSORS1_EN OUTPUT HIGH
# start peripheral power on
PC10 VDD_5V_HIPOWER_EN OUTPUT HIGH
PE2 VDD_5V_PERIPH_EN OUTPUT HIGH
# safety switch and LED on the FMU (were on the IO coprocessor)
PE15 SAFETY_IN INPUT PULLDOWN
PG0 LED_SAFETY OUTPUT
# enable pin of the external switched 12 V video BEC (relay 2)
PG1 VTX_EN OUTPUT LOW GPIO(81)
# RSSI pad
PC0 RSSI_IN ADC1 SCALE(1)
# TPS2121 status: low means POWER2 (IN2) is supplying the board; high means POWER1 or neither.
# Logged by the flight script. The VDD_BRICK*_nVALID inputs mean a supply is present, not in range:
# a brick cut off by the ~5.6 V overvoltage limit still reads valid (th-eda, SCHEMATIC-NOTES.md).
PF8 MUX_ST INPUT PULLUP GPIO(82)
# power sensing
PE3 VDD_5V_PERIPH_nOC INPUT PULLUP
PC11 VDD_5V_HIPOWER_nOC INPUT PULLUP
PA15 VDD_BRICK_nVALID INPUT PULLUP
PB12 VDD_BRICK2_nVALID INPUT PULLUP
# microSD support
PD6 SDMMC2_CK SDMMC2
PD7 SDMMC2_CMD SDMMC2
PB14 SDMMC2_D0 SDMMC2
PB15 SDMMC2_D1 SDMMC2
PB3 SDMMC2_D2 SDMMC2
PB4 SDMMC2_D3 SDMMC2
define FATFS_HAL_DEVICE SDCD2
# LEDs
PD10 LED_RED OUTPUT OPENDRAIN GPIO(90) HIGH
PD11 LED_BLUE OUTPUT OPENDRAIN GPIO(92) HIGH
# setup for BoardLED2
define AP_NOTIFY_GPIO_LED_2_ENABLED 1
define HAL_GPIO_A_LED_PIN 90
define HAL_GPIO_B_LED_PIN 92
# ID pins
PE12 HW_VER_REV_DRIVE OUTPUT LOW
# PC0 HW_REV_SENS ADC1 SCALE(1)
# PC1 HW_VER_SENS ADC1 SCALE(1)
# PWM output for buzzer
PB0 TIM3_CH3 TIM3 GPIO(77) ALARM
# barometers
BARO MS5611 I2C:0:0x76
BARO MS5611 I2C:0:0x77
BARO BMP388 I2C:0:0x77
# prototypes may have no baro
define HAL_BARO_ALLOW_INIT_NO_BARO 1
# compass
define AP_COMPASS_PROBING_ENABLED 1
# we need to stop the probe of an IST8310 as an internal compass with PITCH_180
define AP_COMPASS_IST8310_INTERNAL_BUS_PROBING_ENABLED 0
# Disable generic internal RM3100 probing so the explicit hwdef rotation is honoured
define AP_COMPASS_RM3100_INTERNAL_BUS_PROBING_ENABLED 0
# SPI devices
SPIDEV lsm6dsv SPI6 DEVID1 LSM6DSV_CS MODE3 2*MHZ 8*MHZ
SPIDEV icm42688 SPI1 DEVID3 ICM42688_CS MODE3 2*MHZ 8*MHZ
SPIDEV ramtron SPI2 DEVID1 FRAM_CS MODE3 8*MHZ 8*MHZ
# 2 IMUs
# rotations are the 6C's until the layout fixes the IMU orientation (th-eda)
IMU Invensensev3 SPI:icm42688 ROTATION_PITCH_180_YAW_90
IMU LSM6DSV SPI:lsm6dsv ROTATION_PITCH_180_YAW_90
define HAL_DEFAULT_INS_FAST_SAMPLE 3
# enable RAMTRON parameter storage
define HAL_STORAGE_SIZE 32768
define HAL_WITH_RAMTRON 1
# allow to have have a dedicated safety switch pin
define HAL_HAVE_SAFETY_SWITCH 1
DMA_PRIORITY SPI1* TIM1* SDMMC* ADC* UART* USART* SPI* TIM*
# enable FAT filesystem support (needs a microSD defined via SDMMC)
define HAL_OS_FATFS_IO 1
hw/flight-controller/hwdef/TerraHawkFC/hwdef-bl.dat · 93 lines · open · download
# TerraHawk flight controller, generated by hw/flight-controller/make_hwdef.py
# as a diff of Pixhawk6C/hwdef-bl.dat at ArduPilot 4c98c92. Edit the script, not this file.
# MCU class and specific type
MCU STM32H7xx STM32H743xx
# crystal frequency
OSCILLATOR_HZ 16000000
# USB setup
USB_STRING_MANUFACTURER "TerraHawk"
# board ID. See Tools/AP_Bootloader/board_types.txt
APJ_BOARD_ID 20410
# bootloader is installed at zero offset
FLASH_RESERVE_START_KB 0
# the location where the bootloader will put the firmware
FLASH_BOOTLOADER_LOAD_KB 128
# flash size
FLASH_SIZE_KB 2048
# order of UARTs (and USB)
SERIAL_ORDER OTG1 UART7 UART5 USART3
# USB
PA11 OTG_FS_DM OTG1
PA12 OTG_FS_DP OTG1
PA9 VBUS INPUT OPENDRAIN
# pins for SWD debugging
PA13 JTMS-SWDIO SWD
PA14 JTCK-SWCLK SWD
# CS pins
PC13 ICM42688_CS CS
PC15 LSM6DSV_CS CS
PD4 FRAM_CS CS
# telem1
PE8 UART7_TX UART7
PE7 UART7_RX UART7
# telem2
PC12 UART5_TX UART5
PD2 UART5_RX UART5
# debug uart
PD8 USART3_TX USART3
PD9 USART3_RX USART3
# start peripheral power on
PC10 VDD_5V_HIPOWER_EN OUTPUT HIGH
PE2 VDD_5V_PERIPH_EN OUTPUT HIGH
# LEDs
PD10 LED_ACTIVITY OUTPUT OPENDRAIN GPIO(90) HIGH # red
PD11 LED_BOOTLOADER OUTPUT OPENDRAIN GPIO(92) HIGH # blue
define HAL_LED_ON 0
define HAL_USE_EMPTY_STORAGE 1
define HAL_STORAGE_SIZE 16384
# support flashing from SD card:
# FATFS support:
define CH_CFG_USE_MEMCORE 1
define CH_CFG_USE_HEAP 1
define CH_CFG_USE_SEMAPHORES 0
define CH_CFG_USE_MUTEXES 1
define CH_CFG_USE_DYNAMIC 1
define CH_CFG_USE_WAITEXIT 1
define CH_CFG_USE_REGISTRY 1
# microSD support
PD6 SDMMC2_CK SDMMC2
PD7 SDMMC2_CMD SDMMC2
PB14 SDMMC2_D0 SDMMC2
PB15 SDMMC2_D1 SDMMC2
PB3 SDMMC2_D2 SDMMC2
PB4 SDMMC2_D3 SDMMC2
define FATFS_HAL_DEVICE SDCD2
DMA_PRIORITY SDMMC* ADC* UART* USART* SPI* TIM*
# enable FAT filesystem support (needs a microSD defined via SDMMC)
define HAL_OS_FATFS_IO 1
define AP_BOOTLOADER_FLASH_FROM_SD_ENABLED 1
hw/flight-controller/hwdef/TerraHawkFC/defaults.parm · 11 lines · open · download
# TerraHawk flight controller board defaults (th-avionics, 2026-09-28)
# Two independent DroneCAN networks, never one driver on both buses (docs/electronics-notes.md)
CAN_P1_DRIVER 1
CAN_P2_DRIVER 2
CAN_D1_PROTOCOL 1
CAN_D2_PROTOCOL 1
# lift motors: virtual outputs 11-14 sent over bus 1
CAN_D1_UC_ESC_BM 15360
CAN_D1_UC_ESC_OF 10
# video BEC enable on the VTX_EN pad
RELAY2_PIN 81
hw/flight-controller/make_hwdef.py · 99 lines · open · download
"""Write TerraHawkFC's hwdef as a diff of ArduPilot's Pixhawk6C hwdef.
Usage: uv run --no-project python make_hwdef.py <ardupilot tree>
Pin map: th-eda, terrahawk-eda docs/flight-controller-pinmap.md (2026-09-28).
"""
import re
import sys
from pathlib import Path
ap = Path(sys.argv[1])
src = ap / "libraries/AP_HAL_ChibiOS/hwdef/Pixhawk6C"
out = Path(__file__).parent / "hwdef/TerraHawkFC"
HEADER = ("# TerraHawk flight controller, generated by hw/flight-controller/make_hwdef.py\n"
"# as a diff of Pixhawk6C/hwdef{bl}.dat at ArduPilot 4c98c92. Edit the script, not this file.\n")
IDENT = [ # Holybro's USB ids and board id are theirs; ours uses ArduPilot's defaults and a private id
(r"^USB_VENDOR.*\n", ""),
(r"^USB_PRODUCT.*\n", ""),
(r'^USB_STRING_MANUFACTURER.*$', 'USB_STRING_MANUFACTURER "TerraHawk"'),
(r"^APJ_BOARD_ID .*$", "APJ_BOARD_ID 20410"),
]
MAIN = IDENT + [
# the PX4-bootloader RAM map is not needed with ArduPilot's bootloader
(r"^# to be compatible.*\n# to use a different RAM_MAP\nenv USE_ALT_RAM_MAP 1\n", ""),
# no IO coprocessor: USART6 pins become outputs 9 and 10 on TIM8
(r"^# USART6 is for IOMCU\nPC6 USART6_TX USART6\nPC7 USART6_RX USART6\n\nIOMCU_UART USART6\n",
"# outputs 9-10 (TIM8): camera trigger, exposure feedback (was USART6 to the IO coprocessor).\n# NODMA: PWM needs none, and TIM8_UP would otherwise share a stream with SPI1_TX, the primary IMU\n"
"PC6 TIM8_CH1 TIM8 PWM(9) GPIO(58) NODMA\nPC7 TIM8_CH2 TIM8 PWM(10) GPIO(59) NODMA\n"),
# second IMU on its own bus, SPI6: ST LSM6DSV16X (the BMI088 is out of stock, th-eda 2026-09-28);
# one chip select (PC15) and one data-ready (PE4); PC14 and PE5 are free
(r"^PC14 BMI055_G_CS CS\nPC15 BMI055_A_CS CS\nPE4 BMI055_DRDY_A INPUT\nPE5 BMI055_DRDY_G INPUT\n",
"PE6 ICM42688_DRDY_PLACEHOLDER\n"),
(r"^PE6 ICM42688_DRDY INPUT\n", ""),
(r"^PE6 ICM42688_DRDY_PLACEHOLDER\n", "PE6 ICM42688_DRDY INPUT\n\n# SPI6 - second IMU on its own bus\n"
"PG13 SPI6_SCK SPI6\nPG12 SPI6_MISO SPI6\nPG14 SPI6_MOSI SPI6\n"
"PC15 LSM6DSV_CS CS\nPE4 LSM6DSV_DRDY INPUT\n"),
# outputs in four timer groups (th-avionics): 1-2 TIM5 pusher and spare, 3-6 TIM1 (the 4-in-1's lift
# motors with DShot, or servos), 7-8 TIM4 elevons alone. BIDIR on TIM1 CH1 and CH3 gives bidirectional
# DShot rpm on all four, as in ArduPilot's Pixhawk6C-bdshot.
(r"^PA8 TIM1_CH1 TIM1 PWM\(1\) GPIO\(50\)\n.*?PA1 TIM5_CH2 TIM5 PWM\(8\) GPIO\(57\)\n",
"PA0 TIM5_CH1 TIM5 PWM(1) GPIO(50)\nPA1 TIM5_CH2 TIM5 PWM(2) GPIO(51)\n"
"PA8 TIM1_CH1 TIM1 PWM(3) GPIO(52) BIDIR\nPE11 TIM1_CH2 TIM1 PWM(4) GPIO(53)\n"
"PE13 TIM1_CH3 TIM1 PWM(5) GPIO(54) BIDIR\nPE14 TIM1_CH4 TIM1 PWM(6) GPIO(55)\n"
"PD14 TIM4_CH3 TIM4 PWM(7) GPIO(56)\nPD15 TIM4_CH4 TIM4 PWM(8) GPIO(57)\n"),
# Spektrum power pin is not on LQFP-144
(r"^# Control of Spektrum power pin\nPH2 .*\ndefine HAL_GPIO_SPEKTRUM_PWR 73\n\n# Spektrum Power is Active High\ndefine HAL_SPEKTRUM_PWR_ENABLED 1\n",
"# safety switch and LED on the FMU (were on the IO coprocessor)\nPE15 SAFETY_IN INPUT PULLDOWN\nPG0 LED_SAFETY OUTPUT\n\n"
"# enable pin of the external switched 12 V video BEC (relay 2)\nPG1 VTX_EN OUTPUT LOW GPIO(81)\n\n"
"# RSSI pad\nPC0 RSSI_IN ADC1 SCALE(1)\n\n"
"# TPS2121 status: low means POWER2 (IN2) is supplying the board; high means POWER1 or neither.\n"
"# Logged by the flight script. The VDD_BRICK*_nVALID inputs mean a supply is present, not in range:\n"
"# a brick cut off by the ~5.6 V overvoltage limit still reads valid (th-eda, SCHEMATIC-NOTES.md).\n"
"PF8 MUX_ST INPUT PULLUP GPIO(82)\n"),
# no internal compass: the GPS1 port's external compass is used
(r"^COMPASS IST8310 .*\nCOMPASS RM3100 .*\n", ""),
(r"^# compensate for magnetic field.*\ndefine HAL_HEATER_MAG_OFFSET .*\n", ""),
# IMU devices
(r"^SPIDEV bmi055_g SPI1 .*\nSPIDEV bmi055_a SPI1 .*\n",
"SPIDEV lsm6dsv SPI6 DEVID1 LSM6DSV_CS MODE3 2*MHZ 8*MHZ\n"),
(r"^# alternative to icm42688.*\nSPIDEV lsm6dsv .*\n", ""),
# peripheral 5 V switches active-high, so the better-stocked TPS2553 family fits (th-eda)
(r"^PC10 nVDD_5V_HIPOWER_EN OUTPUT LOW\nPE2 nVDD_5V_PERIPH_EN OUTPUT LOW\n",
"PC10 VDD_5V_HIPOWER_EN OUTPUT HIGH\nPE2 VDD_5V_PERIPH_EN OUTPUT HIGH\n"),
(r"^IMU Invensensev3 .*\nIMU LSM6DSV .*\nIMU BMI055 .*\n# BMI055 is replaced.*\n# They use the same.*\nIMU BMI088 .*\n",
"# rotations are the 6C's until the layout fixes the IMU orientation (th-eda)\n"
"IMU Invensensev3 SPI:icm42688 ROTATION_PITCH_180_YAW_90\nIMU LSM6DSV SPI:lsm6dsv ROTATION_PITCH_180_YAW_90\n"),
# DMA: the primary IMU first, then the lift DShot timer, so both get streams of their own; the sharing
# falls on the FRAM, a UART TX and the PWM-only TIM4/TIM5 (checked with chibios_hwdef.py, 4c98c92)
(r"^DMA_PRIORITY SDMMC\* USART6\* ADC\* UART\* USART\* SPI\* TIM\*\n",
"DMA_PRIORITY SPI1* TIM1* SDMMC* ADC* UART* USART* SPI* TIM*\n"),
# IO coprocessor firmware images
(r"^ROMFS io_firmware\.bin .*\n", ""),
(r"^# build ABIN.*\nenv BUILD_ABIN True\n", ""),
(r"^# enable support for dshot on iomcu\nROMFS .*\ndefine HAL_WITH_IO_MCU_BIDIR_DSHOT 1\n", ""),
]
BL = IDENT + [
(r"^PC14 BMI055_G_CS CS\nPC15 BMI055_A_CS CS\n", "PC15 LSM6DSV_CS CS\n"),
(r"^PC10 nVDD_5V_HIPOWER_EN OUTPUT LOW\nPE2 nVDD_5V_PERIPH_EN OUTPUT LOW\n",
"PC10 VDD_5V_HIPOWER_EN OUTPUT HIGH\nPE2 VDD_5V_PERIPH_EN OUTPUT HIGH\n"),
(r"USART6\* ", ""),
]
def apply(text, rules, name):
for pat, rep in rules:
new, n = re.subn(pat, rep, text, flags=re.M | re.S if ".*?" in pat else re.M)
if n != 1:
sys.exit(f"{name}: rule matched {n} times: {pat[:60]!r}")
text = new
return text
out.mkdir(parents=True, exist_ok=True)
for fname, rules, bl in (("hwdef.dat", MAIN, ""), ("hwdef-bl.dat", BL, "-bl")):
text = (src / fname).read_text()
text = re.sub(r"\A(#[^\n]*\n)+", "", text)
(out / fname).write_text(HEADER.format(bl=bl) + apply(text, rules, fname))
print("wrote", out / fname)
hw/flight-controller/Makefile · 36 lines · open · download
# Build the TerraHawk flight controller firmware in ArduPilot's Docker image.
# ARDUPILOT must be a checkout with submodules that nothing else is using (never the SITL tree).
ARDUPILOT ?=
# Pinned by digest (Ryan, 2026-10-03: tracked builds). This is the image :latest named on 2026-06-25; a tag
# can move to a new compiler without notice. SITL uses the same digest through mcardupilot.
IMAGE ?= ardupilot/ardupilot-dev-chibios@sha256:8bb0f850fb3fe1c170cb12dd577ab64f3708be8a828f9eafd28d73559766e81f
BOARD := TerraHawkFC
HERE := $(abspath .)
.PHONY: hwdef firmware lux-firmware clean
hwdef: ## Regenerate the hwdef from the Pixhawk6C hwdef in $(ARDUPILOT)
@test -n "$(ARDUPILOT)" || { echo "set ARDUPILOT=<private ArduPilot checkout>"; exit 1; }
uv run --no-project python make_hwdef.py "$(ARDUPILOT)"
firmware: hwdef ## Build bootloader and ArduPlane, outputs in build/
rm -rf "$(ARDUPILOT)/libraries/AP_HAL_ChibiOS/hwdef/$(BOARD)"
cp -r hwdef/$(BOARD) "$(ARDUPILOT)/libraries/AP_HAL_ChibiOS/hwdef/"
docker run --rm -v "$(ARDUPILOT)":"$(ARDUPILOT)" -w "$(ARDUPILOT)" -u "$$(id -u):$$(id -g)" -e HOME=/tmp $(IMAGE) \
bash -c 'git config --global --add safe.directory "*" && Tools/scripts/build_bootloaders.py $(BOARD) && ./waf configure --board $(BOARD) && ./waf plane'
mkdir -p build
cp "$(ARDUPILOT)/build/$(BOARD)/bin/arduplane.apj" "$(ARDUPILOT)/build/$(BOARD)/bin/arduplane_with_bl.hex" build/
cp "$(ARDUPILOT)/Tools/bootloaders/$(BOARD)_bl.bin" build/
@ls -la build/
lux-firmware: ## ArduPlane for the stock LUX F765 NDAA with QAUTOTUNE on (lux-qautotune.hwdef); outputs in build/lux/
@test -n "$(ARDUPILOT)" || { echo "set ARDUPILOT=<private ArduPilot checkout>"; exit 1; }
docker run --rm -v "$(ARDUPILOT)":"$(ARDUPILOT)" -v "$(HERE)":"$(HERE)":ro -w "$(ARDUPILOT)" -u "$$(id -u):$$(id -g)" -e HOME=/tmp $(IMAGE) \
bash -c 'git config --global --add safe.directory "*" && ./waf configure --board LumenierLUXF765-NDAA --extra-hwdef="$(HERE)/lux-qautotune.hwdef" && ./waf plane'
mkdir -p build/lux
cp "$(ARDUPILOT)/build/LumenierLUXF765-NDAA/bin/arduplane.apj" "$(ARDUPILOT)/build/LumenierLUXF765-NDAA/bin/arduplane_with_bl.hex" build/lux/
@ls -la build/lux/
clean:
rm -rf build

Source: hw/flight-controller/README.md in the repo. Edit it there; this page is generated.

Comments on

    Comments are reviewed before they appear. Please leave out places, addresses and other personal details.

    A Supported Systems Flight

    This build log is written and kept by Supported Systems, a small studio that makes thoughtful, user-first technology. The airframe, the hangar and the simulation are ours.

    Visit supported.systems