Charge Node
A small DroneCAN node on the aircraft that makes the hangar charge path safe to run unattended.
It reads the six cells of the pack, the charge current, the voltage on the landing shoes and the
pack temperature, and it drives the charge-enable switch and the balancer’s power. It runs
ArduPilot’s AP_Periph firmware from one board definition, so every function is stock and the
firmware flashes over the CAN bus. The reasoning is in hangar.md, “Charge path, revisited”.
This page is the design. Written 2026-09-26. The schematic is drawn and checked; layout waits on
the airframe fit.
| Item | Where |
|---|---|
| Board definition, bootloader, embedded defaults | hw/charge-node/hwdef/TerraHawkChargeNode/ |
| Build | hw/charge-node/Makefile, make firmware ARDUPILOT=<private copy> |
| Interlock script on the FC | tools/charge_interlock.lua |
| Firmware size | 168 KB of the F405’s 1 MB, bootloader 27 KB (built 2026-09-26 at ArduPilot 4c98c92) |
| KiCad project | terrahawk-eda/boards/charge-node/, five schematic sheets and two boards, ERC and DRC clean, netlist checked against the hwdef pin for pin on 2026-09-26, routed 2026-09-27 (EDA commit 59a5cfd) |
| Renders and gerbers | terrahawk-eda/docs/renders/charge-*.png; gerber sets frozen in terrahawk-eda/fab/releases/ as rev A rc5 (silkscreen only over rc4; copper byte-identical). rc2 redid the silkscreen, rc3 ships the CAN terminator jumper open, rc4 fixes the bridge controller’s bottom gate wiring, rc5 follows a pre-order review of every IC pin against its datasheet (terrahawk-eda/docs/charge-node-preorder-review.md); do not build a power stage from rc1 to rc3 |
| Thread with the EDA session | docs/agent-threads/charge-node-eda/ |
Where it sits
Section titled “Where it sits”The node is two boards joined by a five-wire lead, because the aft bay cannot hold it as one board beside the pusher ESC and the SiK radio. The split follows the circuit: the sense board (MCU, CAN, cell dividers, thermistor, LEDs) connects only to the pack’s balance lead, the thermistor and the CAN cable, all of which start at the pack. The power stage (bridge, fuse, charge switch, shunt) connects to the shoes and the pack leads. Both live in the battery bay. The same schematic builds as one board by leaving the two link connectors off; the nets are already joined.
Positions come from tools/space_map.py, a 2 mm voxel map of the free space inside the skin with
the printed parts, spars, pack and FC stack removed, fitted for all four builds (Survey3 or stock,
P50B or Tattu). Assembly frame, mm, and the picture is images/aft-bay-layout.png. The three
positions below are pinned in the tool, which fails if one is not wholly free, so the page and the
map cannot drift apart. The power stage sits 2 mm higher and 2 mm further port than the first
fit put it: at Z 3 it touched the pocket’s lower corner within the sampling noise, and at X -58 its
back face is 5.5 mm from the starboard wall, which gives the bracket a 2 mm wall plate and 3.5 mm bosses
deep enough for heat-set inserts (one M2 × 4 insert, ruthex RX-M2x4, at each of the four holes;
an M2 screw passes the board’s M2.5 hole) instead of screws threaded into 3.5 mm of plastic.
| Board | Hard outline | P50B pack | Tattu pack |
|---|---|---|---|
| Sense board | 55 × 25 mm, 8 mm total height with parts on both sides | Flat on top of the pack’s aft end under the battery hatch, centre (0, 237, 34), Z 30 to 38; 24 mm of spare length there | On edge against the pack’s port face, centre (14, 204, 12), 8 × 55 × 25 (X × Y × Z); the component face looks port into a 30 mm pocket |
| Power stage | 40 × 30 mm, 12 mm height | On its 40 mm edge in the starboard pocket of the battery bay beside the pack, centre (-58, 180, 5), 30 mm side vertical; 4 mm spare length | Same place, zero margin |
Three consequences. The sense board sits where the pack slides in and out. With the P50B it must hang from the underside of the battery hatch, not from the airframe: the pack rests against the nose taper and the board covers its aft 18.5 mm with about 1 mm to spare, so with the board fixed the pack can neither be lowered, tilted (3 degrees lifts it into the board) nor slid into place. The board comes out with the hatch on four corner standoffs of 7 to 9 mm, sized per corner because the hatch underside curves. Its four cables need a 60 to 80 mm service loop so the hatch can be lifted off and set beside the aircraft while still connected, and they leave the housings bending down or aft, never up, because the front hatch lock turns 3.9 mm above the board. The balance lead’s disconnect is at the pack, between the pack’s XH plug and the GH-8 harness, so a pack swap never touches the board. With the Tattu, a printed fence on the tray beside the pack carries four standoffs to the component face, screwed from the back with low-head M2s, and the pack lifts out without disturbing the board. The power stage rests on a ledge in the starboard pocket, held by bosses from a wall plate at four holes set in from the edges, with a zip-tie anchor about 8 mm aft of the pads so cable pull never reaches the solder joints. Its connectors follow one rule that both placements satisfy, checked face by face against the free-space map: the cable housings (CAN in, CAN out, the 8-pin balance-plus-thermistor, the 5-pin link, all JST-GH side-entry, the only family under 8 mm) sit on one long edge, which faces aft with the P50B and up with the Tattu; the USB-C sits on one short end, port with the P50B and aft with the Tattu (the same board, unmirrored, standing on edge), with three solder pads for an LED strip inboard of it. Nothing vertical fits: there is about 4 mm above the board in the flat position. The face away from the connectors stays under 2 mm of parts, because in the Tattu position it lies 2 mm from the pack. The power stage’s XT30 pigtails and feet leads leave from one 30 mm end. And the wiring volume is not in the model: the starboard pocket and the pusher ESC and SiK stack under the FC tray have no spare room for 12 AWG runs, so the routes get planned before the boards are glued in. The VTOL sections turned out to be solid prints, not shells, so the feet wiring runs from the wing roots forward about 90 mm into the bay.
| Link pin (J14 JST-GH 5-pin on the sense board, J15 JST-PH 5-pin on the power stage) | Net | Meaning |
|---|---|---|
| 1 | GND | Reference, tied at the pack-side shunt pad on the power stage |
| 2 | 3V3 | Supply for the current-sense amplifier |
| 3 | SHOE_V_ADC | Shoe voltage after its divider, 0 to 3 V |
| 4 | I_CHG_ADC | Charge current, 0.25 V per A |
| 5 | CHARGE_EN | 3.3 V logic to the switch gate driver |
The aircraft has two CAN buses (see electronics-notes.md, “Our own flight controller”). This node is
on bus 2, the sense bus: a line from the nose back through the airspeed sensor, the rangefinder
node and this sense board in the battery bay, the elevon servo nodes at the wing roots and the FC.
Bus 1 carries only the four lift ESCs, and both of its ends are terminator plugs beyond the last splice at each front pod, so every ESC’s terminator stays open and any ESC fits any pod. Each bus has a 120 Ω terminator at its two ends and nowhere
else. On bus 2 both ends are terminator plugs (a JST-GH 4-pin with 120 Ω across CAN high and
low): one in a three-way GH joiner beside the airspeed sensor, the other in the last connector of the
harness. The airspeed module does not fit the nose cone; it stands in the Fuselage Front’s starboard lobe
(th-blender and th-main, 2026-09-28) on a 60 mm stub from that joiner, with its own software
terminator off, so replacing the module never touches the bus’s termination. The ends do not depend on which nodes are fitted: first flights without servo nodes and
later ones with them use the same harness. Every node, this one included, has its terminator
open and passes the bus through its two CAN connectors; the FC hangs
off a short stub. Do not close a mid-bus jumper: a terminator in the middle of the bus and none at
the end is the configuration that produces intermittent dropouts that look like a wiring fault.
Static node ids: FC 10, charge node 20, airspeed 21, rangefinder node 22, lift ESC nodes 24 to 27 by
motor number, elevon servo nodes 28 (port) and 29 (starboard), and the lights node 70 (th-avionics, hw/lights-node defaults.parm CAN_NODE 70; on the LUX it shares the one CAN port and is turned on by params/payload-lights.parm only when the lights are fitted; GH-4 connectors with pin 1, VCC, not used; its terminator is behind JP1, open as shipped: close it only if the lights node is the last node on the bus). Id 23 was the GPS node; the GPS now
connects to the FC directly (2026-09-28), so 23 is unused. The bus runs at 1 Mbit/s on the four-wire
Pixhawk cable: 5 V, CAN high, CAN low, ground. The node draws about 60 mA from the cable’s 5 V.
Select the drawing to open it at full size.
The aircraft-side charge path
Section titled “The aircraft-side charge path”flowchart LR
SP[Starboard shoes<br/>8 pogo pins] --> A
SS[Port shoes<br/>8 pogo pins] --> B
A & B --> BR[Ideal-diode bridge<br/>LT4320 + 4 N-MOSFETs]
BR -- "+" --> F1[Fuse 10 A] --> Q5[Charge-enable<br/>P-MOSFET] --> PP[Pack +]
BR -- "−" --> SH[Shunt 5 mΩ] --> PN[Pack −]
A & B -. diode-OR, 11:1 .-> ADC12[Shoe voltage]
PP -. 11:1 .-> ADC10[Pack voltage]
SH -. INA180A2 .-> ADC11[Charge current]
BAL[Balance lead<br/>C1 to C6] -. 10:1 each .-> ADC1[Cell taps]
NTC[NTC on the pack] --> ADC13[Pack temperature]
N[Charge node<br/>STM32F405, AP_Periph] --> Q5
N --> Q6[Balancer power switch]
N <-- CAN --> FC[Flight controller, CAN bus 2]
Bridge. An LT4320 drives four N-channel MOSFETs as a full bridge. It accepts 9 to 72 V, so the 6S pack and a charger at up to 30 V are inside its range. One wiring detail is not obvious and cost a release candidate: the controller’s bottom gate outputs cross over, BG1 drives the bottom MOSFET on input 2 and BG2 the one on input 1, as in Analog Devices’ own LTspice example. Wired straight across, the controller shorts the live input to the return. rc4 has it right, checked against ADI’s LT4320 model in simulation. With no plate voltage the MOSFETs are off and their body diodes point away from the shoes, so the pack never reaches the shoes. Either plate polarity charges. Forward drop is about 20 mV per leg, so at 5 A the bridge dissipates a fraction of a watt.
Fuse. A 10 A Littelfuse NANO2 (0453010.MR, board-mounted, not field-replaceable) in the positive leg between the bridge and the switch. It protects the wiring if a bridge MOSFET fails short, since the charger itself is limited to 6 A.
Charge-enable switch. Two P-channel MOSFETs back to back: one with its drain on pack positive,
the other with its drain on the fused bridge positive, sources tied, one gate. With the gate off
nothing conducts in either direction. The first draft used a single MOSFET, which blocks bridge to
pack but lets its body diode carry pack to bridge. That was thought harmless because the bridge
blocks the path to the shoes, but it keeps the LT4320 powered from the pack, and the LT4320 draws
about 1.5 mA, roughly 1 Ah a month from a pack sitting on the pad. The pair removes that drain; the
LT4320 now boots from the plates through the bridge body diodes when a charger arrives, which is
how it is designed to start. A 100 kΩ resistor holds the gate at the sources and a 12 V zener
clamps it, so the switch is off with the node unpowered, in reset, or with the link unplugged
(there is a second pull-down on the driver). A 2N7002 from the node’s CHARGE_EN pin pulls the
gate down through 10 kΩ to turn the switch on. At 5 A the pair dissipates about 1 W with 20 mΩ
parts; both tabs get copper.
Current. A 5 mΩ 2512 shunt in the negative leg, ground-referenced, read by an INA180A2
(50 V/V): 0.25 V per amp, 13 A full scale on a 3.3 V ADC. BATT_AMP_PERVLT is 4.0.
Voltages. Pack voltage is taken from the balance lead’s top tap through 1 MΩ over 100 kΩ,
BATT_VOLT_MULT 11, so pack positive never crosses the link and the reading is valid whether the
switch is open or closed. Shoe voltage through a BAV70 (a 70 V common-cathode diode pair; the
BAT54 first chosen is only rated 30 V, which is the charger’s ceiling with no margin), one anode on
each bridge input, into a 100 kΩ over 10 kΩ divider on the power stage. It reads whichever shoe is
energised minus 0.7 V, without caring about polarity. This is the contact sense: it shows plate
voltage before any current can flow, independent of the pack voltage reading. The 5 V probe reads
about 4.3 V; the interlock’s threshold is 3 V.
Cells. Each balance-lead tap goes through a 910 kΩ over 100 kΩ divider, 0.1 %, to its own ADC
channel (Viking ARG03BTC9103, LCSC C2686431; terrahawk-eda a1d96ca). The divider ratio is fixed in
the firmware as SCALE(10.1) and there is no per-cell
calibration parameter, so the resistor tolerance is the cell-voltage accuracy: 0.1 % on a 25 V tap
is 25 mV. AP_Periph reads the taps as cumulative voltages from pack negative and reports the
differences as the six cell voltages. 100 nF to ground at each ADC input; the capacitor supplies the
sample charge, so the 100 kΩ source impedance does not trouble the ADC.
The values are high on purpose. The balance lead is plugged in whenever the pack is in the aircraft, node powered or not, and the dividers load the pack the whole time. Each tap’s divider draws from every cell below it, so cell 1 carries the sum of all six and cell 6 only its own. With 180 kΩ over 20 kΩ that was about 0.67 mA on cell 1 against 0.13 mA on cell 6, roughly half an amp-hour of imbalance a month on the 10 Ah pack. At 910 kΩ over 100 kΩ it is about a fifth of that. Do not fit 900 kΩ parts with this firmware: the ratio is then 10.0, and every cell reads 1 % high (about 0.04 V).
Temperature. A 10 kΩ NTC (B 3435) taped to the pack, with a 10 kΩ pull-up to 3.3 V. The analog temperature backend converts pin volts to degrees with a polynomial. For this thermistor and pull-up the fit over -10 to 70 °C is within 0.08 °C:
| Parameter | Value |
|---|---|
TEMP1_A0 |
134.58 |
TEMP1_A1 |
-164.769 |
TEMP1_A2 |
126.473 |
TEMP1_A3 |
-62.6887 |
TEMP1_A4 |
16.4309 |
TEMP1_A5 |
-1.81505 |
Check three points after fitting the real part: 0 °C reads 2.447 V, 25 °C reads 1.650 V, 45 °C reads 1.077 V. A different thermistor needs its own fit; the script that produced these is a ten-line NumPy polyfit, reproduced in the git history of this page.
Balancer power. The active balancer’s negative lead goes through an N-channel MOSFET
(AO3400) driven by the node’s BAL_EN pin, so the balancer only runs while the interlock says
charging is under way. Check that the balancer chosen tolerates having its negative lead opened;
most of the small capacitive ones do, since they only see the cell taps.
Node power. 3.3 V from the CAN cable’s 5 V through an LDO (AP2112K-3.3). A USB-C receptacle feeds the same rail through a Schottky so the node runs on the bench from USB alone, and the bootloader uses it for the first flash. The bus 5 V enters through a P-channel load switch whose gate is tied to USB VBUS: with no USB it is on and drops nothing, so the CAN transceiver sees the full 5 V; with USB present it turns off and its body diode points inward, so a node powered from USB can never push voltage back into the FC over the bus. The node is alive whenever the FC is, which is whenever the aircraft is on the pad with the pack connected.
Brackets
Section titled “Brackets”Four printed PETG parts from tools/make_charge_brackets.py, in parts/, pictured in
images/charge-brackets.png. Each is modelled oversize against the airframe and then has the airframe’s
parts subtracted, so its glue faces follow the wall, floor or hatch exactly.
| Part | Mass | What it does |
|---|---|---|
| Charge Power Bracket | 6.6 g | Wall plate glued to the flat starboard wall at X -68, bosses with M2 × 4 heat-set inserts at the four holes, a ledge under the bottom edge at Z -10 with lips into the part-free band, and a zip-tie tunnel aft at Y 206 to 210 where the shoe and pack pigtails are strain-relieved |
| Charge Sense Floor Tray | 4.5 g | For either pack (Ryan, 2026-10-04): the sense board lies flat on the floor under the FC deck, at X -12.5 to 12.5, Y 255 to 310. Glue the tray to the floor across the joint between the Fuselage Front and Mid B. Its underside follows the floor and its top is flat. Four 4.5 mm bosses lift the board 2 mm over its back-side parts. Drive M2 thread-forming screws down through the board into the bosses. The USB-C end is aft, under the open middle of the deck. The GH plugs mate toward -X, with 7.8 mm of room for the plugs and their bends. Clearances: 0.86 mm to the FC Mount, 2.3 mm to the front spar, 1 mm to the 155 x 50 x 58 pack at its aft-most position |
| Charge Sense Hatch Mount | 5.1 g | Superseded by the floor tray (2026-10-04). It was for the P50B: two rails at X ±25 and a forward crossbar under the front hatch, with four standoffs down to the board’s connector face |
| Charge Sense Fence | 12.2 g | Superseded by the floor tray (2026-10-04). It was for the Tattu only: a vertical plate on the tray beside the pack, with the pack against the starboard wall (X -36 to 6.5) |
| Channel Drill Guide, starboard and port | cancelled 2026-10-10, do not print | Aimed the motor-wire exit drill at (±68, 208, 2); that bore is the wing spar’s seat, and the motor leads use the wire channels instead: on Fuselage C, the channels ahead of the front spar (th-blender, th-main) |
Wiring
Section titled “Wiring”Note: This section gives the Fuselage B harness. On Fuselage C v2.4B the lift motor leads go through the wire channels ahead of the front spar (20 x 10 mm at Y 232.5 to 252.5, mouths at X ±78.5), not through the Fuselage Mid stub. th-main’s v2.4B harness pass replaces this section. Until then, do not cut leads to the lengths below.
Routes from tools/wire_routes.py: a shortest path on the 2 mm grid for each cable radius, around the
fitted items, the pack and the brackets, with one hard rule: no route crosses the column above the
pack, so the pack can always lift out. Steps along a wall cost less, so the runs hug walls where they
can be tied. Cut length is path × 1.15 plus 20 mm. images/charge-wiring.png shows the first-flight
harness: the 18 leads of the first build on Fuselage B with the LUX F765, which has no charge node
(2026-10-04), the lift 4-in-1 flat in the belly between the spars and the pusher ESC beside it (2026-10-05). The script also gives
each power lead’s closest approach to the Q43’s base, against the compass rule: 60 mm for a twisted pair
or bundle, 150 mm for a lone conductor. The router keeps every power lead outside that limit. The one
exception is the pusher ESC’s three phase leads to the tail: the fuselage narrows to 57 mm either side
at about Y 430, so 57 mm is the most any path can keep. th-telemetry accepts 57 mm for that triple
(2026-10-05) with a twist of 1 turn in 22 mm or tighter from Y 330 to 480: three phase leads carry no net
DC, and the field of a twisted bundle falls with distance divided by pitch. The bench compass fit is the
final check. The charge-node routes come back with the charge node (wire_routes.py --charge-node).
Warning: do not drill. The exit is under revision (th-blender, 2026-10-10). The bore that ends at X ±101 is the seat of the 8 mm wing spar, swept 28 degrees out to the Winglet, not a wire channel: a drill removes the spar’s inboard stop. Do not print the Channel Drill Guides. On Fuselage C v2.4B the motor leads go through the wire channels ahead of the front spar, which are already open into the bay. The text below stays only until the revised routes replace it.
The shoe leads travel with the lift motor wires. The kit runs each side’s motor wires inside the 14 mm VTOL spar, out through the spar’s 45 degree wire slot, and into the kit’s own 25 x 10 mm wire channel through the Fuselage Mid’s wing-root stub, Y 283 to 308, which opens into the bay at X about ±62 beside the Lucid (th-blender and th-main, 2026-10-10). Until 2026-10-10 this section described a 10 mm hole drilled at (±68, 208, 2) into what was taken for the end of a moulded 8 x 8 mm channel; that bore is the seat of the 8 mm wing spar, not a wire channel, and the drill is cancelled (the warning above). The shoe leads share the kit channel: the two feet on a side are the same plate polarity, so one 16 AWG per side (or two, joined in the bay) runs in the spar and the channel. The first build flies the kit’s 4-in-1 lift ESC (Ryan, 2026-09-29), so each channel carries its two motors’ phase leads (6 x 18 AWG), the shoe lead and the keel antenna coax: about 41 mm² on the port side, which carries two coax runs, and 36 mm² on the starboard side (2.3 mm over 18 AWG silicone), 14 to 16 % of the 250 mm² channel. The DroneCAN pod path that follows puts the two pod power feeds (4 x 14 AWG) and CAN bus 1 there instead, about 58 and 53 mm², 21 to 23 %; the earlier “too full to pull” verdict was against the 8 x 8 mm channel that does not exist. The shoe lead ends and the power bracket’s zip-tie tunnel, which sat at the old exit, need a new place at the channel mouth (th-blender). Nothing is cut in a kit part.
| Route | P50B, cut | Tattu, cut | Where it goes |
|---|---|---|---|
| 4-in-1 feed, XT60, 2 x 12 AWG | 210 | 166 | From the power junction to the 4-in-1 on the starboard bay wall |
| Phase leads, 6 x 18 AWG a side | port 262, starboard 59 | port 264, starboard 59 | Out of the cradle’s two aft notches (th-blender, 3d353be): the lower, Z -7 to 1, for the starboard bundle; the upper, Z 10 to 18, for the port bundle, which crosses the bay. In the bay only; then the channel and the spar to the motors |
| Starboard shoe, 16 AWG | 137 (bay path 102) | not re-run | From the starboard channel mouth to the power stage (th-main 6f7e740, 2026-10-10, ESTIMATE) |
| Port shoe, 16 AWG | 232 (bay path 185) | not re-run | From the port channel mouth across the bay to the power stage (th-main 6f7e740, ESTIMATE) |
| Pack tap, T-branch, XT30 | 155 | 129 | From the main feed’s junction to the power stage |
| Link, PH-5 to GH-5 | 216 with loop | 245 | Up the pocket to the sense board on the hatch (the loop is for the P50B’s hatch-mounted board) |
| CAN bus 2, GH-4 | 232 with loop | 201 | The charge node’s CAN to the FC, with the 6C Mini turned so its ports face aft |
In-bay cut lengths, from th-blender’s rebuild of the bay model (6f16815 and 5edf520, P50B pack,
Survey3 build), re-run for the 4-in-1 on 2026-09-29 (tools/wire_routes.py, both packs); the full list of record is in
agent-threads/harness/. The shoe leads and the phase leads (or the pod feeds, on the pod path) also need the spar run from each tip to the slot at Y about 295.5 (about 229 mm front, 190 mm rear; the earlier
177 and 239 were measured to the wing spar bore) plus about 130 mm
through the kit channel, measured from the stub mouth (th-main, 2026-10-10; was 140 through the cancelled
drill). The in-bay shoe rows below are th-main’s 6f7e740 run from the channel mouths (P50B; Tattu not re-run); wire_routes.py --charge-node also prints the PH-5 link’s parallel length within 20 mm of hub B’s board, its BATT tap and its 5 V pair
(0, 56 and 54 mm with the sense board on top of the pack; the board is the one that must stay clear, the two DC leads
are tolerable at that length, see the sense board’s filtering in the circuit section), and the compass distances
(126 and 120 mm).
Warning: do not drill (see above; th-blender, 2026-10-10).
Order of work for the exit, superseded 2026-10-10: the drilled exit and its guide are cancelled (see the warning). The wires enter the bay from the kit channel’s mouth at X about ±62, Y 283 to 308; the power bracket is glued where th-blender’s revised bay model puts it (moved 3.5 mm aft for hub B, 2026-10-09), entering through the hatch opening and sliding about 10 mm outboard under the pocket’s side lip. Routes are laid biggest first, each reserving its own volume, so a shared passage holds all of its cables. Tie points: the tunnel on the power bracket for the shoes and the pigtail, the starboard wall along the pocket, and the spar top.
Two endpoints are assumed until confirmed. The pack’s lead leaves its aft face near the centreline; both packs are specified that way. The FC’s CAN ports are on the 6C Mini’s aft face, because the board is turned 180 degrees (Ryan, 2026-09-28).
Harness
Section titled “Harness”The power-stage pigtails and their mates follow one rule: the side that is live when unplugged carries the female XT30, with its recessed contacts, and the power stage carries the male on all three, because with the switch off and the bridge unpowered its pads are dead. The plates are live when the pad is energised, so the shoe leads end female. The pack is always live, so its tap ends female. That also means neither shoe pigtail can be plugged into the pack tap by mistake: the two shoe pigtails and the pack pigtail are all male, and only female ends exist on the airframe side. Each shoe carries one conductor, so both pins of a shoe XT30 are paralleled on both sides of the joint, on the board and in the lead.
| Cable | Ends | Wire | Cut length | Source |
|---|---|---|---|---|
| Shoe pigtail, port, J4 | Board pads to XT30 male | 2 × 16 AWG silicone, both the same conductor | 40 mm | Pre-made Amass XT30U-M 16 AWG pigtail, cut |
| Shoe pigtail, starboard, J5 | Board pads to XT30 male | same | 40 mm | same |
| Pack pigtail, J6 | Board pads to XT30 male, + on pad 1 | 2 × 16 AWG | 136 mm (P50B), 118 mm (Tattu) | Pre-made Amass XT30U-M 16 AWG pigtail, extended or cut |
| Shoe lead, starboard | Feet (both shoes on the side joined at the spar) to XT30 female at the zip-tie tunnel | 16 AWG silicone, one conductor into both pins | spar run 229 (front) or 190 mm (rear) plus about 130 mm through the kit channel from the stub mouth, plus 137 mm in the bay (th-main 6f7e740) | XT30U-F housing, crimped |
| Shoe lead, port | same, across the bay behind the pack | same | as starboard but 232 mm (P50B) in the bay; Tattu not re-run | same |
| Pack tap | A T-branch off the pack side of the main feed (10 AWG since 2026-09-28; an XT90-S male on the aircraft side and the pack’s XT90 female since 2026-10-04) to an XT30 female; charge current only, never in series with the main feed | 16 AWG branch | 155 mm branch | Soldered into the main feed’s junction by th-main’s harness, XT30U-F on the branch |
| Servo node DC tap (when the servo nodes are fitted) | A fused tap and its own negative wire, both from the pack junction; never from a point on a lift pod’s feed | as the servo node needs | from the junction | th-eda and th-avionics, 2026-09-28. A tap on a pod feed puts the pod’s return drop (about 0.13 V at 50 A, ESTIMATE) across the servo node’s 26 AWG CAN ground, about 0.8 A in it; from the junction on its own negative, about 0.1 A |
| Link | GH-5 at the sense board to PH-5 at the power stage, pin 1 to pin 1 | 5 × 26 AWG | 197 mm (P50B, with the 80 mm loop); Tattu not re-run | JST-GH pre-crimped leads into a PH housing with PH crimps |
| CAN in, CAN out | GH-4 to GH-4, Pixhawk pinout | 4 × 26 AWG | 236 mm (P50B, with loop), to the FC; Tattu not re-run | The cable supplied with the airspeed sensor, or crimped from parts-list.md H13 to H15; the H4 kit’s pre-crimped leads are 150 mm |
| Balance and thermistor | XH-7 mating the pack’s balance plug to GH-8 at the sense board; NTC on pins 1 and 8 | 7 × 22 AWG plus the thermistor pair | 150 mm plus the 80 mm loop | A 6S JST-XH balance extension lead, one end cut and re-terminated GH, thermistor soldered in |
| Balancer power | Two 1.27 mm pads on the sense board to the balancer’s negative lead | 22 AWG | as the balancer needs | Soldered |
| LED strip | Three 1.27 mm pads to the strip | 3 × 26 AWG | as fitted | Soldered |
| Shoe lead chokes | One snap-on ferrite on each shoe lead where it leaves the keel’s rear core, before it enters the spar | fits 16 AWG | none | A ferrite specified for impedance at 900 MHz, not only at HF. th-telemetry’s NEC2 model (8837eed) shows the unchoked lead in the keel lowers the SiK antenna’s gain from -1.7 to -3.3 dBi |
The XT30s are not on the board bill of materials because they are wire parts; they and the rest of this table are the harness bill of materials, and it lives here.
Manufacturer part numbers. LCSC numbers get filled in when the BOM is placed, and the genuine TDK-style clone problem does not arise here: none of these parts has a common counterfeit.
| Ref | Part | Qty | Notes |
|---|---|---|---|
| U1 | STM32F405RGT6 | 1 | LQFP64, 8 MHz crystal, 2 × 12 pF |
| U2 | TJA1051T/3 | 1 | CAN transceiver, 3.3 V logic, 5 V supply |
| U3 | LT4320IDD | 1 | Ideal-diode bridge controller |
| Q1 to Q4 | PSMN7R0-60YS | 4 | N-MOSFET, 60 V, 6.4 mΩ, LFPAK56 (stocked; the 4 mΩ part was not) |
| Q5, Q9 | SUD50P06-15L (VBsemi 60 V 20 mΩ part as the alternate) | 2 | P-MOSFET pair, back to back, DPAK |
| Q6 | AO3400 | 1 | Balancer power switch |
| Q7 | 2N7002 | 1 | Gate driver for Q5 |
| U4 | INA180A2IDBVR | 1 | Current-shunt amplifier, 50 V/V |
| U5 | AP2112K-3.3 | 1 | LDO, 600 mA |
| R_sh | RLP25FEGMR005 | 1 | 5 mΩ, 3 W, 2512 |
| R | 910 kΩ (Viking ARG03BTC9103), 100 kΩ, 0.1 % | 6 + 6 | Cell dividers, ratio 10.1 |
| R | 1 MΩ over 100 kΩ; 100 kΩ over 10 kΩ, 1 % | 2 + 2 | Pack divider (from the balance lead top tap); shoe divider |
| R | 10 kΩ 1 %, 10 kΩ NTC B3435 | 1 + 1 | Thermistor and pull-up |
| D1 | BAV70 | 1 | Shoe sense diode-OR, 70 V common-cathode pair |
| D3 | 12 V zener | 1 | Q5 gate clamp |
| R_t | 120 Ω | 1 | CAN terminator behind solder jumper JP1, which ships open because this node sits mid-bus on bus 2 |
| F1 | NANO2 SMD fuse, 10 A (Littelfuse 0453010 class) | 1 | Not field-replaceable; it only opens on a hard fault |
| C | C32 10 µF 50 V X7R 1210 (Murata GRM32ER71H106KA12L, LCSC C77102) plus C31 1 µF across the bridge output | 1 + 1 | The LT4320 needs at least 10 µF there. No 22 µF 1210 rated well above the 30 V charger ceiling is in stock, so 10 µF (Ryan, 2026-10-01; terrahawk-eda a1d96ca). The frozen rc6 schematic still prints 22 µF; the BOM and the fitted part are 10 µF |
| U6 | 74AHCT1G125 | 1 | Level shifter so the WS2812 sees a 5 V data signal |
| D4 | B5819W | 1 | USB VBUS into the 5 V rail |
| Q8 | AO3401A | 1 | Load switch on the bus 5 V input, off while USB is present |
| D6 | NUP2105L | 1 | TVS across CAN high and low |
| J12 | USB-C receptacle, 16-pin | 1 | Bootloader and bench power |
| J1, J2 | JST-GH 1.25 mm 4-pin | 2 | CAN in and out, Pixhawk pinout |
| J4, J5 | XT30 pigtails on 16 AWG through-holes | 2 | Bridge inputs from the shoes (one per side); pigtails, not board-mount, where the wiring vibrates |
| J6 | XT30 pigtail | 1 | To the pack’s main leads |
| J14, J15 | JST-GH 5-pin at the sense board, JST-PH 2.0 mm 5-pin at the power stage | 2 | The link between the two boards; GH for height on the sense board, PH for vibration at the power stage |
| J3 | JST-GH 8-pin | 1 | Balance lead on pins 1 to 7 (1 = pack negative) and the thermistor on pin 8, through an adapter pigtail from the pack’s XH-7 |
| J13 | Two 1.27 mm pads, no part | 1 | Balancer negative lead, switched by Q6 |
| J8 | Three 1.27 mm solder pads (5V, DAT, GND) | 1 | WS2812 strip; a connector did not fit on the 25 mm end beside the USB-C and the corner screws |
| J9 | Tag-Connect TC2030 footprint, unpopulated | 1 | SWD |
| J10, J11 | 1.27 mm pads, unpopulated | 2 | Debug UART, I2C |
| LED | WS2812B | 2 | Status, visible through the hatch |
Estimate for five sets from a prototype service: about $40 in parts per set and $70 each
assembled including setup, two boards per set. Estimate, not a quote. Manufacturer part numbers
above are the ones the stockist carries; the EDA repo’s docs/charge-node-parts.md records the
originals and the reasons for each swap.
Pin map
Section titled “Pin map”From hwdef.dat, checked against the schematic netlist on 2026-09-26. Analog pin numbers are
what the parameters use.
| MCU pin | Function | Analog pin | Parameter |
|---|---|---|---|
| PC0, PC1, PC2, PC3, PA0, PA1 | Cell taps 1 to 6, SCALE(10.1) |
1 to 6 | BAL_CELL1_PIN 1 |
| PA6 | Pack voltage | 10 | BATT_VOLT_PIN |
| PA5 | Charge current | 11 | BATT_CURR_PIN |
| PA4 | Shoe voltage | 12 | BATT2_VOLT_PIN |
| PA7 | Pack thermistor | 13 | TEMP1_PIN |
| PB0 | CHARGE_EN |
GPIO 50 | RELAY1_PIN |
| PB1 | BAL_EN |
GPIO 51 | RELAY2_PIN |
| PA15 | WS2812 data, OUT1 | GPIO 58 | OUT1_FUNCTION 120 |
| PB8, PB9 | CAN1 RX, TX | ||
| PC13 | CAN silent | ||
| PA9, PA10 | Debug UART | ||
| PB6, PB7 | I2C1, spare header | ||
| PA13, PA14 | SWD | ||
| PA11, PA12 | USB, bootloader only | ||
| PC12 | Activity LED |
Parameters
Section titled “Parameters”On the node
Section titled “On the node”Embedded in the firmware from defaults.parm. Battery ids are what the FC matches on.
| Parameter | Value | Why |
|---|---|---|
BATT_MONITOR |
4 | Pack: analog voltage and current |
BATT_SERIAL_NUM |
10 | Reported as battery id 10 |
BATT2_MONITOR |
3 | Shoe side: analog voltage only |
BATT2_SERIAL_NUM |
11 | Reported as battery id 11 |
BAL_NUM_CELLS, BAL_ID |
6, 10 | Cell voltages attach to battery 10 |
TEMP1_TYPE, TEMP1_PIN |
5, 13 | Analog thermistor |
TEMP1_SRC, TEMP1_SRC_ID |
3, 1 | Temperature goes into battery 1’s report |
RELAY1_FUNCTION, RELAY2_FUNCTION |
10, 11 | DroneCAN hardpoint 0 and 1 |
RELAY1_DEFAULT, RELAY2_DEFAULT |
0, 0 | Both off at boot |
OUT1_FUNCTION, NTF_LED_LEN |
120, 2 | Two NeoPixels on OUT1 |
On the flight controller
Section titled “On the flight controller”| Parameter | Value | Why |
|---|---|---|
CAN_P1_DRIVER |
1 | Enable CAN port 1 |
CAN_D1_PROTOCOL |
1 | DroneCAN |
ARSPD_TYPE |
8 | DroneCAN airspeed sensor |
BATT2_MONITOR, BATT2_SERIAL_NUM |
8, 10 | Node pack data as battery 2 |
BATT3_MONITOR, BATT3_SERIAL_NUM |
8, 11 | Node shoe voltage as battery 3 |
BATT2_LOW_VOLT, BATT2_CRT_VOLT, BATT3_LOW_VOLT, BATT3_CRT_VOLT |
0 | No failsafes from these; the FC’s own sensor keeps them |
BATT2_FS_LOW_ACT, BATT2_FS_CRT_ACT, BATT3_FS_LOW_ACT, BATT3_FS_CRT_ACT |
0 | Same |
RELAY3_FUNCTION, RELAY3_PIN |
1, 1000 | Charge enable, hardpoint 0 |
RELAY4_FUNCTION, RELAY4_PIN |
1, 1001 | Balancer power, hardpoint 1 |
RELAY3_DEFAULT, RELAY4_DEFAULT |
0, 0 | Off at boot |
SCR_ENABLE |
1 | Lua, then reboot and copy charge_interlock.lua to APM/scripts/ on the SD card |
The interlock
Section titled “The interlock”tools/charge_interlock.lua runs on the FC every 500 ms. It closes the charge-enable switch only
while all of these hold, and opens it as soon as one fails:
- Disarmed and not flying.
- Node pack and shoe data healthy.
- Shoe voltage above 3 V: the pad is probing or charging.
- Pack temperature known, between 2 and 40 °C to start; once charging, it may continue to 45 °C and resumes below 40 °C again (hysteresis, so a warm pack does not chatter). 45 °C is Ryan’s ceiling (“must not charge above 45 °C”, 2026-10-06). A reading at either ADC rail (below -20 or above 80 °C after the polynomial, or INT16_MAX in BATTERY_STATUS) is an open or shorted thermistor: “unknown”, no charge. For reference, the Molicel P50B data sheet (80122) allows -20 to 60 °C ambient and a 70 °C cell-surface cut-off while charging at up to 25 A, so there is room to loosen the band if Ryan wants it; the dock (docs/hangar.md, th-hangar) and the interlock script use the same numbers.
- All six cells above 2.8 V, and their sum within 0.6 V of the pack voltage.
- No cell above 4.20 V. Once a cell trips, the switch stays open until that cell is below 4.15 V.
The balancer power follows the charge current: on above 0.2 A, off otherwise. On load the script opens both switches before its first check. State changes and blocks are announced to the GCS, which the dock controller reads over the SiK link.
The script also resets the FC’s battery count after a full charge. Battery instance 0 counts the
mAh used since boot and does not count the charge. Thus, after a dock charge with no reboot, the
capacity triggers (BATT_LOW_MAH, BATT_CRT_MAH) still see the last flight, and the pre-arm
check refuses to arm. The script sets instance 0 to 100 % when all of these hold for 30 s:
- A charge current above 0.2 A flowed since boot or since the last reset.
- Disarmed, and the node pack data healthy.
- All six cells at 4.15 V or more.
- The charge current below 0.5 A (C/20 for the 10 Ah P50B).
A partial charge does not reset the count. The count then shows more use than is true, which is the safe direction. The GCS shows “charge: full, min cell …, battery count reset” when the script resets the count.
The handshake, revised
Section titled “The handshake, revised”The ground-side sequence in hangar.md changes in two places, both because the node now sees the
shoe side.
- Aircraft reports landed and disarmed. Roof closed.
- Probe: the supply goes to 5.0 V with a 50 mA limit. This is below the pack voltage, so the bridge passes nothing, but the shoe sense sees it.
- Within 2 s the FC reports battery 3 above 3 V. If not, a shoe is off its plate: alert, do not raise the voltage.
- The interlock now closes the switch (shoe voltage is one of its conditions).
- Ramp the supply upward under a 100 mA limit until the node reports charge current above 50 mA, with a ceiling 1 V above the reported pack voltage. If the ceiling is reached with no current, the aircraft has refused: read the interlock’s reason from the GCS messages.
- Raise the current limit to the charge rate. Set the voltage ceiling from the chemistry: 23.1 V storage or 25.2 V full for LiPo, 21.6 to 22.2 V storage or 25.2 V full for the Li-ion pack.
- Stop on target voltage, on any cell at 4.20 V as reported by the FC, or on loss of telemetry for 10 s. De-energise the plates before the roof opens.
- Launch: with the roof at its open limit and the people guard clear, the supply goes to 12.0 V
with a 50 mA limit. This is the “site clear” signal for the pre-arm check on the aircraft
(
clear-to-launch.md): battery 3 between 11 and 13 V. The interlock does not close the switch at this level; it only closes on the ramp in step 4. The supply returns to 0 V when the aircraft reports armed and airborne, or at once if the guard trips.
Bench test
Section titled “Bench test”WARNING: Remove all five props before the test. A servo output or an arming fault can start a motor.
Do this once with a lab supply standing in for the pad, before any pad exists.
- Connect the node to the FC’s CAN port. Power the FC from USB and feed 5 V to the node’s CAN connector J1 as well: on USB alone the node’s 5 V rail sits at about 4.65 V behind its Schottky, marginal for the CAN transceiver’s 4.5 V minimum, so a USB-only node may not talk on the bus.
- In Mission Planner’s DroneCAN tab, confirm the node appears and its parameters read back.
- Connect a 6S pack through the balance lead only. Check the six cell voltages in the FC’s battery 2 status against a multimeter. Each must be within 30 mV.
- Tape the thermistor to the pack. Check battery 2’s temperature against a thermometer.
- Connect the pack’s main leads to J6. Connect the lab supply, off, to J4 with either polarity.
- Set the supply to 5.0 V, 50 mA limit, and turn it on. Battery 3’s voltage must rise above 3 V and the interlock must report “enabled, waiting”.
- Ramp the supply toward the pack voltage under a 100 mA limit. Current must appear in battery 2 at about the pack voltage. Set the limit to 1 A and hold for 5 min. The bridge MOSFETs and Q5 must stay cool to the touch.
- Swap the supply polarity at J4 and repeat step 7.
- Warm the thermistor above 45 °C with a finger and a hot cloth. Current must stop within 1 s and the GCS must show “pack hot”.
- Disconnect one cell tap. Current must stop and the GCS must show “cell missing”.
- Arm the aircraft with the props off. Current must stop and the GCS must show “armed”.
- Turn the supply off. Battery 3’s voltage must fall to 0 and the shoes must read 0 V.
If every step passes, the pad can be wired.
Not decided
Section titled “Not decided”- The pack. The design assumes a 6S pack with a balance lead. The Li-ion recommendation in
battery.mdneeds a pack that exposes one; check before ordering. - The balancer part, and whether opening its negative lead is a clean off.
- The board id. 20401 is not registered with ArduPilot. It must match between the bootloader and
the firmware. This page is public, and another project can register the same number. To
get a registered id, open a pull request to
Tools/AP_Bootloader/board_types.txt. That is Ryan’s decision. - Layout. Done: both boards are at rev A rc6 in
terrahawk-eda/boards/charge-sense/andcharge-power/, frozen 2026-09-29 (see Ordering below). - Ordering. Both boards are routed and DRC clean (power stage 165 tracks, 91 vias, 2 oz outer
copper for the 10 A return; sense board 1025 tracks, 97 vias, 1 oz). The recommended order is
five of each from JLCPCB, 2-layer 1.6 mm, economic assembly for the sense board, the power stage
assembled with the LT4320 consigned or hand-built. The order is Ryan’s decision. The bill of
materials is
terrahawk-eda/docs/charge-node-bom.md.
The source files this page describes, as they are in the repo.
hw/charge-node/hwdef/TerraHawkChargeNode/hwdef.dat · 133 lines · open · download
# TerraHawk charge node: AP_Periph DroneCAN node for the hangar charge path.# Reads the pack's balance lead (six cells), charge current, shoe-side voltage# and pack temperature; drives the charge-enable and balancer switches; one# status LED strip. STM32F405RGT6, 8 MHz crystal. See docs/charge-node.md.
MCU STM32F4xx STM32F405xx
# bootloader in the first 64k, parameters in the next two 16k sectorsFLASH_RESERVE_START_KB 64FLASH_SIZE_KB 1024STORAGE_FLASH_PAGE 2define HAL_STORAGE_SIZE 15360
# Private board id. It only has to agree between hwdef.dat and hwdef-bl.dat.# IDs 1000 to 19999 are allocated upstream by PR; this board is not registered.APJ_BOARD_ID 20401
env AP_PERIPH 1
define STM32_ST_USE_TIMER 5define CH_CFG_ST_RESOLUTION 32
OSCILLATOR_HZ 8000000
STDOUT_SERIAL SD1STDOUT_BAUDRATE 57600
define SERIAL_BUFFERS_SIZE 512define PORT_INT_REQUIRED_STACK 64define DMA_RESERVE_SIZE 0define HAL_DEVICE_THREAD_STACK 768define AP_PARAM_MAX_EMBEDDED_PARAM 512env ROMFS_UNCOMPRESSED True
# activity LEDPC12 LED OUTPUT LOWdefine HAL_LED_ON 0
# SWD stays available: this is a home-made boardPA13 JTMS-SWDIO SWDPA14 JTCK-SWCLK SWD
# ---------------------- CAN bus -------------------------PB8 CAN1_RX CAN1PB9 CAN1_TX CAN1PC13 GPIO_CAN1_SILENT OUTPUT PUSHPULL SPEED_LOW LOW
# ---------------------- UARTs ---------------------------# USART1: debug console on a 3-pin headerSERIAL_ORDER USART1PA9 USART1_TX USART1 SPEED_HIGH NODMAPA10 USART1_RX USART1 SPEED_HIGH NODMA
# ---------------------- I2C1 ----------------------------# spare header for a digital current monitor or temperature sensor laterI2C_ORDER I2C1PB6 I2C1_SCL I2C1PB7 I2C1_SDA I2C1define HAL_I2C_CLEAR_ON_TIMEOUT 0define HAL_I2C_INTERNAL_MASK 0
# ---------------------- ADC -----------------------------define HAL_USE_ADC TRUEdefine STM32_ADC_USE_ADC1 TRUE
# Balance lead taps, cumulative from pack negative, through 10.1:1 dividers# (910k over 100k, 0.1 %, Viking ARG03BTC9103, high so the permanently plugged# balance lead does not drain the pack unevenly; terrahawk-eda a1d96ca, Ryan# 2026-10-01). SCALE(10.1) is fixed in firmware, so the dividers must be# accurate; there is no per-cell calibration parameter. Never fit 900k parts# with SCALE(10.1), or 910k with SCALE(10): each reads 1 % wrong, 0.04 V a cell.PC0 CELL1 ADC1 SCALE(10.1) ANALOG(1)PC1 CELL2 ADC1 SCALE(10.1) ANALOG(2)PC2 CELL3 ADC1 SCALE(10.1) ANALOG(3)PC3 CELL4 ADC1 SCALE(10.1) ANALOG(4)PA0 CELL5 ADC1 SCALE(10.1) ANALOG(5)PA1 CELL6 ADC1 SCALE(10.1) ANALOG(6)
# Pack voltage (1M over 100k from the balance lead top tap), charge current# (INA180A2 on a 5 mOhm shunt, 0.25 V/A, over the link from the power stage),# shoe-side voltage at the bridge input (11:1 after a BAV70 diode-OR, over the# link), pack thermistor (10k NTC, 10k pull-up to 3V3)PA6 BATT_VOLTAGE_SENS ADC1 SCALE(1) ANALOG(10)PA5 BATT_CURRENT_SENS ADC1 SCALE(1) ANALOG(11)PA4 BATT2_VOLTAGE_SENS ADC1 SCALE(1) ANALOG(12)PA7 PACK_TEMP ADC1 SCALE(1) ANALOG(13)
# ---------------------- Battery -------------------------define AP_PERIPH_BATTERY_ENABLED 1define AP_BATT_MONITOR_MAX_INSTANCES 2
# BATT: the pack, voltage and charge currentdefine HAL_BATT_MONITOR_DEFAULT 4define HAL_BATT_VOLT_PIN 10define HAL_BATT_CURR_PIN 11define HAL_BATT_VOLT_SCALE 11.0define HAL_BATT_CURR_SCALE 4.0
# BATT2: shoe-side voltage, voltage only. Reports contact before any current flows.define HAL_BATT2_MONITOR_DEFAULT 3define HAL_BATT2_VOLT_PIN 12define HAL_BATT2_VOLT_SCALE 11.0
# per-cell voltages from the balance leaddefine AP_PERIPH_BATTERY_BALANCE_ENABLED 1define AP_PERIPH_BATTERY_BALANCE_NUMCELLS_DEFAULT 6define AP_PERIPH_BATTERY_BALANCE_RATE_DEFAULT 2define AP_PERIPH_BATTERY_BALANCE_CELL1_PIN_DEFAULT 1define AP_PERIPH_BATTERY_BALANCE_ID_DEFAULT 10
# pack temperature: analog NTC, attached to BATT by TEMP1_SRC in defaults.parmdefine AP_PERIPH_DEVICE_TEMPERATURE_ENABLED 1define AP_TEMPERATURE_SENSOR_ENABLED 1define AP_TEMPERATURE_SENSOR_ANALOG_ENABLED 1
# ---------------------- Relays --------------------------# RELAY1 = charge-enable P-MOSFET gate driver, RELAY2 = balancer power.# Both default off. Driven from the FC by DroneCAN hardpoint 0 and 1.PB0 CHARGE_EN OUTPUT LOW GPIO(50)PB1 BAL_EN OUTPUT LOW GPIO(51)define AP_PERIPH_RELAY_ENABLED 1define AP_RELAY_ENABLED 1define AP_RELAY_NUM_RELAYS 2define RELAY1_PIN_DEFAULT 50define RELAY2_PIN_DEFAULT 51
# ---------------------- Status LEDs ---------------------# WS2812 strip visible through the hatch, on OUT1PA15 TIM2_CH1 TIM2 PWM(1) GPIO(58)define AP_PERIPH_RC_OUT_ENABLED 1define AP_PERIPH_NOTIFY_ENABLED 1# built-in LED plus NeoPixeldefine DEFAULT_NTF_LED_TYPES 257hw/charge-node/hwdef/TerraHawkChargeNode/hwdef-bl.dat · 28 lines · open · download
# TerraHawk charge node bootloader. USB for the first flash, CAN afterwards.
MCU STM32F4xx STM32F405xxOSCILLATOR_HZ 8000000
# must match hwdef.datAPJ_BOARD_ID 20401
env AP_PERIPH 1
FLASH_RESERVE_START_KB 0FLASH_SIZE_KB 1024FLASH_BOOTLOADER_LOAD_KB 64
PC12 LED_BOOTLOADER OUTPUT LOW GPIO(0)define HAL_LED_ON 0
SERIAL_ORDER OTG1
PA11 OTG_FS_DM OTG1PA12 OTG_FS_DP OTG1
PA9 USART1_TX USART1PA10 USART1_RX USART1
PB8 CAN1_RX CAN1PB9 CAN1_TX CAN1PC13 GPIO_CAN1_SILENT OUTPUT PUSHPULL SPEED_LOW LOWhw/charge-node/hwdef/TerraHawkChargeNode/defaults.parm · 26 lines · open · download
# Embedded defaults for the TerraHawk charge node. Change with the DroneCAN# GUI tool or Mission Planner's DroneCAN tab; they persist in flash.
# battery ids the FC matches with BATTn_SERIAL_NUM. BAL_ID must equal BATT_SERIAL_NUM.BATT_SERIAL_NUM 10BATT2_SERIAL_NUM 11BAL_ID 10BAL_NUM_CELLS 6
# pack thermistor on analog pin 13, attached to battery 1.# TEMP1_A0..A5 are the polynomial from ADC volts to degC; fit them to the# thermistor actually fitted (docs/charge-node.md). All zero reads 0 degC.TEMP1_TYPE 5TEMP1_PIN 13TEMP1_SRC 3TEMP1_SRC_ID 1
# relays driven by DroneCAN hardpoint 0 (charge enable) and 1 (balancer)RELAY1_FUNCTION 10RELAY2_FUNCTION 11RELAY1_DEFAULT 0RELAY2_DEFAULT 0
# status strip: two NeoPixels on OUT1OUT1_FUNCTION 120NTF_LED_LEN 2tools/charge_interlock.lua · 174 lines · open · download
-- TerraHawk charge interlock. Runs on the flight controller (Pixhawk 6C Mini first, then our own H743).---- The aircraft carries a charge-enable switch between the landing-shoe-- rectifier and the pack, driven by the charge node over DroneCAN. This-- script closes that switch only while every condition below holds, and-- opens it the moment one fails. The ground-side charger has its own-- limits; both controllers must agree before current flows.---- Battery instances on the FC (0-based here, BATT1.. in parameters):-- 0 the FC's own sensor on the main leads-- 1 charge node, pack: voltage, charge current, cell voltages, temperature (id 10)-- 2 charge node, shoe side of the rectifier: voltage only (id 11)-- Relays on the FC (0-based here, RELAY1.. in parameters):-- 2 charge enable -> DroneCAN hardpoint 0 (RELAY3_PIN 1000)-- 3 balancer power -> DroneCAN hardpoint 1 (RELAY4_PIN 1001)-- See docs/charge-node.md for the parameters that go with this script.---- Full-charge reset: instance 0 counts mAh used since boot and never counts the charge, so after a-- dock charge with no reboot the capacity triggers still see the last flight and refuse to arm-- (th-testpilot, 2026-10-01: the dock harness hung on "Battery 1 low capacity failsafe"). When a-- charge seen in this session tapers off with every cell at or above FULL_CELL, the script sets-- instance 0 back to 100 %. A partial charge does not reset it: the count then overstates what was-- used, which is the safe side.
local PACK = 1local SHOE = 2local RLY_CHARGE = 2local RLY_BAL = 3
local NUM_CELLS = 6local CELL_MAX = 4.20 -- V, open above thislocal CELL_REARM = 4.15 -- V, close again only below thislocal CELL_MIN = 2.80 -- V, do not charge a cell below this-- Pack temperature band (docs/charge-node.md, th-eda 00ba24c; Ryan via th-hangar: never above 45 C): start only-- from 2 to 40 C; once charging, continue up to 45 C; after a trip, close again only below 40 C. A reading below-- -20 or above 80 C (an ADC rail, or INT16_MAX) is an open or shorted thermistor: unknown, so the switch stays open.-- The P50B data sheet allows more (70 C cell surface while charging); 40/45 is a pack-life margin.local TEMP_MIN = 2.0 -- degC, never belowlocal TEMP_START = 40.0 -- degC, highest temperature to start, and to close again after a triplocal TEMP_MAX = 45.0 -- degC, open above this while charginglocal TEMP_VALID_LO, TEMP_VALID_HI = -20.0, 80.0local SHOE_MIN = 3.0 -- V on the shoe side means the pad is probing uslocal SUM_TOL = 0.6 -- V, cells must add up to the pack voltagelocal BAL_ON_AMPS = 0.2 -- balancer runs while this much charge current flowslocal MAIN = 0 -- the FC's own sensor, which the failsafes uselocal FULL_CELL = 4.15 -- V, every cell at or above this for a full chargelocal FULL_AMPS = 0.5 -- A, charge current tapered below this (C/20 on the 10 Ah P50B)local FULL_HOLD_MS = 30000 -- ms the full condition must hold
local PERIOD_MS = 500local MSG_MIN_MS = 5000
local cell_tripped = falselocal temp_tripped = falselocal last_reason = ""local last_msg_ms = 0local charged = false -- a charge flowed since boot or since the last resetlocal full_since = nil
local function say(reason) local now = millis() if reason ~= last_reason or (now - last_msg_ms) > MSG_MIN_MS then gcs:send_text(6, "charge: " .. reason) -- 6 = MAV_SEVERITY_INFO last_reason = reason last_msg_ms = now endend
-- returns ok, reasonlocal function conditions() if arming:is_armed() then return false, "armed" end if vehicle:get_likely_flying() then return false, "flying" end
if not battery:healthy(PACK) then return false, "node pack data stale" end if not battery:healthy(SHOE) then return false, "node shoe data stale" end
local shoe_v = battery:voltage(SHOE) if shoe_v < SHOE_MIN then return false, "no pad contact" end
local temp = battery:get_temperature(PACK) if temp == nil then return false, "no pack temperature" end if temp < TEMP_VALID_LO or temp > TEMP_VALID_HI then return false, string.format("pack temperature unknown (%.0fC: thermistor open or shorted)", temp) end if temp < TEMP_MIN then return false, string.format("pack cold %.1fC", temp) end local charging = relay:get(RLY_CHARGE) ~= 0 if temp > TEMP_MAX then temp_tripped = true end if temp_tripped and temp < TEMP_START then temp_tripped = false end if temp_tripped then return false, string.format("pack hot %.1fC, waiting for %.0fC", temp, TEMP_START) end if not charging and temp > TEMP_START then return false, string.format("pack warm %.1fC, start below %.0fC", temp, TEMP_START) end
local sum = 0 local vmax = 0 for c = 0, NUM_CELLS - 1 do local v = battery:get_cell_voltage(PACK, c) if v == nil then return false, string.format("cell %d missing", c + 1) end if v < CELL_MIN then return false, string.format("cell %d low %.2fV", c + 1, v) end if v > vmax then vmax = v end sum = sum + v end
local pack_v = battery:voltage(PACK) if math.abs(sum - pack_v) > SUM_TOL then return false, string.format("cells %.2fV vs pack %.2fV", sum, pack_v) end
if vmax > CELL_MAX then cell_tripped = true end if cell_tripped and vmax < CELL_REARM then cell_tripped = false end if cell_tripped then return false, string.format("cell high %.3fV", vmax) end
return true, "ok"end
local function set_relay(idx, on) if on then if relay:get(idx) == 0 then relay:on(idx) end else if relay:get(idx) ~= 0 then relay:off(idx) end endend
local function min_cell() local vmin = 99 for c = 0, NUM_CELLS - 1 do local v = battery:get_cell_voltage(PACK, c) if v == nil then return nil end if v < vmin then vmin = v end end return vminend
-- after a charge in this session tapers off with every cell full, set the FC's count to 100 %local function full_reset(amps) if not charged or arming:is_armed() or not battery:healthy(PACK) or amps == nil then full_since = nil return end local vmin = min_cell() if vmin == nil or vmin < FULL_CELL or amps >= FULL_AMPS then full_since = nil return end local now = millis() full_since = full_since or now if now - full_since >= FULL_HOLD_MS then battery:reset_remaining(MAIN, 100) charged, full_since = false, nil gcs:send_text(6, string.format("charge: full, min cell %.2fV, battery count reset", vmin)) endend
function update() local ok, reason = conditions() set_relay(RLY_CHARGE, ok)
local amps = battery:current_amps(PACK) local charging = ok and amps ~= nil and amps > BAL_ON_AMPS set_relay(RLY_BAL, charging) if charging then charged = true end full_reset(amps)
if ok then if charging then say(string.format("charging %.1fA", amps)) else say("enabled, waiting") end else say("blocked: " .. reason) end return update, PERIOD_MSend
-- fail safe on start: both switches open until the first good checkrelay:off(RLY_CHARGE)relay:off(RLY_BAL)gcs:send_text(6, "charge interlock loaded")return update, PERIOD_MShw/charge-node/Makefile · 25 lines · open · download
# Build the TerraHawk charge node firmware in ArduPilot's Docker image.# ARDUPILOT must be a checkout with submodules that nothing else is using.
ARDUPILOT ?= $(abspath ../../../ardupilot-chargenode)# 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:8bb0f850fb3fe1c170cb12dd577ab64f3708be8a828f9eafd28d73559766e81fBOARD := TerraHawkChargeNodeHERE := $(abspath .)
.PHONY: firmware clean
firmware: ## Build bootloader and AP_Periph firmware, outputs in build/ @test -d "$(ARDUPILOT)/libraries/AP_HAL_ChibiOS/hwdef" || { echo "no ArduPilot tree at $(ARDUPILOT); set ARDUPILOT="; exit 1; } 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 AP_Periph' mkdir -p build cp "$(ARDUPILOT)/build/$(BOARD)/bin/AP_Periph.bin" "$(ARDUPILOT)/build/$(BOARD)/bin/AP_Periph_with_bl.hex" build/ cp "$(ARDUPILOT)/Tools/bootloaders/$(BOARD)_bl.bin" build/ @ls -la build/
clean: rm -rf buildhw/charge-node/README.md · 34 lines · open · download
# TerraHawk charge node
An ArduPilot AP_Periph DroneCAN node for the hangar charge path. The design, thecircuit, the parts and the test plan are in `docs/charge-node.md`. This directory holdsthe firmware inputs:
| Path | What || --- | --- || `hwdef/TerraHawkChargeNode/hwdef.dat` | Board definition: pins, ADC channels, features || `hwdef/TerraHawkChargeNode/hwdef-bl.dat` | Bootloader definition (USB for the first flash, CAN after) || `hwdef/TerraHawkChargeNode/defaults.parm` | Parameters embedded in the firmware || `../../tools/charge_interlock.lua` | The interlock script that runs on the flight controller |
## Build
The build runs in ArduPilot's own Docker image against an ArduPilot checkout. Do notpoint it at a checkout another tool is using (the SITL tree beside this repo): waf keepsone configuration per tree, and configuring this board there breaks the SITL build untilit is configured back. Use a copy.
```bashmake firmware ARDUPILOT=/path/to/a/private/ardupilot/copy```
The target copies `hwdef/TerraHawkChargeNode` into the tree, builds the bootloader, thenthe firmware, and copies the outputs to `build/`:
| File | Use || --- | --- || `AP_Periph_with_bl.hex` | First flash, over USB DFU (hold BOOT0 at power-up) or SWD || `AP_Periph.bin` | Later updates over CAN, from Mission Planner's DroneCAN tab || `TerraHawkChargeNode_bl.bin` | Bootloader alone, if it ever needs replacing |
The `build/` directory is not tracked.Source: docs/charge-node.md in the repo. Edit it there; this page is generated.
Comments
Comment on the whole page here, or on one section or step with the button beside it.