Files
proxmark3/armsrc/bwm_forward.c
T
Msprg 22e9b37b9f armsrc: resync BWM link baud after an AT32-only reset
The ESP keeps its negotiated UART baud (921600) across a bootloader round
trip, hw reset or flash, while the AT32 restarts at the boot baud (460800)
and its baud-set request is then garbage to the ESP. The link stayed at
460800 and every BWM command failed until the module lost power.

Probe GET_UART_BAUD at both rates on boot and adopt the one the ESP answers
at; only run the set-baud handshake when it really is at the boot baud.
2026-09-03 01:02:38 +02:00

462 lines
17 KiB
C

//-----------------------------------------------------------------------------
// 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.
//
// See LICENSE.txt for the text of the license.
//-----------------------------------------------------------------------------
// Proxmark5 Battery Wireless Module (BWM) transport shim - see bwm_forward.h.
//-----------------------------------------------------------------------------
#include "bwm_forward.h"
#include "bwm_uart_at32.h"
#include "pm3_cmd.h" // PM3_CMD_DATA_SIZE, PM3_* return codes
#include "ticks_apis.h" // SpinDelay
#include "string.h"
#ifndef MIN
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif
// CRC-16/CCITT-FALSE, byte-identical to the BWM firmware's crc16_ccitt()
// (poly 0x1021, init 0xFFFF, MSB-first, no reflection, no xorout).
static uint16_t bwm_crc16(const uint8_t *data, size_t len, uint16_t crc) {
for (size_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (uint8_t b = 0; b < 8; b++) {
if (crc & 0x8000) {
crc = (uint16_t)((crc << 1) ^ BWM_CRC16_POLY);
} else {
crc = (uint16_t)(crc << 1);
}
}
}
return crc;
}
// ---------------------------------------------------------------------------
// TX: wrap one reply frame into a SEND_FORWARD_DATA app_com frame.
// A full NG/OLD frame is <= PM3_CMD_DATA_SIZE + a small header/postamble, well
// under the BWM 4096-byte payload cap, so a single frame always suffices.
// ---------------------------------------------------------------------------
#define BWM_TX_OVERHEAD (2 + 2 + 2 + 2) // hdr + cmd + len + crc
#define BWM_TX_MAX_PAYLOAD (PM3_CMD_DATA_SIZE + 64) // NG/OLD frame ceiling
#define BWM_TX_BUFSZ (BWM_TX_OVERHEAD + BWM_TX_MAX_PAYLOAD)
static void bwm_pump(void); // fwd decl: TX gate pumps RX to collect forward-frame acks
// --- Flow control (ack window) ---------------------------------------------
// s_fwd_inflight: forward frames sent but not yet acked by the ESP. Bumped on
// send, decremented when a SLAVE_RESP echoing cmd=SEND_FORWARD_DATA arrives.
// We may send while it is below BWM_FC_WINDOW; at the cap we wait for an ack.
static volatile int16_t s_fwd_inflight = 0;
// Set true by the parser when a SLAVE_RESP echoing BWM_CMD_SET_UART_BAUD arrives
// (the ESP's ack for a baud-set request). Consumed by bwm_fwd_negotiate_baud().
static volatile bool s_baud_ack = false;
// Set true by the parser on a SLAVE_RESP for BWM_CMD_GET_UART_BAUD - proof the
// ESP is alive and responding at the baud we just switched to (verify step).
static volatile bool s_getbaud_ack = false;
int bwm_fwd_writebuffer_sync(const uint8_t *data, size_t len) {
static uint8_t frame[BWM_TX_BUFSZ]; // single-threaded bare-metal: static OK
if (len > BWM_TX_MAX_PAYLOAD) {
len = BWM_TX_MAX_PAYLOAD; // defensive; should never trigger
}
size_t idx = 0;
// Flow control: block while the in-flight window is full, waiting for the
// ESP to ack an earlier forward frame. bwm_pump() drains the IRQ-filled RX
// ring, so acks are collected even while we sit inside a tight download loop
// (the reply_old firehose). The spin cap is a safety valve so a dead or
// disconnected ESP can't hard-hang us. A window >= 1 means single command
// replies never block - only sustained bursts hit the cap.
{
uint32_t t0 = GetTickCount();
while (s_fwd_inflight >= BWM_FC_WINDOW) {
bwm_pump();
if (GetTickCountDelta(t0) > BWM_FC_ACK_TIMEOUT_MS) {
s_fwd_inflight = 0; // best-effort: assume the pipe cleared, never hard-hang
break;
}
}
}
frame[idx++] = BWM_HDR_HOST_CMD_1;
frame[idx++] = BWM_HDR_HOST_CMD_2;
frame[idx++] = (uint8_t)(BWM_CMD_SEND_FORWARD_DATA & 0xFF);
frame[idx++] = (uint8_t)((BWM_CMD_SEND_FORWARD_DATA >> 8) & 0xFF);
frame[idx++] = (uint8_t)(len & 0xFF);
frame[idx++] = (uint8_t)((len >> 8) & 0xFF);
if (len) {
memcpy(&frame[idx], data, len);
idx += len;
}
uint16_t crc = bwm_crc16(frame, idx, BWM_CRC16_INIT);
frame[idx++] = (uint8_t)(crc & 0xFF);
frame[idx++] = (uint8_t)((crc >> 8) & 0xFF);
int wr = bwm_uart_write(frame, idx);
s_fwd_inflight++; // one more forward frame awaiting its ack
return wr;
}
// ---------------------------------------------------------------------------
// RX: persistent app_com de-framer. Feeds raw FPC bytes through a state machine
// and pushes the payloads of valid DATA_FORWARD (0xD2 0xD3 / cmd 8089) frames
// into a byte FIFO that bwm_read_ng() drains. Non-DATA_FORWARD frames (slave
// responses, forwarded logs, cmd-error reports) are validated and discarded.
// ---------------------------------------------------------------------------
#define BWM_DEFIFO_SZ 2048 // >= one full NG frame's payload
#define BWM_RXFRAME_MAX (PM3_CMD_DATA_SIZE + 64)
typedef enum {
S_IDLE = 0, S_HDR2, S_CMD_LO, S_CMD_HI, S_LEN_LO, S_LEN_HI, S_PAYLOAD, S_CRC_LO, S_CRC_HI
} bwm_state_t;
typedef struct {
bwm_state_t state;
uint8_t hdr1;
bool is_bcast; // header pair is 0xD2 0xD3
uint16_t cmd;
uint16_t len;
uint16_t got; // payload bytes received
uint16_t crc_calc; // running CRC over hdr..payload
uint16_t crc_recv;
uint8_t payload[BWM_RXFRAME_MAX];
} bwm_parser_t;
static bwm_parser_t s_p = { .state = S_IDLE };
// De-framed payload ring
static uint8_t s_fifo[BWM_DEFIFO_SZ];
static volatile uint16_t s_fifo_head = 0; // write
static volatile uint16_t s_fifo_tail = 0; // read
static uint16_t fifo_count(void) {
return (uint16_t)((s_fifo_head - s_fifo_tail) & (BWM_DEFIFO_SZ - 1));
}
static void fifo_push(uint8_t b) {
uint16_t next = (uint16_t)((s_fifo_head + 1) & (BWM_DEFIFO_SZ - 1));
if (next != s_fifo_tail) { // drop on overflow rather than corrupt
s_fifo[s_fifo_head] = b;
s_fifo_head = next;
}
}
static uint8_t fifo_pop(void) {
uint8_t b = s_fifo[s_fifo_tail];
s_fifo_tail = (uint16_t)((s_fifo_tail + 1) & (BWM_DEFIFO_SZ - 1));
return b;
}
static void bwm_reset_frame(bwm_parser_t *p) {
p->state = S_IDLE;
}
// Update running CRC one byte at a time (mirrors the streaming update in the
// BWM firmware parser).
static void crc_step(bwm_parser_t *p, uint8_t byte) {
p->crc_calc = bwm_crc16(&byte, 1, p->crc_calc);
}
static void bwm_feed_byte(bwm_parser_t *p, uint8_t byte) {
switch (p->state) {
case S_IDLE:
if (byte == BWM_HDR_SLAVE_BCAST_1) {
p->hdr1 = byte;
p->is_bcast = true;
p->state = S_HDR2;
} else if (byte == BWM_HDR_SLAVE_RESP_1) {
p->hdr1 = byte;
p->is_bcast = false;
p->state = S_HDR2;
}
// any other byte: stay idle (resync)
break;
case S_HDR2: {
bool ok = (p->is_bcast && byte == BWM_HDR_SLAVE_BCAST_2) ||
(!p->is_bcast && byte == BWM_HDR_SLAVE_RESP_2);
if (!ok) {
// header mismatch: reset and re-examine this byte as a potential SOF
p->state = S_IDLE;
bwm_feed_byte(p, byte);
return;
}
uint8_t hdr[2] = { p->hdr1, byte };
p->crc_calc = bwm_crc16(hdr, 2, BWM_CRC16_INIT);
p->state = S_CMD_LO;
break;
}
case S_CMD_LO:
p->cmd = byte;
crc_step(p, byte);
p->state = S_CMD_HI;
break;
case S_CMD_HI:
p->cmd |= (uint16_t)byte << 8;
crc_step(p, byte);
p->state = S_LEN_LO;
break;
case S_LEN_LO:
p->len = byte;
crc_step(p, byte);
p->state = S_LEN_HI;
break;
case S_LEN_HI:
p->len |= (uint16_t)byte << 8;
crc_step(p, byte);
p->got = 0;
if (p->len > BWM_RXFRAME_MAX) { // oversized -> drop frame
bwm_reset_frame(p);
} else {
p->state = (p->len == 0) ? S_CRC_LO : S_PAYLOAD;
}
break;
case S_PAYLOAD:
p->payload[p->got++] = byte;
crc_step(p, byte);
if (p->got >= p->len) {
p->state = S_CRC_LO;
}
break;
case S_CRC_LO:
p->crc_recv = byte;
p->state = S_CRC_HI;
break;
case S_CRC_HI:
p->crc_recv |= (uint16_t)byte << 8;
if (p->crc_recv == p->crc_calc) {
if (p->is_bcast && p->cmd == BWM_CMD_DATA_FORWARD) {
for (uint16_t i = 0; i < p->len; i++) {
fifo_push(p->payload[i]);
}
} else if ((p->is_bcast == false) && p->cmd == BWM_CMD_SEND_FORWARD_DATA) {
// SLAVE_RESP ack for a forward frame -> one slot freed
if (s_fwd_inflight > 0) {
s_fwd_inflight--;
}
} else if ((p->is_bcast == false) && p->cmd == BWM_CMD_SET_UART_BAUD) {
// SLAVE_RESP ack for a baud-set request (see negotiate below)
s_baud_ack = true;
} else if ((p->is_bcast == false) && p->cmd == BWM_CMD_GET_UART_BAUD) {
// SLAVE_RESP for our GET_BAUD verify probe
s_getbaud_ack = true;
}
}
// valid non-DATA_FORWARD frames and CRC failures alike: just resync
bwm_reset_frame(p);
break;
default:
bwm_reset_frame(p);
break;
}
}
// Pull whatever raw framed bytes are waiting and run them through the parser.
static void bwm_pump(void) {
uint8_t scratch[64];
uint16_t avail = bwm_uart_rx_available();
while (avail) {
uint32_t n = bwm_uart_read(scratch, MIN((uint32_t)avail, (uint32_t)sizeof(scratch)));
if (n == 0) {
break;
}
for (uint32_t i = 0; i < n; i++) {
bwm_feed_byte(&s_p, scratch[i]);
}
avail = bwm_uart_rx_available();
}
}
uint16_t bwm_fwd_rxdata_available(void) {
if (fifo_count() > 0) {
return fifo_count();
}
// No de-framed payload yet, but raw frame bytes may be waiting; pump once so
// receive_ng()'s gate reflects real forward data.
bwm_pump();
return fifo_count();
}
uint32_t bwm_read_ng(uint8_t *data, size_t len) {
if (len == 0) {
return 0;
}
// Same bounded-retry budget shape as bwm_uart_read(); USART_SLOW_LINK (set
// for the BWM/BLE link) widens it so a slow round-trip doesn't time out.
uint32_t tryconstant = 0;
#ifdef USART_SLOW_LINK
tryconstant = 50000;
#endif
uint32_t maxtry = 10 * (3000000 / BWM_UART_BAUD) + tryconstant;
uint32_t out = 0;
uint32_t try = 0;
while (out < len) {
while (out < len && fifo_count() > 0) {
data[out++] = fifo_pop();
try = 0;
}
if (out >= len) {
break;
}
uint16_t before = fifo_count();
bwm_pump();
if (fifo_count() != before) {
try = 0;
continue;
}
if (try++ >= maxtry) {
break;
}
}
return out;
}
// ---------------------------------------------------------------------------
// Runtime baud negotiation.
// The ESP boots at BWM_UART_BAUD and accepts a HOST_CMD (cmd 1011) carrying a
// u32 LE target baud. Its handler test-switches, rolls back, acks at the OLD
// baud with a SLAVE_RESP echoing cmd 1011, then commits to the new baud. So we
// send the request at the current baud, wait for that ack, then switch our own
// UART4 to match. On timeout (old ESP without the command, or a lost ack) we
// leave the link at the boot baud - it keeps working, just slower.
//
// The ESP keeps its baud across an AT32-only reset (bootloader, hw reset,
// flash), so probe both rates first and adopt the one it answers at.
// ---------------------------------------------------------------------------
#define BWM_BAUD_ACK_WAIT_MS 300 // per-attempt wait for the ESP ack
#define BWM_BAUD_ATTEMPTS 3 // resend attempts before giving up
#define BWM_BAUD_VERIFY_MS 150 // per-probe wait for the GET_BAUD reply
#define BWM_BAUD_VERIFY_TRIES 5 // GET_BAUD probes before declaring the switch failed
#define BWM_BAUD_PROBE_ROUNDS 4 // boot/target probe pairs
// Probe the ESP at the CURRENT baud with GET_UART_BAUD; true only if it answers,
// i.e. it really is running at the baud we just switched to. Retried by the
// caller so a single lost probe on a good link does not force a needless revert.
static bool bwm_verify_baud(void) {
uint8_t f[2 + 2 + 2 + 2];
size_t i = 0;
f[i++] = BWM_HDR_HOST_CMD_1;
f[i++] = BWM_HDR_HOST_CMD_2;
f[i++] = (uint8_t)(BWM_CMD_GET_UART_BAUD & 0xFF);
f[i++] = (uint8_t)((BWM_CMD_GET_UART_BAUD >> 8) & 0xFF);
f[i++] = 0;
f[i++] = 0;
uint16_t crc = bwm_crc16(f, i, BWM_CRC16_INIT);
f[i++] = (uint8_t)(crc & 0xFF);
f[i++] = (uint8_t)((crc >> 8) & 0xFF);
s_getbaud_ack = false;
s_p.state = S_IDLE;
bwm_uart_write(f, i);
uint32_t t0 = GetTickCount();
while (GetTickCountDelta(t0) < BWM_BAUD_VERIFY_MS) {
bwm_pump();
if (s_getbaud_ack) {
return true;
}
}
return false;
}
// Switch to `baud` and probe up to `tries` times.
static bool bwm_probe_at(uint32_t baud, int tries) {
bwm_uart_set_baud(baud);
SpinDelay(2);
for (int v = 0; v < tries; v++) {
if (bwm_verify_baud()) {
return true;
}
}
return false;
}
// Reset framer, FIFO and flow control after a baud change.
static void bwm_link_reset(void) {
s_p.state = S_IDLE;
s_fifo_head = 0;
s_fifo_tail = 0;
s_fwd_inflight = 0;
}
bool bwm_fwd_negotiate_baud(uint32_t target) {
if (target == 0 || target == bwm_uart_get_baud()) {
return false;
}
const uint32_t boot_baud = bwm_uart_get_baud();
// Locate the ESP: alternate so a cold-booted one (boot baud) and one that
// survived our reset (target baud) are both found.
bool at_boot = false, at_target = false;
for (int r = 0; r < BWM_BAUD_PROBE_ROUNDS && !at_boot && !at_target; r++) {
at_boot = bwm_probe_at(boot_baud, 1);
if (!at_boot) {
at_target = bwm_probe_at(target, 1);
}
}
if (at_target) {
bwm_link_reset(); // already at target
return true;
}
if (!at_boot) {
bwm_uart_set_baud(boot_baud); // no ESP: stay at boot baud
bwm_link_reset();
return false;
}
// Build the SET_UART_BAUD host-command frame once (payload = u32 LE baud).
uint8_t frame[2 + 2 + 2 + 4 + 2];
size_t idx = 0;
frame[idx++] = BWM_HDR_HOST_CMD_1;
frame[idx++] = BWM_HDR_HOST_CMD_2;
frame[idx++] = (uint8_t)(BWM_CMD_SET_UART_BAUD & 0xFF);
frame[idx++] = (uint8_t)((BWM_CMD_SET_UART_BAUD >> 8) & 0xFF);
frame[idx++] = (uint8_t)(4 & 0xFF);
frame[idx++] = (uint8_t)((4 >> 8) & 0xFF);
frame[idx++] = (uint8_t)(target & 0xFF);
frame[idx++] = (uint8_t)((target >> 8) & 0xFF);
frame[idx++] = (uint8_t)((target >> 16) & 0xFF);
frame[idx++] = (uint8_t)((target >> 24) & 0xFF);
uint16_t crc = bwm_crc16(frame, idx, BWM_CRC16_INIT);
frame[idx++] = (uint8_t)(crc & 0xFF);
frame[idx++] = (uint8_t)((crc >> 8) & 0xFF);
for (int attempt = 0; attempt < BWM_BAUD_ATTEMPTS; attempt++) {
s_baud_ack = false;
s_p.state = S_IDLE; // drop any half-frame before we listen
bwm_uart_write(frame, idx);
for (int ms = 0; ms < BWM_BAUD_ACK_WAIT_MS; ms++) {
bwm_pump(); // parser sets s_baud_ack on the SLAVE_RESP
if (s_baud_ack) {
// ESP acked and is switching; verify it before trusting the
// new baud, else fall back so both ends stay in sync.
bool verified = bwm_probe_at(target, BWM_BAUD_VERIFY_TRIES);
if (verified == false) {
bwm_uart_set_baud(boot_baud);
}
bwm_link_reset();
return verified;
}
SpinDelay(1);
}
}
bwm_link_reset();
return false; // no ack: stay at the boot baud, link still usable
}