固件 A15:启动顺序 + 降级矩阵(bringup.{h,c},已上板)
- 启动序列从 app_main.c 拆进 main/bringup.{h,c},宿主机可测:
bringup_devices()(§10.9 第 1~4 步)→ 起 t_safety → bringup_power_restore()(第 5~6 步)
- 六片 INA226 写 Config/Calibration(之前没做,电流恒 0);输入侧 0x43 登记对调接
- 按「意图」逐路上电、错开 100 ms、每路等 PG 500 ms,等不到关回去并记 LOG_EV_PG_TIMEOUT(17)
- 意图只由 power 命令改(cmd_env_t.intent → NVS phonerail/rail_want)
- §10.10 逐行降级:INA/DAC/PCA9555/mux/PRSNT#/CH224A;mux 失联时不碰主干
- BOOT 事件 arg = 复位原因;HUB_RST# 仍只在上电复位时脉冲
- rail_volt_init() 一片失败不再放弃另一片;CH224A 主干 / 通道 5 两处都找
- sim:sim_gpio_rise_us / sim_gpio_set_hook,sim_gpio_drive 可在配置前调
- test_bringup.c 24 例 + test_cmd 意图 1 例,共 425 例全绿;版本 0.1.0-a15
- rack:事件表加 17 PG_TIMEOUT(已部署)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+36
-31
@@ -1,44 +1,49 @@
|
||||
# 交接
|
||||
更新:2026-09-27 完成 **A14 + C1**(WiFi/MQTT 链路 + 经 USB 配网,需求 §10.8「A14 定稿的 MQTT 链路」)
|
||||
更新:2026-09-27 完成 **A15**(启动顺序 + 降级矩阵,需求 §10.9 / §10.10)
|
||||
|
||||
## 一句话现状
|
||||
板 B(`b81f3fc9c8bc`)经 WiFi `CANBOX-5G` → 公网 → home 服务器 mosquitto 8883(TLS)**真连网跑通**:
|
||||
retained state、1 s 遥测、命令 / 判重 / `link` / `empty` / `parse` 回应全对。两条链路(USB + MQTT)都已上板。
|
||||
**23 个 ctest、400 个用例全绿**,`idf.py build` 通过(**0xE2220,分区只剩 12%**)。板 B 烧的是 0.1.0-a14,配置已存 NVS。
|
||||
启动序列从 `app_main.c`(只进 IDF、一行测不到)拆进 `main/bringup.{h,c}`,宿主机 `test_bringup.c` **24 例**逐个「拔掉」器件对 §10.10 逐行。
|
||||
**24 个 ctest、425 个用例全绿**,`idf.py build` 通过(**0xE2C60,分区剩 11%**)。板 B(COM5)已烧 **0.1.0-a15**。
|
||||
|
||||
## 下一步
|
||||
固件还欠 **A15**(启动顺序 + 降级矩阵)/ **A16**(风扇闭环接进 t_thermal 任务体)。
|
||||
或者转**服务器侧前后端**(需求 D7):后端以 `phonerail-backend` 订阅 `phonerail/+/rsp|tele|state`,经 `<base>/cmd` 下发。
|
||||
- **A16**:风扇闭环接进 `t_thermal` 任务体(app_main.c 的 `task_thermal` 还是空壳;`cmd_env.thermal` 仍是 NULL)。
|
||||
- 服务器:`rack/backend/prrack/protocol.py` 已加事件 17,已部署(HOST=10.5.5.13,26 例过)。
|
||||
- 待核:CH224A 读到 5000 mA(100 W),而 PROGRESS 早上记的是 20V/3A —— 换了充电器,还是 0x50 的理解有误?
|
||||
|
||||
## 本次新增/修改的文件
|
||||
- `main/netcfg.{h,c}` **新** — 配置校验 / 持有 / 存储回调;网络状态;「密码永不外泄」
|
||||
- `main/mlink.{h,c}` **新** — topic、入站消息校验、WiFi 退避、遥测节拍、上线消息
|
||||
- `main/cmd_net.c` **新** — `wifi` / `mqtt`(只收 USB)/ `net` 三条命令;新增错误码 `link`(11)、命令号 10~12(冻结)
|
||||
- `main/mlink_esp.{h,c}` **新**(只进 IDF)— NVS、WiFi STA、esp-mqtt(TLS,enqueue 不阻塞)、遥测
|
||||
- `main/certs/evse_ca.pem` **新** — 服务器 mosquitto 的自签 CA(公钥证书),`EMBED_TXTFILES`
|
||||
- `main/cmd_json.h` — 字符串上限 32 → 65(WPA2 口令 63)
|
||||
- `main/app_main.c` / `CMakeLists.txt` — 接线;REQUIRES 加 wifi / netif / event / timer / nvs / mqtt
|
||||
- `test/test_net.c`(10 例)**新**;`test_cmd.c` 冻结编号更新;版本号 `0.1.0-a14`
|
||||
- `main/bringup.{h,c}` **新** — 两段:`bringup_devices()`(§10.9 第 1~4 步 + 降级)、`bringup_power_restore(want)`(第 5~6 步)
|
||||
- `main/app_main.c` — 启动段换成两段调用;**`t_safety` 在两段之间 spawn**(上电时巡检已在看);
|
||||
`evlog_init_ex(esp_reset_reason())` 挪到最前;NVS 键 `phonerail/rail_want` 存「意图」;`hub_reset_pulse_once()` 搬进 bringup.c
|
||||
- `main/cmd.h` / `cmd_exec.c` — `cmd_env_t` 加 `intent` 回调,只有 `power` 命令真的生效时调
|
||||
- `main/evlog.{h,c}` — `evlog_init_ex(boot_arg)`;新事件 **`LOG_EV_PG_TIMEOUT = 17`**
|
||||
- `main/rail.{h,c}` — `rail_volt_init_one(addr)`;`rail_volt_init()` 一片失败**不再放弃另一片**
|
||||
- sim:`sim_gpio_rise_us()`、`sim_gpio_set_hook()`;`sim_gpio_drive()` 可在配置前调、跨 config 保持(刀片把 PRSNT# 短到地与固件何时配脚无关)
|
||||
- `test/test_bringup.c` **新**(24 例);`test_cmd.c` +1(意图);`test_evlog.c` 冻结号 +17;版本 `0.1.0-a15`
|
||||
|
||||
## 服务器上改了什么(home 服务器,mosquitto 1.6.9,evse 项目共用)
|
||||
- `evse.passwd` 加用户 `b81f3fc9c8bc`、`phonerail-backend`;`evse.acl` 末尾追加 phonerail 段(`pattern … phonerail/%u/…`)。
|
||||
- ⛔ **修了 evse 原有的漏洞**:8883 listener 缺 `cafile` ⇒ TLS 根本没生效、**公网明文**。补 `cafile /etc/mosquitto/certs/ca.crt` 后重启;
|
||||
改前 8883 上零成功连接(日志自 09-24 起),evse-backend 走本机 1884,重启后 5 s 自动重连。
|
||||
- 三个文件都有 `.bak-20260927-*` 备份。口令在 `/root/phonerail/mqtt.env`(700 目录,仅 root)。
|
||||
## §10.10 各行落在哪
|
||||
| 行 | 行为 | 用例 |
|
||||
|---|---|---|
|
||||
| 刀片 INA226 无应答 | 照常上电、`blind`、I2C_LOST(rail, 0x40) | `test_row_rail_ina_missing_keeps_power` |
|
||||
| DAC 无应答 | 不关输出、另一片照样初始化、影子回 3.90 | `test_row_dac_missing_…` |
|
||||
| PCA9555 无应答 | 照常上电,PG 改按 INA226 电压;两样都看不见 = 开着报 unverified | `test_row_pca_*`(3 例) |
|
||||
| mux 选不中 | 脉冲一次 MUX_RST#,仍不通 ⇒ 一路不开、MUX_LOST,不碰主干 | `test_row_mux_*`(2 例) |
|
||||
| PRSNT# 高 | 不发一个总线字节、不上电;想开的才记 SLOT_EMPTY | `test_row_empty_slot_*`(2 例) |
|
||||
| CH224A 读不到 / 偏低 | 按 60 W / 按读到的,PD_LIMIT 事件 | `test_row_pd_*` + 通道 5 兜底 |
|
||||
| 看门狗复位 | BOOT.arg = 复位原因 | `test_row_watchdog_reset_is_recorded_in_boot` |
|
||||
| **I²C 总线挂死** | ⛔ **没做**(9 个时钟恢复在 HAL 层) | — |
|
||||
|
||||
## 验收命令与结果
|
||||
注错验证过:删掉 `ina226_set_reversed(0x43)` ⇒ 第一条红;错开改成 1 ms ⇒ 两条红。
|
||||
|
||||
## 上板实测(板 B,五槽空)
|
||||
```
|
||||
$ bash host/test.sh # 100% tests passed, 0 tests failed out of 23(400 例)
|
||||
$ python tools/prlink.py COM5 net # "wifi":"up","ip":"192.168.8.108","mqtt":"up"
|
||||
服务器:mosquitto_sub -h 127.0.0.1 -p 1884 -u phonerail-backend … -t 'phonerail/#' -v
|
||||
status: "fw":"0.1.0-a15","in":{"mv":12034,"ua":98500} ← 输入电流第一次是真值(以前 Calibration 没写,恒 0)
|
||||
events: [27, 51, 0, 1(BOOT), 11(ESP_RST_USB), …] [28, 155, 0, 16(PD_LIMIT), 0, 1000(=100 W), 5000 mA]
|
||||
```
|
||||
注错验证过:放开「只收 USB」、去掉 JSON 字符串清洗 → 各自有用例变红。
|
||||
没有 I2C_LOST ⇒ 两片 DAC / 输入侧 INA226 / CH224A 都在主干应答。复位原因是 USB ⇒ 没碰 HUB_RST#,COM5 没掉。
|
||||
**上电复位路径(Hub 脉冲、DAC NVM 核对)和有刀片时的逐路上电还没在实物上走过**——要等刀片 + 手机上架。
|
||||
|
||||
## 踩到的坑(只写会再犯的)
|
||||
- ⛔ **`openssl s_client … | grep "Verify return code"` 不能证明 TLS 通了**:握手失败、没拿到证书时它也打 `0 (ok)`。要看 `Cipher is` / `Protocol`。
|
||||
- ⛔ mosquitto 1.6:listener 只有 certfile/keyfile、没有 cafile/capath ⇒ **静默退化成明文**。TLS 选项改动要 restart,HUP 不生效。
|
||||
- esp-tls 的「Certificate verified.」在握手失败时也会打,别被它带偏;服务器抓包(tcpdump 看 RST 时机)最快。
|
||||
- 服务器 pid 文件不在 `/var/run/mosquitto.pid`,HUP 用 `pgrep -x mosquitto`。`mosquitto_passwd -b`(1.6.9)会把文件内容打到终端,不是出错。
|
||||
- 本机(192.168.8.x)与服务器(10.5.5.x)不同网段,板子只能走公网 `home.example:8883`;**明文 mqtt:// 只许用在同一局域网**。
|
||||
- 固件 0xE2220 / 1 MB:WiFi + MQTT + TLS ≈ 600 KB。**C2(OTA 双分区)前必须重排分区表**(模组是 16 MB,sdkconfig 只按 8 MB 用)。
|
||||
- WiFi 断开原因码 3(AUTH_LEAVE)是重配网时我们主动断的,不是故障;201 = 找不到 AP(比如只有 5 GHz)。
|
||||
- ⛔ **mux 死了主干也够不着**:`i2cbus_xact(TRUNK)` 先写 mux「全关」,失败就不跑回调。别以为「DAC 在主干上就不受 mux 影响」。
|
||||
- sim 里同一地址在主干和某通道各挂一个,主干那颗 `present=false` 时仍会先被匹配 ⇒ NAK。测「只在通道 5」要主干上干脆不挂。
|
||||
- 设定电压**不持久化**:热重启后五路回到 3.90 V(bringup 把 DAC 重写成安全值)。上位机要的话重下。
|
||||
- `python` 在这台机器的 Git Bash 里是 Windows Store 占位程序,会挂住;一律用 `/c/Python314/python`。
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
│ 是同一条规矩:**能烧板子的动作不给接口,而不是给了再叮嘱别用。** │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
挂在 TCA9548A 的**通道 5**(`TCA9548A_CH_PD`)上,不在主干。 */
|
||||
设计上挂在 TCA9548A 的**通道 5**(`TCA9548A_CH_PD`)上。
|
||||
⚠️ **但 2026-09-27 板 B 实测它在主干应答**(0x22/0x23,mux 全关时也在),与设计不一致、原因未查。
|
||||
`bringup.c` 的 `pd_up()` 两处都找(先主干);弄清之后删掉一处。 */
|
||||
#ifndef CH224A_H
|
||||
#define CH224A_H
|
||||
|
||||
|
||||
@@ -10,8 +10,13 @@ typedef struct {
|
||||
int level;
|
||||
uint32_t writes;
|
||||
uint32_t seq; /* 最后一次被写时的全局序号,0 = 没写过 */
|
||||
uint64_t rise_us; /* 最后一次 0→1 的虚拟时刻(µs),0 = 没有过 */
|
||||
bool ext; /* 外部世界在驱这根线(`sim_gpio_drive`),跨 config 保持 */
|
||||
int ext_level;
|
||||
} pin_t;
|
||||
|
||||
static sim_gpio_hook_t s_hook;
|
||||
|
||||
static pin_t s_pin[SIM_GPIO_PINS];
|
||||
static uint32_t s_seq; /* 全局写计数,从 1 开始发号 */
|
||||
|
||||
@@ -30,6 +35,7 @@ void sim_gpio_reset_internal(void)
|
||||
{
|
||||
memset(s_pin, 0, sizeof(s_pin));
|
||||
s_seq = 0;
|
||||
s_hook = NULL;
|
||||
}
|
||||
|
||||
static bool valid(int pin) { return pin >= 0 && pin < SIM_GPIO_PINS; }
|
||||
@@ -45,6 +51,11 @@ hal_err_t hal_gpio_config(int pin, hal_gpio_mode_t mode)
|
||||
/* 上拉输入没人驱动时读回高电平 —— 五路 FLT/PG 都是开漏 + 上拉,
|
||||
「没有故障」在假板子上也必须是 1,不然测试会把默认 0 当成一直有故障。 */
|
||||
p->level = (mode == HAL_GPIO_INPUT_PULLUP) ? 1 : 0;
|
||||
/* 外部驱着的线,配成输入之后读到的是外部电平 —— 真板上刀片把 `PRSNT#` 短到地,
|
||||
与 ESP32 什么时候配这个脚无关(A15:启动序列自己配脚、配完立刻读)。 */
|
||||
if (mode != HAL_GPIO_OUTPUT && p->ext) {
|
||||
p->level = p->ext_level;
|
||||
}
|
||||
/* 配成输出的那一刻引脚就开始驱动了(ESP-IDF 的 gpio_config 也是这个行为,
|
||||
输出寄存器复位值 0)。`KEY_OE#` 正是靠这一条:配成输出 = 输出立刻使能,
|
||||
所以 sr595_init() 必须等安全值锁存完了再配它。 */
|
||||
@@ -60,11 +71,18 @@ hal_err_t hal_gpio_write(int pin, int level)
|
||||
if (!valid(pin) || !s_pin[pin].configured || s_pin[pin].mode != HAL_GPIO_OUTPUT) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
const int was = s_pin[pin].level;
|
||||
s_pin[pin].level = level ? 1 : 0;
|
||||
s_pin[pin].writes++;
|
||||
s_pin[pin].seq = ++s_seq;
|
||||
if (was == 0 && s_pin[pin].level == 1) {
|
||||
s_pin[pin].rise_us = hal_time_now_us();
|
||||
}
|
||||
sim_sr595_gpio_hook(pin, s_pin[pin].level);
|
||||
sim_mux_gpio_hook(pin, s_pin[pin].level);
|
||||
if (s_hook != NULL) {
|
||||
s_hook(pin, s_pin[pin].level);
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
@@ -80,9 +98,14 @@ hal_err_t hal_gpio_read(int pin, int *level)
|
||||
void sim_gpio_drive(int pin, int level)
|
||||
{
|
||||
assert(valid(pin));
|
||||
assert(s_pin[pin].configured);
|
||||
assert(s_pin[pin].mode != HAL_GPIO_OUTPUT); /* 外部驱一个输出脚 = 打架,测试写错了 */
|
||||
s_pin[pin].level = level ? 1 : 0;
|
||||
/* 外部驱一个输出脚 = 打架,测试写错了。**没配过的脚可以驱**:外部世界不等固件
|
||||
(电平记下来,配成输入的那一刻生效)。 */
|
||||
assert(!s_pin[pin].configured || s_pin[pin].mode != HAL_GPIO_OUTPUT);
|
||||
s_pin[pin].ext = true;
|
||||
s_pin[pin].ext_level = level ? 1 : 0;
|
||||
if (s_pin[pin].configured) {
|
||||
s_pin[pin].level = level ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
int sim_gpio_level(int pin)
|
||||
@@ -96,3 +119,7 @@ int sim_gpio_level(int pin)
|
||||
uint32_t sim_gpio_writes(int pin) { return valid(pin) ? s_pin[pin].writes : 0; }
|
||||
|
||||
uint32_t sim_gpio_write_seq(int pin) { return valid(pin) ? s_pin[pin].seq : 0; }
|
||||
|
||||
uint64_t sim_gpio_rise_us(int pin) { return valid(pin) ? s_pin[pin].rise_us : 0; }
|
||||
|
||||
void sim_gpio_set_hook(sim_gpio_hook_t fn) { s_hook = fn; }
|
||||
|
||||
@@ -113,7 +113,8 @@ void sim_i2c_set_bus_time(bool on);
|
||||
直接 HAL_ERR_ARG)。边界写成「个数」而不是「上限」,就是这么错的。 */
|
||||
#define SIM_GPIO_PINS 49
|
||||
|
||||
/* 外部世界把某个**输入**脚拉成 level。对输出脚调用会 abort(那是总线打架)。 */
|
||||
/* 外部世界把某个**输入**脚拉成 level。对输出脚调用会 abort(那是总线打架)。
|
||||
可以在脚配置之前调:电平记着,配成输入的那一刻生效(压过内部上拉)。 */
|
||||
void sim_gpio_drive(int pin, int level);
|
||||
|
||||
/* 该脚当前电平:输出脚 = 固件驱动的值,输入脚 = 外部驱动的值。没配过返回 -1。 */
|
||||
@@ -127,6 +128,16 @@ uint32_t sim_gpio_writes(int pin);
|
||||
「17 ms 上电时序」(A9)这类事,光看每个脚的终值和次数是看不出来的。 */
|
||||
uint32_t sim_gpio_write_seq(int pin);
|
||||
|
||||
/* 该脚**最后一次 0→1** 时的虚拟时刻(µs),从没抬高过返回 0。
|
||||
「五路上电错开 100 ms」(A15)要的是线上的时刻,不是被测代码自己报的时刻。 */
|
||||
uint64_t sim_gpio_rise_us(int pin);
|
||||
|
||||
/* 每次 `hal_gpio_write()` 成功之后调一次(`sim_hal_reset()` 清掉)。
|
||||
给测试扮演「外部世界对引脚的反应」用 —— 比如 eFuse 的 EN 一抬高,
|
||||
那一路 PCA9555 上的 PGOOD 就跟着来(A15 的上电序列靠它才测得出「等 PG」)。 */
|
||||
typedef void (*sim_gpio_hook_t)(int pin, int level);
|
||||
void sim_gpio_set_hook(sim_gpio_hook_t fn);
|
||||
|
||||
/* ---------------- 74HC595 ×3 的 24 位链 ---------------- */
|
||||
|
||||
/* 按 datasheet 建模的假移位链,**只看线上真实发生的边**(SRCLK / RCLK 上升沿)。
|
||||
|
||||
@@ -40,6 +40,7 @@ set(PHONERAIL_CORE_SRCS
|
||||
${FW_ROOT}/main/cmd_net.c
|
||||
${FW_ROOT}/main/netcfg.c
|
||||
${FW_ROOT}/main/mlink.c
|
||||
${FW_ROOT}/main/bringup.c
|
||||
)
|
||||
|
||||
# HAL:接口在 include/,宿主机链 sim/ 的实现(esp/ 那份只有 IDF 目标编)。
|
||||
|
||||
@@ -24,6 +24,7 @@ idf_component_register(
|
||||
"cmd_net.c"
|
||||
"netcfg.c"
|
||||
"mlink.c"
|
||||
"bringup.c"
|
||||
INCLUDE_DIRS "."
|
||||
EMBED_TXTFILES "certs/evse_ca.pem"
|
||||
REQUIRES phonerail_hal drv esp_driver_usb_serial_jtag
|
||||
|
||||
+116
-142
@@ -3,6 +3,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "board_pins.h"
|
||||
#include "bringup.h"
|
||||
#include "cmd.h"
|
||||
#include "cmd_queue.h"
|
||||
#include "esp_log.h"
|
||||
@@ -26,6 +27,8 @@
|
||||
#include "t_safety.h"
|
||||
#include "tasks.h"
|
||||
#include "mlink_esp.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "ulink_esp.h"
|
||||
|
||||
static const char *TAG = "phonerail";
|
||||
@@ -236,44 +239,77 @@ static void task_log(void *arg)
|
||||
}
|
||||
}
|
||||
|
||||
/* USB2517I 的复位脉冲 —— **整个固件里唯一碰 `HUB_RST#` 的地方,而且只在启动调一次**。
|
||||
/* ---- 每路「意图」的持久化(A15,§10.9 第 4 步「上次的每路使能状态」)--------
|
||||
一个字节,bit0 = CH1。只有 `power` 命令改它(cmd.h 的 `cmd_intent_fn`)。
|
||||
与网络配置同一个 NVS 命名空间(mlink_esp.c),各用各的键。
|
||||
新板 / 读不到 = 0 = 一路都不开:宁可让人手动开一次,也不在不知道的情况下给五台手机上电。 */
|
||||
#define WANT_NS "phonerail"
|
||||
#define WANT_KEY "rail_want"
|
||||
|
||||
低有效(板上 R716 上拉到 +3V3_HUB),给 1 ms 足够:手册要的是几十微秒量级。
|
||||
static uint8_t s_want;
|
||||
|
||||
⛔⛔ **运行期一个字都不许再写它**(R2 D13 代价 2)。ESP32 自己的
|
||||
USB-Serial-JTAG 就挂在这个 Hub 的第 6 个下行口上,所以复位 Hub =
|
||||
**自己把自己的控制台拔了**,而且下一次 `idf.py flash` 也进不来
|
||||
—— 到那时候唯一的退路是 J33 那个 5P 应急串口(UART0,IO43/IO44),
|
||||
得拆机接线。写一行 "Hub 卡了就复位一下" 的自愈代码,代价是整机变砖。
|
||||
|
||||
⚠️ 这一下放在 `rail_en_init()` 之后、其它一切之前:Hub 复位会让 USB 重新枚举,
|
||||
期间串口日志是丢的。越早做,后面越多的启动日志能被电脑收到。
|
||||
失败**不 abort**:Hub 没起来只影响调试与手机的数据口,五路供电照常
|
||||
(§10.2:失联 ≠ 断电)。
|
||||
|
||||
⛔ **只在上电复位(`ESP_RST_POWERON`)时脉冲**(2026-09-27 上板改)。
|
||||
原来是「每次启动都脉冲」,而 ESP32 的每一次重启都算启动:esptool 经 USB 复位芯片、
|
||||
崩溃 / 看门狗重启、`esp_restart()` —— 每一次都把 Hub 复位、把自己的 COM 口拔掉。
|
||||
实测后果:板上一旦有这份固件,`esptool --before usb_reset` 连三次全断
|
||||
(`ClearCommError PermissionError 13`),只能走 J33 手动进下载模式。
|
||||
上电复位时 Hub 自己也刚上电,脉冲没有代价;J33 的 EN 脚复位在 S3 上也报 POWERON,
|
||||
应急路径不受影响。其余原因一律不碰 `HUB_RST#`(此时它仍是复位态的高阻,靠 R716 拉高)。 */
|
||||
static void hub_reset_pulse_once(void)
|
||||
static void nvs_up(void)
|
||||
{
|
||||
const esp_reset_reason_t why = esp_reset_reason();
|
||||
if (why != ESP_RST_POWERON) {
|
||||
ESP_LOGI(TAG, "复位原因 %d 不是上电复位 —— 不动 HUB_RST#(保住 USB 控制台)", (int)why);
|
||||
return;
|
||||
esp_err_t e = nvs_flash_init();
|
||||
if (e == ESP_ERR_NVS_NO_FREE_PAGES || e == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
nvs_flash_erase(); /* 同 mlink_esp.c:格式不认就清掉重来 */
|
||||
e = nvs_flash_init();
|
||||
}
|
||||
hal_err_t err = hal_gpio_config(BOARD_PIN_HUB_RST_N, HAL_GPIO_OUTPUT);
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "HUB_RST# 配置失败:%s —— 跳过复位脉冲", hal_err_str(err));
|
||||
return;
|
||||
if (e != ESP_OK) {
|
||||
ESP_LOGE(TAG, "NVS 起不来:%s —— 本次按「一路都不开」启动", esp_err_to_name(e));
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t want_load(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
uint8_t v = 0;
|
||||
if (nvs_open(WANT_NS, NVS_READONLY, &h) == ESP_OK) {
|
||||
if (nvs_get_u8(h, WANT_KEY, &v) != ESP_OK) {
|
||||
v = 0;
|
||||
}
|
||||
nvs_close(h);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
static void want_save(void *ctx, int rail, bool on)
|
||||
{
|
||||
(void)ctx;
|
||||
const uint8_t v = bringup_want_update(s_want, rail, on);
|
||||
if (v == s_want) {
|
||||
return; /* 没变就不写 flash */
|
||||
}
|
||||
nvs_handle_t h;
|
||||
esp_err_t e = nvs_open(WANT_NS, NVS_READWRITE, &h);
|
||||
if (e == ESP_OK) {
|
||||
e = nvs_set_u8(h, WANT_KEY, v);
|
||||
if (e == ESP_OK) {
|
||||
e = nvs_commit(h);
|
||||
}
|
||||
nvs_close(h);
|
||||
}
|
||||
if (e == ESP_OK) {
|
||||
s_want = v;
|
||||
} else {
|
||||
/* 存不下不影响这一次的开关(已经生效了),只影响下次开机恢复成什么样。 */
|
||||
ESP_LOGW(TAG, "意图没存进 NVS:%s", esp_err_to_name(e));
|
||||
}
|
||||
}
|
||||
|
||||
static void log_report(const bringup_report_t *r)
|
||||
{
|
||||
ESP_LOGI(TAG, "启动:降级位 0x%02x,空槽 0x%02x,盲跑 0x%02x,PCA 失联 0x%02x,PD %u mA → 上限 %u mW",
|
||||
(unsigned)r->deg, (unsigned)r->empty, (unsigned)r->blind, (unsigned)r->io_lost,
|
||||
(unsigned)r->pd_ma, (unsigned)r->pd_limit_mw);
|
||||
if ((r->deg & BRINGUP_DEG_MUX) != 0) {
|
||||
ESP_LOGE(TAG, "TCA9548A 复位后仍选不中 —— 五路一路都不上电(§5.8)");
|
||||
}
|
||||
if ((r->deg & BRINGUP_DEG_DAC_NVM) != 0) {
|
||||
ESP_LOGW(TAG, "DAC 上电值不是 3.90 V(NVM 被改过?):%u %u %u %u %u mV",
|
||||
r->dac_nvm_mv[0], r->dac_nvm_mv[1], r->dac_nvm_mv[2], r->dac_nvm_mv[3],
|
||||
r->dac_nvm_mv[4]);
|
||||
}
|
||||
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 0); /* 低 = 复位 */
|
||||
hal_time_delay_ms(1);
|
||||
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 1); /* 放开,之后再也不动 */
|
||||
ESP_LOGI(TAG, "USB Hub 已复位一次(运行期不再动 HUB_RST#)");
|
||||
}
|
||||
|
||||
static void spawn(task_id_t id, TaskFunction_t fn)
|
||||
@@ -299,96 +335,38 @@ void app_main(void)
|
||||
|
||||
selftest_or_die();
|
||||
|
||||
/* ⭐ R2:**五根** `EFUSE_EN` 配成输出并全部拉低(R1 是十根:另外五根 `VBUS_EN`
|
||||
已经挪到刀片的 PCA9555 上了)。**先于一切外设**:DAC / eFuse 一上电就有
|
||||
输出,这五根线这时必须已经确定是低的。 */
|
||||
hal_err_t err = rail_en_init();
|
||||
const esp_reset_reason_t why = esp_reset_reason();
|
||||
|
||||
/* 事件日志**最先**起(A15 挪到这里):下面的启动序列会记事件(缺件 / mux / PD),
|
||||
它们必须排在这次的 BOOT 之后,否则时间线上看着像是上一次运行留下的。
|
||||
BOOT 的 arg = 复位原因 —— §10.10「看门狗复位 … 记一条复位事件」就是它。
|
||||
只碰 flash,不碰总线,放在 EN 脚配置之前也安全(那几根线此刻靠下拉关着)。
|
||||
⚠️ 失败**不 abort**:日志是排查手段,不是安全链的一环(§10.2)。 */
|
||||
hal_err_t err = evlog_init_ex((uint8_t)why);
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "EN 引脚初始化失败:%s", hal_err_str(err));
|
||||
ESP_LOGE(TAG, "事件日志落盘不可用:%s —— 事件只留在 RAM 里", hal_err_str(err));
|
||||
} else {
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
ESP_LOGI(TAG, "事件日志:容量 %u 条,flash 里已有 %u~%u(复位原因 %d)",
|
||||
(unsigned)evlog_capacity(), (unsigned)st.oldest_seq, (unsigned)st.next_seq,
|
||||
(int)why);
|
||||
}
|
||||
|
||||
nvs_up();
|
||||
s_want = want_load();
|
||||
|
||||
/* §10.9 第 1~4 步:EN 脚安全态 → Hub 脉冲(仅上电复位)→ 总线 → 认件 / 初始化。
|
||||
整段在 bringup.c 里、宿主机上测得到(A15 之前它在这里,一行也测不到)。
|
||||
缺件一律降级不 abort;返回错误 = 引脚 / 配置常量写错了,那才 abort。 */
|
||||
static bringup_report_t boot;
|
||||
const bringup_in_t in = { .power_on_reset = (why == ESP_RST_POWERON) };
|
||||
err = bringup_devices(&in, &boot);
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "启动序列失败(固件配置错误):%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
/* ⚠️ R2:这一步关掉的**只有五路直供**(`EFUSE_EN` 是 GPIO 直连)。
|
||||
`VBUS_EN` 已经挪到刀片的 PCA9555 P0.4 上了(计划 §5.4),此刻还不受控 ——
|
||||
它靠的是硬件:PCA9555 复位后全是输入(高阻)+ R701 100 kΩ 下拉 = VBUS 断。
|
||||
真正"固件说了算"要等到下面的 `slot_init()`。 */
|
||||
ESP_LOGI(TAG, "五路 eFuse 使能已全部拉低(VBUS 此刻靠下拉电阻关着)");
|
||||
|
||||
/* USB Hub 复位脉冲 —— 只在上电复位时、只此一次,见函数头上那一段。 */
|
||||
hub_reset_pulse_once();
|
||||
|
||||
/* 锁必须早于任何一个用总线的任务。
|
||||
⚠️ R2 之后**全系统只剩一把** `HAL_LOCK_I2C`(595 那把随着移位链一起退役),
|
||||
所以「两把锁嵌套」在物理上不可能了;代价是执行器与巡检开始争同一把锁
|
||||
(计划 §5.6)。 */
|
||||
(void)hal_lock_init();
|
||||
|
||||
const hal_i2c_cfg_t i2c = {
|
||||
.sda_gpio = BOARD_PIN_I2C_SDA, /* ⭐ R2:IO41(R1 是 IO14) */
|
||||
.scl_gpio = BOARD_PIN_I2C_SCL, /* ⭐ R2:IO42(R1 是 IO15) */
|
||||
.freq_hz = 400000,
|
||||
};
|
||||
err = hal_i2c_init(&i2c);
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "I²C 初始化失败:%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
|
||||
/* ⭐ R2 新增:带 mux 的总线门面。配 `MUX_RST#`、认 TCA9548A(0x70)、把通道全关。
|
||||
**必须在 `hal_i2c_init()` 与 `hal_lock_init()` 之后**(i2cbus.h)。
|
||||
⚠️ 失败**不 abort**:mux 认不到是**运行期**的降级情形(计划 §5.8 —— 判据会
|
||||
先脉冲 `MUX_RST#` 再决定要不要五路全断),不是固件写错了。这与
|
||||
`selftest_or_die()` 的分界线始终是同一条:常量表错 = abort,器件不在 = 降级。 */
|
||||
err = i2cbus_init();
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "TCA9548A(0x%02x) 初始化失败:%s —— 五路的电压电流这一刻全看不见",
|
||||
(unsigned)TCA9548A_ADDR, hal_err_str(err));
|
||||
}
|
||||
|
||||
/* ⭐ R2 新增:五根 `PRSNT#` 配成上拉输入并立刻读一遍(`slot_prsnt_init()` 自带
|
||||
一次 poll)。**必须在 `slot_init()` 之前**:下面要拿它决定哪几槽值得初始化。
|
||||
⚠️ 这一步失败就 abort:它只可能因为引脚号非法而失败,而那正是
|
||||
`rail_selftest()` 的 `PINS` 位刚刚查过的东西 —— 走到这里还失败 = 固件写错了。
|
||||
而且降级的方向是**不安全**的那一侧:`slot_present()` 读不到时返回"在位",
|
||||
于是空槽也会被允许上电 = 金手指裸露带 12V(计划 §5.8)。 */
|
||||
err = slot_prsnt_init();
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "PRSNT# 引脚初始化失败:%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
|
||||
/* ⭐⭐ R2 必须有的一行(计划 §5.9):底板输入侧那片 INA226(U13, 0x43)的
|
||||
`IN+`/`IN−` **在板上是对调接的**(pin9 = `VIN_SW` 上游、pin10 = `+12V` 下游)。
|
||||
漏了这一行,正常供电时它读回来的电流是负的,SW-8 会把「板子在好好工作」
|
||||
判成**反灌**,于是一上电就断 VBUS —— 而电压、功率、温度全都正常,
|
||||
日志里只会看到反复的 `LOG_EV_REVERSE`。
|
||||
⚠️ 这是纯软件状态(驱动里一个标志位),不碰总线,所以放在认件之前也没关系;
|
||||
但必须早于 `t_safety` 起来,那才是读它的人。 */
|
||||
ina226_set_reversed(INA226_ADDR_INPUT, true);
|
||||
|
||||
/* ⭐ R2 新增:逐片刀片的 PCA9555 初始化(自带 `i2cbus_xact`,各走各的 mux 通道)。
|
||||
驱动里那个框保证了顺序 **输出锁存器先清零、方向最后改** —— 反过来的那一刻
|
||||
P0.0~P0.4 全输出 1 = 三个键一起按下 + EDL 短接 + VBUS 上电(计划 §5.3)。
|
||||
这一步之后 `VBUS_EN` 才真的由固件说了算。
|
||||
⚠️ 失败**不 abort**(同 `i2cbus_init()`:这是运行期降级)。代价写在 §5.4:
|
||||
PCA9555 失联 ⇒ 该路 VBUS **关不掉**,兜底只剩关 `EFUSE_EN`。 */
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
if (!slot_present(rail)) {
|
||||
/* 空槽不去初始化:那一片器件本来就不在,只会白等五次 NAK。
|
||||
这一路也不许上电 —— 判据在 `t_safety` 里每圈查(计划 §5.8)。 */
|
||||
ESP_LOGW(TAG, "槽 %d 没插刀片(PRSNT# 为高):跳过初始化,且不许上电", rail);
|
||||
continue;
|
||||
}
|
||||
const hal_err_t e = slot_init(rail);
|
||||
if (e != HAL_OK) {
|
||||
ESP_LOGE(TAG, "槽 %d 的 PCA9555(0x%02x) 初始化失败:%s —— 这一路的 VBUS "
|
||||
"关不掉了,兜底只剩 EFUSE_EN",
|
||||
rail, (unsigned)PCA9555_ADDR, hal_err_str(e));
|
||||
}
|
||||
}
|
||||
|
||||
/* ⚠️ 还没做的:INA226 的 Config/Calibration 与 DAC 的上电值(A15 那一包)。
|
||||
在那之前 t_safety 读回来的电流恒为 0(Calibration 还没写),
|
||||
这是预期的,不是故障。 */
|
||||
log_report(&boot);
|
||||
|
||||
/* 执行器:PG 从巡检的快照里取,本任务自己不碰 I²C(t_actuator.h 开头那一段)。
|
||||
⚠️ 必须在 `spawn(TASK_SAFETY, ...)` 之前:判据要关路时会先 `t_actuator_abort()`,
|
||||
@@ -396,15 +374,31 @@ void app_main(void)
|
||||
t_actuator_init(act_pg_from_safety, NULL);
|
||||
|
||||
/* 命令处理器(A12)。status 读巡检快照;热管理快照等 A16 把任务体接上再给。
|
||||
`power` 命令的意图经 `want_save` 存 NVS(A15)。
|
||||
队列要在执行器任务起来之前建好 —— 那个任务第一件事就是等它。 */
|
||||
const cmd_env_t cmd_env = { .safety = &s_snap, .thermal = NULL };
|
||||
const cmd_env_t cmd_env = { .safety = &s_snap, .thermal = NULL, .intent = want_save };
|
||||
cmd_init(&cmd_env);
|
||||
s_cmd_q = xQueueCreate(CMD_QUEUE_DEPTH, sizeof(cmd_msg_t));
|
||||
if (s_cmd_q == NULL) {
|
||||
ESP_LOGE(TAG, "命令队列建不起来");
|
||||
abort();
|
||||
}
|
||||
/* A13:USB 命令链路。装驱动 + 把日志复制一份到 USB(UART0 照旧)。
|
||||
|
||||
/* 事件出口。要在巡检起来之前接上,否则第一批事件没人接。 */
|
||||
t_safety_set_event_sink(log_safety_event, NULL);
|
||||
|
||||
/* ⭐ 巡检**先于上电**起来(bringup.h「分两段」那一段):
|
||||
五路一开始带电,SR-5 / SW-8 就已经在看了。 */
|
||||
spawn(TASK_SAFETY, task_safety);
|
||||
|
||||
/* §10.9 第 5~6 步:按上次的意图逐路上电、错开 100 ms、等 PG(阻塞,最坏约 0.9 s)。 */
|
||||
bringup_power_restore(s_want, &boot);
|
||||
ESP_LOGI(TAG, "上电恢复:意图 0x%02x → 开着 0x%02x(等不到 PG 0x%02x,确认不了 0x%02x)",
|
||||
(unsigned)boot.want, (unsigned)boot.on, (unsigned)boot.pg_fail,
|
||||
(unsigned)boot.unverified);
|
||||
|
||||
/* §10.9 第 7 步:链路与其余任务。
|
||||
A13:USB 命令链路。装驱动 + 把日志复制一份到 USB(UART0 照旧)。
|
||||
失败不 abort:USB 命令链路不可用,供电与别的链路照常(§10.2)。 */
|
||||
err = ulink_esp_init();
|
||||
if (err != HAL_OK) {
|
||||
@@ -416,26 +410,6 @@ void app_main(void)
|
||||
ESP_LOGE(TAG, "WiFi 命令链路不可用:%s", hal_err_str(err));
|
||||
}
|
||||
|
||||
/* 事件日志(§10.7)。**要在接事件出口之前 init**,否则第一条事件进的是
|
||||
一个还没扫过 flash 的缓冲。
|
||||
⚠️ 失败**不 abort**:没有 `evlog` 分区时日志落不了盘,但五路照供电
|
||||
—— 日志是排查手段,不是安全链的一环(§10.2)。这与 `selftest_or_die()`
|
||||
的区别在于:那里错的是「命令会发到哪一路」,这里错的只是「记不下来」。 */
|
||||
err = evlog_init();
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "事件日志落盘不可用:%s —— 事件只留在 RAM 里", hal_err_str(err));
|
||||
} else {
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
ESP_LOGI(TAG, "事件日志:容量 %u 条,flash 里已有 %u~%u",
|
||||
(unsigned)evlog_capacity(), (unsigned)st.oldest_seq,
|
||||
(unsigned)st.next_seq);
|
||||
}
|
||||
|
||||
/* 事件出口。同样要在巡检起来之前接上,否则第一批事件没人接。 */
|
||||
t_safety_set_event_sink(log_safety_event, NULL);
|
||||
|
||||
spawn(TASK_SAFETY, task_safety);
|
||||
spawn(TASK_THERMAL, task_thermal);
|
||||
spawn(TASK_ACTUATOR, task_actuator);
|
||||
spawn(TASK_LINK, task_link);
|
||||
|
||||
@@ -50,7 +50,7 @@ extern "C" {
|
||||
/* USB2517I 的复位,上拉到 +3V3_HUB ⇒ 低有效。
|
||||
⛔⛔ **运行期一个字都不许写它**(R2 D13 代价 2):ESP32 自己的控制台与烧录口
|
||||
就挂在这个 Hub 的第 6 个下行口上,误复位 = 自断控制台,而且下一次烧录也进不来。
|
||||
只在**上电复位**时脉冲一次(其余复位原因不碰,见 app_main.c);真出事的退路是 J33 那个 5P 应急串口。 */
|
||||
只在**上电复位**时脉冲一次(其余复位原因不碰,见 bringup.c 的 `hub_pulse()`);真出事的退路是 J33 那个 5P 应急串口。 */
|
||||
#define BOARD_PIN_HUB_RST_N 8 /* 模组脚 12 → U32.43 */
|
||||
|
||||
#define BOARD_PIN_UP_VBUS_DET 6 /* 模组脚 6 → R721/R722 分压,上游 USB-C 插没插 */
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
/* 启动顺序 + 降级矩阵。契约与 §10.10 各行的落点见 bringup.h。 */
|
||||
#include "bringup.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "board_pins.h"
|
||||
#include "ch224a.h"
|
||||
#include "dac63204.h"
|
||||
#include "evlog.h"
|
||||
#include "hal_gpio.h"
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_lock.h"
|
||||
#include "hal_time.h"
|
||||
#include "i2cbus.h"
|
||||
#include "ina226.h"
|
||||
#include "pca9555.h"
|
||||
#include "rail.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "slot.h"
|
||||
#include "tca9548a.h"
|
||||
|
||||
/* 启动期拿锁等多久。此刻只有巡检(第二段)在和我们抢,它一次最多持锁 ~2 ms。 */
|
||||
#define BRINGUP_LOCK_MS SLOT_LOCK_TIMEOUT_MS
|
||||
|
||||
static uint8_t bit_of(int rail) { return (uint8_t)(1u << (rail - 1)); }
|
||||
|
||||
static void lost(int rail, uint8_t addr, hal_err_t err)
|
||||
{
|
||||
/* arg = 地址(evlog.h 的约定),current_ua 顺手带上 HAL 错误码 ——
|
||||
NAK(不在)与 BUS(在但 ID 不对 = 贴错件)是两种完全不同的现场。 */
|
||||
evlog_put((uint8_t)rail, LOG_EV_I2C_LOST, addr, 0, (int32_t)err);
|
||||
}
|
||||
|
||||
/* ---- 一次 xact 的正文们 -------------------------------------------------- */
|
||||
|
||||
static hal_err_t nop_fn(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
return HAL_OK; /* 选通道本身就是探测:选得中 = mux 在 */
|
||||
}
|
||||
|
||||
static hal_err_t ina_up_fn(void *arg)
|
||||
{
|
||||
const uint8_t addr = *(const uint8_t *)arg;
|
||||
hal_err_t err = ina226_probe(addr);
|
||||
return (err == HAL_OK) ? ina226_init(addr) : err;
|
||||
}
|
||||
|
||||
static hal_err_t dac_up_fn(void *arg)
|
||||
{
|
||||
return rail_volt_init_one(*(const uint8_t *)arg);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint16_t mv[PHONERAIL_RAIL_COUNT];
|
||||
} dac_peek_t;
|
||||
|
||||
static hal_err_t dac_peek_fn(void *arg)
|
||||
{
|
||||
dac_peek_t *p = (dac_peek_t *)arg;
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
int mv = 0;
|
||||
p->mv[rail - 1] = (rail_get_mv(rail, &mv) == HAL_OK) ? (uint16_t)mv : 0u;
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
static hal_err_t pd_fn(void *arg)
|
||||
{
|
||||
return ch224a_read_max_ma(CH224A_ADDR_DEFAULT, (uint32_t *)arg);
|
||||
}
|
||||
|
||||
/* ---- 第一段 -------------------------------------------------------------- */
|
||||
|
||||
/* USB2517I 的复位脉冲 —— **整个固件里唯一碰 `HUB_RST#` 的地方,而且只在启动调一次**
|
||||
(A15 从 app_main.c 搬过来的,理由原样保留)。
|
||||
|
||||
低有效(板上 R716 上拉到 +3V3_HUB),给 1 ms 足够:手册要的是几十微秒量级。
|
||||
|
||||
⛔⛔ **运行期一个字都不许再写它**(R2 D13 代价 2)。ESP32 自己的
|
||||
USB-Serial-JTAG 就挂在这个 Hub 的第 6 个下行口上,所以复位 Hub =
|
||||
**自己把自己的控制台拔了**,而且下一次 `idf.py flash` 也进不来
|
||||
—— 到那时候唯一的退路是 J33 那个 5P 应急串口(UART0,IO43/IO44),
|
||||
得拆机接线。写一行 "Hub 卡了就复位一下" 的自愈代码,代价是整机变砖。
|
||||
|
||||
⛔ **只在上电复位(`ESP_RST_POWERON`)时脉冲**(2026-09-27 上板改)。
|
||||
ESP32 的每一次重启都算启动:esptool 经 USB 复位芯片、崩溃 / 看门狗重启、
|
||||
`esp_restart()` —— 原来每一次都把 Hub 复位、把自己的 COM 口拔掉,实测
|
||||
`esptool --before usb_reset` 连三次全断(`ClearCommError PermissionError 13`)。
|
||||
上电复位时 Hub 自己也刚上电,脉冲没有代价;J33 的 EN 脚复位在 S3 上也报 POWERON,
|
||||
应急路径不受影响。其余原因**连配置都不做**(配成输出那一刻就是输出 0 = 一次复位),
|
||||
它仍是复位态的高阻,靠 R716 拉高。
|
||||
|
||||
失败**不 abort**:Hub 没起来只影响调试与手机的数据口,五路供电照常(§10.2)。 */
|
||||
static void hub_pulse(void)
|
||||
{
|
||||
if (hal_gpio_config(BOARD_PIN_HUB_RST_N, HAL_GPIO_OUTPUT) != HAL_OK) {
|
||||
return;
|
||||
}
|
||||
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 0); /* 低 = 复位 */
|
||||
hal_time_delay_ms(1);
|
||||
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 1); /* 放开,之后再也不动 */
|
||||
}
|
||||
|
||||
/* mux:`i2cbus_init()` 认不到 ⇒ 按 §5.8 先脉冲一次 `MUX_RST#` 再判。 */
|
||||
static bool mux_up(bringup_report_t *r)
|
||||
{
|
||||
if (i2cbus_init() == HAL_OK) {
|
||||
return true;
|
||||
}
|
||||
(void)i2cbus_recover(); /* 影子作废 ⇒ 下面那次 xact 一定真的发选通字节 */
|
||||
if (i2cbus_xact(I2CBUS_CH_TRUNK, BRINGUP_LOCK_MS, nop_fn, NULL) == HAL_OK) {
|
||||
return true;
|
||||
}
|
||||
r->deg |= BRINGUP_DEG_MUX;
|
||||
evlog_put(0, LOG_EV_MUX_LOST, 0, 0, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void dac_up(const bringup_in_t *in, bringup_report_t *r)
|
||||
{
|
||||
/* §10.9 第 3 步:读回 NVM 默认值与期望比对。**只在上电复位时有意义** ——
|
||||
热重启时 DAC 没断过电,里面是上一条 set_voltage 设的值,不是 NVM 默认。
|
||||
它要证明的是「DAC 不应答时输出会停在 3.90 V」这条降级前提还成立。 */
|
||||
if (in->power_on_reset) {
|
||||
dac_peek_t p;
|
||||
memset(&p, 0, sizeof(p));
|
||||
(void)i2cbus_xact(I2CBUS_CH_TRUNK, BRINGUP_LOCK_MS, dac_peek_fn, &p);
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
r->dac_nvm_mv[rail - 1] = p.mv[rail - 1];
|
||||
/* 读不到(0)归「不应答」那一行管,这里只抓「读到了但不对」。
|
||||
容差一个码以内:码 ↔ mV 的换算本身有 ±1 mV 的取整。 */
|
||||
const int d = (int)p.mv[rail - 1] - RAIL_MV_SAFE;
|
||||
if (p.mv[rail - 1] != 0 && (d > 1 || d < -1)) {
|
||||
r->deg |= BRINGUP_DEG_DAC_NVM;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const uint32_t deg_bit[DAC63204_COUNT] = { BRINGUP_DEG_DAC_U6, BRINGUP_DEG_DAC_U7 };
|
||||
_Static_assert(DAC63204_COUNT == 2, "deg_bit 跟着改");
|
||||
bool any_fail = false;
|
||||
for (int i = 0; i < DAC63204_COUNT; i++) {
|
||||
uint8_t addr = dac63204_all_addr[i];
|
||||
const hal_err_t err = i2cbus_xact(I2CBUS_CH_TRUNK, BRINGUP_LOCK_MS, dac_up_fn, &addr);
|
||||
if (err != HAL_OK) {
|
||||
r->deg |= deg_bit[i];
|
||||
lost(0, addr, err);
|
||||
any_fail = true;
|
||||
}
|
||||
}
|
||||
/* §10.10:DAC 不应答 **不关输出** —— 输出停在 NVM 默认 3.90 V,是安全值。
|
||||
影子也得说这句实话,否则判据拿一个「上次设过的值」去比,会把 3.90 判成偏离。 */
|
||||
if (any_fail) {
|
||||
rail_setpoint_reset();
|
||||
}
|
||||
}
|
||||
|
||||
static void pd_up(bringup_report_t *r)
|
||||
{
|
||||
/* ⚠️ CH224A 在哪条支路上,设计文档与实物**不一致**:驱动头写的是 mux 通道 5,
|
||||
而 2026-09-27 板 B 实测它在**主干**应答(0x22/0x23,PROGRESS 顶部)。
|
||||
两处都试:先主干(实测),不应答再试通道 5(设计)。
|
||||
选到通道 5 时主干照样看得见,所以这不会读到别的东西。 */
|
||||
uint32_t ma = 0;
|
||||
hal_err_t err = i2cbus_xact(I2CBUS_CH_TRUNK, BRINGUP_LOCK_MS, pd_fn, &ma);
|
||||
if (err != HAL_OK) {
|
||||
err = i2cbus_xact(TCA9548A_CH_PD, BRINGUP_LOCK_MS, pd_fn, &ma);
|
||||
}
|
||||
|
||||
uint8_t arg = 0;
|
||||
if (err != HAL_OK || ma == 0) {
|
||||
r->deg |= BRINGUP_DEG_PD;
|
||||
r->pd_ma = 0;
|
||||
r->pd_limit_mw = ch224a_power_limit_mw(BRINGUP_PD_FALLBACK_MA);
|
||||
arg = 1; /* 1 = 按保守值 */
|
||||
if (err != HAL_OK) {
|
||||
lost(0, CH224A_ADDR_DEFAULT, err);
|
||||
}
|
||||
} else {
|
||||
r->pd_ma = ma;
|
||||
r->pd_limit_mw = ch224a_power_limit_mw(ma);
|
||||
if (ma < BRINGUP_PD_FALLBACK_MA) {
|
||||
r->deg |= BRINGUP_DEG_PD_LOW;
|
||||
}
|
||||
}
|
||||
/* 不管读没读到都记一条:「这次上电按多少瓦跑」是排查降载的第一个问题。
|
||||
⚠️ 本包只**定**上限;逼近上限时按优先级降载(§8.3)还没人做(HANDOFF)。 */
|
||||
evlog_put(0, LOG_EV_PD_LIMIT, arg, (int32_t)(r->pd_limit_mw / 100u), (int32_t)ma);
|
||||
}
|
||||
|
||||
hal_err_t bringup_devices(const bringup_in_t *in, bringup_report_t *r)
|
||||
{
|
||||
assert(in != NULL && r != NULL);
|
||||
memset(r, 0, sizeof(*r));
|
||||
|
||||
/* ① 五根 `EFUSE_EN` 配成输出并拉低。**先于一切外设**:DAC / eFuse 一上电就有
|
||||
输出,这五根线这时必须已经确定是低的。VBUS 此刻靠硬件关着(PCA9555 复位后
|
||||
全是输入 + R701 下拉),真正「固件说了算」要等下面的 `slot_init()`。 */
|
||||
hal_err_t err = rail_en_init();
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Hub 越早复位越好:它会让 USB 重新枚举,期间日志是丢的。 */
|
||||
if (in->power_on_reset) {
|
||||
hub_pulse();
|
||||
}
|
||||
|
||||
/* 锁必须早于任何一个用总线的人(R2 之后全系统只剩这一把)。 */
|
||||
(void)hal_lock_init();
|
||||
const hal_i2c_cfg_t i2c = {
|
||||
.sda_gpio = BOARD_PIN_I2C_SDA,
|
||||
.scl_gpio = BOARD_PIN_I2C_SCL,
|
||||
.freq_hz = 400000,
|
||||
};
|
||||
err = hal_i2c_init(&i2c);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
const bool mux_ok = mux_up(r);
|
||||
|
||||
/* `PRSNT#`:失败 = 引脚号非法 = 固件写错了,而且降级方向是不安全的那一侧
|
||||
(`slot_present()` 读不到时返回「在位」⇒ 空槽也会被允许上电)。 */
|
||||
err = slot_prsnt_init();
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ⭐⭐ 底板输入侧 U13(0x43)的 IN+/IN− **在板上是对调接的**(计划 §5.9)。
|
||||
漏了这一行,正常供电时它读回来的电流是负的,被当成反灌 ——
|
||||
`test_bringup.c` 的第一条用例就是它。纯软件状态,不碰总线。 */
|
||||
ina226_set_reversed(INA226_ADDR_INPUT, true);
|
||||
|
||||
/* ② 主干:两片 DAC、输入侧 INA226、CH224A。
|
||||
⚠️ **mux 失联时主干也够不着**:`i2cbus_xact(TRUNK)` 要先给 mux 写「全关」,
|
||||
那一步失败 fn 就不会被调(i2cbus.h 规矩 2)。物理上主干器件还在,但这一层
|
||||
没有「绕过 mux」的路 —— 也不该有,否则通道影子就不可信了。
|
||||
这时五路反正一路都不开(§5.8),所以不去试、不刷三条误导性的 I2C_LOST,
|
||||
只把状态说成实话:DAC 停在 NVM 默认、功率按保守值。 */
|
||||
if (mux_ok) {
|
||||
dac_up(in, r);
|
||||
uint8_t addr = INA226_ADDR_INPUT;
|
||||
const hal_err_t e = i2cbus_xact(I2CBUS_CH_TRUNK, BRINGUP_LOCK_MS, ina_up_fn, &addr);
|
||||
if (e != HAL_OK) {
|
||||
r->deg |= BRINGUP_DEG_INPUT_INA;
|
||||
lost(0, addr, e);
|
||||
}
|
||||
pd_up(r);
|
||||
} else {
|
||||
rail_setpoint_reset();
|
||||
r->pd_limit_mw = ch224a_power_limit_mw(BRINGUP_PD_FALLBACK_MA);
|
||||
}
|
||||
|
||||
/* ③ 五片刀片。空槽不去碰(只会白等 NAK);mux 失联时一路也够不着。 */
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
if (!slot_present(rail)) {
|
||||
r->empty |= bit_of(rail);
|
||||
continue;
|
||||
}
|
||||
if (!mux_ok) {
|
||||
continue; /* 整板级已经记过 MUX_LOST,不再逐路刷十条 I2C_LOST */
|
||||
}
|
||||
/* PCA9555:驱动保证「输出锁存器先清零、方向最后改」(计划 §5.3)。
|
||||
这一步之后 VBUS 才真的由固件说了算。 */
|
||||
hal_err_t e = slot_init(rail);
|
||||
if (e != HAL_OK) {
|
||||
r->io_lost |= bit_of(rail);
|
||||
lost(rail, PCA9555_ADDR, e);
|
||||
}
|
||||
uint8_t addr = rail_cfg(rail)->ina_addr;
|
||||
e = i2cbus_xact(slot_mux_ch(rail), BRINGUP_LOCK_MS, ina_up_fn, &addr);
|
||||
if (e != HAL_OK) {
|
||||
r->blind |= bit_of(rail);
|
||||
lost(rail, addr, e);
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* ---- 第二段 -------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
int rail;
|
||||
pca9555_in_t in;
|
||||
ina226_vi_t vi;
|
||||
} probe_t;
|
||||
|
||||
static hal_err_t read_io_fn(void *arg)
|
||||
{
|
||||
probe_t *p = (probe_t *)arg;
|
||||
return slot_read_inputs_locked(p->rail, &p->in);
|
||||
}
|
||||
|
||||
static hal_err_t read_vi_fn(void *arg)
|
||||
{
|
||||
probe_t *p = (probe_t *)arg;
|
||||
return ina226_read_vi(rail_cfg(p->rail)->ina_addr, &p->vi);
|
||||
}
|
||||
|
||||
typedef enum { PG_WAIT, PG_OK, PG_BLIND } pg_t;
|
||||
|
||||
/* 这一路的电源好了没有。优先看 PCA9555 的 PGOOD;读不到就退到 INA226 的电压
|
||||
(§10.10 PCA9555 那一行「改用 INA226 的电压判据兜底」);两样都读不到 = 看不见。 */
|
||||
static pg_t pg_of(int rail, const bringup_report_t *r, uint8_t *arg)
|
||||
{
|
||||
probe_t p = { .rail = rail };
|
||||
if ((r->io_lost & bit_of(rail)) == 0 &&
|
||||
i2cbus_xact(slot_mux_ch(rail), BRINGUP_LOCK_MS, read_io_fn, &p) == HAL_OK) {
|
||||
*arg = pca9555_efuse_fault(&p.in) ? BRINGUP_ARG_FLT : 0u;
|
||||
return pca9555_efuse_pg(&p.in) ? PG_OK : PG_WAIT;
|
||||
}
|
||||
*arg = BRINGUP_ARG_BY_VOLT;
|
||||
if ((r->blind & bit_of(rail)) == 0 &&
|
||||
i2cbus_xact(slot_mux_ch(rail), BRINGUP_LOCK_MS, read_vi_fn, &p) == HAL_OK) {
|
||||
return (p.vi.bus_mv >= rail_setpoint_mv(rail) - BRINGUP_PG_VOLT_MARGIN_MV) ? PG_OK
|
||||
: PG_WAIT;
|
||||
}
|
||||
return PG_BLIND;
|
||||
}
|
||||
|
||||
uint8_t bringup_want_update(uint8_t want, int rail, bool on)
|
||||
{
|
||||
if (rail < 1 || rail > PHONERAIL_RAIL_COUNT) {
|
||||
return want;
|
||||
}
|
||||
return on ? (uint8_t)(want | bit_of(rail)) : (uint8_t)(want & ~bit_of(rail));
|
||||
}
|
||||
|
||||
void bringup_power_restore(uint8_t want, bringup_report_t *r)
|
||||
{
|
||||
assert(r != NULL);
|
||||
const uint8_t all = (uint8_t)((1u << PHONERAIL_RAIL_COUNT) - 1u);
|
||||
want &= all;
|
||||
r->want = want;
|
||||
r->on = 0;
|
||||
r->pg_fail = 0;
|
||||
r->unverified = 0;
|
||||
|
||||
/* 能开的:想开、在位、mux 在。空槽且想开 = 刀片被拔走了(或 PRSNT# 虚接),记一条。 */
|
||||
uint8_t todo = 0;
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
if ((want & bit_of(rail)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if ((r->empty & bit_of(rail)) != 0) {
|
||||
evlog_put((uint8_t)rail, LOG_EV_SLOT_EMPTY, 0, 0, 0);
|
||||
continue;
|
||||
}
|
||||
if ((r->deg & BRINGUP_DEG_MUX) != 0) {
|
||||
continue; /* §5.8:mux 失联时五路电压电流全看不见 ⇒ 一路也不开 */
|
||||
}
|
||||
todo |= bit_of(rail);
|
||||
}
|
||||
|
||||
uint8_t pending = 0;
|
||||
uint32_t last_on = 0;
|
||||
bool any_on = false;
|
||||
int next = 1;
|
||||
|
||||
while (todo != 0 || pending != 0) {
|
||||
const uint32_t now = hal_time_now_ms();
|
||||
|
||||
/* §10.9 第 5 步:到点了就开下一路。**按上一路 EN 抬高的时刻算**,
|
||||
不按「上一路 PG 来了没有」算 —— 后者会让一路慢的拖住后面四路。 */
|
||||
if (todo != 0 && (!any_on || hal_time_elapsed_ms(last_on) >= BRINGUP_STAGGER_MS)) {
|
||||
while ((todo & bit_of(next)) == 0) {
|
||||
next++;
|
||||
}
|
||||
const int rail = next;
|
||||
todo &= (uint8_t)~bit_of(rail);
|
||||
if (rail_efuse_enable(rail, true) == HAL_OK) {
|
||||
r->on_ms[rail - 1] = now;
|
||||
r->on |= bit_of(rail);
|
||||
pending |= bit_of(rail);
|
||||
last_on = now;
|
||||
any_on = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* 第 6 步:每一路各自的 500 ms。 */
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
if ((pending & bit_of(rail)) == 0) {
|
||||
continue;
|
||||
}
|
||||
uint8_t arg = 0;
|
||||
const pg_t pg = pg_of(rail, r, &arg);
|
||||
if (pg == PG_OK) {
|
||||
pending &= (uint8_t)~bit_of(rail);
|
||||
} else if (pg == PG_BLIND) {
|
||||
/* 看不见 ≠ 坏了(`ACT_RES_NO_PG` 同一条理由):开着,报「确认不了」。 */
|
||||
pending &= (uint8_t)~bit_of(rail);
|
||||
r->unverified |= bit_of(rail);
|
||||
} else if (hal_time_elapsed_ms(r->on_ms[rail - 1]) >= BRINGUP_PG_TIMEOUT_MS) {
|
||||
/* 等不到:关回去(§10.9 原文)。VBUS 在这一段里从没开过,只关 eFuse。 */
|
||||
(void)rail_efuse_enable(rail, false);
|
||||
pending &= (uint8_t)~bit_of(rail);
|
||||
r->on &= (uint8_t)~bit_of(rail);
|
||||
r->pg_fail |= bit_of(rail);
|
||||
evlog_put((uint8_t)rail, LOG_EV_PG_TIMEOUT, arg, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (todo != 0 || pending != 0) {
|
||||
hal_time_delay_ms(BRINGUP_PG_POLL_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/* `bringup` —— 启动顺序 + 降级矩阵(A15,需求 §10.9 / §10.10)。
|
||||
|
||||
A14 之前这一段整个写在 `app_main.c` 里,而 `app_main.c` 只进 IDF 目标 ——
|
||||
于是像 `ina226_set_reversed(INA226_ADDR_INPUT, true)` 这种「漏了就把正常供电
|
||||
判成反灌」的单行,**唯一的拦网是人眼**(计划 A15 那一行的 ⭐)。
|
||||
现在整段都在这里,宿主机上逐个「拔掉」器件跑得到。
|
||||
|
||||
---- 分两段,中间让 `t_safety` 起来 --------------------------------------
|
||||
|
||||
bringup_devices() §10.9 第 1~4 步:引脚安全态 → 总线 → 认件 / 初始化
|
||||
(缺件按 §10.10 降级,**不 abort**)
|
||||
── app_main 在这里 spawn(TASK_SAFETY) ──
|
||||
bringup_power_restore() §10.9 第 5~6 步:按上次的意图逐路上电、错开 100 ms、等 PG
|
||||
|
||||
拆开的理由:上电那一刻巡检必须已经在跑。§10.9 原文把「起链路任务」放在第 7 步,
|
||||
但没说巡检什么时候起;若等五路全开完(最坏 0.4 s 错开 + 0.5 s 等 PG)才起,
|
||||
这将近一秒里 SR-5 / SW-8 是瞎的。两段之间锁是通的(一切都经 `i2cbus_xact`),
|
||||
所以巡检与上电序列并发没有问题。
|
||||
|
||||
---- 分界线:什么 abort、什么降级 -----------------------------------------
|
||||
与 `selftest_or_die()` 同一条线:**固件自己错了 = 返回错误码(app_main abort),
|
||||
器件不在 = 降级、记事件、接着起**。前者只有三种:EN 脚配不上、I²C 外设起不来、
|
||||
`PRSNT#` 脚配不上 —— 全是引脚号 / 配置常量的问题,换块板子也一样。
|
||||
|
||||
---- §10.10 各行在这里怎么落地 -----------------------------------------
|
||||
某片 INA226 无应答 → 该路照常上电,`blind` 置位,记 I2C_LOST;
|
||||
判据自己会因为 `rail_ok` 为假而不武装(t_safety.c 的三道闸)
|
||||
DAC 无应答 → 不关输出,设定值影子回到 NVM 默认(3.90 V),记 I2C_LOST
|
||||
某片 PCA9555 无应答 → 不关输出(照常上电),`io_lost` 置位,记 I2C_LOST;
|
||||
等 PG 改按 INA226 电压判
|
||||
mux 连续选不中 → 先脉冲一次 `MUX_RST#`;仍不通 ⇒ 五路一路都不上电,记 MUX_LOST
|
||||
`PRSNT#` 为高 → 不初始化、不上电;只在「上次是开着的」时记 SLOT_EMPTY
|
||||
CH224A 读不到 / 偏低 → 不关输出,按读到的档位(读不到按 60 W)定上限,记 PD_LIMIT
|
||||
看门狗复位 → app_main 把复位原因填进 BOOT 事件的 arg(evlog_init_ex)
|
||||
I²C 总线挂死 → ⚠️ **本包没做**(9 个时钟的总线恢复在 HAL 层,见 HANDOFF) */
|
||||
#ifndef BRINGUP_H
|
||||
#define BRINGUP_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
#include "phonerail.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* §10.9 第 5 步:路与路之间错开多久。单路缓启动涌流 0.8 A,五路同开是 4 A 叠在
|
||||
12 V 输入上;错开 100 ms 后各路 11.5 ms 的斜坡完全不重叠。**是必须,不是讲究**。 */
|
||||
#define BRINGUP_STAGGER_MS 100
|
||||
|
||||
/* §10.9 第 6 步:开了电之后等 PG 最多等多久。与硬重启的 `ACT_HR_PG_TIMEOUT_MS`
|
||||
同一个数(PG 在斜坡中段就该来,~30 倍余量)。⚠️ 与硬重启**处理不同**:
|
||||
硬重启等不到 PG 不断电(那是运行中的手机),启动时等不到就关回去(§10.9 原文)。
|
||||
—— 但「PCA9555 读不到」不算「等不到」,那是看不见,见 `BRINGUP_ARG_BY_VOLT`。 */
|
||||
#define BRINGUP_PG_TIMEOUT_MS 500
|
||||
|
||||
/* 等 PG 期间多久看一眼。ESP 上 `hal_time_delay_ms()` 至少一个 tick(10 ms),
|
||||
所以实际是 10 ms;错开的 100 ms 因此会落在 100~110 ms 之间。 */
|
||||
#define BRINGUP_PG_POLL_MS 5
|
||||
|
||||
/* PCA9555 读不到时的 PG 替身:INA226 母线电压到了「设定值 − 这个数」就算好了。
|
||||
与 SR-5 的 ±100 mV 同一个数 —— 到了这个电压判据就不会再判它欠压。 */
|
||||
#define BRINGUP_PG_VOLT_MARGIN_MV 100
|
||||
|
||||
/* CH224A 读不到 / 读到 0 时按这个电流算上限(20 V × 3 A = 60 W,§10.10「最保守的 60 W」)。 */
|
||||
#define BRINGUP_PD_FALLBACK_MA 3000
|
||||
|
||||
/* `LOG_EV_PG_TIMEOUT` 的 arg 位。 */
|
||||
#define BRINGUP_ARG_FLT 0x01u /* 超时那一刻 FLT# 在报 */
|
||||
#define BRINGUP_ARG_BY_VOLT 0x02u /* 这一路 PCA9555 读不到,是按 INA226 电压判的 */
|
||||
|
||||
/* 整板级降级位(`bringup_report_t.deg`)。 */
|
||||
typedef enum {
|
||||
BRINGUP_DEG_MUX = 1u << 0, /* mux 复位后仍选不中:五路不上电 */
|
||||
BRINGUP_DEG_DAC_U6 = 1u << 1, /* 0x4A 不应答:CH1/2/4 停在 NVM 默认 */
|
||||
BRINGUP_DEG_DAC_U7 = 1u << 2, /* 0x4B 不应答:CH3/5 停在 NVM 默认 */
|
||||
BRINGUP_DEG_DAC_NVM = 1u << 3, /* 上电复位时读回的码不是 3.90 V(NVM 被改过) */
|
||||
BRINGUP_DEG_INPUT_INA = 1u << 4, /* 输入侧 0x43 不应答:整板功率看不见 */
|
||||
BRINGUP_DEG_PD = 1u << 5, /* CH224A 读不到:按 60 W */
|
||||
BRINGUP_DEG_PD_LOW = 1u << 6, /* CH224A 读到了,但低于 60 W */
|
||||
} bringup_deg_t;
|
||||
|
||||
typedef struct {
|
||||
/* 这次复位是不是上电复位。只有它为真才脉冲 `HUB_RST#`(app_main 的那一大段理由:
|
||||
其余复位原因下脉冲 = 自己把自己的 USB 控制台拔了),也只有它为真才核对
|
||||
DAC 的 NVM 默认值(热重启时 DAC 没断过电,里面是上一次命令设的值)。 */
|
||||
bool power_on_reset;
|
||||
} bringup_in_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t deg; /* bringup_deg_t 的或 */
|
||||
|
||||
/* 每路一位,bit0 = CH1。 */
|
||||
uint8_t empty; /* `PRSNT#` 高:刀片不在位 */
|
||||
uint8_t blind; /* 刀片 INA226 不应答:失去遥测、判据不武装(「盲跑」) */
|
||||
uint8_t io_lost; /* 刀片 PCA9555 不应答:FAULT/PG 看不见,VBUS 关不掉 */
|
||||
|
||||
/* 第二段填。 */
|
||||
uint8_t want; /* 请求恢复的(上次的意图) */
|
||||
uint8_t on; /* 最后真的开着的 */
|
||||
uint8_t pg_fail; /* 等不到 PG、已关回去 */
|
||||
uint8_t unverified; /* PCA9555 与 INA226 都读不到:开着,但确认不了 */
|
||||
uint32_t on_ms[PHONERAIL_RAIL_COUNT]; /* 每路 EN 抬高的时刻(没开是 0) */
|
||||
|
||||
uint32_t pd_ma; /* CH224A 读到的档位电流(读不到是 0) */
|
||||
uint32_t pd_limit_mw; /* 整板功率上限(读不到按 60 W) */
|
||||
uint16_t dac_nvm_mv[PHONERAIL_RAIL_COUNT]; /* 上电复位时读回的每路设定(没读是 0) */
|
||||
} bringup_report_t;
|
||||
|
||||
/* §10.9 第 1~4 步。返回非 HAL_OK **只**表示固件 / 引脚配置本身错了(见文件头),
|
||||
调用方应当 abort;器件缺失一律 HAL_OK + `r` 里的降级位 + 事件。
|
||||
⚠️ 调之前 `evlog_init_ex()` 要已经跑过:这里产生的事件要排在 BOOT 那一条之后。 */
|
||||
hal_err_t bringup_devices(const bringup_in_t *in, bringup_report_t *r);
|
||||
|
||||
/* §10.9 第 5~6 步。`want` 是上次的每路意图(bit0 = CH1,来自 NVS;新板是 0 = 全不开)。
|
||||
逐路:能开的开、错开 `BRINGUP_STAGGER_MS`、各自等 PG(`BRINGUP_PG_TIMEOUT_MS`),
|
||||
等不到就关回去并记 `LOG_EV_PG_TIMEOUT`。阻塞,最坏约 0.4 + 0.5 s。
|
||||
不能开的(空槽 / mux 失联)直接跳过,空槽且想开的记一条 SLOT_EMPTY。 */
|
||||
void bringup_power_restore(uint8_t want, bringup_report_t *r);
|
||||
|
||||
/* 「意图」掩码的更新(`power` 命令改它,app_main 把结果存 NVS)。纯函数。 */
|
||||
uint8_t bringup_want_update(uint8_t want, int rail, bool on);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BRINGUP_H */
|
||||
@@ -102,9 +102,18 @@ typedef enum {
|
||||
/* 请求行的最大长度(含结尾 0)。超长的行由适配器整行丢弃(A13 的分帧规矩)。 */
|
||||
#define CMD_LINE_MAX 256
|
||||
|
||||
/* `power` 命令改了某一路的「意图」(A15)。启动时按这个意图逐路恢复(§10.9 第 4 步
|
||||
「上次的每路使能状态」),所以 app_main 在这里把它存进 NVS。
|
||||
⚠️ 只有 `power` 命令改意图:判据跳闸、硬重启、mux 失联全断都**不改** ——
|
||||
那些是事故,不是操作员的决定;重启之后按意图恢复,跳闸的那一路再由判据管。
|
||||
在执行器任务里同步调(可以写 flash,毫秒级,命令本来就稀)。 */
|
||||
typedef void (*cmd_intent_fn)(void *ctx, int rail, bool on);
|
||||
|
||||
typedef struct {
|
||||
const safety_snap_t *safety; /* NULL = status 里不报这一块 */
|
||||
const thermal_snap_t *thermal; /* NULL = 同上 */
|
||||
cmd_intent_fn intent; /* NULL = 不记意图(宿主机测试) */
|
||||
void *intent_ctx;
|
||||
} cmd_env_t;
|
||||
|
||||
/* 清判重窗口、记下两份快照的位置。幂等。
|
||||
|
||||
+17
-1
@@ -199,6 +199,15 @@ cmd_err_t do_vbus(ctx_t *c)
|
||||
return from_hal(slot_vbus_enable(c->rail, on));
|
||||
}
|
||||
|
||||
/* A15:意图出口(cmd.h 的 `cmd_intent_fn`)。 */
|
||||
static void note_intent(int rail, bool on)
|
||||
{
|
||||
const cmd_env_t *env = cmd_env();
|
||||
if (env != NULL && env->intent != NULL) {
|
||||
env->intent(env->intent_ctx, rail, on);
|
||||
}
|
||||
}
|
||||
|
||||
cmd_err_t do_power(ctx_t *c)
|
||||
{
|
||||
cmd_err_t e = need_rail(c);
|
||||
@@ -213,13 +222,20 @@ cmd_err_t do_power(ctx_t *c)
|
||||
return CMD_ERR_BUSY; /* 硬重启走到一半再开关一次是自作主张(t_actuator_abort 同理) */
|
||||
}
|
||||
if (on) {
|
||||
return from_hal(rail_efuse_enable(c->rail, true));
|
||||
const hal_err_t ee = rail_efuse_enable(c->rail, true);
|
||||
if (ee == HAL_OK) {
|
||||
note_intent(c->rail, true);
|
||||
}
|
||||
return from_hal(ee);
|
||||
}
|
||||
/* 关:与 `rail_all_off()` 同序 —— **先断 VBUS 再关 eFuse**。
|
||||
VBUS 走 I²C 可能断不掉,eFuse 是 GPIO 永远关得掉:前者失败也照样关后者,
|
||||
回应报 io,上位机据此知道「直供断了,VBUS 可能还通着」。 */
|
||||
const hal_err_t ev = slot_vbus_enable(c->rail, false);
|
||||
const hal_err_t ee = rail_efuse_enable(c->rail, false);
|
||||
if (ee == HAL_OK) {
|
||||
note_intent(c->rail, false); /* 直供确实断了:下次开机也别开它 */
|
||||
}
|
||||
return from_hal(ee != HAL_OK ? ee : ev);
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -437,7 +437,9 @@ static hal_err_t format_all(void)
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t evlog_init(void)
|
||||
hal_err_t evlog_init(void) { return evlog_init_ex(0); }
|
||||
|
||||
hal_err_t evlog_init_ex(uint8_t boot_arg)
|
||||
{
|
||||
/* RAM 侧无条件清干净:真板上这里刚上电,宿主机上这条让 `sim_hal_reboot()`
|
||||
之后的行为和真重启一致(statics 在进程里是活着的)。 */
|
||||
@@ -465,7 +467,7 @@ hal_err_t evlog_init(void)
|
||||
if (err != HAL_OK) {
|
||||
/* 没有分区。**不 abort、不影响入队**:日志是排查手段,不是安全链
|
||||
(evlog.h 上面那段)。事件照进 RAM,只是没人能把它们写下去。 */
|
||||
evlog_put(0, LOG_EV_BOOT, 0, 0, 0);
|
||||
evlog_put(0, LOG_EV_BOOT, boot_arg, 0, 0);
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -474,7 +476,7 @@ hal_err_t evlog_init(void)
|
||||
if (g.sector_bytes < 2u * sizeof(evlog_rec_t) ||
|
||||
(g.sector_bytes % sizeof(evlog_rec_t)) != 0u || g.sectors < 2u) {
|
||||
s_have_store = false;
|
||||
evlog_put(0, LOG_EV_BOOT, 0, 0, 0);
|
||||
evlog_put(0, LOG_EV_BOOT, boot_arg, 0, 0);
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
s_sector_bytes = g.sector_bytes;
|
||||
@@ -525,7 +527,7 @@ hal_err_t evlog_init(void)
|
||||
|
||||
/* §10.7「上电从 0 计」⇒ 时间戳会跳回去。没有这一条的话,重启前后的记录
|
||||
在时间轴上互相穿插,而日志里看不出重启发生过。 */
|
||||
evlog_put(0, LOG_EV_BOOT, 0, 0, 0);
|
||||
evlog_put(0, LOG_EV_BOOT, boot_arg, 0, 0);
|
||||
return s_have_store ? HAL_OK : HAL_ERR_STATE;
|
||||
}
|
||||
|
||||
@@ -569,6 +571,7 @@ const char *evlog_ev_str(log_ev_t ev)
|
||||
case LOG_EV_MUX_LOST: return "mux失联全断";
|
||||
case LOG_EV_SLOT_EMPTY: return "刀片不在位";
|
||||
case LOG_EV_PD_LIMIT: return "PD限功率";
|
||||
case LOG_EV_PG_TIMEOUT: return "上电等不到PG";
|
||||
case LOG_EV_COUNT: break;
|
||||
}
|
||||
return "?";
|
||||
|
||||
@@ -72,6 +72,11 @@ typedef enum {
|
||||
LOG_EV_SLOT_EMPTY = 15,/* `PRSNT#` 为高:刀片不在位,这一路不许带电 */
|
||||
LOG_EV_PD_LIMIT = 16, /* PD 档位换了 / 触发限功率降载,bus_mv = 上限 mW/100(R-6) */
|
||||
|
||||
/* A15 新增(需求 §10.9 第 6 步「等各路 PG,超时则记事件并保持该路关闭」)。
|
||||
与 `PG_CHANGE`(8)不是一回事:那是「PG 变了」,这是「开了电、PG 一直没来、已经关回去」。
|
||||
arg:bit0 = 超时那一刻 FLT# 在报;bit1 = 这一片 PCA9555 读不到,改按 INA226 电压判的。 */
|
||||
LOG_EV_PG_TIMEOUT = 17,
|
||||
|
||||
LOG_EV_COUNT,
|
||||
} log_ev_t;
|
||||
|
||||
@@ -123,6 +128,11 @@ typedef struct {
|
||||
这种情况下入队照常工作(事件留在 RAM 里),只是落不了盘。 */
|
||||
hal_err_t evlog_init(void);
|
||||
|
||||
/* 同上,只是 `LOG_EV_BOOT` 那一条的 arg 由调用方给(A15:ESP 上填 `esp_reset_reason()`,
|
||||
§10.10「看门狗复位 … 记一条复位事件」就是这一条 —— 不另开事件号,
|
||||
因为「这次上电是怎么来的」本来就是 BOOT 的属性)。`evlog_init()` = `evlog_init_ex(0)`。 */
|
||||
hal_err_t evlog_init_ex(uint8_t boot_arg);
|
||||
|
||||
/* 入队一条。**不阻塞、不碰 flash、不会失败** —— 缓冲满了就盖掉最旧的一条
|
||||
并记一次 `dropped`(下一次落盘时会补一条 LOST)。
|
||||
任何任务、任何优先级都可以调,包括 t_safety 断电之后的那一下。 */
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a14"
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a15"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §2。 */
|
||||
#define PHONERAIL_RAIL_COUNT 5
|
||||
|
||||
+17
-12
@@ -46,23 +46,28 @@ int32_t rail_code_to_vd_uv(uint16_t code)
|
||||
return dac63204_code_to_uv(code);
|
||||
}
|
||||
|
||||
hal_err_t rail_volt_init_one(uint8_t addr)
|
||||
{
|
||||
hal_err_t err = dac63204_probe(addr);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
/* init 自己就把用到的通道写成 safe 了(上电之前写,见 dac63204_init 的 ③)。 */
|
||||
return dac63204_init(addr, rail_mv_to_code(RAIL_MV_SAFE));
|
||||
}
|
||||
|
||||
hal_err_t rail_volt_init(void)
|
||||
{
|
||||
const uint16_t safe = rail_mv_to_code(RAIL_MV_SAFE);
|
||||
|
||||
/* ⚠️ 一片失败**不放弃另一片**(A15):U6 不应答时 U7 那两路照样该被初始化,
|
||||
而不是也停在 NVM 默认值上 —— 那样一片坏件就「降级」了五路。 */
|
||||
hal_err_t first = HAL_OK;
|
||||
for (int i = 0; i < DAC63204_COUNT; i++) {
|
||||
uint8_t addr = dac63204_all_addr[i];
|
||||
hal_err_t err = dac63204_probe(addr);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
/* init 自己就把用到的通道写成 safe 了(上电之前写,见 dac63204_init 的 ③)。 */
|
||||
err = dac63204_init(addr, safe);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
const hal_err_t err = rail_volt_init_one(dac63204_all_addr[i]);
|
||||
if (err != HAL_OK && first == HAL_OK) {
|
||||
first = err;
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
return first;
|
||||
}
|
||||
|
||||
/* 五路设定值的影子(契约见 rail.h)。初值就是 DAC 的 NVM 出厂默认,
|
||||
|
||||
+5
-1
@@ -57,9 +57,13 @@ int32_t rail_code_to_vd_uv(uint16_t code); /* 码 → DAC 自己的输出 Vd(
|
||||
/* ---- 器件操作 ------------------------------------------------------------ */
|
||||
|
||||
/* 认件 + 初始化两片 DAC,并把五路都置到 RAIL_MV_SAFE。
|
||||
任何一片失败就返回它的错误码(降级矩阵在 A15 处理)。 */
|
||||
两片都会试;返回第一个错误码(降级矩阵在 bringup.c)。 */
|
||||
hal_err_t rail_volt_init(void);
|
||||
|
||||
/* 只做其中一片(`dac63204_all_addr[]` 里的地址)。A15 的启动序列按片调它,
|
||||
好分别记「哪一片没应答」(§10.10 DAC 那一行)。两片的失败互不影响。 */
|
||||
hal_err_t rail_volt_init_one(uint8_t addr);
|
||||
|
||||
/* rail 是 **1~5**(不是 0~4),越界 → HAL_ERR_ARG。
|
||||
mv > 4010 → HAL_ERR_ARG(SR-13:越界即拒,让操作者早点知道自己写错了)。
|
||||
mv < 3524 → HAL_ERR_ARG(DAC 到不了)。
|
||||
|
||||
@@ -0,0 +1,597 @@
|
||||
/* A15:启动顺序 + 降级矩阵(需求 §10.9 / §10.10,计划 A15 那一行)。
|
||||
|
||||
验收原话:「逐个『拔掉』器件,行为与 §10.10 的表逐行对上;五路上电错开 100 ms;
|
||||
`HUB_RST` 只脉冲一次」,外加 ⭐ 第一条用例必须是 `ina226_set_reversed()` 那一行。
|
||||
|
||||
---- 这块假板子和别的测试有什么不一样 ------------------------------------
|
||||
· **PGOOD 跟着 EN 走**:`sim_gpio_set_hook()` 让某一路 `EFUSE_EN` 抬高的那一刻,
|
||||
那一片 PCA9555 的 P1.3 跟着变高。没有这一条,「等 PG」要么永远等到(PG 静态拉高,
|
||||
测不出超时),要么永远等不到。
|
||||
· 时刻一律从 `sim_gpio_rise_us()` 取(线上真实的边),**不信** `bringup_report_t`
|
||||
自己报的 `on_ms` —— 那是自己证明自己。
|
||||
· 事件从 flash 读回来数(evlog 的真路径),不在被测代码里插探针。 */
|
||||
#include <string.h>
|
||||
|
||||
#include "board_pins.h"
|
||||
#include "bringup.h"
|
||||
#include "ch224a.h"
|
||||
#include "dac63204.h"
|
||||
#include "evlog.h"
|
||||
#include "hal_gpio.h"
|
||||
#include "i2cbus.h"
|
||||
#include "ina226.h"
|
||||
#include "pca9555.h"
|
||||
#include "rail.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "sim_hal.h"
|
||||
#include "slot.h"
|
||||
#include "tca9548a.h"
|
||||
#include "unity.h"
|
||||
|
||||
#define N PHONERAIL_RAIL_COUNT
|
||||
#define ALL ((uint8_t)((1u << N) - 1u))
|
||||
#define BIT(rail) ((uint8_t)(1u << ((rail) - 1)))
|
||||
|
||||
/* Port1 静息:FLT# 高(无故障)、Buck PG 高、ALERT 高、**eFuse PG 低**(还没上电)。 */
|
||||
#define P1_IDLE ((uint8_t)(0x0Fu & ~(1u << PCA9555_BIT_EFUSE_PG)))
|
||||
#define P1_PG ((uint8_t)0x0Fu)
|
||||
|
||||
static bool g_pg_follows[N]; /* false = 这一路的 PG 永远不来 */
|
||||
static bool g_flt[N]; /* true = 这一路的 FLT# 在报 */
|
||||
static bool g_pca_gone[N]; /* 这一片被拔了:钩子别再去驱它 */
|
||||
|
||||
static bringup_report_t g_r;
|
||||
static bool g_pd_on_trunk = true; /* false = board() 不在主干上挂 CH224A */
|
||||
|
||||
static int ch_of(int rail) { return (int)rail_cfg(rail)->mux_ch; }
|
||||
|
||||
/* 外部世界:EN 一抬高 PG 就来(真板上是 ~17 ms 之后;这里早来晚来只改变「等了几圈」)。 */
|
||||
static void world(int pin, int level)
|
||||
{
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
if (pin != rail_cfg(rail)->efuse_en || g_pca_gone[rail - 1]) {
|
||||
continue;
|
||||
}
|
||||
uint8_t v = (level != 0 && g_pg_follows[rail - 1]) ? P1_PG : P1_IDLE;
|
||||
if (g_flt[rail - 1]) {
|
||||
v &= (uint8_t)~(1u << PCA9555_BIT_EFUSE_FLT);
|
||||
}
|
||||
sim_pca9555_drive(rail - 1, 1, v);
|
||||
}
|
||||
}
|
||||
|
||||
static void put_ina_ids(int ch, uint8_t addr)
|
||||
{
|
||||
if (ch < 0) {
|
||||
sim_i2c_set_reg(addr, INA226_REG_MFG_ID, INA226_MFG_ID);
|
||||
sim_i2c_set_reg(addr, INA226_REG_DIE_ID, INA226_DIE_ID);
|
||||
} else {
|
||||
sim_i2c_set_reg_ch(ch, addr, INA226_REG_MFG_ID, INA226_MFG_ID);
|
||||
sim_i2c_set_reg_ch(ch, addr, INA226_REG_DIE_ID, INA226_DIE_ID);
|
||||
}
|
||||
}
|
||||
|
||||
/* 满配、五槽全插的假板子。各用例在 `bringup_devices()` 之前「拔掉」自己关心的那一个。 */
|
||||
static void board(void)
|
||||
{
|
||||
sim_i2c_add_mux(TCA9548A_ADDR, BOARD_PIN_MUX_RST_N);
|
||||
|
||||
for (int i = 0; i < DAC63204_COUNT; i++) {
|
||||
const uint8_t a = dac63204_all_addr[i];
|
||||
sim_i2c_add(a, 2);
|
||||
sim_i2c_set_reg(a, DAC63204_REG_GENERAL_STATUS, (uint16_t)(DAC63204_DEVICE_ID << 2));
|
||||
sim_i2c_set_reg(a, DAC63204_REG_COMMON_CONFIG, 0x0FFF);
|
||||
}
|
||||
/* NVM 默认:五路都是 3.90 V(上电复位时读回来比对的就是它)。 */
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
sim_i2c_set_reg(dac63204_rail_addr(rail), DAC63204_REG_DATA(dac63204_rail_ch(rail)),
|
||||
DAC63204_CODE_TO_REG(rail_mv_to_code(RAIL_MV_SAFE)));
|
||||
}
|
||||
|
||||
sim_i2c_add(INA226_ADDR_INPUT, 2);
|
||||
put_ina_ids(-1, INA226_ADDR_INPUT);
|
||||
sim_i2c_set_reg(INA226_ADDR_INPUT, INA226_REG_BUS_V, (uint16_t)(12000 * 4 / 5));
|
||||
|
||||
/* CH224A:实测在主干(PROGRESS 顶部),20 V / 3 A 档 = 60 W。 */
|
||||
if (g_pd_on_trunk) {
|
||||
sim_i2c_add(CH224A_ADDR_DEFAULT, 1);
|
||||
sim_i2c_set_reg(CH224A_ADDR_DEFAULT, CH224A_REG_CURRENT, 3000 / 50);
|
||||
}
|
||||
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
const int ch = ch_of(rail);
|
||||
sim_i2c_add_ch(ch, rail_cfg(rail)->ina_addr, 2);
|
||||
put_ina_ids(ch, rail_cfg(rail)->ina_addr);
|
||||
sim_pca9555_attach_ch(rail - 1, ch, PCA9555_ADDR);
|
||||
sim_pca9555_drive(rail - 1, 1, P1_IDLE);
|
||||
sim_gpio_drive(rail_cfg(rail)->prsnt_n, 0); /* 插着:刀片把 PRSNT# 短到地 */
|
||||
g_pg_follows[rail - 1] = true;
|
||||
g_flt[rail - 1] = false;
|
||||
g_pca_gone[rail - 1] = false;
|
||||
}
|
||||
sim_gpio_set_hook(world);
|
||||
}
|
||||
|
||||
static void pull_slot(int rail) /* 整片刀片拔走:PRSNT# 高 + 两片器件都不在 */
|
||||
{
|
||||
sim_gpio_drive(rail_cfg(rail)->prsnt_n, 1);
|
||||
sim_i2c_set_present_ch(ch_of(rail), rail_cfg(rail)->ina_addr, false);
|
||||
sim_i2c_set_present_ch(ch_of(rail), PCA9555_ADDR, false);
|
||||
g_pca_gone[rail - 1] = true;
|
||||
}
|
||||
|
||||
static void devices(bool por)
|
||||
{
|
||||
const bringup_in_t in = { .power_on_reset = por };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, bringup_devices(&in, &g_r));
|
||||
}
|
||||
|
||||
/* 某类事件(可限定路号,-1 = 不限)在 flash 里有几条。 */
|
||||
static int count_ev(log_ev_t type, int rail)
|
||||
{
|
||||
(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) {
|
||||
break;
|
||||
}
|
||||
if (rec.type == (uint8_t)type && (rail < 0 || rec.rail == (uint8_t)rail)) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static bool last_ev(log_ev_t type, evlog_rec_t *out)
|
||||
{
|
||||
(void)evlog_flush(0);
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
bool hit = false;
|
||||
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)type) {
|
||||
*out = rec;
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
sim_hal_reset();
|
||||
pca9555_shadow_reset();
|
||||
rail_setpoint_reset();
|
||||
ina226_set_reversed(INA226_ADDR_INPUT, false); /* 进程里是活的:清掉上一条用例留下的 */
|
||||
board();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init_ex(0));
|
||||
}
|
||||
|
||||
void tearDown(void) { TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous()); }
|
||||
|
||||
/* ---- ⭐ 第一条:输入侧 INA226 的对调接 ------------------------------------ */
|
||||
|
||||
/* 计划 A15 的 ⭐:这一行以前只在 `app_main.c` 里,唯一的拦网是人眼。
|
||||
板上 U13 的 IN+/IN− 对调 ⇒ 正常供电时芯片自己读出的电流是**负**的;
|
||||
启动之后驱动必须把它翻回来,否则整板功率与反灌判据全拿的是反号。
|
||||
⚠️ 断的是「读出来的电流是正的」这个**后果**,不只是那个标志位。 */
|
||||
static void test_first_input_ina_is_marked_reversed(void)
|
||||
{
|
||||
/* 12 V × 2 A 正常供电,芯片按对调接读出 −2 A。 */
|
||||
sim_i2c_set_reg(INA226_ADDR_INPUT, INA226_REG_CURRENT, (uint16_t)(int16_t)(-2000000 / 500));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL));
|
||||
|
||||
ina226_vi_t vi;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_vi(INA226_ADDR_INPUT, &vi));
|
||||
TEST_ASSERT_TRUE_MESSAGE(vi.current_ua < 0, "前提:不登记对调时读出来是负的");
|
||||
|
||||
devices(true);
|
||||
|
||||
TEST_ASSERT_TRUE(ina226_is_reversed(INA226_ADDR_INPUT));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_vi(INA226_ADDR_INPUT, &vi));
|
||||
TEST_ASSERT_EQUAL_INT32(2000000, vi.current_ua);
|
||||
/* 刀片上那五片是正接的,别被一起翻了。 */
|
||||
TEST_ASSERT_FALSE(ina226_is_reversed(INA226_ADDR_RAIL));
|
||||
}
|
||||
|
||||
/* ---- 满配:什么都在 ------------------------------------------------------ */
|
||||
|
||||
/* 六片 INA226 都写了 Config / Calibration。A14 之前这一步**根本没做**
|
||||
(app_main 里那句「在那之前 t_safety 读回来的电流恒为 0」)。 */
|
||||
static void test_full_board_initialises_every_device(void)
|
||||
{
|
||||
devices(true);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(0, g_r.deg);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, g_r.empty | g_r.blind | g_r.io_lost);
|
||||
TEST_ASSERT_EQUAL_UINT16(INA226_CALIB, sim_i2c_get_reg(INA226_ADDR_INPUT, INA226_REG_CALIB));
|
||||
TEST_ASSERT_EQUAL_UINT16(INA226_CONFIG, sim_i2c_get_reg(INA226_ADDR_INPUT, INA226_REG_CONFIG));
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
TEST_ASSERT_EQUAL_UINT16(INA226_CALIB, sim_i2c_get_reg_ch(ch_of(rail), INA226_ADDR_RAIL,
|
||||
INA226_REG_CALIB));
|
||||
/* PCA9555 按板级方向字配好,锁存器全 0(键全松、VBUS 断)。 */
|
||||
TEST_ASSERT_EQUAL_UINT8(PCA9555_CONFIG0_INIT, sim_pca9555_config(rail - 1, 0));
|
||||
TEST_ASSERT_EQUAL_UINT8(0, sim_pca9555_output(rail - 1, 0));
|
||||
TEST_ASSERT_EQUAL_UINT16(RAIL_MV_SAFE, (uint16_t)g_r.dac_nvm_mv[rail - 1]);
|
||||
/* 第一段一路电都不上。 */
|
||||
TEST_ASSERT_FALSE(rail_efuse_is_on(rail));
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(rail)->efuse_en));
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_unsafe_enable());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
|
||||
TEST_ASSERT_EQUAL_UINT32(60000, g_r.pd_limit_mw);
|
||||
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_I2C_LOST, -1));
|
||||
TEST_ASSERT_EQUAL_INT(1, count_ev(LOG_EV_PD_LIMIT, 0));
|
||||
}
|
||||
|
||||
/* ⭐ §10.9 第 5 步:五路上电**错开 100 ms**,按 CH1→CH5 的顺序。
|
||||
时刻取线上的边,不取被测代码自报的 `on_ms`。上界 = 100 ms + 一次轮询 + 总线时间。 */
|
||||
static void test_five_rails_come_up_100ms_apart(void)
|
||||
{
|
||||
devices(true);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, g_r.pg_fail | g_r.unverified);
|
||||
for (int rail = 2; rail <= N; rail++) {
|
||||
const uint64_t a = sim_gpio_rise_us(rail_cfg(rail - 1)->efuse_en);
|
||||
const uint64_t b = sim_gpio_rise_us(rail_cfg(rail)->efuse_en);
|
||||
TEST_ASSERT_TRUE_MESSAGE(a != 0 && b > a, "后一路必须在前一路之后抬高");
|
||||
TEST_ASSERT_TRUE_MESSAGE(b - a >= BRINGUP_STAGGER_MS * 1000u, "错开不足 100 ms");
|
||||
TEST_ASSERT_TRUE_MESSAGE(b - a < (BRINGUP_STAGGER_MS + BRINGUP_PG_POLL_MS + 2u) * 1000u,
|
||||
"错开拖得太久:前一路的 PG 等待不该拖住后一路");
|
||||
TEST_ASSERT_TRUE(rail_efuse_is_on(rail));
|
||||
}
|
||||
/* 上电序列从不开 VBUS(那是命令的事)。 */
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
TEST_ASSERT_FALSE(slot_vbus_is_on(rail));
|
||||
}
|
||||
}
|
||||
|
||||
/* 只恢复「上次开着的」那几路;跳过的路**不占** 100 ms 的格子。 */
|
||||
static void test_only_wanted_rails_come_up(void)
|
||||
{
|
||||
devices(true);
|
||||
bringup_power_restore(BIT(1) | BIT(4), &g_r);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(1) | BIT(4), g_r.on);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(2)->efuse_en));
|
||||
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(3)->efuse_en));
|
||||
const uint64_t d = sim_gpio_rise_us(rail_cfg(4)->efuse_en) -
|
||||
sim_gpio_rise_us(rail_cfg(1)->efuse_en);
|
||||
TEST_ASSERT_TRUE(d >= 100000u && d < 110000u);
|
||||
}
|
||||
|
||||
/* 新板(NVS 里什么都没有)= 0 = 一路都不开,而且一个时钟都不花。 */
|
||||
static void test_nothing_wanted_nothing_powered(void)
|
||||
{
|
||||
devices(true);
|
||||
const uint32_t delays = sim_time_delay_calls();
|
||||
bringup_power_restore(0, &g_r);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, g_r.on);
|
||||
TEST_ASSERT_EQUAL_UINT32(delays, sim_time_delay_calls());
|
||||
}
|
||||
|
||||
/* ---- `HUB_RST#` 只脉冲一次 ----------------------------------------------- */
|
||||
|
||||
/* 上电复位:正好一个低脉冲(写两次:0 再 1),而且第二段**一个字都不再写它**。 */
|
||||
static void test_hub_reset_pulses_exactly_once_on_power_on(void)
|
||||
{
|
||||
devices(true);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, sim_gpio_writes(BOARD_PIN_HUB_RST_N));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(BOARD_PIN_HUB_RST_N));
|
||||
}
|
||||
|
||||
/* 其余复位原因(看门狗 / esptool 的 USB 复位 / esp_restart):**碰都不碰**,
|
||||
连「配成输出」都不做 —— 配成输出那一刻 HAL 两侧都是输出 0 = 就是一次复位。 */
|
||||
static void test_hub_reset_untouched_on_warm_reboot(void)
|
||||
{
|
||||
devices(false);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_gpio_writes(BOARD_PIN_HUB_RST_N));
|
||||
TEST_ASSERT_EQUAL_INT(-1, sim_gpio_level(BOARD_PIN_HUB_RST_N)); /* 没配过 */
|
||||
}
|
||||
|
||||
/* ---- §10.10 逐行 --------------------------------------------------------- */
|
||||
|
||||
/* 行 1:某片 INA226 无应答 ⇒ **保持供电**、标盲跑、记事件。PG 照样从 PCA9555 来。 */
|
||||
static void test_row_rail_ina_missing_keeps_power(void)
|
||||
{
|
||||
sim_i2c_set_present_ch(ch_of(3), INA226_ADDR_RAIL, false);
|
||||
devices(true);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(3), g_r.blind);
|
||||
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
|
||||
TEST_ASSERT_TRUE(rail_efuse_is_on(3));
|
||||
evlog_rec_t rec;
|
||||
TEST_ASSERT_TRUE(last_ev(LOG_EV_I2C_LOST, &rec));
|
||||
TEST_ASSERT_EQUAL_UINT8(3, rec.rail);
|
||||
TEST_ASSERT_EQUAL_UINT8(INA226_ADDR_RAIL, rec.arg);
|
||||
TEST_ASSERT_EQUAL_INT(1, count_ev(LOG_EV_I2C_LOST, -1));
|
||||
}
|
||||
|
||||
/* 行 3:DAC 无应答 ⇒ **不关输出**;另一片照样初始化;影子回到 NVM 默认。 */
|
||||
static void test_row_dac_missing_does_not_stop_the_other_or_the_rails(void)
|
||||
{
|
||||
rail_setpoint_reset();
|
||||
sim_i2c_set_present(DAC63204_ADDR_U7, false);
|
||||
devices(true);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
TEST_ASSERT_TRUE((g_r.deg & BRINGUP_DEG_DAC_U7) != 0);
|
||||
TEST_ASSERT_FALSE((g_r.deg & BRINGUP_DEG_DAC_U6) != 0);
|
||||
/* U6 真的被初始化了:内部基准开、用到的通道上电(不再是复位值 0x0FFF)。 */
|
||||
TEST_ASSERT_EQUAL_UINT16(dac63204_cc_run(DAC63204_ADDR_U6),
|
||||
sim_i2c_get_reg(DAC63204_ADDR_U6, DAC63204_REG_COMMON_CONFIG));
|
||||
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
TEST_ASSERT_EQUAL_INT(RAIL_MV_SAFE, rail_setpoint_mv(rail));
|
||||
}
|
||||
evlog_rec_t rec;
|
||||
TEST_ASSERT_TRUE(last_ev(LOG_EV_I2C_LOST, &rec));
|
||||
TEST_ASSERT_EQUAL_UINT8(0, rec.rail);
|
||||
TEST_ASSERT_EQUAL_UINT8(DAC63204_ADDR_U7, rec.arg);
|
||||
}
|
||||
|
||||
/* 行 4:某片 PCA9555 无应答 ⇒ **不关输出**,PG 改按 INA226 电压判。电压到了就算好。 */
|
||||
static void test_row_pca_missing_falls_back_to_voltage(void)
|
||||
{
|
||||
sim_i2c_set_present_ch(ch_of(2), PCA9555_ADDR, false);
|
||||
g_pca_gone[1] = true;
|
||||
sim_i2c_set_reg_ch(ch_of(2), INA226_ADDR_RAIL, INA226_REG_BUS_V, (uint16_t)(3900 * 4 / 5));
|
||||
devices(true);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(2), g_r.io_lost);
|
||||
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, g_r.pg_fail);
|
||||
TEST_ASSERT_EQUAL_INT(1, count_ev(LOG_EV_I2C_LOST, 2));
|
||||
}
|
||||
|
||||
/* 同一行的反面:PCA9555 看不见、电压也一直起不来 ⇒ 超时关回去,arg 标明是按电压判的。 */
|
||||
static void test_row_pca_missing_and_no_voltage_times_out(void)
|
||||
{
|
||||
sim_i2c_set_present_ch(ch_of(2), PCA9555_ADDR, false);
|
||||
g_pca_gone[1] = true;
|
||||
devices(true);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(2), g_r.pg_fail);
|
||||
TEST_ASSERT_FALSE(rail_efuse_is_on(2));
|
||||
TEST_ASSERT_EQUAL_UINT8((uint8_t)(ALL & ~BIT(2)), g_r.on);
|
||||
evlog_rec_t rec;
|
||||
TEST_ASSERT_TRUE(last_ev(LOG_EV_PG_TIMEOUT, &rec));
|
||||
TEST_ASSERT_EQUAL_UINT8(2, rec.rail);
|
||||
TEST_ASSERT_EQUAL_UINT8(BRINGUP_ARG_BY_VOLT, rec.arg);
|
||||
}
|
||||
|
||||
/* PCA9555 与 INA226 都看不见:**开着**、报「确认不了」,不当成坏了去断电。 */
|
||||
static void test_row_pca_and_ina_missing_stays_on_unverified(void)
|
||||
{
|
||||
sim_i2c_set_present_ch(ch_of(5), PCA9555_ADDR, false);
|
||||
sim_i2c_set_present_ch(ch_of(5), INA226_ADDR_RAIL, false);
|
||||
g_pca_gone[4] = true;
|
||||
devices(true);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(5), g_r.unverified);
|
||||
TEST_ASSERT_TRUE(rail_efuse_is_on(5));
|
||||
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_PG_TIMEOUT, -1));
|
||||
}
|
||||
|
||||
/* 行 5:mux 选不中 ⇒ 先脉冲一次 `MUX_RST#`,仍不通 ⇒ **五路一路都不上电** + MUX_LOST。 */
|
||||
static void test_row_mux_missing_pulses_reset_then_powers_nothing(void)
|
||||
{
|
||||
sim_i2c_set_present(TCA9548A_ADDR, false);
|
||||
devices(true);
|
||||
const uint32_t resets = sim_i2c_mux_resets();
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
TEST_ASSERT_TRUE((g_r.deg & BRINGUP_DEG_MUX) != 0);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, i2cbus_recoveries()); /* 就一次,不是每试一次复位一次 */
|
||||
TEST_ASSERT_EQUAL_UINT32(resets, sim_i2c_mux_resets()); /* 第二段不再动它 */
|
||||
TEST_ASSERT_EQUAL_UINT8(0, g_r.on);
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(rail)->efuse_en));
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(1, count_ev(LOG_EV_MUX_LOST, 0));
|
||||
/* 整板级记一条就够:不逐路刷十条,也不替够不着的主干器件刷 I2C_LOST
|
||||
(主干要经 mux 写「全关」,mux 死了主干也够不着 —— bringup.c 的那段)。 */
|
||||
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_I2C_LOST, -1));
|
||||
TEST_ASSERT_EQUAL_UINT32(60000, g_r.pd_limit_mw);
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
TEST_ASSERT_EQUAL_INT(RAIL_MV_SAFE, rail_setpoint_mv(rail));
|
||||
}
|
||||
}
|
||||
|
||||
/* 同一行:复位**救回来了**(第一次认件失败、复位后应答)⇒ 照常起。 */
|
||||
static void test_row_mux_recovers_after_one_reset(void)
|
||||
{
|
||||
sim_i2c_inject_nak(TCA9548A_ADDR, 1); /* 只 NAK 认件那一次 */
|
||||
devices(true);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
TEST_ASSERT_FALSE((g_r.deg & BRINGUP_DEG_MUX) != 0);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, i2cbus_recoveries());
|
||||
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
|
||||
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_MUX_LOST, -1));
|
||||
}
|
||||
|
||||
/* 行 6:`PRSNT#` 高 ⇒ 不初始化(一次总线都不发)、不上电;想开的才记 SLOT_EMPTY。 */
|
||||
static void test_row_empty_slot_is_never_touched(void)
|
||||
{
|
||||
pull_slot(4);
|
||||
devices(true);
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(4), g_r.empty);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_dev_ch(ch_of(4), PCA9555_ADDR)->n_write);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_dev_ch(ch_of(4), INA226_ADDR_RAIL)->n_write);
|
||||
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_I2C_LOST, -1)); /* 空槽不是失联 */
|
||||
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
TEST_ASSERT_EQUAL_UINT8((uint8_t)(ALL & ~BIT(4)), g_r.on);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(rail_cfg(4)->efuse_en));
|
||||
TEST_ASSERT_EQUAL_INT(1, count_ev(LOG_EV_SLOT_EMPTY, 4));
|
||||
}
|
||||
|
||||
static void test_row_empty_slot_not_wanted_is_silent(void)
|
||||
{
|
||||
pull_slot(4);
|
||||
devices(true);
|
||||
bringup_power_restore((uint8_t)(ALL & ~BIT(4)), &g_r);
|
||||
TEST_ASSERT_EQUAL_INT(0, count_ev(LOG_EV_SLOT_EMPTY, -1)); /* 三槽机器每次开机不刷事件 */
|
||||
}
|
||||
|
||||
/* 行 7:CH224A 读不到 ⇒ **不关输出**,按 60 W。 */
|
||||
static void test_row_pd_missing_assumes_60w(void)
|
||||
{
|
||||
sim_i2c_set_present(CH224A_ADDR_DEFAULT, false);
|
||||
devices(true);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
TEST_ASSERT_TRUE((g_r.deg & BRINGUP_DEG_PD) != 0);
|
||||
TEST_ASSERT_EQUAL_UINT32(60000, g_r.pd_limit_mw);
|
||||
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
|
||||
evlog_rec_t rec;
|
||||
TEST_ASSERT_TRUE(last_ev(LOG_EV_PD_LIMIT, &rec));
|
||||
TEST_ASSERT_EQUAL_UINT8(1, rec.arg);
|
||||
TEST_ASSERT_EQUAL_INT32(600, rec.bus_mv); /* mW/100 */
|
||||
}
|
||||
|
||||
/* 同一行:档位偏低 ⇒ 照读到的来(真相比保守值更可信),标 PD_LOW。 */
|
||||
static void test_row_pd_low_uses_what_it_reads(void)
|
||||
{
|
||||
sim_i2c_set_reg(CH224A_ADDR_DEFAULT, CH224A_REG_CURRENT, 2250 / 50); /* 45 W */
|
||||
devices(true);
|
||||
TEST_ASSERT_TRUE((g_r.deg & BRINGUP_DEG_PD_LOW) != 0);
|
||||
TEST_ASSERT_FALSE((g_r.deg & BRINGUP_DEG_PD) != 0);
|
||||
TEST_ASSERT_EQUAL_UINT32(45000, g_r.pd_limit_mw);
|
||||
}
|
||||
|
||||
/* 设计文档说 CH224A 在 mux 通道 5,实测在主干 —— 两处都得找得到。 */
|
||||
static void test_pd_found_behind_channel_5_too(void)
|
||||
{
|
||||
sim_hal_reset();
|
||||
pca9555_shadow_reset();
|
||||
g_pd_on_trunk = false; /* 主干上不挂 —— 同址挂两处,sim 会先匹配到主干那颗 */
|
||||
board();
|
||||
g_pd_on_trunk = true;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init_ex(0));
|
||||
sim_i2c_add_ch(TCA9548A_CH_PD, CH224A_ADDR_DEFAULT, 1);
|
||||
sim_i2c_set_reg_ch(TCA9548A_CH_PD, CH224A_ADDR_DEFAULT, CH224A_REG_CURRENT, 5000 / 50);
|
||||
devices(true);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, g_r.deg);
|
||||
TEST_ASSERT_EQUAL_UINT32(100000, g_r.pd_limit_mw);
|
||||
}
|
||||
|
||||
/* 行 8:看门狗复位 ⇒ BOOT 事件带复位原因;输出照 §10.9 恢复。 */
|
||||
static void test_row_watchdog_reset_is_recorded_in_boot(void)
|
||||
{
|
||||
sim_hal_reboot();
|
||||
pca9555_shadow_reset();
|
||||
board();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init_ex(7)); /* ESP_RST_TASK_WDT = 7 */
|
||||
devices(false);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
evlog_rec_t rec;
|
||||
TEST_ASSERT_TRUE(last_ev(LOG_EV_BOOT, &rec));
|
||||
TEST_ASSERT_EQUAL_UINT8(7, rec.arg);
|
||||
TEST_ASSERT_EQUAL_UINT8(ALL, g_r.on);
|
||||
}
|
||||
|
||||
/* ---- 等 PG --------------------------------------------------------------- */
|
||||
|
||||
/* §10.9 第 6 步:PG 等不到 ⇒ 500 ms 后关回去 + 记事件(带 FLT#);别的路不受牵连。 */
|
||||
static void test_pg_timeout_turns_that_rail_back_off(void)
|
||||
{
|
||||
g_pg_follows[2] = false;
|
||||
g_flt[2] = true;
|
||||
devices(true);
|
||||
bringup_power_restore(ALL, &g_r);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(3), g_r.pg_fail);
|
||||
TEST_ASSERT_FALSE(rail_efuse_is_on(3));
|
||||
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(rail_cfg(3)->efuse_en));
|
||||
TEST_ASSERT_EQUAL_UINT8((uint8_t)(ALL & ~BIT(3)), g_r.on);
|
||||
evlog_rec_t rec;
|
||||
TEST_ASSERT_TRUE(last_ev(LOG_EV_PG_TIMEOUT, &rec));
|
||||
TEST_ASSERT_EQUAL_UINT8(3, rec.rail);
|
||||
TEST_ASSERT_EQUAL_UINT8(BRINGUP_ARG_FLT, rec.arg);
|
||||
/* 关回去的时刻 ≈ 抬高 + 500 ms(不早于)。 */
|
||||
TEST_ASSERT_TRUE(hal_time_now_ms() - g_r.on_ms[2] >= BRINGUP_PG_TIMEOUT_MS);
|
||||
/* 后面两路没有被它拖住:仍是 100 ms 一格。 */
|
||||
const uint64_t d = sim_gpio_rise_us(rail_cfg(4)->efuse_en) -
|
||||
sim_gpio_rise_us(rail_cfg(3)->efuse_en);
|
||||
TEST_ASSERT_TRUE(d < 110000u);
|
||||
}
|
||||
|
||||
/* ---- DAC 的 NVM 核对 ----------------------------------------------------- */
|
||||
|
||||
/* 上电复位时读回的不是 3.90 V ⇒ 「DAC 不应答时停在安全值」这条前提不成立了,要报。 */
|
||||
static void test_dac_nvm_mismatch_is_flagged_on_power_on(void)
|
||||
{
|
||||
sim_i2c_set_reg(dac63204_rail_addr(3), DAC63204_REG_DATA(dac63204_rail_ch(3)),
|
||||
DAC63204_CODE_TO_REG(rail_mv_to_code(3600)));
|
||||
devices(true);
|
||||
TEST_ASSERT_TRUE((g_r.deg & BRINGUP_DEG_DAC_NVM) != 0);
|
||||
TEST_ASSERT_EQUAL_UINT16(3600, g_r.dac_nvm_mv[2]);
|
||||
}
|
||||
|
||||
/* 热重启时 DAC 没断过电,里面是上次命令设的值 —— 不许报 NVM 不对。 */
|
||||
static void test_dac_nvm_not_checked_on_warm_reboot(void)
|
||||
{
|
||||
sim_i2c_set_reg(dac63204_rail_addr(3), DAC63204_REG_DATA(dac63204_rail_ch(3)),
|
||||
DAC63204_CODE_TO_REG(rail_mv_to_code(3600)));
|
||||
devices(false);
|
||||
TEST_ASSERT_FALSE((g_r.deg & BRINGUP_DEG_DAC_NVM) != 0);
|
||||
/* 而初始化照样把它写回安全值。 */
|
||||
int mv = 0;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_get_mv(3, &mv));
|
||||
TEST_ASSERT_EQUAL_INT(RAIL_MV_SAFE, mv);
|
||||
}
|
||||
|
||||
/* ---- 意图掩码 ------------------------------------------------------------ */
|
||||
|
||||
static void test_want_update(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(3), bringup_want_update(0, 3, true));
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(1), bringup_want_update(BIT(1) | BIT(3), 3, false));
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(2), bringup_want_update(BIT(2), 0, true)); /* 越界不动 */
|
||||
TEST_ASSERT_EQUAL_UINT8(BIT(2), bringup_want_update(BIT(2), N + 1, true));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_first_input_ina_is_marked_reversed);
|
||||
RUN_TEST(test_full_board_initialises_every_device);
|
||||
RUN_TEST(test_five_rails_come_up_100ms_apart);
|
||||
RUN_TEST(test_only_wanted_rails_come_up);
|
||||
RUN_TEST(test_nothing_wanted_nothing_powered);
|
||||
RUN_TEST(test_hub_reset_pulses_exactly_once_on_power_on);
|
||||
RUN_TEST(test_hub_reset_untouched_on_warm_reboot);
|
||||
RUN_TEST(test_row_rail_ina_missing_keeps_power);
|
||||
RUN_TEST(test_row_dac_missing_does_not_stop_the_other_or_the_rails);
|
||||
RUN_TEST(test_row_pca_missing_falls_back_to_voltage);
|
||||
RUN_TEST(test_row_pca_missing_and_no_voltage_times_out);
|
||||
RUN_TEST(test_row_pca_and_ina_missing_stays_on_unverified);
|
||||
RUN_TEST(test_row_mux_missing_pulses_reset_then_powers_nothing);
|
||||
RUN_TEST(test_row_mux_recovers_after_one_reset);
|
||||
RUN_TEST(test_row_empty_slot_is_never_touched);
|
||||
RUN_TEST(test_row_empty_slot_not_wanted_is_silent);
|
||||
RUN_TEST(test_row_pd_missing_assumes_60w);
|
||||
RUN_TEST(test_row_pd_low_uses_what_it_reads);
|
||||
RUN_TEST(test_pd_found_behind_channel_5_too);
|
||||
RUN_TEST(test_row_watchdog_reset_is_recorded_in_boot);
|
||||
RUN_TEST(test_pg_timeout_turns_that_rail_back_off);
|
||||
RUN_TEST(test_dac_nvm_mismatch_is_flagged_on_power_on);
|
||||
RUN_TEST(test_dac_nvm_not_checked_on_warm_reboot);
|
||||
RUN_TEST(test_want_update);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -524,6 +524,47 @@ static void test_power_off_drops_vbus_and_always_drops_the_efuse(void)
|
||||
TEST_ASSERT_TRUE(slot_vbus_is_on(2)); /* 影子如实留在 on(slot.h:倒向安全的一侧) */
|
||||
}
|
||||
|
||||
/* A15:只有「真的开成 / 关成了」才改意图(下次开机按它恢复);被拒的、判重命中的都不改。 */
|
||||
static int g_intent_n;
|
||||
static int g_intent_rail;
|
||||
static bool g_intent_on;
|
||||
static void intent_spy(void *ctx, int rail, bool on)
|
||||
{
|
||||
(void)ctx;
|
||||
g_intent_n++;
|
||||
g_intent_rail = rail;
|
||||
g_intent_on = on;
|
||||
}
|
||||
|
||||
static void test_power_records_intent_only_when_it_took_effect(void)
|
||||
{
|
||||
g_intent_n = 0;
|
||||
const cmd_env_t env = { .intent = intent_spy };
|
||||
cmd_init(&env);
|
||||
|
||||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true");
|
||||
TEST_ASSERT_EQUAL_INT(1, g_intent_n);
|
||||
TEST_ASSERT_EQUAL_INT(3, g_intent_rail);
|
||||
TEST_ASSERT_TRUE(g_intent_on);
|
||||
|
||||
send("{\"seq\":1,\"cmd\":\"power\",\"rail\":3,\"on\":true}"); /* 判重命中:不再执行 */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_intent_n);
|
||||
|
||||
set_present(4, false);
|
||||
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"power\",\"rail\":4,\"on\":true}"), "\"err\":\"empty\"");
|
||||
TEST_ASSERT_EQUAL_INT(1, g_intent_n);
|
||||
|
||||
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"power\",\"rail\":3,\"on\":false}"), "\"ok\":true");
|
||||
TEST_ASSERT_EQUAL_INT(2, g_intent_n);
|
||||
TEST_ASSERT_FALSE(g_intent_on);
|
||||
|
||||
/* 硬重启不是意图(那是事故处理),不许碰它。 */
|
||||
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"power\",\"rail\":1,\"on\":true}"), "\"ok\":true");
|
||||
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"power_cycle\",\"rail\":1}"), "\"ok\":true");
|
||||
TEST_ASSERT_EQUAL_INT(3, g_intent_n);
|
||||
cmd_init(NULL);
|
||||
}
|
||||
|
||||
/* 风扇:手动值只是下限,0 交回自动。 */
|
||||
static void test_fan_floor(void)
|
||||
{
|
||||
@@ -649,6 +690,7 @@ int main(void)
|
||||
RUN_TEST(test_reboots_are_logged_with_kind_and_link);
|
||||
RUN_TEST(test_vbus_needs_the_rail_powered);
|
||||
RUN_TEST(test_power_off_drops_vbus_and_always_drops_the_efuse);
|
||||
RUN_TEST(test_power_records_intent_only_when_it_took_effect);
|
||||
RUN_TEST(test_fan_floor);
|
||||
RUN_TEST(test_events_pages_with_contiguous_seqs);
|
||||
RUN_TEST(test_status_fits_and_reports_unknowns_as_null);
|
||||
|
||||
@@ -84,7 +84,8 @@ static void test_flash_format_is_frozen(void)
|
||||
TEST_ASSERT_EQUAL_INT(14, LOG_EV_MUX_LOST);
|
||||
TEST_ASSERT_EQUAL_INT(15, LOG_EV_SLOT_EMPTY);
|
||||
TEST_ASSERT_EQUAL_INT(16, LOG_EV_PD_LIMIT);
|
||||
TEST_ASSERT_EQUAL_INT(17, LOG_EV_COUNT);
|
||||
TEST_ASSERT_EQUAL_INT(17, LOG_EV_PG_TIMEOUT); /* A15 */
|
||||
TEST_ASSERT_EQUAL_INT(18, LOG_EV_COUNT);
|
||||
}
|
||||
|
||||
/* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住,
|
||||
|
||||
在新工单中引用
屏蔽一个用户