From cc98f9560b6aea8e754b52985b0122ba1755c2b2 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sun, 23 Aug 2026 13:23:08 +0200 Subject: [PATCH 1/5] ADD: hw bwmsetcap commandhandler Signed-off-by: Niel Nielsen --- armsrc/appmain.c | 87 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index bdda4c234..25283c60f 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -220,6 +220,80 @@ static void print_pm5_battery_status(void) { Dbprintf(" Fuel gauge.......... " _YELLOW_("not responding") " (BQ27427 absent or I2C down)"); } } + +// --- BQ27427 provisioning: set Design Capacity for the fitted cell ------------- +// One-time. The gauge ships with a ~1000+ mAh default profile, so RemainingCapacity +// reads wrong for the fitted pack until Design Capacity is programmed. Invoked by the +// `hw bwmsetcap` client command (CMD_PM5_BWM_SET_CAP) - deliberately NOT run at boot, +// because a config-update cycle disrupts the Impedance Track learning cycle. +// Per BQ27427 TRM (SLUUCD5): State subclass 82 (0x52), Design Capacity at offset 6 +// -> block addr 0x46 (MSB)/0x47 (LSB), big-endian. Assumes gauge UNSEALED (factory default). +#define BWM_DEFAULT_DESIGN_CAP_MAH 500 // VXE 502540 on the reference BWM + +static bool bq_control(uint16_t sub) { + uint8_t d[2] = { (uint8_t)(sub & 0xFF), (uint8_t)(sub >> 8) }; + return I2C_BufferWrite(d, 2, 0x00, BWM_GAUGE_ADDR); // Control() 0x00 +} +static bool bq_flags(uint16_t *f) { + uint8_t d[2] = {0}; + if (I2C_BufferReadRaw(d, 2, 0x06, BWM_GAUGE_ADDR) <= 0) return false; + *f = (uint16_t)(d[0] | (d[1] << 8)); // Flags() 0x06 + return true; +} +static bool bq_select_state_block(void) { + uint8_t v; + v = 0x00; if (!I2C_BufferWrite(&v, 1, 0x61, BWM_GAUGE_ADDR)) return false; // BlockDataControl + v = 0x52; if (!I2C_BufferWrite(&v, 1, 0x3E, BWM_GAUGE_ADDR)) return false; // DataClass = 82 (State) + v = 0x00; if (!I2C_BufferWrite(&v, 1, 0x3F, BWM_GAUGE_ADDR)) return false; // DataBlock = 0 + WaitMS(5); + return true; +} +static bool bq_read_design_cap(uint16_t *cap) { + if (!bq_select_state_block()) return false; + uint8_t d[2] = {0}; + if (I2C_BufferReadRaw(d, 2, 0x46, BWM_GAUGE_ADDR) <= 0) return false; + *cap = (uint16_t)((d[0] << 8) | d[1]); // big-endian + return true; +} + +// Program Design Capacity (and matching Design Energy). Idempotent: returns true +// without a config-update cycle if the value is already correct. +static bool bwm_gauge_provision_capacity(uint16_t cap_mah) { + uint16_t cur = 0; + if (bq_read_design_cap(&cur) && cur == cap_mah) { + return true; // already correct - do NOT run another CFGUPDATE cycle + } + + uint16_t energy_mwh = (uint16_t)(((uint32_t)cap_mah * 37) / 10); // ~3.7 V nominal + + if (!bq_control(0x0013)) return false; // SET_CFGUPDATE + uint16_t flags = 0; int tries = 0; + do { WaitMS(50); if (!bq_flags(&flags)) return false; } + while (((flags & 0x0010) == 0) && (++tries < 40)); // wait CFGUPDATE (Flags bit4), ~2s + if ((flags & 0x0010) == 0) return false; + + if (!bq_select_state_block()) return false; + + uint8_t blk[32] = {0}; + if (I2C_BufferReadRaw(blk, 32, 0x40, BWM_GAUGE_ADDR) <= 0) return false; + + blk[6] = (uint8_t)(cap_mah >> 8); blk[7] = (uint8_t)(cap_mah & 0xFF); // DesignCapacity + blk[8] = (uint8_t)(energy_mwh >> 8); blk[9] = (uint8_t)(energy_mwh & 0xFF); // DesignEnergy + I2C_BufferWrite(&blk[6], 2, 0x46, BWM_GAUGE_ADDR); + I2C_BufferWrite(&blk[8], 2, 0x48, BWM_GAUGE_ADDR); + + uint16_t sum = 0; // block checksum = 255 - (sum mod 256) + for (int i = 0; i < 32; i++) sum += blk[i]; + uint8_t csum = (uint8_t)(0xFF - (uint8_t)(sum & 0xFF)); + I2C_BufferWrite(&csum, 1, 0x60, BWM_GAUGE_ADDR); // commit block + WaitMS(10); + + if (!bq_control(0x0042)) return false; // SOFT_RESET (exit CFGUPDATE) + tries = 0; + do { WaitMS(50); if (!bq_flags(&flags)) return false; } + while (((flags & 0x0010) != 0) && (++tries < 40)); + return ((flags & 0x0010) == 0); +} #endif // WITH_BWM_STATUS #ifdef WITH_LCD @@ -3788,6 +3862,19 @@ static void PacketReceived(PacketCommandNG *packet) { reply_ng(CMD_PM5_RGB_SET, PM3_SUCCESS, NULL, 0); break; } +#ifdef WITH_BWM_STATUS + case CMD_PM5_BWM_SET_CAP: { + // One-time BWM fuel-gauge (BQ27427) Design Capacity provisioning. + // Payload: optional uint16 mAh (LE); absent -> reference default. + uint16_t cap = (packet->length >= 2) + ? (uint16_t)(packet->data.asBytes[0] | (packet->data.asBytes[1] << 8)) + : BWM_DEFAULT_DESIGN_CAP_MAH; + I2C_init(true); + bool ok = bwm_gauge_provision_capacity(cap); + reply_ng(CMD_PM5_BWM_SET_CAP, ok ? PM3_SUCCESS : PM3_EFAILED, (uint8_t *)&cap, sizeof(cap)); + break; + } +#endif #endif default: { Dbprintf("%s: 0x%04x", "unknown command:", packet->cmd); From 6c605c3f5ddfd3782b0f7d8ff0292047c9c1439c Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sun, 23 Aug 2026 13:26:13 +0200 Subject: [PATCH 2/5] ADD the bwmsetcap command function Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index d2671d105..6a2db7285 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -1500,6 +1500,49 @@ static int CmdTearoff(const char *Cmd) { return handle_tearoff(¶ms, !silent); } +static int CmdBwmSetCap(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwmsetcap", + "Program the BWM fuel gauge (BQ27427) Design Capacity for the fitted cell.\n" + "Run ONCE after fitting or replacing the battery. This triggers a gauge\n" + "config-update; do not run it repeatedly, as that disrupts the Impedance\n" + "Track learning cycle. PM5 only.", + "hw bwmsetcap --> set design capacity to default 500 mAh\n" + "hw bwmsetcap --cap 500 --> set design capacity to 500 mAh"); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "cap", "", "design capacity in mAh (default 500)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + int cap = arg_get_int_def(ctx, 1, 500); + CLIParserFree(ctx); + + if (cap <= 0 || cap > 32000) { + PrintAndLogEx(WARNING, "capacity out of range: %d mAh", cap); + return PM3_EINVARG; + } + + uint8_t payload[2] = { (uint8_t)(cap & 0xFF), (uint8_t)((cap >> 8) & 0xFF) }; + PrintAndLogEx(INFO, "Programming BWM gauge design capacity to " _YELLOW_("%d mAh") "...", cap); + PrintAndLogEx(INFO, "Run this " _YELLOW_("once") "; then perform a full charge/discharge learning cycle."); + + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_SET_CAP, payload, sizeof(payload)); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_PM5_BWM_SET_CAP, &resp, 5000) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); + return PM3_ETIMEOUT; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "gauge provisioning failed - check BWM present and gauge unsealed"); + return resp.status; + } + PrintAndLogEx(SUCCESS, "Design capacity programmed. `hw status` should now report sane capacity."); + return PM3_SUCCESS; +} + static int CmdTia(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hw tia", @@ -1965,6 +2008,7 @@ static command_t CommandTable[] = { {"setmux", CmdSetMux, IfPm3Present, "Set the ADC mux to a specific value"}, {"standalone", CmdStandalone, IfPm3Present, "Start installed standalone mode on device"}, {"tia", CmdTia, IfPm3Present, "Trigger a Timing Interval Acquisition to re-adjust the RealTimeCounter divider"}, + {"bwmsetcap", CmdBwmSetCap, IfPm3Present, "Set BWM fuel-gauge design capacity (PM5, run once after battery change)"}, {"tune", CmdTune, IfPm3Lf, "Measure tuning of device antenna"}, {"decay", CmdDecay, IfPm3Present, "Measure HF antenna decay after field-off"}, {NULL, NULL, NULL, NULL} From ca129dcafaf0d364f922b4fc76253927b271e1a3 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sun, 23 Aug 2026 13:28:07 +0200 Subject: [PATCH 3/5] ADD bwmsetcap command id i Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 057ed9f15..0df3c1f68 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -639,7 +639,8 @@ typedef struct { #define CMD_PM5_QC_TEST 0x0177 // PM5, set the antenna RGB LED colour (payload: r,g,b). Used by `hf/lf tune --rgb`. #define CMD_PM5_RGB_SET 0x0178 - +// PM5, provision BWM fuel-gauge (BQ27427) Design Capacity. Used by `hw bwmsetcap`. +#define CMD_PM5_BWM_SET_CAP 0x0179 // For low-frequency tags #define CMD_LF_TI_READ 0x0202 #define CMD_LF_TI_WRITE 0x0203 From f47f7017e747ee9aabda67ed773c7851d982880d Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sun, 23 Aug 2026 14:05:30 +0200 Subject: [PATCH 4/5] Update BWM set capacity command for PM5 Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 6a2db7285..5796179f3 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2008,7 +2008,7 @@ static command_t CommandTable[] = { {"setmux", CmdSetMux, IfPm3Present, "Set the ADC mux to a specific value"}, {"standalone", CmdStandalone, IfPm3Present, "Start installed standalone mode on device"}, {"tia", CmdTia, IfPm3Present, "Trigger a Timing Interval Acquisition to re-adjust the RealTimeCounter divider"}, - {"bwmsetcap", CmdBwmSetCap, IfPm3Present, "Set BWM fuel-gauge design capacity (PM5, run once after battery change)"}, + {"bwmsetcap", CmdBwmSetCap, IfPm5, "Set BWM fuel-gauge design capacity (PM5, run once after battery change)"}, {"tune", CmdTune, IfPm3Lf, "Measure tuning of device antenna"}, {"decay", CmdDecay, IfPm3Present, "Measure HF antenna decay after field-off"}, {NULL, NULL, NULL, NULL} From 7377dae83dd64f6dbbd3675ae00df18120679480 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sun, 23 Aug 2026 14:17:50 +0200 Subject: [PATCH 5/5] Add files via upload Signed-off-by: Niel Nielsen --- armsrc/appmain.c | 61 +++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 25283c60f..7d184a3cf 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -171,7 +171,7 @@ static void print_pm5_battery_status(void) { } else { Dbprintf(" Charger MainCtl..... 0x%02x %s", mainctl, (mainctl & BWM_CHG_FET_DIS) ? _RED_("FET_DIS (VMIX off / shipping)") - : _GREEN_("power path enabled")); + : _GREEN_("power path enabled")); I2C_BufferReadRaw(&f1, 1, BWM_CHG_REG_FAULT, BWM_CHG_ADDR); // 1st = latched history I2C_BufferReadRaw(&fault, 1, BWM_CHG_REG_FAULT, BWM_CHG_ADDR); // 2nd = current state @@ -242,9 +242,12 @@ static bool bq_flags(uint16_t *f) { } static bool bq_select_state_block(void) { uint8_t v; - v = 0x00; if (!I2C_BufferWrite(&v, 1, 0x61, BWM_GAUGE_ADDR)) return false; // BlockDataControl - v = 0x52; if (!I2C_BufferWrite(&v, 1, 0x3E, BWM_GAUGE_ADDR)) return false; // DataClass = 82 (State) - v = 0x00; if (!I2C_BufferWrite(&v, 1, 0x3F, BWM_GAUGE_ADDR)) return false; // DataBlock = 0 + v = 0x00; + if (!I2C_BufferWrite(&v, 1, 0x61, BWM_GAUGE_ADDR)) return false; // BlockDataControl + v = 0x52; + if (!I2C_BufferWrite(&v, 1, 0x3E, BWM_GAUGE_ADDR)) return false; // DataClass = 82 (State) + v = 0x00; + if (!I2C_BufferWrite(&v, 1, 0x3F, BWM_GAUGE_ADDR)) return false; // DataBlock = 0 WaitMS(5); return true; } @@ -266,19 +269,33 @@ static bool bwm_gauge_provision_capacity(uint16_t cap_mah) { uint16_t energy_mwh = (uint16_t)(((uint32_t)cap_mah * 37) / 10); // ~3.7 V nominal - if (!bq_control(0x0013)) return false; // SET_CFGUPDATE - uint16_t flags = 0; int tries = 0; - do { WaitMS(50); if (!bq_flags(&flags)) return false; } - while (((flags & 0x0010) == 0) && (++tries < 40)); // wait CFGUPDATE (Flags bit4), ~2s - if ((flags & 0x0010) == 0) return false; + // Enter CONFIG_UPDATE and wait for the gauge to acknowledge it. + if (bq_control(0x0013) == false) { // SET_CFGUPDATE + return false; + } + + uint16_t flags = 0; + int tries = 0; + do { + WaitMS(50); + if (bq_flags(&flags) == false) { + return false; + } + } while (((flags & 0x0010) == 0) && (++tries < 40)); // wait for CFGUPDATE (Flags bit 4), ~2 s + + if ((flags & 0x0010) == 0) { + return false; + } if (!bq_select_state_block()) return false; uint8_t blk[32] = {0}; if (I2C_BufferReadRaw(blk, 32, 0x40, BWM_GAUGE_ADDR) <= 0) return false; - blk[6] = (uint8_t)(cap_mah >> 8); blk[7] = (uint8_t)(cap_mah & 0xFF); // DesignCapacity - blk[8] = (uint8_t)(energy_mwh >> 8); blk[9] = (uint8_t)(energy_mwh & 0xFF); // DesignEnergy + blk[6] = (uint8_t)(cap_mah >> 8); + blk[7] = (uint8_t)(cap_mah & 0xFF); // DesignCapacity + blk[8] = (uint8_t)(energy_mwh >> 8); + blk[9] = (uint8_t)(energy_mwh & 0xFF); // DesignEnergy I2C_BufferWrite(&blk[6], 2, 0x46, BWM_GAUGE_ADDR); I2C_BufferWrite(&blk[8], 2, 0x48, BWM_GAUGE_ADDR); @@ -1260,7 +1277,7 @@ static bool QCTestPM5(uint8_t *failed_item) { gpio_init_struct.gpio_pins = BEEPER_EN_GPIO_PIN; gpio_init_struct.gpio_pull = GPIO_PULL_NONE; gpio_init(BEEPER_EN_GPIO, &gpio_init_struct); - gpio_bits_write(BEEPER_EN_GPIO,BEEPER_EN_GPIO_PIN, FALSE); + gpio_bits_write(BEEPER_EN_GPIO, BEEPER_EN_GPIO_PIN, FALSE); // 蜂鸣器调制脚 gpio_init_struct.gpio_mode = GPIO_MODE_MUX; gpio_init_struct.gpio_pins = BEEPER_MOD_GPIO_PIN; @@ -1298,9 +1315,9 @@ static bool QCTestPM5(uint8_t *failed_item) { LED_A_ON(); BEEPER_MOD_TMR->pr = 999; - gpio_bits_write(BEEPER_EN_GPIO,BEEPER_EN_GPIO_PIN, TRUE); + gpio_bits_write(BEEPER_EN_GPIO, BEEPER_EN_GPIO_PIN, TRUE); SpinDelay(20); - gpio_bits_write(BEEPER_EN_GPIO,BEEPER_EN_GPIO_PIN, FALSE); + gpio_bits_write(BEEPER_EN_GPIO, BEEPER_EN_GPIO_PIN, FALSE); SpinDelay(200); LED_A_OFF(); @@ -1314,9 +1331,9 @@ static bool QCTestPM5(uint8_t *failed_item) { LED_B_ON(); BEEPER_MOD_TMR->pr = 1100; tmr_channel_value_set(BEEPER_MOD_TMR, BEEPER_MOD_TMR_CH, 550); - gpio_bits_write(BEEPER_EN_GPIO,BEEPER_EN_GPIO_PIN, TRUE); + gpio_bits_write(BEEPER_EN_GPIO, BEEPER_EN_GPIO_PIN, TRUE); SpinDelay(20); - gpio_bits_write(BEEPER_EN_GPIO,BEEPER_EN_GPIO_PIN, FALSE); + gpio_bits_write(BEEPER_EN_GPIO, BEEPER_EN_GPIO_PIN, FALSE); SpinDelay(200); LED_B_OFF(); @@ -1330,9 +1347,9 @@ static bool QCTestPM5(uint8_t *failed_item) { LED_C_ON(); BEEPER_MOD_TMR->pr = 1200; tmr_channel_value_set(BEEPER_MOD_TMR, BEEPER_MOD_TMR_CH, 600); - gpio_bits_write(BEEPER_EN_GPIO,BEEPER_EN_GPIO_PIN, TRUE); + gpio_bits_write(BEEPER_EN_GPIO, BEEPER_EN_GPIO_PIN, TRUE); SpinDelay(20); - gpio_bits_write(BEEPER_EN_GPIO,BEEPER_EN_GPIO_PIN, FALSE); + gpio_bits_write(BEEPER_EN_GPIO, BEEPER_EN_GPIO_PIN, FALSE); SpinDelay(200); LED_C_OFF(); @@ -1346,9 +1363,9 @@ static bool QCTestPM5(uint8_t *failed_item) { LED_D_ON(); BEEPER_MOD_TMR->pr = 1300; tmr_channel_value_set(BEEPER_MOD_TMR, BEEPER_MOD_TMR_CH, 650); - gpio_bits_write(BEEPER_EN_GPIO,BEEPER_EN_GPIO_PIN, TRUE); + gpio_bits_write(BEEPER_EN_GPIO, BEEPER_EN_GPIO_PIN, TRUE); SpinDelay(20); - gpio_bits_write(BEEPER_EN_GPIO,BEEPER_EN_GPIO_PIN, FALSE); + gpio_bits_write(BEEPER_EN_GPIO, BEEPER_EN_GPIO_PIN, FALSE); SpinDelay(200); LED_D_OFF(); } @@ -3752,7 +3769,7 @@ static void PacketReceived(PacketCommandNG *packet) { // Response if (res == PM3_EFAILED) { uint32_t plat_status = FpgaConfigPlatformStatus(); // Return status code of platform when res is PM3_EFAILED - reply_ng(packet->cmd, res, (uint8_t*)&plat_status, sizeof(plat_status)); + reply_ng(packet->cmd, res, (uint8_t *)&plat_status, sizeof(plat_status)); } else { reply_ng(packet->cmd, res, NULL, 0); } @@ -3840,7 +3857,7 @@ static void PacketReceived(PacketCommandNG *packet) { #endif case CMD_MAIN_CHIP_UNIQUEID: { uint8_t size = 0; - uint8_t* uid = GetChipUniqueId(&size); + uint8_t *uid = GetChipUniqueId(&size); reply_ng(CMD_MAIN_CHIP_UNIQUEID, PM3_SUCCESS, uid, size); break; }