mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2026-10-12 02:09:28 +00:00
pm5: reduce idle power (core clock scaling + WFI idle)
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 <[email protected]> Co-authored-by: noproto <[email protected]> Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Soei Nova
noproto
Claude Fable 5.1
parent
622547351b
commit
4f4acedbdd
@@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file.
|
||||
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...
|
||||
|
||||
## [unreleased][unreleased]
|
||||
- Added `hw powersave` - PM5 low-power idle, on by default: between commands the core drops to 48 MHz with the PLL off, the FPGA clock stopped and the CPU halted (WFI); USB now runs crystal-less off HICK. Ported from the Fantasi firmware (@Msprg)
|
||||
- Fixed `hf mf autopwn` on FM11RF08S - static encrypted nonce is now detected via the backdoor key (@iceman1001)
|
||||
- Added `hf mfu view -v` - prints the UL-C 3DES key or UL-AES key stored in the dump file (@iceman1001)
|
||||
- Added `hf mf sen --ns` - skip writing the key and dump files (@iceman1001)
|
||||
|
||||
@@ -313,6 +313,7 @@ if (PM5)
|
||||
list(APPEND THUMBSRC startup_at32f435_437.s)
|
||||
list(APPEND THUMBSRC i2c.c)
|
||||
list(APPEND THUMBSRC buzzer.c) # generic mainboard buzzer (QC self-test + BWM low-batt)
|
||||
list(APPEND THUMBSRC pm5_power.c) # low-power idle (core clock scaling + WFI)
|
||||
list(APPEND THUMBSRC at32_unit_test.c) # TODO DXL: AT32单元测试代码
|
||||
endif ()
|
||||
|
||||
|
||||
@@ -138,6 +138,13 @@ else
|
||||
SRC_BUZZER =
|
||||
endif
|
||||
|
||||
# PM5 low-power idle (core clock scaling + WFI)
|
||||
ifeq ($(PLATFORM),PM5)
|
||||
SRC_PM5_POWER = pm5_power.c
|
||||
else
|
||||
SRC_PM5_POWER =
|
||||
endif
|
||||
|
||||
# PM5 antenna-RGB power/battery indicator
|
||||
ifneq (,$(findstring WITH_PM5_PWR_LED,$(APP_CFLAGS)))
|
||||
SRC_RGB_INDICATOR = rgb_indicator.c
|
||||
@@ -204,6 +211,7 @@ THUMBSRC = start.c \
|
||||
$(SRC_BWM) \
|
||||
$(SRC_BWM_CHARGER) \
|
||||
$(SRC_BUZZER) \
|
||||
$(SRC_PM5_POWER) \
|
||||
$(SRC_RGB_INDICATOR) \
|
||||
$(SRC_HITAG) \
|
||||
$(SRC_EM4x50) \
|
||||
|
||||
+30
-2
@@ -89,6 +89,7 @@
|
||||
#include "bwm_charger.h" // BWM charger / fuel-gauge + low-batt warning (WITH_BWM_*)
|
||||
#include "buzzer.h" // PM5 mainboard buzzer API
|
||||
#include "rgb_indicator.h" // PM5 antenna-RGB power/battery indicator (WITH_PM5_PWR_LED)
|
||||
#include "pm5_power.h" // PM5 power-save idle (clock scaling + WFI)
|
||||
|
||||
|
||||
#ifdef WITH_PM5_AUTOOFF
|
||||
@@ -605,6 +606,9 @@ static void SendStatus(uint32_t wait) {
|
||||
Dbprintf(" BWM fw version...... " _YELLOW_("%s"), "unknown");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef PM5
|
||||
pm5_power_print_status();
|
||||
#endif
|
||||
printConnSpeed(wait);
|
||||
DbpString(_CYAN_("Various"));
|
||||
@@ -4291,8 +4295,15 @@ static void PacketReceived(PacketCommandNG *packet) {
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#endif // WITH_BWM_STATUS
|
||||
case CMD_PM5_POWERSAVE: {
|
||||
// Payload: 1 byte, non-zero = enable (the default), zero = disable.
|
||||
pm5_power_set_enabled((packet->length >= 1) ? (packet->data.asBytes[0] != 0) : true);
|
||||
uint8_t state = pm5_power_get_enabled() ? 1 : 0;
|
||||
reply_ng(CMD_PM5_POWERSAVE, PM3_SUCCESS, &state, 1);
|
||||
break;
|
||||
}
|
||||
#endif // PM5
|
||||
default: {
|
||||
Dbprintf("%s: 0x%04x", "unknown command:", packet->cmd);
|
||||
break;
|
||||
@@ -4377,12 +4388,19 @@ void __attribute__((noreturn)) AppMain(void) {
|
||||
// Negotiate the link up from the boot baud; harmless no-op if the ESP is
|
||||
// older or the target is unreachable (stays at BWM_UART_BAUD).
|
||||
bwm_fwd_negotiate_baud(BWM_UART_BAUD_TARGET);
|
||||
// The ESP only light-sleeps once it has seen a preamble, and ours went out
|
||||
// while it was still booting if both powered up together: send it again.
|
||||
bwm_uart_wake_next();
|
||||
#endif
|
||||
|
||||
#ifdef WITH_BWM_CHARGERKICK
|
||||
bwm_charger_kick();
|
||||
#endif
|
||||
|
||||
#ifdef PM5
|
||||
pm5_power_init();
|
||||
#endif
|
||||
|
||||
for (;;) {
|
||||
WDT_HIT();
|
||||
|
||||
@@ -4410,7 +4428,13 @@ void __attribute__((noreturn)) AppMain(void) {
|
||||
|
||||
int ret = receive_ng(&rx);
|
||||
if (ret == PM3_SUCCESS) {
|
||||
#ifdef PM5
|
||||
pm5_power_boost(); // commands assume the full 288 MHz clock
|
||||
#endif
|
||||
PacketReceived(&rx);
|
||||
#ifdef PM5
|
||||
pm5_power_unboost();
|
||||
#endif
|
||||
last_activity_label = GetTickCountLabel();
|
||||
last_activity_tick = GetTickCount();
|
||||
} else if (ret != PM3_ENODATA) {
|
||||
@@ -4499,5 +4523,9 @@ void __attribute__((noreturn)) AppMain(void) {
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef PM5
|
||||
pm5_power_idle();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
+16
-7
@@ -27,6 +27,7 @@
|
||||
#include "i2c.h"
|
||||
#include "dbprint.h"
|
||||
#include "ticks_apis.h" // WaitMS / GetTickCount / GetTickCountDelta
|
||||
#include "pm5_power.h" // pm5_power_boost / unboost around the low-batt poll
|
||||
#include "ansi.h"
|
||||
|
||||
#ifdef WITH_BWM_LOWBATT_BEEP
|
||||
@@ -470,18 +471,12 @@ static void bwm_beep_low_batt(void) {
|
||||
// charging or if the BWM/gauge is not present. With WITH_PM5_LOWBATT_SHUTDOWN it
|
||||
// also powers the board off once the pack drops to the critical floor, confirmed
|
||||
// across several consecutive polls so a transient HF-field sag can't trip it.
|
||||
void bwm_lowbatt_check(void) {
|
||||
static uint32_t last_tick = 0;
|
||||
static void bwm_lowbatt_poll(void) {
|
||||
#ifdef WITH_PM5_LOWBATT_SHUTDOWN
|
||||
static uint8_t crit = 0;
|
||||
static bool s_vusb_setup = false;
|
||||
#endif
|
||||
|
||||
if ((last_tick != 0) && (GetTickCountDelta(last_tick) < BWM_LOWBATT_PERIOD_MS)) {
|
||||
return;
|
||||
}
|
||||
last_tick = GetTickCount();
|
||||
|
||||
StartTicks();
|
||||
I2C_init(true);
|
||||
|
||||
@@ -539,4 +534,18 @@ void bwm_lowbatt_check(void) {
|
||||
|
||||
bwm_beep_low_batt(); // BuzzerBeep() lazily sets the buzzer up on first use
|
||||
}
|
||||
|
||||
void bwm_lowbatt_check(void) {
|
||||
static uint32_t last_tick = 0;
|
||||
|
||||
if ((last_tick != 0) && (GetTickCountDelta(last_tick) < BWM_LOWBATT_PERIOD_MS)) {
|
||||
return;
|
||||
}
|
||||
last_tick = GetTickCount();
|
||||
|
||||
// Ticks timer, I2C and buzzer timing assume the full core clock.
|
||||
pm5_power_boost();
|
||||
bwm_lowbatt_poll();
|
||||
pm5_power_unboost();
|
||||
}
|
||||
#endif // WITH_BWM_LOWBATT_BEEP
|
||||
|
||||
+40
-1
@@ -21,6 +21,7 @@
|
||||
#include "bwm_uart_at32.h"
|
||||
|
||||
#include "pm3_cmd.h"
|
||||
#include "ticks_apis.h" // GetTickCount / GetTickCountDelta / SpinDelay
|
||||
#include "at32f435_437.h"
|
||||
#include "at32f435_437_crm.h"
|
||||
#include "at32f435_437_gpio.h"
|
||||
@@ -52,6 +53,16 @@ static volatile uint16_t s_rx_tail = 0; // software read cursor; head comes fr
|
||||
static volatile bool s_inited = false;
|
||||
static volatile uint32_t s_cur_baud = BWM_UART_BAUD;
|
||||
|
||||
// The ESP light-sleeps BWM_ESP_AWAKE_MS after the last byte either way and loses
|
||||
// the bytes whose edges wake it, so the first write, and any after
|
||||
// BWM_ESP_WAKE_AFTER_MS of silence of ours (RX is not tracked, see bwm_uart_read),
|
||||
// leads with a disposable preamble; ESP fw without light sleep resyncs past it.
|
||||
#define BWM_ESP_AWAKE_MS 2000 // == UART_LINK_AWAKE_MS on the ESP
|
||||
#define BWM_ESP_WAKE_AFTER_MS 1000 // preamble when quiet longer than this (margin for drift)
|
||||
#define BWM_ESP_WAKE_SETTLE_MS 10 // light-sleep exit + UART driver back up
|
||||
static volatile uint32_t s_last_traffic_tick = 0;
|
||||
static volatile bool s_tx_ever = false; // nothing sent yet: the tick above means nothing
|
||||
|
||||
// Bytes the DMA controller has written so far, wrapped into the ring.
|
||||
// The channel's DTCNT counts DOWN from buffer_size and reloads to buffer_size
|
||||
// at wrap (loop mode), so head = size - remaining, always in [0, size-1].
|
||||
@@ -138,7 +149,17 @@ uint32_t bwm_uart_get_baud(void) {
|
||||
return s_cur_baud;
|
||||
}
|
||||
|
||||
int bwm_uart_write(const uint8_t *data, size_t len) {
|
||||
void bwm_uart_clock_update(void) {
|
||||
if (s_inited == false) {
|
||||
return;
|
||||
}
|
||||
// Recompute the baud divider for the new APB1 clock. usart_init() only
|
||||
// rewrites the divider and the (unchanged) 8N1 frame bits; it does not
|
||||
// touch the enable bits or the DMA request, so it is safe on a live port.
|
||||
usart_init(BWM_UART, s_cur_baud, USART_DATA_8BITS, USART_STOP_1_BIT);
|
||||
}
|
||||
|
||||
static void bwm_uart_write_raw(const uint8_t *data, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
while (usart_flag_get(BWM_UART, USART_TDBE_FLAG) == RESET) {
|
||||
}
|
||||
@@ -146,9 +167,25 @@ int bwm_uart_write(const uint8_t *data, size_t len) {
|
||||
}
|
||||
while (usart_flag_get(BWM_UART, USART_TDC_FLAG) == RESET) {
|
||||
}
|
||||
}
|
||||
|
||||
int bwm_uart_write(const uint8_t *data, size_t len) {
|
||||
if ((s_tx_ever == false) || (GetTickCountDelta(s_last_traffic_tick) > BWM_ESP_WAKE_AFTER_MS)) {
|
||||
// 0x55 = five rising edges per byte; the ESP wakes after three.
|
||||
static const uint8_t wake[4] = { 0x55, 0x55, 0x55, 0x55 };
|
||||
bwm_uart_write_raw(wake, sizeof(wake));
|
||||
SpinDelay(BWM_ESP_WAKE_SETTLE_MS);
|
||||
}
|
||||
bwm_uart_write_raw(data, len);
|
||||
s_last_traffic_tick = GetTickCount();
|
||||
s_tx_ever = true;
|
||||
return PM3_SUCCESS;
|
||||
}
|
||||
|
||||
void bwm_uart_wake_next(void) {
|
||||
s_tx_ever = false;
|
||||
}
|
||||
|
||||
uint16_t bwm_uart_rx_available(void) {
|
||||
// An unhandled overrun (ROERR) latches on this USART and stops it feeding the
|
||||
// DMA - after one overrun every subsequent byte is lost until a re-init, which
|
||||
@@ -170,5 +207,7 @@ uint32_t bwm_uart_read(uint8_t *data, size_t len) {
|
||||
data[n++] = s_rx_ring[s_rx_tail];
|
||||
s_rx_tail = (uint16_t)((s_rx_tail + 1) & (BWM_RX_RING_SZ - 1));
|
||||
}
|
||||
// RX does not refresh s_last_traffic_tick: the ring can be drained long after
|
||||
// the ESP sent the bytes, so it says nothing about the module's awake window.
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -36,8 +36,18 @@ void bwm_uart_set_baud(uint32_t baud);
|
||||
// Baud the link is currently running at (updated by bwm_uart_set_baud).
|
||||
uint32_t bwm_uart_get_baud(void);
|
||||
|
||||
// Re-derive the baud divider for the current APB1 clock. Called on every core
|
||||
// clock switch (pm5_power.c); the divider set at 288 MHz is 6x off at 48 MHz.
|
||||
void bwm_uart_clock_update(void);
|
||||
|
||||
// Blocking write of one whole frame. On the first write, and after ~1 s without
|
||||
// a write of our own, it first sends a wake preamble for the ESP's light sleep
|
||||
// and waits ~10 ms (see the BWM_ESP_* defines in bwm_uart_at32.c).
|
||||
int bwm_uart_write(const uint8_t *data, size_t len);
|
||||
|
||||
// Lead the next write with the wake preamble, whatever the quiet time.
|
||||
void bwm_uart_wake_next(void);
|
||||
|
||||
uint16_t bwm_uart_rx_available(void);
|
||||
|
||||
uint32_t bwm_uart_read(uint8_t *data, size_t len);
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// See LICENSE.txt for the text of the license.
|
||||
//-----------------------------------------------------------------------------
|
||||
// Proxmark5 (AT32F435) power-save idle - see pm5_power.h.
|
||||
//
|
||||
// Idle: SCLK PLL-288 -> HICK-48, PLL off, LDO 1.3 -> 1.1 V (good to 144 MHz),
|
||||
// FPGA CLKOUT1 parked low, core in WFI until an IRQ or a 1 ms SysTick wake.
|
||||
// USB is crystal-less off HICK-48 (ACC-trimmed on the SOF, usb_cdc_at32.c) and
|
||||
// the 1 kHz tick counter is on the ERTC (HEXT/20), so neither notices the PLL
|
||||
// going away. Everything else keeps its 288 MHz assumption via the boost.
|
||||
//
|
||||
// Ported from the Fantasi firmware.
|
||||
//-----------------------------------------------------------------------------
|
||||
#include "pm5_power.h"
|
||||
|
||||
#include "proxmark3_arm.h"
|
||||
#include "dbprint.h"
|
||||
#include "ticks_apis.h"
|
||||
#include "fpga_apis.h"
|
||||
#include "config_gpio_proxmark5.h"
|
||||
#ifdef WITH_BWM_FORWARD
|
||||
#include "bwm_uart_at32.h"
|
||||
#endif
|
||||
|
||||
#include "at32f435_437.h"
|
||||
#include "at32f435_437_crm.h"
|
||||
#include "at32f435_437_pwc.h"
|
||||
#include "at32f435_437_gpio.h"
|
||||
|
||||
// Full speed through USB enumeration and the rest of the boot.
|
||||
#define PM5_POWER_BOOT_GRACE_MS 3000
|
||||
// Quiet time after the last boost before downclocking: back-to-back commands
|
||||
// (autopwn, scripts, BWM forwarded traffic) don't pay the switch each time.
|
||||
#define PM5_POWER_IDLE_MS 200
|
||||
#define PM5_POWER_CLK_WAIT 500000u
|
||||
|
||||
static volatile uint32_t s_boost; // >0 = something holds 288 MHz
|
||||
static volatile bool s_clk_high = true; // boot runs at 288 MHz
|
||||
static bool s_powersave = true;
|
||||
static uint32_t s_last_busy_tick;
|
||||
|
||||
// Stats for `hw status`.
|
||||
static uint32_t s_downclocks;
|
||||
static uint32_t s_low_ms;
|
||||
static uint32_t s_low_since_tick;
|
||||
static uint32_t s_sleeps;
|
||||
static uint32_t s_asleep_ms;
|
||||
static uint32_t s_asleep_us_resid;
|
||||
|
||||
static void pm5_fpga_clk_park(void) {
|
||||
// Static low, not floating, while the PLL behind CLKOUT1 is off.
|
||||
gpio_init_type gpio_init_struct;
|
||||
gpio_default_para_init(&gpio_init_struct);
|
||||
gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;
|
||||
gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL;
|
||||
gpio_init_struct.gpio_mode = GPIO_MODE_OUTPUT;
|
||||
gpio_init_struct.gpio_pins = AT32_GPIO_FPGA_24M_CLK_PIN;
|
||||
gpio_init_struct.gpio_pull = GPIO_PULL_NONE;
|
||||
gpio_bits_reset(AT32_GPIO_FPGA_24M_CLK, AT32_GPIO_FPGA_24M_CLK_PIN);
|
||||
gpio_init(AT32_GPIO_FPGA_24M_CLK, &gpio_init_struct);
|
||||
}
|
||||
|
||||
// Voltage-safe ordering: up = LDO before frequency, down = frequency before LDO.
|
||||
// The LDO is only writable while SCLK is on HICK/HEXT, which holds on both paths.
|
||||
// Runs with IRQs masked.
|
||||
static void pm5_clk_apply(bool high) {
|
||||
uint32_t i;
|
||||
if (high) {
|
||||
pwc_ldo_output_voltage_set(PWC_LDO_OUTPUT_1V3);
|
||||
for (volatile uint32_t d = 0; d < 2000; d++) {} // LDO settle
|
||||
|
||||
crm_clock_source_enable(CRM_CLOCK_SOURCE_PLL, TRUE);
|
||||
for (i = 0; i < PM5_POWER_CLK_WAIT; i++) {
|
||||
if (crm_flag_get(CRM_PLL_STABLE_FLAG) == SET) break;
|
||||
}
|
||||
crm_auto_step_mode_enable(TRUE);
|
||||
crm_sysclk_switch(CRM_SCLK_PLL);
|
||||
for (i = 0; i < PM5_POWER_CLK_WAIT; i++) {
|
||||
if (crm_sysclk_switch_status_get() == CRM_SCLK_PLL) break;
|
||||
}
|
||||
crm_auto_step_mode_enable(FALSE);
|
||||
system_core_clock_update();
|
||||
|
||||
FpgaSetup24MHzClk(); // CLKOUT1 back on PA8
|
||||
} else {
|
||||
pm5_fpga_clk_park();
|
||||
|
||||
crm_clock_source_enable(CRM_CLOCK_SOURCE_HICK, TRUE);
|
||||
for (i = 0; i < PM5_POWER_CLK_WAIT; i++) {
|
||||
if (crm_flag_get(CRM_HICK_STABLE_FLAG) == SET) break;
|
||||
}
|
||||
crm_auto_step_mode_enable(TRUE);
|
||||
crm_sysclk_switch(CRM_SCLK_HICK);
|
||||
for (i = 0; i < PM5_POWER_CLK_WAIT; i++) {
|
||||
if (crm_sysclk_switch_status_get() == CRM_SCLK_HICK) break;
|
||||
}
|
||||
crm_auto_step_mode_enable(FALSE);
|
||||
crm_clock_source_enable(CRM_CLOCK_SOURCE_PLL, FALSE);
|
||||
pwc_ldo_output_voltage_set(PWC_LDO_OUTPUT_1V1);
|
||||
system_core_clock_update();
|
||||
}
|
||||
#ifdef WITH_BWM_FORWARD
|
||||
bwm_uart_clock_update(); // UART4 baud follows APB1
|
||||
#endif
|
||||
s_clk_high = high;
|
||||
}
|
||||
|
||||
void pm5_power_init(void) {
|
||||
// SCLK-from-HICK must be 48 MHz, not the 8 MHz reset default. usb_enable()
|
||||
// already selects this for the crystal-less USB clock; make it explicit.
|
||||
crm_hick_sclk_frequency_select(CRM_HICK_SCLK_48MHZ);
|
||||
|
||||
// Free-running cycle counter: measures the time actually spent in WFI.
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CYCCNT = 0;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
|
||||
s_last_busy_tick = GetTickCount();
|
||||
}
|
||||
|
||||
void pm5_power_boost(void) {
|
||||
__disable_irq();
|
||||
if (s_boost++ == 0 && s_clk_high == false) {
|
||||
pm5_clk_apply(true);
|
||||
s_low_ms += GetTickCountDelta(s_low_since_tick);
|
||||
}
|
||||
__enable_irq();
|
||||
}
|
||||
|
||||
void pm5_power_unboost(void) {
|
||||
__disable_irq();
|
||||
if (s_boost > 0) {
|
||||
s_boost--;
|
||||
}
|
||||
__enable_irq();
|
||||
// The downclock itself is left to pm5_power_idle() after PM5_POWER_IDLE_MS.
|
||||
if (s_boost == 0) {
|
||||
s_last_busy_tick = GetTickCount();
|
||||
}
|
||||
}
|
||||
|
||||
static bool pm5_power_may_downclock(void) {
|
||||
if (s_boost != 0 || s_clk_high == false) {
|
||||
return false;
|
||||
}
|
||||
if (GetTickCount() < PM5_POWER_BOOT_GRACE_MS) {
|
||||
return false;
|
||||
}
|
||||
if (GetTickCountDelta(s_last_busy_tick) < PM5_POWER_IDLE_MS) {
|
||||
return false;
|
||||
}
|
||||
// A reader field or emulation left running needs the FPGA clock.
|
||||
return FpgaIsOff();
|
||||
}
|
||||
|
||||
// Halt until the next IRQ, at most ~1 ms (one-shot SysTick, AHB/8 source like
|
||||
// SpinDelayUs which reprograms SysTick anyway). IRQs stay masked across the
|
||||
// WFI so the cycle delta measures only the sleep; the waking ISR runs after.
|
||||
static void pm5_power_wfi(void) {
|
||||
SysTick->CTRL = 0;
|
||||
SysTick->LOAD = (system_core_clock / 8 / 1000) - 1;
|
||||
SysTick->VAL = 0;
|
||||
SysTick->CTRL = SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
|
||||
|
||||
__disable_irq();
|
||||
uint32_t t0 = DWT->CYCCNT;
|
||||
__WFI();
|
||||
uint32_t cycles = DWT->CYCCNT - t0;
|
||||
__enable_irq();
|
||||
|
||||
SysTick->CTRL = 0;
|
||||
|
||||
s_sleeps++;
|
||||
s_asleep_us_resid += cycles / (system_core_clock / 1000000);
|
||||
if (s_asleep_us_resid >= 1000) {
|
||||
s_asleep_ms += s_asleep_us_resid / 1000;
|
||||
s_asleep_us_resid %= 1000;
|
||||
}
|
||||
}
|
||||
|
||||
void pm5_power_idle(void) {
|
||||
if (s_powersave == false) {
|
||||
return;
|
||||
}
|
||||
if (pm5_power_may_downclock()) {
|
||||
SpinDelayUs(10); // let a just-sent FPGA conf word land before its clock stops
|
||||
__disable_irq();
|
||||
pm5_clk_apply(false);
|
||||
__enable_irq();
|
||||
s_downclocks++;
|
||||
s_low_since_tick = GetTickCount();
|
||||
}
|
||||
pm5_power_wfi();
|
||||
}
|
||||
|
||||
void pm5_power_set_enabled(bool on) {
|
||||
s_powersave = on;
|
||||
if (on == false && s_clk_high == false) {
|
||||
__disable_irq();
|
||||
pm5_clk_apply(true);
|
||||
__enable_irq();
|
||||
s_low_ms += GetTickCountDelta(s_low_since_tick);
|
||||
}
|
||||
}
|
||||
|
||||
bool pm5_power_get_enabled(void) {
|
||||
return s_powersave;
|
||||
}
|
||||
|
||||
// Per-mille of uptime, printed as a percentage with one decimal.
|
||||
static uint32_t pm5_power_permille(uint32_t ms, uint32_t uptime_ms) {
|
||||
if (uptime_ms == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (uint32_t)(((uint64_t)ms * 1000) / uptime_ms);
|
||||
}
|
||||
|
||||
void pm5_power_print_status(void) {
|
||||
uint32_t uptime = GetTickCount();
|
||||
uint32_t low = pm5_power_permille(s_low_ms, uptime);
|
||||
uint32_t asleep = pm5_power_permille(s_asleep_ms, uptime);
|
||||
|
||||
DbpString(_CYAN_("Power"));
|
||||
Dbprintf(" Power-save idle..... %s", s_powersave ? _GREEN_("enabled") : _YELLOW_("disabled"));
|
||||
DbpString(" Core clock.......... 288 MHz active, 48 MHz idle ( PLL off, LDO 1.1 V )");
|
||||
Dbprintf(" Uptime.............. %u ms", uptime);
|
||||
Dbprintf(" Idle at 48 MHz...... %u.%u %% ( %u downclocks )", low / 10, low % 10, s_downclocks);
|
||||
Dbprintf(" Halted in WFI....... %u.%u %% ( %u sleeps )", asleep / 10, asleep % 10, s_sleeps);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// See LICENSE.txt for the text of the license.
|
||||
//-----------------------------------------------------------------------------
|
||||
// Proxmark5 power-save idle: core clock scaling + WFI - see pm5_power.c.
|
||||
//-----------------------------------------------------------------------------
|
||||
#ifndef PM5_POWER_H__
|
||||
#define PM5_POWER_H__
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#ifdef PM5
|
||||
|
||||
// Call once from AppMain after USB is up.
|
||||
void pm5_power_init(void);
|
||||
|
||||
// Refcounted hold on the full 288 MHz core clock. Wrap anything with timing
|
||||
// that assumes the boot clock (commands, standalone mode, I2C, buzzer).
|
||||
void pm5_power_boost(void);
|
||||
void pm5_power_unboost(void);
|
||||
|
||||
// Main-loop idle step: drop to the idle clock when allowed, then halt the core
|
||||
// until the next IRQ or the 1 ms wake tick.
|
||||
void pm5_power_idle(void);
|
||||
|
||||
// Runtime toggle (default on). Off = stay at 288 MHz and spin.
|
||||
void pm5_power_set_enabled(bool on);
|
||||
bool pm5_power_get_enabled(void);
|
||||
|
||||
// `hw status` section.
|
||||
void pm5_power_print_status(void);
|
||||
|
||||
#endif // PM5
|
||||
|
||||
#endif // PM5_POWER_H__
|
||||
@@ -21,6 +21,7 @@
|
||||
#ifdef WITH_PM5_PWR_LED
|
||||
|
||||
#include "rgb_apis.h" // RgbLedSet
|
||||
#include "pm5_power.h" // pm5_power_boost / unboost
|
||||
#include "gpio_apis.h" // gpio_vusb_setup / Gpio_VUSB_Read (USB-present detection)
|
||||
#include "ticks_apis.h" // GetTickCount / GetTickCountDelta
|
||||
|
||||
@@ -68,6 +69,7 @@ void rgb_indicator_update(void) {
|
||||
return; // edge-triggered: only write RGB when the state changes
|
||||
}
|
||||
|
||||
pm5_power_boost(); // I2C bit-bang timing assumes the full core clock
|
||||
// Commit last_state only if the controller actually ACKed. On a cold boot the
|
||||
// I2C RGB controller can still be powering up when the first write fires; if we
|
||||
// latched last_state regardless, a NAK here would leave the LED dark until the
|
||||
@@ -78,6 +80,7 @@ void rgb_indicator_update(void) {
|
||||
if (ok) {
|
||||
last_state = on_battery;
|
||||
}
|
||||
pm5_power_unboost();
|
||||
}
|
||||
|
||||
#endif // WITH_PM5_PWR_LED
|
||||
|
||||
@@ -2033,6 +2033,50 @@ static int CmdPM5QCTest(const char *Cmd) {
|
||||
}
|
||||
|
||||
|
||||
static int CmdPowerSave(const char *Cmd) {
|
||||
// Positional sub-action (no dashes): hw powersave on | off
|
||||
char verb[16] = {0};
|
||||
sscanf(Cmd, "%15s", verb);
|
||||
bool on = (strcmp(verb, "on") == 0);
|
||||
bool off = (strcmp(verb, "off") == 0);
|
||||
|
||||
if (!on && !off) {
|
||||
CLIParserContext *ctx;
|
||||
CLIParserInit(&ctx, "hw powersave",
|
||||
"Toggle the PM5 power-save idle. Default is " _GREEN_("on") ": between commands the\n"
|
||||
"core drops from 288 to 48 MHz with the PLL off, the FPGA clock stopped and the\n"
|
||||
"CPU halted (WFI) until the next interrupt or the 1 ms wake tick. Commands run\n"
|
||||
"at full speed; turn it off to keep the core at 288 MHz and spinning.\n"
|
||||
_YELLOW_("Runtime only:") " resets to on at each boot. See `hw status` for the idle stats.",
|
||||
"hw powersave off --> stay at 288 MHz, no idle sleep\n"
|
||||
"hw powersave on --> re-enable the power-save idle");
|
||||
void *argtable[] = {
|
||||
arg_param_begin,
|
||||
arg_param_end
|
||||
};
|
||||
CLIExecWithReturn(ctx, Cmd, argtable, true);
|
||||
CLIParserFree(ctx);
|
||||
PrintAndLogEx(WARNING, "specify " _YELLOW_("on") " or " _YELLOW_("off"));
|
||||
return PM3_EINVARG;
|
||||
}
|
||||
|
||||
uint8_t payload = off ? 0 : 1;
|
||||
|
||||
clearCommandBuffer();
|
||||
SendCommandNG(CMD_PM5_POWERSAVE, &payload, sizeof(payload));
|
||||
PacketResponseNG resp;
|
||||
if (WaitForResponseTimeout(CMD_PM5_POWERSAVE, &resp, 2500) == false) {
|
||||
PrintAndLogEx(WARNING, "command timeout (is this a PM5?)");
|
||||
return PM3_ETIMEOUT;
|
||||
}
|
||||
if (resp.status != PM3_SUCCESS) {
|
||||
PrintAndLogEx(FAILED, "failed to set power-save idle");
|
||||
return resp.status;
|
||||
}
|
||||
PrintAndLogEx(SUCCESS, "Power-save idle %s.", payload ? _GREEN_("enabled") : _YELLOW_("disabled"));
|
||||
return PM3_SUCCESS;
|
||||
}
|
||||
|
||||
static command_t CommandTable[] = {
|
||||
{"help", CmdHelp, AlwaysAvailable, "This help"},
|
||||
{"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operation") " -----------------------"},
|
||||
@@ -2054,6 +2098,7 @@ static command_t CommandTable[] = {
|
||||
{"lcd", CmdLCD, IfPm3Lcd, "Send command/data to LCD"},
|
||||
{"lcdreset", CmdLCDReset, IfPm3Lcd, "Hardware reset LCD"},
|
||||
{"ping", CmdPing, IfPm3Present, "Test if the Proxmark3 is responsive"},
|
||||
{"powersave", CmdPowerSave, IfPm5, "Enable/disable the PM5 power-save idle"},
|
||||
{"readmem", CmdReadmem, IfPm3Present, "Read from MCU flash"},
|
||||
{"reset", CmdReset, IfPm3Present, "Reset the device"},
|
||||
{"setlfdivisor", CmdSetDivisor, IfPm3Lf, "Drive LF antenna at 12MHz / (divisor + 1)"},
|
||||
|
||||
@@ -327,6 +327,13 @@ void FpgaSendCommand(uint16_t cmd, uint16_t v);
|
||||
//-----------------------------------------------------------------------------
|
||||
void FpgaWriteConfWord(uint16_t v);
|
||||
|
||||
#ifdef PM5
|
||||
//-----------------------------------------------------------------------------
|
||||
// True when the last configuration word selected FPGA_MAJOR_MODE_OFF.
|
||||
//-----------------------------------------------------------------------------
|
||||
bool FpgaIsOff(void);
|
||||
#endif
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// enable FPGA internal tracing
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
@@ -15,8 +15,15 @@ bool FpgaIs16BitMsbMode(uint16_t fpga_mode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef PM5
|
||||
static uint16_t s_fpga_conf_word = FPGA_MAJOR_MODE_OFF;
|
||||
#endif
|
||||
|
||||
void FpgaWriteConfWord(uint16_t v) {
|
||||
const int current = FpgaGetCurrent();
|
||||
#ifdef PM5
|
||||
s_fpga_conf_word = v;
|
||||
#endif
|
||||
|
||||
// Keep track of whether or not we should be monitoring the HF field timeout
|
||||
if (current == FPGA_BITSTREAM_HF || current == FPGA_BITSTREAM_HF_15 || current == FPGA_BITSTREAM_HF_FELICA) {
|
||||
@@ -44,6 +51,12 @@ void FpgaWriteConfWord(uint16_t v) {
|
||||
FpgaSendCommand(FPGA_CMD_SET_CONFREG, v);
|
||||
}
|
||||
|
||||
#ifdef PM5
|
||||
bool FpgaIsOff(void) {
|
||||
return (s_fpga_conf_word & FPGA_MAJOR_MODE_MASK) == FPGA_MAJOR_MODE_OFF;
|
||||
}
|
||||
#endif
|
||||
|
||||
void FpgaEnableTracing(void) {
|
||||
FpgaSendCommand(FPGA_CMD_TRACE_ENABLE, 1);
|
||||
}
|
||||
|
||||
@@ -162,26 +162,23 @@ static usbd_desc_handler cdc_desc_handler = {
|
||||
*/
|
||||
static void usb_clock48m_select(usb_clk48_s clk_s) {
|
||||
if (clk_s == USB_CLK_HICK) {
|
||||
/* UNUSED!!!
|
||||
|
||||
// HICK-48 (also makes SCLK-from-HICK 48 MHz), trimmed by the ACC against
|
||||
// the USB SOF to +/-0.25%. Independent of the PLL, so the PLL can be
|
||||
// powered down at idle (armsrc/pm5_power.c) without touching USB.
|
||||
crm_usb_clock_source_select(CRM_USB_CLOCK_SOURCE_HICK);
|
||||
|
||||
// enable the acc calibration ready interrupt
|
||||
crm_periph_clock_enable(CRM_ACC_PERIPH_CLOCK, TRUE);
|
||||
|
||||
// update the c1\c2\c3 value
|
||||
// compare window around the ideal 8000 HICK/6 counts per 1 ms SOF
|
||||
acc_write_c1(7980);
|
||||
acc_write_c2(8000);
|
||||
acc_write_c3(8020);
|
||||
#if (USB_ID == 0)
|
||||
#if (USB_ID == 0)
|
||||
acc_sof_select(ACC_SOF_OTG1);
|
||||
#else
|
||||
#else
|
||||
acc_sof_select(ACC_SOF_OTG2);
|
||||
#endif
|
||||
// open acc calibration
|
||||
#endif
|
||||
acc_calibration_mode_enable(ACC_CAL_HICKTRIM, TRUE);
|
||||
|
||||
*/
|
||||
} else {
|
||||
switch (system_core_clock) {
|
||||
/* 48MHz */
|
||||
@@ -374,7 +371,11 @@ void usb_enable(void) {
|
||||
usb_gpio_config();
|
||||
|
||||
crm_periph_clock_enable(OTG_CLOCK, TRUE); // enable otgfs clock
|
||||
#ifdef AS_BOOTROM
|
||||
usb_clock48m_select(USB_CLK_HEXT); // select usb 48m clcok source
|
||||
#else
|
||||
usb_clock48m_select(USB_CLK_HICK); // crystal-less, survives the idle PLL-off (pm5_power.c)
|
||||
#endif
|
||||
nvic_irq_enable(OTG_IRQ, 0, 0); // enable otgfs irq
|
||||
usbd_init(&otg_core_struct, USB_FULL_SPEED_CORE_ID, USB_ID, &cdc_class_handler, &cdc_desc_handler); // init usb
|
||||
|
||||
|
||||
+15
-2
@@ -11261,6 +11261,19 @@
|
||||
],
|
||||
"usage": "hw ping [-h] [-l <dec>]"
|
||||
},
|
||||
"hw powersave": {
|
||||
"command": "hw powersave",
|
||||
"description": "Toggle the PM5 power-save idle. Default is on: between commands the core drops from 288 to 48 MHz with the PLL off, the FPGA clock stopped and the CPU halted (WFI) until the next interrupt or the 1 ms wake tick. Commands run at full speed; turn it off to keep the core at 288 MHz and spinning. Runtime only: resets to on at each boot. See `hw status` for the idle stats.",
|
||||
"notes": [
|
||||
"hw powersave off -> stay at 288 MHz, no idle sleep",
|
||||
"hw powersave on -> re-enable the power-save idle"
|
||||
],
|
||||
"offline": false,
|
||||
"options": [
|
||||
"-h, --help This help"
|
||||
],
|
||||
"usage": "hw powersave [-h]"
|
||||
},
|
||||
"hw qc_pm5": {
|
||||
"command": "hw qc_pm5",
|
||||
"description": "QC Test for the PM5",
|
||||
@@ -16651,8 +16664,8 @@
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"commands_extracted": 926,
|
||||
"commands_extracted": 927,
|
||||
"extracted_by": "PM3Help2JSON v1.00",
|
||||
"extracted_on": "2026-09-24T08:29:19+00:00"
|
||||
"extracted_on": "2026-09-24T08:29:44+00:00"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1029,6 +1029,7 @@ Check column "offline" for their availability.
|
||||
|`hw lcd `|N |`Send command/data to LCD`
|
||||
|`hw lcdreset `|N |`Hardware reset LCD`
|
||||
|`hw ping `|N |`Test if the Proxmark3 is responsive`
|
||||
|`hw powersave `|N |`Enable/disable the PM5 power-save idle`
|
||||
|`hw readmem `|N |`Read from MCU flash`
|
||||
|`hw reset `|N |`Reset the device`
|
||||
|`hw setlfdivisor `|N |`Drive LF antenna at 12MHz / (divisor + 1)`
|
||||
|
||||
@@ -924,6 +924,8 @@ typedef struct {
|
||||
#define BWM_BLE_NAME_ACTION_GET 0x00 // resp: current BLE device name string
|
||||
#define BWM_BLE_NAME_ACTION_SET 0x01 // req: name bytes follow the action byte; BWM reboots to apply
|
||||
#define BWM_BLE_NAME_MAX_LEN 15 // usable chars; ESP name buffer is 16 incl NUL
|
||||
// PM5, toggle the power-save idle (48 MHz + WFI when idle). Used by `hw powersave`.
|
||||
#define CMD_PM5_POWERSAVE 0x0180
|
||||
#define BWM_OTA_ACTION_BEGIN 0x00
|
||||
#define BWM_OTA_ACTION_WRITE 0x01
|
||||
#define BWM_OTA_ACTION_END 0x02
|
||||
|
||||
Reference in New Issue
Block a user