Skip to content

Add 4pi support. (Extremely experimental) - #28533

Draft
ellensp wants to merge 8 commits into
MarlinFirmware:bugfix-2.1.xfrom
ellensp:add-4pi
Draft

Add 4pi support. (Extremely experimental)#28533
ellensp wants to merge 8 commits into
MarlinFirmware:bugfix-2.1.xfrom
ellensp:add-4pi

Conversation

@ellensp

@ellensp ellensp commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

SAM3U4E / 4pi — new HAL

Description

Adds a new HAL family, SAM3U, for the Atmel AT91SAM3U4E, and board support
for the 4pi controller (BOARD_4PI, 3200) — a 96 MHz Cortex-M3 board that
Marlin has never supported.

This is the first bare-metal HAL in the tree. There is no Arduino core for
the SAM3U, so the environment builds directly on the CMSIS device headers with
framework deliberately empty, and the HAL supplies everything a core would
normally provide: the vector table and Reset_Handler, clock bring-up, a
millis()/micros() time base, and an Arduino.h shim covering the slice of
the Arduino API that Marlin's shared layer calls.

What is implemented:

Area Notes
Clocks, startup, timers PLLA to 96 MHz; TC0's three channels drive stepper, temperature and tone
GPIO / fastio Pins numbered (port * 32 + bit), so pin files read like the schematic (PA20, PC28)
ADC 12-bit ADC12B
Serial Five hardware ports (UART + 4 USARTs) plus native USB, any combination across Marlin's three slots
Native USB CDC-ACM on the SAM3U's UDPHS controller — the 4pi has no USB-serial chip, so this is the normal host link
Onboard SD The socket is on HSMCI, not SPI, so this implements Marlin's SDIO_* interface (ONBOARD_SDIO)
EEPROM Emulated in a reserved region at the top of flash, programmed from a RAM-resident routine
Motor current AD5206 digipot over the existing HAS_MOTOR_CURRENT_SPI path
Microstepping MS1/MS2 per axis

Three hardware constraints shaped the design and are worth a reviewer's
attention, each covered in detail below:

  • The SAM3U's timer/counter channels are 16-bit, not 32-bit like the Due's,
    so hal_timer_t is uint16_t as on AVR.
  • There are only three timer channels total, so servos and sound cannot both
    be enabled.
  • No hardware PWM reaches any of the board's outputs, so heaters and fans use
    Marlin's software PWM — as the original 4pi firmware did.

Status: this compiles and links correctly but has never been run on
hardware.
I do not have a 4pi. Everything static has been verified — memory
layout, symbol placement, ISR wiring, feature combinations — and the sections
below record exactly what was checked, what is most likely to be wrong, and a
bring-up order for testing it. Please treat it as such.

Requirements

  • A 4pi controller (AT91SAM3U4E). No other board uses this HAL.
  • SAM-BA to flash it. Short the RESET pads on top of the board and power
    cycle to reach the bootloader.
  • Nothing else — no particular LCD, probe or driver is assumed.

Benefits

  • Brings a board Marlin could not previously target into the tree.
  • Establishes a bare-metal CMSIS HAL pattern for parts with no Arduino core,
    which the SAM3U is unlikely to be the last of.
  • Documents the 4pi's hardware in-tree: the pin file carries the full expansion
    header pinouts, the RGB indicator LEDs, and the UART/USB pin assignments,
    all traced from the board's schematic rather than copied from its pinmap.

Companion documents:

  • Marlin/src/HAL/SAM3U/AGENTS.md — how the HAL is put together, and the traps
    in it. Read that before changing HAL code.
  • Marlin/src/pins/sam3u/pins_4PI.h — the board pin map and the full expansion
    header pinouts, traced from the schematic.

Status

It compiles and links correctly. It has never been run on hardware.

That distinction matters for everything below. What has actually been verified:

Check Result
pio run -e 4pi clean, no warnings from HAL code
Flash / RAM 30.1% (77,772 / 258,048) · 10.4% (5,132 / 49,152)
Vector table placement exception_table @ 0x00080000
EEPROM region reserved _seeprom @ 0x000BF000, outside the rom region
Stack / heap split _estack @ 0x2000C000, _sstack @ 0x2000B000
Flash writer is RAM-resident flash_write_page @ 0x20000000
Real ISRs, not stubs TC0, TC1, UART, SysTick, PIOA–C all distinct from Dummy_Handler
Feature paths link EEPROM, emergency parser, endstop interrupts, digipot, microstepping, 2 extruders, SD over HSMCI, USB CDC

Everything else — that the clock actually comes up, that steps come out of the
right pins, that the ADC reads sane values — is unverified. See
Bring-up checklist.


Build and flash

pio run -e 4pi

The environment is bare-metal: framework is deliberately empty, and
buildroot/share/PlatformIO/scripts/sam3u_build.py supplies the CMSIS include
paths and restores libc/libm/libgcc (the bare builder links -nostdlib).

Upload is over SAM-BA. To reach the bootloader, short the RESET pads on
top of the board and power-cycle it.

Six host links are available and any of them can go in any of Marlin's three
slots (SERIAL_PORT, SERIAL_PORT_2, SERIAL_PORT_3) in any combination —
SERIAL_PORT 0 with SERIAL_PORT_2 -1 is valid, and so on. Only the ports
actually named get compiled in.

On the 4pi, though, only two of them are practically usable:

  • -1 — native USB CDC (the default, and how the board is meant to be used).
    The 4pi has no USB-serial chip; this is the chip's own UDPHS device
    controller. It enumerates as 1d50:6019, the same VID/PID the original 4pi
    firmware used, so existing udev rules and host tooling keep working.

  • 0 — the chip's UART, on the 14-pin peripheral header. Worth keeping in
    mind for bring-up, especially if USB enumeration is itself what's broken:

    Header pin Signal MCU
    13 RX PA11
    14 TX PA12
    2 or 3 GND

    250000 baud, 8N1. (96 MHz / (250000 × 16) = 24 exactly, so no baud error.)

The HAL also implements ports 14 (USART0–3), but on this board every one
of them shares pins with something already in use, so the board's SanityCheck
rejects or warns on them:

Port Peripheral Pins Conflict on the 4pi
1 USART0 PA19 / PA18 Expansion CS3, motor-supply detect — warns
2 USART1 PA21 / PA20 HOTEND1 and BED heaters — rejected
3 USART2 PA23 / PA22 HOTEND2 heater, Y enable — rejected
4 USART3 PC13 / PC12 E1 enable, Z_MIN endstop — rejected

The hardware, versus the Due

The SAM3X8E HAL (HAL/DUE) is the closest relative — same vendor, same PIO/TC/
UART peripherals — and this HAL is recognisably descended from it. The parts are
not interchangeable:

SAM3X8E (Due) SAM3U4E (4pi)
Arduino core yes none
Flash / SRAM 512 KB / 96 KB 256 KB / 48 KB usable
Timer/Counter 3 blocks × 3 ch, 32-bit 1 block × 3 ch, 16-bit
PWM channels 8 4 (none reach 4pi loads)
PIO pulls up and down pull-up only
ADC one 12-bit, 16 ch 12-bit ADC12B (8 ch) + separate 10-bit ADC
USB UOTGHS UDPHS
Peripheral mux A/B/C/D A/B only (PIO_ABSR)

Design decisions worth knowing

16-bit timers. hal_timer_t is uint16_t, as on AVR — not uint32_t as on
the Due. Marlin's stepper ISR already copes by clamping the interval to
HAL_TIMER_TYPE_MAX and re-entering. Do not "fix" this by widening the type.

Stepper clock = MCK/32 = 3 MHz. Chosen to mirror AVR's proven 2 MHz rather
than for maximum resolution. The slowest representable step rate is
3000000 / 65535 ≈ 46 steps/s, far below any usable feedrate. TIMER_CLOCK2
(12 MHz) would give finer resolution but raise that floor to ~183 steps/s,
which slow Z and extruder moves genuinely reach.

All three timer channels are spoken for — stepper, temperature, tone. There
is no fourth, so inc/SanityCheck.h rejects enabling servos and sound together.

Software PWM for heaters and fans. The four hardware PWM channels do not
reach the 4pi's FETs, so inc/Conditionals_adv.h forces FAN_SOFT_PWM and
set_pwm_duty() degrades to an on/off threshold. This is exactly what the
original 4pi firmware did (soft PWM every 100 µs).

4 flash wait states, not 2. The datasheet requires FWS = 3 (4 read cycles)
at 96 MHz with VDDCORE 1.80V (Table 42-55). The original 4pi/Sprinter firmware
programmed 2 while still running at 96 MHz, which is out of spec. Do not copy
that.

WDT_MR is write-once after reset. clock.cpp disables the watchdog only
when USE_WATCHDOG is off; otherwise it leaves the 16 s default running so that
MarlinHAL::watchdog_init() gets the one write that counts.

USB CDC forced to full speed. UDPHS is high-speed capable and the original
firmware ran at 480 Mbit/s with 512-byte bulk endpoints. This driver pins the
link to full speed: 12 Mbit/s is already far more than Marlin can use, and it
means one descriptor set instead of two (a high-speed device must also publish
a device_qualifier and an other_speed_configuration and keep both endpoint-size
sets in step) and 64-byte banks instead of 512 out of the 4 KB DPRAM. Switching
back is three small edits, documented at the top of usb/usb_cdc.cpp.

Onboard SD over HSMCI, with programmed I/O. The socket is wired to the
High Speed Multimedia Card Interface in 4-bit mode, not SPI, so Marlin's stock
Sd2Card cannot drive it; sdio.cpp implements the SDIO_* interface that
DiskIODriver_SDIO calls instead, and the board file selects it with
ONBOARD_SDIO. Transfers are programmed I/O rather than DMA — RDPROOF and
WRPROOF stall the card clock whenever the FIFO would over- or underrun, so
the transfer waits for the CPU instead of losing data, which is what makes PIO
safe at 24 MHz. DMA would only matter if the card had to be serviced
concurrently with something else, and Marlin reads it from the foreground.

Bit-banged SPI. The only device on the bus is the AD5206 digipot — write-only
and low-rate — so a bit-banged master costs nothing and keeps the bus usable on
any pin combination.


Working around the CMSIS package

framework-cmsis-atmel is incomplete for the SAM3U, in two ways that both had
to be handled:

  1. No instance/ headers. It ships component/ and pio/ but not
    instance/, which sam3u4e.h includes unconditionally — so the vendor
    header will not even parse. Marlin/src/HAL/SAM3U/include/instance/ holds 31
    generated replacements (600 register macros). Regenerate with
    buildroot/share/scripts/gen_sam3u_instance_headers.py if the package is
    updated. Names are instance-qualified (REG_PIOA_PER, not REG_PIO_PER) so
    instances sharing a struct type do not collide; the script fails loudly if
    any do.
  2. Broken startup code. Its startup_sam3u.c does not compile against its
    own sam3u4e.h — it names reserved vector slots (pvReserved7, 12, 16)
    that the SAM3U4E DeviceVectors struct does not have.
    Marlin/src/HAL/SAM3U/startup.c replaces it.

Also note SPI is a macro in sam3u4e.h (the peripheral base pointer),
which collides with the global SPI object Marlin core code expects.
include/SPI.h pulls the device header in first, saves the base as
SAM3U_SPI_REGS, then #undefs the macro. Keep that ordering.


Not done, and why

1. Hardware SPI

HAL_SPI.cpp bit-bangs. Worth revisiting only if something fast lands on the
bus (an external SD reader would qualify). The peripheral's pins are the ones
already in use (MISO=PA13, MOSI=PA14, SPCK=PA15, NPCS0=PA16), and the
expansion header exposes three more chip selects, so the move is mechanical.

2. Servos

SHARED_SERVOS is declared but there is no free timer channel to drive it (see
above). inc/SanityCheck.h catches the conflicting case rather than letting it
compile into something that silently does not work.

3. TFT / graphical LCD

No u8g or tft subfolder. The 20-pin GPIO header has sixteen uncommitted PIO
lines, which is where a display would attach — but nothing is wired up for it
yet. Character LCDs over the software SPI/parallel paths are the least work.


Known risks in what is written

These are the places a first hardware test is most likely to catch something,
roughly in order of suspicion:

  1. USB enumeration (usb/usb_cdc.cpp). The newest and largest untested
    piece. If the device does not enumerate, fall back to SERIAL_PORT 0 on the
    UART to get console output, then work the USB problem with visibility.
    Specific things to check first:
    • The SETUP packet read. The old at91lib driver reads the same 32-bit FIFO
      word twice; this driver reads the 8 bytes linearly, per the datasheet and
      Atmel's later ASF driver. If enumeration fails immediately (no descriptor
      request ever completes), that is the first thing to question.
    • EPT_MAPD after ep_configure() — reads back clear if the endpoint
      table asks for more than the 4 KB DPRAM.
    • The UTMI PLL locking (PMC_SR_LOCKU) in usb_cdc_init().
  2. Clock bring-up (clock.cpp). The PLLA/MCK sequence follows the datasheet
    and the original firmware, but a wrong MOSCXTST or a missed MCKRDY wait
    shows up as a dead board. If nothing at all happens, suspect this first.
  3. SD card over HSMCI (sdio.cpp). Untested, and the card init sequence
    has several places to get wrong. If the card never mounts, the useful
    split is identification (CMD0/CMD8/ACMD41/CMD2/CMD3) versus transfer
    (CMD17/CMD24) — a card that reports a plausible size from M21 got through
    identification and read its CSD, so the problem is in the data path.
    Specific things to check: the two masked error cases (RCRCE on ACMD41's
    R3, RTOE on CMD8 for a v1.x card), the 4-bit switch order (ACMD6 must
    reach the card before SDCR changes), and byte-vs-block addressing on a
    standard-capacity card — SDHC is far more common, so that path is the more
    likely to be wrong and the less likely to be noticed.
  4. ADC timing (adc.cpp). ADC_STARTUP and ADC_SHTIM are computed from
    the datasheet's minimums with integer arithmetic; if temperatures read noisy
    or biased, widen the sample-and-hold time.
  5. Flash EEPROM (eeprom/eeprom_flash.cpp). The riskiest code in the HAL —
    it programs flash from a RAM-resident routine with interrupts masked. The
    placement is verified statically, but a bad M500/M501 cycle is the way
    this fails. Test with M502M500M501 early, before trusting it.
  6. Serial framing. The baud divisor is exact at 250000, but the UART's
    fixed 8N1 and the XON/XOFF path have not been exercised.
  7. Step/dir polarity and endstop logic. Taken from the schematic and the
    original firmware, but never observed.

Two latent bugs of this shape were already found and fixed by compiling with
SDSUPPORT enabled — a missing <ctype.h> in the Arduino.h shim, and
SD_SS_PIN == -1 reaching the unguarded fastio macros (where it resolves to
PA31, i.e. Y_DIR). Enabling unusual feature combinations is a cheap way to
find more; the HAL is only as tested as the config it has been built against.


Bring-up checklist

Roughly the cheapest-first order:

  1. Is it alive? Flash, then look for the USB device to enumerate
    (lsusb should show 1d50:6019) and for Marlin's startup banner on the
    resulting /dev/ttyACM*. If nothing enumerates, rebuild with
    SERIAL_PORT 0 and watch the UART at 250000 baud instead — that separates
    "the board is dead" from "USB is wrong".
  2. M115 — confirms the board string and that the G-code pipeline runs.
  3. M105 — confirms ADC12B. Room temperature on the hotend and bed with
    thermistors fitted; TEMP_0_PIN is PB4, TEMP_BED_PIN is PC16.
  4. M119 — confirms endstop reads and pull-ups. Trigger each by hand.
  5. M502M500M501 — confirms flash EEPROM emulation survives a
    power cycle. Do this before relying on stored settings.
  6. M17, then single-axis G0 moves — confirms the stepper timer, step/dir
    pins, and the AD5206 current setting. Start slow; check direction against
    INVERT_*_DIR.
  7. M104/M140 with a low setpoint — confirms software PWM on the heater
    FETs. Watch it. Have the power switch in reach.
  8. M106 — confirms fan output on the FET header.
  9. M42/M43 — the board has three RGB indicator LEDs on nine free pins
    (LED1_PIN..LED9_PIN, colour-mapped in the pins file); toggling one with
    M42 is the cheapest possible "is the firmware
    running and are pins reaching the board" test, and getting the colour you
    expect confirms the mapping. M43 pin reporting needs PINS_DEBUGGING in Configuration_adv.h,
    which is off by default and costs about 3.4KB of flash — worth turning on
    while bringing the board up.
  10. M21, then M20 — confirms the HSMCI SD driver: mount the card and
    list it. A plausible card size means identification and the CSD read worked;
    a successful M20 means the data path does too. Follow with M28/M29 to
    exercise writes.

Configuration changes on this branch

Marlin/Configuration.h and Marlin/Configuration_adv.h are set up for the
4pi so the branch builds and runs as-is. These are working-tree changes for
convenience — keep them out of any upstream PR; only the HAL, pins, board
and build-system files belong there.

Only board-specific settings are changed. Machine-specific values — steps/mm,
feedrates, accelerations, travel limits, axis directions — are deliberately
left at Marlin's defaults, because they describe a printer rather than a
controller and would be wrong for anyone else's machine:

Setting Value Why it is board-specific
MOTHERBOARD BOARD_4PI Selects this board
CUSTOM_MACHINE_NAME "4pi" Identifies the board in M115
SERIAL_PORT -1 The 4pi's host link is native USB; there is no USB-serial chip
EEPROM_SETTINGS enabled The HAL provides flash-backed emulation on this chip
SDSUPPORT enabled The onboard socket works, over HSMCI
MICROSTEP_MODES { 16, … } The soldered A4982 drivers do 1/2/4/16 — not 8
DIGIPOT_MOTOR_CURRENT { 128, … } AD5206 scaling: count × 7.43 ≈ mA, so ~950 mA

The two driver values come from the original 4pi/Sprinter firmware's
init_configuration.h (_AXIS_CURRENT), and are a starting point rather than a
recommendation — set the current to suit the motors actually fitted.


File map

Everything added by this branch:

Marlin/src/HAL/SAM3U/            the HAL (see its AGENTS.md)
  include/                       Arduino-core stand-in: Arduino.h, SPI.h,
                                 pinmapping.h, generated instance/
  startup.c                      vector table + Reset_Handler
  main.cpp                       entry point: clock, SysTick, setup()/loop()
  clock.*, adc.*, timers.*       peripherals
  arduino.cpp, fastio.h          the Arduino API and fast pin access
  MarlinSerial.*, HAL_SPI.cpp    UART and bit-banged SPI
  MarlinSerialUSB.*              Marlin serial over native USB
  usb/usb_cdc.*                  CDC-ACM device on UDPHS
  eeprom/eeprom_flash.cpp        flash-backed PersistentStore
  sdio.cpp                       onboard SD over HSMCI
  inc/                           family Conditionals + SanityCheck
Marlin/src/pins/sam3u/pins_4PI.h board pin map + expansion header pinouts
buildroot/share/PlatformIO/
  boards/marlin_4pi.json         board definition
  scripts/sam3u_build.py         CMSIS include paths, libc restore
  variants/SAM3U/                sam3u4e_marlin.ld
buildroot/share/scripts/
  gen_sam3u_instance_headers.py  regenerates the missing CMSIS headers
ini/sam3u.ini                    env:4pi

Modified: Marlin/src/HAL/platforms.h (family branch),
Marlin/src/core/boards.h (BOARD_4PI), Marlin/src/pins/pins.h
(pins file), platformio.ini (ini include).


Reference material used

  • SAM3U datasheetAtmel-6430-…-SAM3U4-…_Datasheet.pdf. Sections used
    here: 8.2.3 flash organisation, 35.6 timer/counter, 42-55 flash wait states.
  • 4pi-firmware/ — the original Sprinter port for this board. The best
    source for UDPHS USB (at91lib/usb/), HSMCI SD (at91lib/memories/sdmmc/),
    and the stock machine defaults (src/init_configuration.h).
  • reup/ — the board's EAGLE schematic and pinmap. The expansion header
    tables in pins_4PI.h were traced from untitled.sch.

Configurations

4pi example configuration.zip

Related Issues

@ellensp ellensp added Needs: Testing Testing is needed for this change PR: New Feature T: HAL & APIs Topic related to the HAL and internal APIs. labels Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Testing Testing is needed for this change PR: New Feature T: HAL & APIs Topic related to the HAL and internal APIs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants