### Summary
RP2350 support in AP_HAL_ChibiOS: dual-core SMP, and the PIO-based …peripherals the chip needs in place of the STM32 timer/DMA paths ArduPilot normally relies on. This is the first non-STM32 hardware in this HAL.
The port flies. RPI_UAVFC is a real flight controller running this, with bidirectional DShot600, an eRPM-driven harmonic notch, analog OSD and microSD logging in the air.
### Classification & Testing (check all that apply and add your own)
- [x] Checked by a human programmer
- [x] Tested on hardware
- [x] Logs available on request
- [ ] Automated test(s) verify changes (e.g. unit test, autotest)
### Description
#### Feature support
| Feature | Status | Notes |
|---|---|---|
| Dual-core SMP | Yes | First SMP implementation in ChibiOS. IMU, rate loop and DShot on core1; main loop and EKF on core0. See "Cores, and what runs where" below |
| USB CDC | Yes | Console and MAVLink |
| Hardware UARTs | Yes | The chip has two PL011s |
| PIO UARTs | Yes | Two more, since two hardware UARTs are not enough for a flight controller. Inverted and half-duplex too: `INOVER`/`OUTOVER` on the pad, and a dedicated 8E2 receive program for SBUS |
| SPI | Yes | IMU and microSD |
| I2C | Yes | Internal baro, external GPS/compass |
| ADC | Yes | Including the RP2350B pins above GPIO29 |
| PWM output | Yes | 4 motor outputs |
| Bidirectional DShot600 | Yes | PIO driven. Flown |
| Harmonic notch from eRPM | Yes | Per-motor; the four motors span 164-212 Hz so per-motor tracking is not optional |
| NeoPixel / serial LED | Yes | PIO driven |
| Analog OSD | Yes | PIO scan-out. Costs about 0.1% of core0, because PIO and DMA do the video timing. Detects PAL or NTSC from the camera's field rate and re-checks it while disarmed |
| MSP DisplayPort | Yes | |
| microSD logging | Yes | SPI mode |
| Parameter storage | Yes | XIP flash |
| Watchdog | Yes | 2 s hardware watchdog, with the monitor thread's lockup detection. Lockup-tested on RPI_UAVFC: resets, detects the reset and restores the persistent data. Still compiled out on Laurel |
| Bootloader | Yes | RP2350 IMAGE_DEF; flash over SWD or UF2 |
| VTX control and power switching | Yes | SmartAudio |
| Battery voltage monitoring | Yes | |
| Analog RSSI | Yes | |
| External compass | Yes | I2C. No compass on the board itself, so without one the EKF stays in constant-position mode |
| Battery current monitoring | Partial | Reads, but does not track load on the tested vehicle; scale is still a placeholder |
| DShot rates other than 600 | No | Anything else raises a configuration error at boot |
| OneShot / OneShot125 | No | The RP2350 PWM slice has no EGR equivalent. Would need the PWM wrap IRQ - novel work, not a port |
| BLHeli passthrough | No | |
| CAN / DroneCAN | No | No hardware for it |
| ProfiLED | No | NeoPixel only |
| Hardware SPI OSD | No | No device on RPI_UAVFC. On Laurel it contends with microSD for SPI1 |
| SBUS | Yes, with #33057 | Inverted 8E2 on a PIO UART, which assembles whole frames, aligns them on the 2 ms wire gap and debounces the failsafe flag; the PL011 path sets parity, stop bits and `INOVER` as well. Wire the receiver to the RADIO pad and set `SERIAL3_OPTIONS` 1. Alignment is deliberately enforced at the PIO layer, because it is the last one that knows the wire timing - a batched port has none left by the time the bytes reach the decoder, which is what #33057 is for. Not yet run against a receiver on this port, where CRSF is what has flown |
| SBUS on the dedicated pad (GPIO41) | No | Its only hardware-UART function is `UART1_RX`, which the GPS owns, and a third PIO UART is mapped to PIO1, which the analog OSD claims. Use the RADIO pad |
| Serial ESC telemetry pad | No | Can only reach the UART the GPS owns. Use bidirectional DShot instead |
| IOMCU | No | Outputs are driven directly |
| Safety switch | No | Deliberate board choice |
| IMU heater | No | |
| Hardware RTC | No | |
| Dual USB CDC | No | Attempted, enumerated, never passed bytes. Abandoned |
#### Boards
RPI_UAVFC is the flight target and the one that matters. It has flown.
Laurel was the bring-up board - an RP2350B carrier used to get the platform working before RPI_UAVFC existed. It is bench-verified, not a flight target, and does not need flying. Its feature table is kept in-tree as the record of what was proven on hardware during bring-up.
Pico2 is a reference target for anyone wanting to run this on a bare Pico 2. It compiles and boots; nothing in its feature table is marked hardware-verified.
RPI_UAVFC-SimOnHardWare is a simulation-on-hardware target, not a flight configuration.
#### Cores, and what runs where
Two Cortex-M33s, and one 16 KB XIP instruction cache shared between them. Both facts drive the split: the flight-critical chain gets a core to itself, and that core is kept out of flash so the other one can have the cache.
Assignment is one header, `hwdef/common/rp2350_core_affinity.h`, with a define per thread. IRQ routing follows for free - ChibiOS routes a peripheral's IRQ to whichever core called the driver start function, and each bus thread does its own init on first wakeup, so the IRQ goes where the thread went. Moving a driver between cores is a one-line change with no NVIC work.
| Core 0 | Core 1 |
|---|---|
| main loop at 200 Hz, EKF3 inline, AHRS, navigation, GCS, logging | SPI0 bus thread: IMU FIFO at 3200 Hz, gyro LPF, harmonic notch |
| CRSF RC input on the PIO0 interrupt, USB, UART, I2C | rate thread at 1600 Hz: three AC_PID updates, AP_MotorsMatrix mixing |
| SPI1 microSD, parameter storage, PIO OSD | rcout thread: bidirectional DShot600 at 1600 Hz, GCR decode, eRPM |
Those rates are R2 Rev C with the ICM-56686, which takes the ICM-456xy path: an 800 Hz base backend rate, so `INS_GYRO_RATE` 2 lands on 3200 Hz where an ICM42688P's 1000 Hz base gives 4000 Hz. `FSTRATE_DIV` 2 halves that into the rate thread. `SERVO_DSHOT_RATE` multiplies the rate-loop rate and not `SCHED_LOOP_RATE`, which is a trap worth naming - at 1 the DShot frame rate is simply the rate-loop rate, and 0 is a fixed 1 kHz regardless. The equivalent ICM42688P build runs 4000 / 2000 / 2000 Hz.
Parameter storage has to stay on core0, because the XIP lockout below is driven from core0 and parks core1. A flash write issued from core1 would park the core doing the writing.
The obvious move with a spare core is to put more on it, and that is wrong here. Adding USB, UART, I2C and RC input to core1 during Laurel bring-up took the main loop from 296 Hz to 225 Hz: every extra thread there is more context switches, and every SMP spinlock acquire on one core stalls the other. Core1 runs the fewest threads the IMU-to-motors chain needs and nothing else.
```mermaid
flowchart TB
subgraph C1["CORE 1 - gyro sample to DShot frame"]
direction LR
A["SPI0 bus thread<br/>IMU FIFO 3200 Hz<br/>gyro LPF, harmonic notch"]
B["rate thread 1600 Hz<br/>AC_PID x3<br/>AP_MotorsMatrix mixing"]
C["rcout thread<br/>bidir DShot600 1600 Hz<br/>GCR decode, eRPM"]
A --> B --> C
end
subgraph C0["CORE 0 - everything else"]
direction LR
M["main loop 200 Hz<br/>EKF3 inline, AHRS<br/>nav, GCS, logging"]
R["CRSF RC in<br/>PIO0 IRQ, 2.1 kHz"]
S["storage, USB, UART, I2C<br/>SPI1 microSD, PIO OSD"]
end
SY["Scratch Y<br/>SRAM9, 4 KB<br/>core1 dedicated I-bus"]
SR["striped SRAM, 512 KB<br/>registry relocations"]
SX["Scratch X<br/>SRAM8, 4 KB<br/>core0 dedicated I-bus"]
XIP["QSPI flash via XIP<br/>shared 16 KB cache<br/>disabled during flash writes"]
C1 -- "9.7%" --> SY
C1 -- "77.1%" --> SR
C1 -- "13.2%" --> XIP
C0 -- "8.3%" --> SX
C0 -- "19.4%" --> SR
C0 -- "72.2%" --> XIP
```
Edge labels are where each core fetches instructions from, as a share of non-idle PC samples. Box rates are the ICM-56686 build; every measured share here and below came off the ICM42688P build at 4000 Hz gyro, 2000 Hz rate loop and `SERVO_DSHOT_RATE` 1.
#### IMU to motors, in SRAM
Everything on core1 between the gyro sample and the DShot frame is relocated out of flash: the FIFO read, the filters, the rate controller and its PIDs, the mixer, and the bidirectional DShot send and decode. Two mechanisms do it. A registry file lists functions to move into striped SRAM, and two 4 KB scratch banks take the hottest leaves - SRAM8 sits on core0's dedicated instruction bus and SRAM9 on core1's, so a fetch from either is single-cycle and contends with nothing.
The DShot chain is the measurement that made the case, because it was entirely flash-resident and had never been profiled. Same board, same rates, on-chip PC sampler, shares of core1 non-idle samples:
| | PWM baseline | bidir DShot, in flash | bidir DShot, relocated |
|---|---|---|---|
| flash | 1.8% | 65.6% | 17.7% |
| SRAM | 97.2% | 34.1% | 73.3% |
| scratch | 1.0% | 0.2% | 9.0% |
Core1 idle went from 21.9% of emitted samples to 51.4%, so the core is doing about half the work it was. `RCOutput_pico::read_telemetry` alone fell from 19.2% to 3.5%.
That 5.5x settles something worth stating, because the opposite assumption is the easy one to make: a high sample count does not by itself mean code is fetch-bound. `AP_MotorsMatrix` is core1's largest single consumer at around 2.1% armed, and moving it to scratch gained nothing - it is data-heavy mixing math, not fetch-bound. The only way to tell the two apart was to move it and measure again.
#### Why core0 keeps the flash
Core1 being in SRAM is not only about core1. The XIP cache is shared, so core1's fetches evict core0's working set, and the failure that exposed it was not subtle: once the bidirectional DShot chain started fetching from flash, CRSF RC input went into continuous failsafe. The PIO UART has no DMA, so every RX interrupt drains the FIFO in software - a byte every 24 us at 420 kbaud - and that handler was being fetched from flash on the core that was losing the cache. It now sits in Scratch X together with the rest of the PIO0 interrupt path.
There is a second reason core1 cannot be in flash. The QMI controller disables XIP for the duration of every erase and page program, so a fetch from flash inside that window returns DECERR and faults. Core0 parks core1 first, over the SIO doorbell:
```mermaid
sequenceDiagram
participant C0 as Core 0, storage
participant Q as QMI / XIP
participant C1 as Core 1
C0->>C1: SIO doorbell, IRQ26
C1->>C1: mask all IRQs, spin in .ramtext
C1-->>C0: ack
C0->>Q: exit XIP
C0->>Q: erase or program
C0->>Q: enter XIP
C0->>C1: release
C1->>C1: restore IRQs, resume
```
Core1 spins in `.ramtext` with its interrupts masked until core0 releases it. Flash writes come only from parameter storage, so this happens at boot and on a GCS parameter save, never in flight, and `perf_report` emits `XIPpark: n=<count> max=<us>` so the cost is visible rather than assumed. IRQ26 runs at `CORTEX_MINIMUM_PRIORITY` deliberately: at priority 0 it fires inside ChibiOS kernel locks and deadlocks against the SMP spinlock.
What falls out is the asymmetry the whole arrangement is aiming for. Bidirectional DShot600 at 2 kHz, shares of non-idle samples:
| | core 1 | core 0 |
|---|---|---|
| idle | 58.3% | 53.9% |
| flash | 13.2% | 72.2% |
| SRAM | 77.1% | 19.4% |
| scratch | 9.7% | 8.3% |
Every one of core1's top 24 functions is SRAM-resident, and nothing in flash appears until 0.4%. Core0 at 72% flash is the intended state, not an unfinished one: it has the cache largely to itself, and it is the core that can absorb a miss.
Core0 relocation does pay where it is affordable. `AP_DAL` was 3.9% of core0 non-idle in 7.2 KB and entirely flash-resident; relocating it measured 8.0x and 9.9x across two independent A/B pairs, against boot-to-boot drift of 18% and 6% on the two halves. It is measured as a ratio of DAL samples to NavEKF3 samples rather than as an absolute share, because absolute core0 load moves by 20 points across a reflash depending on EKF and GPS state, and the first attempt at this read as a 28% win that was almost entirely that drift. Core0 relocation stops there because the budget is heap: the flying branch has been down to 13 KB free after arming, with a 7 KB largest block.
#### Still to do
- [x] Triage three failing autotests: CANGPSCopterMission, GpsSensorPreArmEAHRS, ModeFlowHold. They may be unrelated flakiness, but this branch touches AP_Param, AP_GPS, GCS_MAVLink and AP_RCProtocol on every board including SITL, so that should not be assumed
- [ ] Autotest coverage for the RP2350 itself - there is none
- [ ] Run SBUS against a receiver: the RADIO pad on SERIAL3 with `SERIAL3_OPTIONS` 1, and #33057 in
- [ ] Calibrate the battery current scale, or document the input as unusable
- [ ] Characterise silicon spread at the 225 MHz system clock. The datasheet limit is 150 MHz and it has been flight-tested on one sample
- [x] Split the MAVLink FTP and logger work into their own PRs: #34474 and #34473
- [x] Split the microSD work into its own PRs: #34476, #34477, #34478, with the MMC-SPI driver change moved from ArduPilot/ChibiOS#113 to ArduPilot/ChibiOS#115
- [x] Validate the ICM-56686 named in the R2 Rev C schematic - flown on logs 96 and 97. ArduPilot/ardupilot#34368 corrects four register-level errors in that driver and is a dependency for boards using it
#### What this costs boards that will never see an RP2350
Shared-library changes in this diff land flash on every ChibiOS board. MatekF405 copter built at this branch's merge-base is 873816 B and at its head 874128 B, so +312 B. An `nm` diff of the two ELFs puts it here:
| | bytes | |
|---|---:|---|
| rate thread and fast-rate INS | +102 | |
| SMP plumbing in `Scheduler` | +62 | `thread_create_pinned_to_core`, `cores_are_independent` |
| flash parameter storage | +60 | sector-bounded reads, health timing from init |
| `UARTDriver`, `AP_OSD` | +46 | |
| long tail of inlining noise | +20 | |
The FTP, logger and microSD changes that made up most of the earlier figure are out of this diff and proposed on their own. What stays in shared code is one `GCS_MAVLink` commit that lets a board set the FTP thread priority and TX back-pressure through `#ifndef` defaults carrying master's values, which the RP2350 MCU definition overrides; it adds 0 B to other boards.
#### Dependent PRs
This branch is the PR branch and carries only the RP2350 port. Work that is not RP2350-specific has been split out and is not in this diff:
| PR | What | State |
|---|---|---|
| ArduPilot/ChibiOS#113 | RP2350 support in ChibiOS, including SMP | Open. Must merge first, and ArduPilot must move to a ChibiOS carrying it |
| #33055 | AP_Baro: DPS280/310 poll rate | Open |
| #33056 | AP_Filesystem: memset d_name before strncpy | Open, approved |
| #33057 | AP_RCProtocol/SBUS: anchor framing on the header and resync on failure | Open. SBUS on this port needs it: upstream requires a wire-time gap before a header, which a batched PIO or DMA port does not preserve |
| #33058 | AP_RCProtocol: backend polling and CRSF startup guards | Open, changes requested |
| #34436 | Copter: honour the harmonic notch loop rate option in the rate thread | Open. Found here: the rate thread re-centred every notch at the rate loop rate whatever `INS_HNTCH_OPTS` said, so the "update at loop rate" option did nothing. Dropped from this branch - with the option set, which is how this board flies, the behaviour is identical either way, so the port does not need it |
| #34368 | AP_InertialSensor: ICM-56686 IREG offsets and FIFO_CONFIG2 | Merged 2026-09-15. Was needed for RPI_UAVFC R2 Rev C, which specifies that IMU |
| #34473 | AP_Logger: mark start_new_log() IO steps for the IO timer watchdog | Open. Found here: a stall inside start_new_log() was reported as "stuck thread ()" with no hint of which step. Dropped from this branch, which flies without it |
| #34474 | GCS_MAVLink: wake the FTP worker on request and stop it spinning forever | Open. Found here. Dropped from this branch; FTP on the board has been rechecked without it |
| #34476 | AP_HAL_ChibiOS: honour AP_FATFS_MAX_IO_SIZE on the microSD SPI path | Open. Not needed for the port to work, but highly desirable for logging: with it and #34477 RPI_UAVFC logs at about 265 KB/s instead of 115 KB/s. Dropped from this branch; a bench test without it logs correctly at the reduced rate |
| #34477 | AP_HAL_ChibiOS: write microSD blocks in one SPI exchange | Open, needs ArduPilot/ChibiOS#115. Not needed for the port to work, but highly desirable for logging, as above. Dropped from this branch |
| #34478 | AP_HAL_ChibiOS: recover microSD parameter storage after an I/O error | Open. Found here, but the RP2350 boards keep parameters in flash, so the port does not use it. Dropped from this branch |
| #34430 | AP_Relay: don't re-apply a relay pin's mode on every read | Dropped. Re-applying the mode is deliberate - it takes a pin back from scripting driving it as a GPIO. The rail drop it guarded against on RPI_UAVFC was the RP2350 pad driver releasing the output while rewriting the mode, now fixed in ArduPilot/ChibiOS#113 |
| #34437 | AP_Math: keep angle shaping in float | Open. Dropped from this branch: setting `HAL_WITH_POSTYPE_DOUBLE` 0 for the RP2350 boards makes the shaping path float throughout, and saves 8180 B of flash on RPI_UAVFC, so the port no longer depends on it |
| #34497 | AP_OSD: do not leave a hole in the backend list | Open. Found here: with OSD_TYPE set to TXONLY or to an OSD that fails to start (for instance MSP DisplayPort with no MSP port configured), and a second OSD in OSD_TYPE2 (for instance the PIO overlay), the OSD thread calls through a null slot and the autopilot crashes a few seconds after boot, on master as on this branch. Dropped from this branch; until it merges, run the PIO overlay as OSD_TYPE, or give OSD_TYPE an OSD that starts |
| #34530 | AP_HAL_ChibiOS: count unallocated core memory in memory.txt | Open. Found here: `@SYS/memory.txt` showed a few hundred bytes free on RP2350 with 160 KB to spare, because it left out the part of the default heap the core allocator has not handed out yet. Every ChibiOS board's first region reads low the same way. Not in this branch; `MEMINFO` was already right |
ChibiOS#113 is the gating item. The rest can land in any order; this PR does not contain their changes and does not conflict with them.
These are also upstream on their own, but unlike the ones above their changes are still in this diff, because the port needs them to work as it stands. Each copy here drops when its PR merges:
| PR | What | State |
|---|---|---|
| #34443 | AP_Param: enforce @READONLY defaults over stale stored values | Open. Found here: `@READONLY` in a board's defaults.parm loses to a stored value, and the parameter being read-only then stops MAVLink correcting it. The ODID boards use it to lock the RemoteID parameters |
| #34438 | AP_HAL_ChibiOS: fix the threads.txt worst-slice mark | Open. The `*` converted the cycle counter at the crystal rate, and threads that had not run were printed as `LOAD 0.0%*` |
#### How the commits are laid out
The branch was rebuilt from 330 commits into 36. Code that STM32 boards also build is in a few commits by area: AP_HAL, the common hwdef code and generator, waf, the UART, bus, RC, scheduler and storage parts of AP_HAL_ChibiOS, and one commit per library touched. Each of those says in its message what, if anything, changes for existing boards. #34443 and #34438 are carried as their own commits so they drop cleanly when merged. The RP2350-only work follows in logical groups: PIO drivers, profiling, the shared `rp2350/` files, the MCU and common board files, one commit per board, bootloaders and tools.
Every commit builds for MatekF405 and SITL, and each RP2350 board builds from the commit that adds it. The history before the rebuild is on andyp1per:rp2350-v7.
#### On sharing the HAL
AP_HAL_ChibiOS was STM32-only before this, and when this PR opened it was an open question whether RP2350 belonged in the same HAL, in an overlay, or in a hard fork. Having built it, the shared HAL is the right answer. The STM32 paths are not distorted by it, and the genuinely chip-specific work - the PIO peripherals, SMP, the XIP relocation registry - lives in its own files rather than as conditionals threaded through shared code.
Vehicle code is almost untouched: 14 changed lines in ArduCopter, and no mention of RP2350 or ChibiOS in any vehicle directory. Board-dependent values go through HAL defaults a board overrides.
The one piece that is genuinely novel rather than ported is the XIP relocation registry, and it earns its keep: relocating the notch coefficient update and the rate-loop logging chain alone took core1 from 69.7% to 58.2% busy armed. See "Cores, and what runs where" above.