固件 0.1.17~0.1.18-c5:CH224A 的 PD 档位实时进遥测(pdmon)

原来只在启动时读一次(bringup.c),而多口充电器重新分配功率时 20 V 档的电流上限当场会变。

- 新模块 pdmon:每秒读 0x09(协议状态)+ 0x50(档位电流),只读;先试上次应答的支路(主干 / mux 通道 5)。
  档位变了(连续两拍一致、非 0)记 LOG_EV_PD_LIMIT,arg 2 = 运行中换档。只报不动,降载仍没人做。
- status 新增 "pd":{"st":..,"ma":..},这一拍没读到是 null(满配 1164 / 1536 字节,余量断言改成至少留 256)。
- 0.1.17 放在热管理任务里读,板 B 实测 safety.skipped 每秒涨 1(持锁撞上巡检的下一拍);
  0.1.18 挪进巡检任务(t_safety_step 之后、每 50 圈一次),skipped 不再涨。
- 测试:新增 test_pdmon(6 例),test_cmd 加 pd 断言;28 ctest 全绿。

板 B 已 OTA 到 0.1.18-c5(两次都是热重启,五路没断电),读到 st 0x88 / 3000 mA。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-06 06:31:04 +08:00
共同撰写人 Claude Opus 5.5
父节点 ca7435242f
当前提交 489cd6a6f3
共修改 10 个文件,包含 352 行新增和 3 行删除
+12 -1
查看文件
@@ -1,5 +1,16 @@
# 交接
更新:2026-09-30 固件 **0.1.16-c5**(开机键统一 5 s);板 B 经 OTA 推上;rack 已部署
更新:2026-10-06 固件 **0.1.18-c5**(CH224A 档位进遥测);板 B 经 OTA 推上;rack 已部署
## 0.1.17 / 0.1.18:CH224A 档位实时进遥测(2026-10-06)
- 起因:rack 要显示 PD 供电信息,而档位只在 `bringup.c` 启动时读一次;板 B 历史上读到过 5 A → 3.25 A → 3 A。
- 新模块 `pdmon.{h,c}`:每秒读 `0x09`(协议状态)+ `0x50`(档位电流),**只读**。先试上次应答的支路(主干 / mux 通道 5)。
档位变了(连续两拍一致、非 0、和日志里最后那条不同)记 `LOG_EV_PD_LIMIT`,**arg 2 = 运行中换档**(0 / 1 仍是启动那条)。
只报不动:`pd_limit_mw` 仍是启动时定的,降载(§8.3)照旧没人做。
- status 新增 `"pd":{"st":136,"ma":3000}`,这一拍没读到是 `null`(满配 1164 / 1536 字节,`test_cmd` 的余量断言改成「至少留 256」)。
- ⚠️ **0.1.17 放在热管理任务里读,板 B 实测 `safety.skipped` 每秒涨 1**(持锁那一下正好撞上巡检的下一拍,那一圈五路都没读)。
0.1.18 挪进巡检任务(`t_safety_step()` 之后、每 50 圈一次),`skipped` 停在启动时的 1 不再涨。别再挪出去。
- 板 B 实测:`st` = 0x88(bit3 = PD 握上;bit7 手册没写,rack 只认已知位)、`ma` = 3000。两次 OTA 都是热重启,五路 en 全程保持。
- 测试:新增 `test_pdmon`(6 例),`test_cmd` 加 `pd` 两条断言;**28 ctest 全绿**。rack 侧见 `rack/frontend/src/pages/Board.vue` 的「PD 供电」格。
## 0.1.15 / 0.1.16:开机长按 3 s 试验失败,所有开机操作统一 5 s(2026-09-30)
- 0.1.15 把 `ACT_KEY_MS_ON` 5000 → 3000:板 B 第 2 路 `power_cycle` 三轮全是「按住 330~390 mA、松手掉回 0」⇒ `no-boot`(事件 19);
+1
查看文件
@@ -30,6 +30,7 @@ set(PHONERAIL_CORE_SRCS
${FW_ROOT}/main/tasks.c
${FW_ROOT}/main/chgmode.c
${FW_ROOT}/main/hubctl.c
${FW_ROOT}/main/pdmon.c
${FW_ROOT}/main/t_safety.c
${FW_ROOT}/main/t_actuator.c
${FW_ROOT}/main/t_thermal.c
+1
查看文件
@@ -14,6 +14,7 @@ idf_component_register(
"tasks.c"
"chgmode.c"
"hubctl.c"
"pdmon.c"
"t_safety.c"
"t_actuator.c"
"t_thermal.c"
+8
查看文件
@@ -33,6 +33,7 @@
#include "nvs.h"
#include "nvs_flash.h"
#include "ota.h"
#include "pdmon.h"
#include "ota_esp.h"
#include "warm_esp.h"
#include "ulink_esp.h"
@@ -151,6 +152,12 @@ static void task_safety(void *arg)
for (;;) {
(void)t_safety_step(snapp); /* 器件读失败不在这里处理:§10.2 失联 ≠ 断电 */
/* 0.1.17:CH224A 的档位进遥测,1 s 一拍(pdmon.h「线程」那一段:为什么非得在这里读)。
错开 250 的倍数,不和下面那行日志挤在同一圈。 */
if ((snapp->cycles % 50u) == 25u) {
pdmon_poll();
}
/* 每 5 s 报一次总线占用。§10.3 手算是「约 1.5 ms / 约 8 %」,
首板上电时要拿逻辑分析仪复核这个数(§10.3 末尾那句)。 */
if ((snapp->cycles % 250u) == 0u) {
@@ -462,6 +469,7 @@ void app_main(void)
abort();
}
log_report(&boot);
pdmon_init(boot.pd_ma);
if (adopt != 0) {
evlog_put(0, LOG_EV_OTA, OTA_EV_WARM, (int32_t)boot.adopted, (int32_t)boot.adopt_dropped);
ESP_LOGI(TAG, "接管:一直开着 0x%02x,关掉 0x%02x(刀片不在 / mux 失联)",
+15
查看文件
@@ -15,6 +15,7 @@
#include "hubctl.h"
#include "ota.h"
#include "pca9555.h"
#include "pdmon.h"
#include "phonerail.h"
#include "rail.h"
#include "rail_cfg.h"
@@ -114,6 +115,19 @@ void cmd_report_hub(cj_writer_t *w)
(unsigned long)hubctl_hold_left_s(now), (unsigned long)hubctl_idle_left_s(now));
}
/* 0.1.17:`"pd":{"st":8,"ma":3000}` —— CH224A 此刻的协议状态(0x09 原值)与 20 V 档的最大电流(pdmon.h)。
上一拍没读到(或还没读过第一拍)写 null,**不报旧值**:上位机拿它当「现在」。 */
static void pd_obj(cj_writer_t *w)
{
pdmon_snap_t pd;
pdmon_get(&pd);
if (pd.ok) {
cj_w(w, ",\"pd\":{\"st\":%u,\"ma\":%lu}", (unsigned)pd.status, (unsigned long)pd.ma);
} else {
cj_w(w, ",\"pd\":null");
}
}
void cmd_report_status(cj_writer_t *w, const cmd_env_t *env)
{
const safety_snap_t *s = env->safety;
@@ -127,6 +141,7 @@ void cmd_report_status(cj_writer_t *w, const cmd_env_t *env)
cj_w(w, ",\"host\":null");
}
cmd_report_hub(w);
pd_obj(w);
if (s != NULL) {
if (s->input_ok) {
cj_w(w, ",\"in\":{\"mv\":%ld,\"ua\":%ld}", (long)s->input.bus_mv,
+72
查看文件
@@ -0,0 +1,72 @@
/* CH224A 运行期轮询。契约见 pdmon.h。 */
#include "pdmon.h"
#include <stddef.h>
#include "ch224a.h"
#include "evlog.h"
#include "i2cbus.h"
#include "slot.h"
#include "tca9548a.h"
typedef struct {
uint8_t status;
uint32_t ma;
} pd_read_t;
static pdmon_snap_t s_snap;
static int s_ch = I2CBUS_CH_TRUNK; /* 上次应答的支路 */
static uint32_t s_logged_ma; /* 最近一次进了日志的档位 */
static uint32_t s_prev_ma; /* 上一拍读到的(去抖用) */
static hal_err_t read_fn(void *arg)
{
pd_read_t *r = (pd_read_t *)arg;
const hal_err_t err = ch224a_read_status(CH224A_ADDR_DEFAULT, &r->status);
return (err == HAL_OK) ? ch224a_read_max_ma(CH224A_ADDR_DEFAULT, &r->ma) : err;
}
void pdmon_init(uint32_t boot_ma)
{
s_snap = (pdmon_snap_t){ 0 };
s_ch = I2CBUS_CH_TRUNK;
s_logged_ma = boot_ma;
s_prev_ma = boot_ma;
}
void pdmon_poll(void)
{
pd_read_t r = { 0 };
hal_err_t err = i2cbus_xact(s_ch, SLOT_LOCK_TIMEOUT_MS, read_fn, &r);
if (err != HAL_OK) {
const int other = (s_ch == I2CBUS_CH_TRUNK) ? TCA9548A_CH_PD : I2CBUS_CH_TRUNK;
err = i2cbus_xact(other, SLOT_LOCK_TIMEOUT_MS, read_fn, &r);
if (err == HAL_OK) {
s_ch = other;
}
}
s_snap.polls++;
if (err != HAL_OK) {
s_snap.ok = false; /* 值留着:旧的档位比一个 0 可信(ch224a.h 同一条) */
s_snap.errors++;
return;
}
s_snap.status = r.status;
s_snap.ma = r.ma;
s_snap.ok = true;
/* 换档:连续两拍读到同一个非零值、且和日志里最后那条不一样。0 = 协商中途 / 没协商上,不记。 */
if (r.ma != 0 && r.ma == s_prev_ma && r.ma != s_logged_ma) {
s_logged_ma = r.ma;
evlog_put(0, LOG_EV_PD_LIMIT, PDMON_EV_ARG_CHANGED,
(int32_t)(ch224a_power_limit_mw(r.ma) / 100u), (int32_t)r.ma);
}
s_prev_ma = r.ma;
}
void pdmon_get(pdmon_snap_t *out)
{
if (out != NULL) {
*out = s_snap;
}
}
+51
查看文件
@@ -0,0 +1,51 @@
/* CH224A 运行期轮询(0.1.17):把充电器**此刻**给的 PD 档位放进遥测(status 的 `pd`)。
为什么不够用启动那一次(bringup.c 的 `pd_up()`):多口充电器在别的口插拔时会重新分配功率、
重发 Source_Capabilities,20 V 档的电流上限当场就变(板 B 实测过 5 A → 3.25 A → 3 A),
而 20 V 不一定掉、板子不一定重启 —— 只在启动读一次,面板上看到的就是旧档位。
做的事只有读:`0x09`(协议状态)和 `0x50`(档位电流)。**不写任何寄存器**(ch224a.h 那个框)。
读到的档位和上一次记进日志的不一样(连续两拍一致才算数,免得协商中途的瞬时值刷日志)
⇒ 记一条 `LOG_EV_PD_LIMIT`,arg = 2(运行中换档)。
⚠️ 和 bringup 一样只**报**不**动**:逼近上限的降载(§8.3)还没人做(HANDOFF)。
---- 线程 ----------------------------------------------------------------
`pdmon_poll()` 只在**巡检任务**里调,排在 `t_safety_step()` 之后、每 50 圈(1 s)一次,
一次 ≈ 选主干 + 两个单字节读。status 跨任务读快照,遥测无害(cmd.h 同一条)。
⚠️ 别挪到别的任务去:2026-10-06 板 B 实测放在热管理任务里时,它持锁的那一下正好撞上
巡检的下一拍,`skipped` 每秒整整涨 1(每秒有一圈五路都没读)。巡检自己读就没有人跟它抢。 */
#ifndef PDMON_H
#define PDMON_H
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* `LOG_EV_PD_LIMIT` 的 arg:0 = 启动时读到,1 = 启动时读不到 / 读到 0、按保守值(bringup.c),
2 = 运行中档位变了。⚠️ 冻结(上位机 protocol.py)。 */
#define PDMON_EV_ARG_CHANGED 2u
typedef struct {
bool ok; /* 上一拍读到了;false 时下面两个是最后一次读到的值 */
uint8_t status; /* 0x09 原值 */
uint32_t ma; /* 0x50 换算成 mA(没协商上 PD 时是 0) */
uint32_t polls;
uint32_t errors;
} pdmon_snap_t;
/* `boot_ma` = 启动时读到的档位(`bringup_report_t.pd_ma`,读不到是 0):换档事件拿它当基线。 */
void pdmon_init(uint32_t boot_ma);
/* 读一拍。先试上次应答的那条支路,不应答再试另一条(主干 / mux 通道 5,bringup.c `pd_up()` 的注释)。 */
void pdmon_poll(void);
void pdmon_get(pdmon_snap_t *out);
#ifdef __cplusplus
}
#endif
#endif /* PDMON_H */
+1 -1
查看文件
@@ -3,7 +3,7 @@
#define PHONERAIL_H
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
#define PHONERAIL_FW_VERSION "0.1.16-c5"
#define PHONERAIL_FW_VERSION "0.1.18-c5"
/* 通道数。五路直供,见设计文档 §2。 */
#define PHONERAIL_RAIL_COUNT 5
+13 -1
查看文件
@@ -11,6 +11,7 @@
#include <string.h>
#include "board_pins.h"
#include "ch224a.h"
#include "chgmode.h"
#include "cmd.h"
#include "dac63204.h"
@@ -21,6 +22,7 @@
#include "hal_time.h"
#include "i2cbus.h"
#include "pca9555.h"
#include "pdmon.h"
#include "phonerail.h"
#include "rail.h"
#include "rail_cfg.h"
@@ -689,9 +691,18 @@ static void test_status_fits_and_reports_unknowns_as_null(void)
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"host\":false");
sim_gpio_drive(BOARD_PIN_UP_VBUS_DET, 1); /* 留着高电平:下面量满配长度 */
/* 0.1.17 CH224A:没读到过 ⇒ null;读到了报原值。 */
pdmon_init(0);
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"pd\":null");
sim_i2c_add(CH224A_ADDR_DEFAULT, 1);
sim_i2c_set_reg(CH224A_ADDR_DEFAULT, CH224A_REG_STATUS, 0x08);
sim_i2c_set_reg(CH224A_ADDR_DEFAULT, CH224A_REG_CURRENT, 5000 / 50);
pdmon_poll();
const char *r = send("{\"seq\":4000000000,\"cmd\":\"status\"}");
ASSERT_HAS(r, "\"host\":true");
ASSERT_HAS(r, "{\"seq\":4000000000,\"ok\":true,\"fw\":\"" PHONERAIL_FW_VERSION "\"");
ASSERT_HAS(r, "\"pd\":{\"st\":8,\"ma\":5000}");
ASSERT_HAS(r, "\"in\":{\"mv\":12030,\"ua\":1234567}");
ASSERT_HAS(r, "{\"r\":2,\"pr\":true,\"en\":false,\"vb\":false,\"set\":3900,\"mv\":null,\"ua\":null");
ASSERT_HAS(r, "\"r\":4,");
@@ -701,7 +712,8 @@ static void test_status_fits_and_reports_unknowns_as_null(void)
ASSERT_HAS(r, "\"st\":\"idle\"");
TEST_ASSERT_EQUAL_CHAR('}', r[strlen(r) - 1]);
printf("status 满配 %u 字节(上限 %u)\n", (unsigned)strlen(r), (unsigned)CMD_OUT_MAX);
TEST_ASSERT_TRUE(strlen(r) < CMD_OUT_MAX * 3 / 4); /* 留四分之一给以后加字段 */
/* 原来留的是四分之一;0.1.17 加 `pd` 用掉了一点(1164 字节)。再加字段先看这个数,不够就该涨 CMD_OUT_MAX 了。 */
TEST_ASSERT_TRUE(strlen(r) < CMD_OUT_MAX - 256);
/* 不给快照:只报影子,照样是一行合法回应。 */
cmd_init(NULL);
+178
查看文件
@@ -0,0 +1,178 @@
/* 0.1.17:CH224A 运行期轮询(pdmon.h)。
守四件事:
1. 读到的就是芯片此刻的值,换了档下一拍就跟着变(这个模块存在的理由);
2. 读不到时 `ok` 落下、旧值留着,恢复后自己回来;
3. 换档事件:连续两拍一致才记、同一个档位只记一次、和启动时那条不重复;
4. 主干不应答就去 mux 通道 5 找(设计与实物不一致,bringup.c `pd_up()`)。 */
#include "pdmon.h"
#include "board_pins.h"
#include "ch224a.h"
#include "evlog.h"
#include "hal_i2c.h"
#include "hal_lock.h"
#include "i2cbus.h"
#include "sim_hal.h"
#include "tca9548a.h"
#include "unity.h"
static int count_pd_ev(evlog_rec_t *last)
{
(void)evlog_flush(0);
evlog_stats_t st;
evlog_stats(&st);
int n = 0;
evlog_rec_t rec;
for (uint32_t seq = st.oldest_seq; seq < st.next_seq; seq++) {
if (evlog_read(seq, &rec, 1) == 1 && rec.type == (uint8_t)LOG_EV_PD_LIMIT) {
n++;
if (last != NULL) {
*last = rec;
}
}
}
return n;
}
static void charger(uint32_t ma)
{
sim_i2c_set_reg(CH224A_ADDR_DEFAULT, CH224A_REG_STATUS, 0x08);
sim_i2c_set_reg(CH224A_ADDR_DEFAULT, CH224A_REG_CURRENT, (uint16_t)(ma / 50u));
}
void setUp(void)
{
sim_hal_reset();
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL));
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init());
sim_i2c_add_mux(TCA9548A_ADDR, BOARD_PIN_MUX_RST_N);
TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init());
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init_ex(0));
}
void tearDown(void) {}
static void test_reports_what_the_chip_says_now(void)
{
sim_i2c_add(CH224A_ADDR_DEFAULT, 1);
charger(3000);
pdmon_init(3000);
pdmon_snap_t s;
pdmon_get(&s);
TEST_ASSERT_FALSE(s.ok); /* 还没读过第一拍 */
pdmon_poll();
pdmon_get(&s);
TEST_ASSERT_TRUE(s.ok);
TEST_ASSERT_EQUAL_UINT8(0x08, s.status);
TEST_ASSERT_EQUAL_UINT32(3000, s.ma);
charger(5000);
pdmon_poll();
pdmon_get(&s);
TEST_ASSERT_EQUAL_UINT32(5000, s.ma); /* 下一拍就是新值,不等去抖 */
}
static void test_read_failure_keeps_the_old_value_and_recovers(void)
{
sim_i2c_add(CH224A_ADDR_DEFAULT, 1);
charger(3250);
pdmon_init(3250);
pdmon_poll();
sim_i2c_set_present(CH224A_ADDR_DEFAULT, false);
pdmon_poll();
pdmon_snap_t s;
pdmon_get(&s);
TEST_ASSERT_FALSE(s.ok);
TEST_ASSERT_EQUAL_UINT32(3250, s.ma);
TEST_ASSERT_EQUAL_UINT32(1, s.errors);
sim_i2c_set_present(CH224A_ADDR_DEFAULT, true);
pdmon_poll();
pdmon_get(&s);
TEST_ASSERT_TRUE(s.ok);
TEST_ASSERT_EQUAL_INT(0, count_pd_ev(NULL)); /* 掉线再回来、档位没变:不记 */
}
static void test_change_is_logged_once_after_two_matching_reads(void)
{
sim_i2c_add(CH224A_ADDR_DEFAULT, 1);
charger(5000);
pdmon_init(5000); /* 启动时 bringup 已经记过 5 A 那条 */
pdmon_poll();
pdmon_poll();
TEST_ASSERT_EQUAL_INT(0, count_pd_ev(NULL));
charger(0); /* 重新协商的中途 */
pdmon_poll();
pdmon_poll();
TEST_ASSERT_EQUAL_INT(0, count_pd_ev(NULL));
charger(3000);
pdmon_poll();
TEST_ASSERT_EQUAL_INT(0, count_pd_ev(NULL)); /* 只见过一拍 */
pdmon_poll();
evlog_rec_t rec;
TEST_ASSERT_EQUAL_INT(1, count_pd_ev(&rec));
TEST_ASSERT_EQUAL_UINT8(PDMON_EV_ARG_CHANGED, rec.arg);
TEST_ASSERT_EQUAL_INT32(600, rec.bus_mv); /* 60 W,单位 mW/100 */
TEST_ASSERT_EQUAL_INT32(3000, rec.current_ua);
for (int i = 0; i < 5; i++) {
pdmon_poll();
}
TEST_ASSERT_EQUAL_INT(1, count_pd_ev(NULL));
}
static void test_boot_missed_it_but_runtime_reads_it(void)
{
sim_i2c_add(CH224A_ADDR_DEFAULT, 1);
charger(3000);
pdmon_init(0); /* 启动时没读到,日志里是「按保守值」那条 */
pdmon_poll();
pdmon_poll();
evlog_rec_t rec;
TEST_ASSERT_EQUAL_INT(1, count_pd_ev(&rec));
TEST_ASSERT_EQUAL_INT32(3000, rec.current_ua);
}
static void test_found_behind_channel_5(void)
{
sim_i2c_add_ch(TCA9548A_CH_PD, CH224A_ADDR_DEFAULT, 1);
sim_i2c_set_reg_ch(TCA9548A_CH_PD, CH224A_ADDR_DEFAULT, CH224A_REG_STATUS, 0x08);
sim_i2c_set_reg_ch(TCA9548A_CH_PD, CH224A_ADDR_DEFAULT, CH224A_REG_CURRENT, 5000 / 50);
pdmon_init(5000);
pdmon_poll();
pdmon_snap_t s;
pdmon_get(&s);
TEST_ASSERT_TRUE(s.ok);
TEST_ASSERT_EQUAL_UINT32(5000, s.ma);
TEST_ASSERT_EQUAL_UINT32(0, s.errors);
}
static void test_absent_chip_is_just_not_ok(void)
{
pdmon_init(0);
pdmon_poll();
pdmon_snap_t s;
pdmon_get(&s);
TEST_ASSERT_FALSE(s.ok);
TEST_ASSERT_EQUAL_UINT32(1, s.polls);
TEST_ASSERT_EQUAL_UINT32(1, s.errors);
TEST_ASSERT_EQUAL_INT(0, count_pd_ev(NULL));
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_reports_what_the_chip_says_now);
RUN_TEST(test_read_failure_keeps_the_old_value_and_recovers);
RUN_TEST(test_change_is_logged_once_after_two_matching_reads);
RUN_TEST(test_boot_missed_it_but_runtime_reads_it);
RUN_TEST(test_found_behind_channel_5);
RUN_TEST(test_absent_chip_is_just_not_ok);
return UNITY_END();
}