The two g_tx_completed spin loops in usb_write() had no timeout at all -
if the host ever stopped pulling an IN packet while the device still read
as enumerated, PM5 would spin forever with no recovery path. Add a
spin-iteration bound (not tick-based: g_tx_completed is set from the USB
core's own ISR, and the shared ticks timer is routinely stopped by LF/HF
code at arbitrary points), matching the existing bounded-wait pattern
used elsewhere in this codebase.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Port the "reduce idle power" feature from the Fantasi firmware
(soeinova/Fantasi @ 9671309, plus its later ACC-SOF fix) to the PM5
(AT32F435) target.
Problem
-------
The PM5 firmware ran the ARM core at 288 MHz with a 1.3 V LDO and a
main loop that busy-polled at full speed even when the device was idle.
The 288 MHz PLL and the 1.3 V LDO dominate idle draw, and the busy loop
adds to it. Nothing scaled back when there was no work.
Change
------
New armsrc/pm5_power.c drives a refcounted clock/voltage governor:
- Idle (nothing pending, boost count 0, boot grace elapsed, FPGA off):
take SCLK off the PLL onto HICK-48, power the PLL down, drop the LDO
to 1.1 V, park the FPGA 24 MHz clock (PA8 low), and halt the core in
WFI until the next IRQ or the 1 ms SysTick wake.
- Any timing-critical work holds a 288 MHz boost: AppMain wraps
PacketReceived (which on the PM5 also covers a standalone mode, since
it is only entered through a command there); the BWM low-batt poll
and the RGB indicator wrap their I2C. Boost is refcounted and
IRQ-guarded.
- USB (OS image) moves to the crystal-less HICK-48 clock, ACC-trimmed
against the SOF, so powering the PLL down never touches USB. The
bootrom keeps the HEXT/PLL USB path (AS_BOOTROM), so DFU flashing is
unchanged.
- bwm_uart_clock_update() re-inits UART4 through the SDK usart_init()
on each clock switch (the divider set at 288 MHz is 6x off at 48 MHz).
- FpgaIsOff() (fpga_core.c, PM5 only) tracks the last conf word so the
governor never downclocks while a reader field / emulation is running.
- Wake preamble on the BWM link: the companion module firmware
(Proxmark5_BWM_esp32) light-sleeps 2 s after the last byte on the link
and its UART wakes on RX edges but loses the bytes that carried them.
bwm_uart_write() therefore leads with four 0x55 bytes and a 10 ms
settle on its first write and after 1 s without a write of its own
(RX is not tracked: the ring can be drained long after the bytes
came in). The module only starts light-sleeping once it has seen a
preamble, so the one sent while it was still booting is repeated
after the boot-time link negotiation. A module firmware without light
sleep drops the preamble as noise.
New `hw powersave on|off` toggles the governor at runtime (default on);
`hw status` reports the idle-clock and WFI-asleep fractions.
Behaviour change
----------------
- PM5 idles at 48 MHz with the PLL off and the CPU halted; it boosts to
288 MHz for commands and BWM housekeeping. Command timing and
throughput are unchanged (intra-command work is always boosted).
- PM5 USB now runs off HICK-48 (ACC-trimmed) instead of the HEXT PLL.
- New command id CMD_PM5_POWERSAVE (0x0180).
Testing
-------
Built warning-free (gcc) for PLATFORM=PM5 (with and without
PLATFORM_EXTRAS=BWM), PM3RDV4 and PM3GENERIC, armsrc and bootrom. Client
built via the default Makefile. Not built with clang or on
Windows/macOS; nothing outside PM5/AT32 guards changes.
Flashed to a PM5 and exercised against a MIFARE Classic 4K card:
hw version/status/ping (4000 & 512 B), hw tune, hf search, hf 14a
info/reader (x12 across idle gaps), hf mf info/chk/rdbl, lf search/read,
hw powersave off/on, held-field (raw -sk) correctly blocks the downclock,
hw reset + re-enumerate. All pass; USB stays up across downclocks. Idle
reaches ~80-95% at 48 MHz and ~90%+ WFI-halted after a few seconds idle.
Measured at the USB port: idle draw 283 mA -> 207 mA with the stock
BWM firmware.
Co-authored-by: Soei Nova <soeinova@proton.me>
Co-authored-by: noproto <noproto@zeroday.engineering>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A transfer whose size is an exact multiple of the endpoint size ends with
a zero-length packet. Nothing acknowledged it, so that receive bank stayed
selected and data queued in the other bank stalled. A command already
buffered by usb_read_ng was also not recognised until another packet came
in. Both AT91 and AT32 reuse their receive cleanup to acknowledge the empty
packet and rearm reception; frame parsing is unchanged.
Seen as a small packet lost now and then right behind a burst of
CMD_HF_MIFARE_EML_MEMSET chunks from `hf mfdes eload`.
(cherry picked from cindersocket/proxmark3 495bc201d, upstream PR #3628)
Co-Authored-By: gpt-daybreak-blue <noreply@openai.com>
A realtime `lf read` streams samples straight to the host at the LF sample rate,
around 125 kB/s. If the host stopped draining for even a moment,
async_usb_write_requestWrite() returned false, and ReadLF_realtime() treated that
as fatal: it returned PM3_EIO through a goto that also skipped
async_usb_write_stop(). The IN endpoint was left busy, every later usb_write()
then returned PM3_EIO, and the device went silent until it was physically
replugged. A USB bus reset does not clear it -- EP0 keeps working, descriptors
read fine, only bulk IN is dead. That is what a long `lf read` looked like from
the client: a transfer that stopped early, and then a device that would not
answer the next command.
A busy endpoint is back pressure, not an error. The sampling loop now waits for
the host, up to 100ms, and only gives up if it never comes back. Every exit path
closes the async write. The spins inside the USB helpers are bounded and drop the
stuck packet instead of turning into an infinite loop, which is the other half of
why the device never recovered. Measured against a reader deliberately held at
41 kB/s: before, the stream died at 212238 of 300000 bytes and the unit needed a
replug; after, 300000 of 300000 and it still answers.
The last packet of a stream was being lost as well. The host's CDC read buffer is
two max sized packets, and a bulk IN transfer only completes on a short packet or
a full buffer, so a stream ending on a full 64 byte packet was left sitting in a
half filled buffer. It showed as exact parity on the packet count:
513 packets requested -> 512 delivered 514 -> 514
515 packets requested -> 514 delivered 516 -> 516
async_usb_write_stop() now always sends a closing packet, the leftover partial
bytes when there are any and a zero length packet otherwise, the same way
usb_write() already did for its own transfers.
On the client side a short transfer is reported instead of being presented as a
complete read, the device is told to stop streaming on that path too, and the in
place byte counter is repeated with its final value, since the loop only samples
it every 10ms and the last line printed was stale.
Lastly `lf read` and `lf sniff` cap the sample count to the graph buffer size.
Anything past it was streamed and then discarded by getSamplesFromBufEx(), so
`-s 1500000` spent about two extra seconds collecting 220000 samples that were
thrown away, and printed "Received 1500000 / 1500000 bytes" followed by "Got
1280000 samples" with nothing to explain the gap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AT91F_USB_SendStall spun on STALLSENT with no exit. That bit only
arrives once the host polls the endpoint and takes the handshake, so a
host that vanishes in between wedges the device. It was the last
unbounded wait in the file without an escape.
Exit on RXSETUP (host abandoned the request and sent a new SETUP), on
ENDBUSRES, or on a spin count, reusing the 0x1FFF that usb_read already
uses. usb_check() is not usable here: it calls back into
AT91F_CDC_Enumerate, which is what calls this.
Do not test RXSUSP. Nothing writes it back to UDP_ICR, so it latches on
the first bus idle and stays set, which makes the guard fire on every
stall and stops the STALL from ever being delivered.
The second wait is bounded too, since leaving the first one early can
let the host set STALLSENT after the clear.
Also ISOERROR -> STALLSENT. Same bit 3, but only one of those names is
true on a control endpoint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
uint32_t plus three uint8_t pads to 8, so sizeof(line) was 8 where CDC
defines 7. Hosts asking for more than 7 got a byte of uninitialised
static padding. The commented out SET_LINE_CODING loop already hardcodes
i < 7, so the wire format was understood; the struct just did not match.
No observable change for cdc_acm or usbser.sys, which both ask for
exactly 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AT91F_USB_SendData only took length, and every caller passed
MIN(sizeof(x), wLength), so the function could not tell it had
truncated. A control IN transfer ends on wLength bytes or on a short
packet; returning fewer than wLength in whole 8 byte packets is neither,
and the host polls until it times out.
Take wLength too, do the MIN inside, and send a ZLP when the reply is
shorter than asked and lands on a packet boundary. The length > 0 guard
avoids a second termination, since the send loop already emits an empty
packet for an empty payload.
This is what the serial number descriptor's '(size % 8) == 0 OS bug
workaround' padding was dodging. Not an OS bug.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
StrMS_OSDescriptor advertises MSFT100 and vendor code 0x1C, but the
handler for that request was commented out, so it fell through to the
standard switch and stalled. Windows caches that under
usbflags\<VID><PID><bcdDevice> and stops asking, and bcdDevice never
changes, so no reflash could recover it.
Enable both feature descriptors and answer 0x1C in AT91F_CDC_Enumerate.
The compatible ID is left empty so usbser.sys keeps binding; WINUSB
there would take interface 0 away from CDC and kill the COM port.
DeviceInterfaceGUID was GUID_DEVCLASS_PORTS, a setup class GUID in an
interface GUID field - replaced with a fixed project GUID.
AT32 is unchanged; both descriptors are behind #ifndef PM5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Send a synchronous zero-length packet after CDC responses whose length is
an exact multiple of the endpoint packet size, preventing oversized host
reads from remaining pending.