"""Write TerraHawkFC's hwdef as a diff of ArduPilot's Pixhawk6C hwdef. Usage: uv run --no-project python make_hwdef.py 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)