固件 0.1.1-c5:硬重启后自动开机、MQTT 发布死锁、欠压放宽到 −250 mV
- 硬重启等到 PG 后长按电源键 5 s(新状态 hr-boot):电池端上电的小米8 停在关机态,原来的 power_cycle 等于关机。ACT_KEY_MS_ON 2000 → 5000 (实测 2 s 松手即掉电)。NO_PG 时不按。 - 修死锁:执行器持 s_mtx 等 esp-mqtt 的 api_lock,esp-mqtt 任务持 api_lock 分发 DISCONNECTED 时等 s_mtx。热点一断 USB/WiFi 全失联、按键可能松不开。 执行器现在只往 s_outbox 拷贝,由 t_link 交给 esp-mqtt。 - SR-5 欠压一侧 −100 → −250 mV(T_SAFETY_UNDER_MV),过压仍 +100 mV: 带屏开机峰值 2.1 A 时实测 −115 mV 被误关。需求 SR-5 / SW-2 / SW-5 / 验收表同步。 板 B 上板:power_cycle → hr-boot → 手机开机,最低 3778 mV / 2.12 A 不再跳闸。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+15
-1
@@ -1,5 +1,19 @@
|
|||||||
# 交接
|
# 交接
|
||||||
更新:2026-09-27 深夜 固件 **0.1.1-c3**(I²C 不应答不再报成 STATE;重启不断电)+ rack 界面改版 / 推固件页 / 底板温度曲线
|
更新:2026-09-28 固件 **0.1.1-c5**(硬重启末尾自动开机 + MQTT 发布死锁 + 欠压放宽到 −250 mV);板 B 经 USB 整片烧上,WiFi / MQTT 已通
|
||||||
|
|
||||||
|
## c4:两处修复(2026-09-28,第一片刀片 + 小米8 上板时发现)
|
||||||
|
- **硬重启后手机不开机**:电池端上电的小米8 停在关机态(只漏 ~2 mA)。现在 `power_cycle` 等到 PG 后
|
||||||
|
**长按电源键 5 s**(新状态 `hr-boot`,`ACT_HR_BOOT_KEY_MS`);NO_PG 时不按。`ACT_KEY_MS_ON` 2000 → **5000**:
|
||||||
|
实测按 2 s 时 PMIC 起来了(415 mA),一松手就掉回 1 mA。需求 SW-5 已同步。rack 的二级恢复(power_cycle)因此真的能拉起手机了。
|
||||||
|
上板:power_cycle → 4.1 s 上电 → hr-boot 5 s → res ok,手机进入开机电流曲线。
|
||||||
|
- **热点一断整板失联(死锁)**:执行器「持 `s_mtx` → `esp_mqtt_client_enqueue` 等 `api_lock`」,
|
||||||
|
esp-mqtt 任务「持 `api_lock` 分发 DISCONNECTED → `on_mqtt` 的 `ST_EDIT` 等 `s_mtx`」。
|
||||||
|
现象:USB 命令无应答、WiFi 不重连、遥测停,只有 t_safety / t_thermal 的日志还在 —— **按到一半的键也会松不开**。
|
||||||
|
修:执行器只往 `s_outbox`(深度 4)拷一份,`t_link` 里 `outbox_drain()` 交给 esp-mqtt;持 `s_mtx` 时不调 esp-mqtt。
|
||||||
|
⚠️ 死锁状态下 OTA 进不去,只能 USB 烧(esptool `--before usb_reset`,第一次常报 ClearCommError,再跑一次就过)。
|
||||||
|
- **c5:带载欠压误关**。小米8 插屏幕后开机峰值 ~2.1 A,轨 1 读回 3785 mV(设定 3900,−115 mV),SR-5 的 −100 mV 直接关路。
|
||||||
|
用户拍板:**欠压一侧放宽到 −250 mV**(`T_SAFETY_UNDER_MV`),过压仍 +100 mV;需求 SR-5 / SW-2 / 验收表同步。
|
||||||
|
压降 ≈ 55 mΩ × 2 A 的来源(Buck 负载调整 / eFuse / 采样电阻 / 走线)还没拆开查。
|
||||||
|
|
||||||
## 一句话现状
|
## 一句话现状
|
||||||
OTA 换固件 / 超时回滚现在走 `warm_restart()`(`main/warm_esp.c`):目标固件认接管 ⇒ 手机不断电;
|
OTA 换固件 / 超时回滚现在走 `warm_restart()`(`main/warm_esp.c`):目标固件认接管 ⇒ 手机不断电;
|
||||||
|
|||||||
+64
-15
@@ -7,10 +7,14 @@
|
|||||||
| WiFi / IP 事件(掉线、拿到地址) | 系统事件循环 |
|
| WiFi / IP 事件(掉线、拿到地址) | 系统事件循环 |
|
||||||
| MQTT 事件(连上、收到命令) | esp-mqtt 自己的任务,优先级压到与 `t_link` 相同(§10.3:链路最低) |
|
| MQTT 事件(连上、收到命令) | esp-mqtt 自己的任务,优先级压到与 `t_link` 相同(§10.3:链路最低) |
|
||||||
| **MQTT 客户端的创建 / 销毁、重配置、遥测节拍** | `t_link`(`mlink_esp_poll()`)—— 生命周期只在一个任务里管 |
|
| **MQTT 客户端的创建 / 销毁、重配置、遥测节拍** | `t_link`(`mlink_esp_poll()`)—— 生命周期只在一个任务里管 |
|
||||||
| 回应 / 遥测的发布 | 执行器任务(cmd_queue 的回调)—— 只用 `esp_mqtt_client_enqueue()`,**不阻塞** |
|
| 回应 / 遥测的发布 | 执行器任务只把一份拷贝塞进 `s_outbox`(不等);`t_link` 取出来 `esp_mqtt_client_enqueue()` |
|
||||||
|
|
||||||
`esp_mqtt_client_publish()` 在网络慢的时候会阻塞到超时,放在执行器任务里就是让
|
⚠️ **执行器任务一律不碰 esp-mqtt 的任何 API**,连 `esp_mqtt_client_enqueue()` 也不行:
|
||||||
「按键该松了」等 TCP。enqueue 只进发件箱,由 esp-mqtt 自己的任务发。
|
它要拿客户端的 `api_lock`,而 esp-mqtt 任务在连接 / 写超时 / 分发事件时一直持着这把锁。
|
||||||
|
0.1.1-c3 之前执行器是「持 `s_mtx` → 等 `api_lock`」,esp-mqtt 任务分发 DISCONNECTED 时是
|
||||||
|
「持 `api_lock` → `on_mqtt` 里 `ST_EDIT` 等 `s_mtx`」—— 2026-09-28 热点一断就死锁:
|
||||||
|
USB 命令没人执行、WiFi 不重连、**按到一半的键松不开**,只有 t_safety / t_thermal 还活着。
|
||||||
|
现在的规矩:**持 `s_mtx` 时不调 esp-mqtt;执行器根本不调 esp-mqtt**。
|
||||||
|
|
||||||
---- 失联不动作(§10.2 / §10.8) -----------------------------------------
|
---- 失联不动作(§10.2 / §10.8) -----------------------------------------
|
||||||
WiFi 断、broker 断,本文件只做一件事:**重连**(WiFi 指数退避,MQTT 由 esp-mqtt 每 10 s 重试)。
|
WiFi 断、broker 断,本文件只做一件事:**重连**(WiFi 指数退避,MQTT 由 esp-mqtt 每 10 s 重试)。
|
||||||
@@ -25,6 +29,7 @@
|
|||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
#include "hal_time.h"
|
#include "hal_time.h"
|
||||||
#include "mlink.h"
|
#include "mlink.h"
|
||||||
@@ -65,6 +70,20 @@ static mlink_tele_t s_tele;
|
|||||||
static volatile bool s_tele_inflight;
|
static volatile bool s_tele_inflight;
|
||||||
static bool s_up;
|
static bool s_up;
|
||||||
|
|
||||||
|
/* 发件队列(见文件头):执行器 / esp-mqtt 任务往里拷,t_link 取出来交给 esp-mqtt。
|
||||||
|
深度 4 × 1.5 KB:遥测 1 s 一条、回应偶尔一条,t_link 至少每 100 ms 来取一次;
|
||||||
|
满了说明 t_link 被 esp-mqtt 堵住了 —— 丢掉这一条,遥测记 skipped,回应靠上位机重传。 */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t topic; /* mlink_topic_t */
|
||||||
|
uint8_t qos;
|
||||||
|
uint16_t len;
|
||||||
|
char data[CMD_OUT_MAX];
|
||||||
|
} out_msg_t;
|
||||||
|
|
||||||
|
#define OUTBOX_DEPTH 4
|
||||||
|
static QueueHandle_t s_outbox;
|
||||||
|
static SemaphoreHandle_t s_out_mtx; /* 只护 enqueue() 的暂存缓冲 */
|
||||||
|
|
||||||
/* ---- 状态:改完立刻推给 netcfg,命令处理器随时读得到一份完整的 ------------ */
|
/* ---- 状态:改完立刻推给 netcfg,命令处理器随时读得到一份完整的 ------------ */
|
||||||
|
|
||||||
/* 可变参数:语句里带逗号(snprintf 的实参)也能整个传进来。 */
|
/* 可变参数:语句里带逗号(snprintf 的实参)也能整个传进来。 */
|
||||||
@@ -119,39 +138,66 @@ static void on_cfg_changed(void *ctx)
|
|||||||
s_dirty = true; /* 执行器任务里只置标志;断开重连在 t_link 里做(esp_mqtt_client_stop 会阻塞) */
|
s_dirty = true; /* 执行器任务里只置标志;断开重连在 t_link 里做(esp_mqtt_client_stop 会阻塞) */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 发布(执行器任务里调,只进发件箱) ----------------------------------- */
|
/* ---- 发布:任何任务都只进 s_outbox,不等、不碰 esp-mqtt ------------------- */
|
||||||
|
|
||||||
static void enqueue(mlink_topic_t t, const char *data, size_t len, int qos, int retain)
|
static bool enqueue(mlink_topic_t t, const char *data, size_t len, int qos)
|
||||||
{
|
{
|
||||||
if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(10)) != pdTRUE) {
|
/* 1.5 KB 不放栈上。执行器和 esp-mqtt 任务(link_error)都会进来 ⇒ s_out_mtx 护着。
|
||||||
return;
|
这把锁持有期间只有 memcpy 和一次**不等**的入队,不会再去等任何别的东西。 */
|
||||||
|
static out_msg_t m;
|
||||||
|
if (s_outbox == NULL || len > sizeof(m.data)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (s_mqtt != NULL && s_st.mqtt == NET_UP) {
|
/* 没连上就不攒(上线后 on_mqtt 会先补一条遥测)。读一个枚举,不拿 s_mtx。 */
|
||||||
(void)esp_mqtt_client_enqueue(s_mqtt, mlink_topic(t), data, (int)len, qos, retain, true);
|
if (s_st.mqtt != NET_UP) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
xSemaphoreGive(s_mtx);
|
if (xSemaphoreTake(s_out_mtx, pdMS_TO_TICKS(10)) != pdTRUE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m.topic = (uint8_t)t;
|
||||||
|
m.qos = (uint8_t)qos;
|
||||||
|
m.len = (uint16_t)len;
|
||||||
|
memcpy(m.data, data, len);
|
||||||
|
const bool ok = xQueueSend(s_outbox, &m, 0) == pdTRUE;
|
||||||
|
xSemaphoreGive(s_out_mtx);
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void reply_mqtt(void *ctx, const char *line, size_t len)
|
static void reply_mqtt(void *ctx, const char *line, size_t len)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
enqueue(MLINK_T_RSP, line, len, 1, 0);
|
(void)enqueue(MLINK_T_RSP, line, len, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void reply_tele(void *ctx, const char *line, size_t len)
|
static void reply_tele(void *ctx, const char *line, size_t len)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
enqueue(MLINK_T_TELE, line, len, 0, 0);
|
const bool ok = enqueue(MLINK_T_TELE, line, len, 0);
|
||||||
ST_EDIT(s_st.tele_sent++);
|
ST_EDIT(if (ok) { s_st.tele_sent++; } else { s_st.tele_skipped++; });
|
||||||
s_tele_inflight = false;
|
s_tele_inflight = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* t_link 里调:把发件队列交给 esp-mqtt。s_mqtt 只在 t_link 里改,这里读它不用锁;
|
||||||
|
esp_mqtt_client_enqueue 可能等 api_lock 等到 esp-mqtt 的网络超时 —— 堵的是 t_link,
|
||||||
|
而 t_link 此刻不持任何我们的锁,esp-mqtt 的事件回调照样拿得到 s_mtx。 */
|
||||||
|
static void outbox_drain(void)
|
||||||
|
{
|
||||||
|
static out_msg_t m;
|
||||||
|
while (s_outbox != NULL && xQueueReceive(s_outbox, &m, 0) == pdTRUE) {
|
||||||
|
if (s_mqtt != NULL) {
|
||||||
|
(void)esp_mqtt_client_enqueue(s_mqtt, mlink_topic((mlink_topic_t)m.topic), m.data,
|
||||||
|
(int)m.len, m.qos, 0, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void link_error(const char *payload, const char *err)
|
static void link_error(const char *payload, const char *err)
|
||||||
{
|
{
|
||||||
char out[96];
|
char out[96];
|
||||||
const size_t n = ulink_link_error(payload, err, out, sizeof(out));
|
const size_t n = ulink_link_error(payload, err, out, sizeof(out));
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
enqueue(MLINK_T_RSP, out, n, 1, 0);
|
(void)enqueue(MLINK_T_RSP, out, n, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,12 +337,14 @@ static void wifi_apply(void)
|
|||||||
hal_err_t mlink_esp_init(void)
|
hal_err_t mlink_esp_init(void)
|
||||||
{
|
{
|
||||||
s_mtx = xSemaphoreCreateMutex();
|
s_mtx = xSemaphoreCreateMutex();
|
||||||
|
s_out_mtx = xSemaphoreCreateMutex();
|
||||||
|
s_outbox = xQueueCreate(OUTBOX_DEPTH, sizeof(out_msg_t));
|
||||||
esp_err_t e = nvs_flash_init();
|
esp_err_t e = nvs_flash_init();
|
||||||
if (e == ESP_ERR_NVS_NO_FREE_PAGES || e == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
if (e == ESP_ERR_NVS_NO_FREE_PAGES || e == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||||
nvs_flash_erase(); /* NVS 分区格式不认:清掉重来(里面只有本模块的配置与 WiFi 校准) */
|
nvs_flash_erase(); /* NVS 分区格式不认:清掉重来(里面只有本模块的配置与 WiFi 校准) */
|
||||||
e = nvs_flash_init();
|
e = nvs_flash_init();
|
||||||
}
|
}
|
||||||
if (s_mtx == NULL || e != ESP_OK) {
|
if (s_mtx == NULL || s_out_mtx == NULL || s_outbox == NULL || e != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "NVS 起不来:%s —— WiFi 链路不可用", esp_err_to_name(e));
|
ESP_LOGE(TAG, "NVS 起不来:%s —— WiFi 链路不可用", esp_err_to_name(e));
|
||||||
netcfg_init(NULL, NULL, NULL, NULL);
|
netcfg_init(NULL, NULL, NULL, NULL);
|
||||||
return HAL_ERR_STATE;
|
return HAL_ERR_STATE;
|
||||||
@@ -352,6 +400,7 @@ void mlink_esp_poll(void)
|
|||||||
if (s_mqtt == NULL && s_st.wifi == NET_UP && s_cur.uri[0] != '\0') {
|
if (s_mqtt == NULL && s_st.wifi == NET_UP && s_cur.uri[0] != '\0') {
|
||||||
mqtt_start();
|
mqtt_start();
|
||||||
}
|
}
|
||||||
|
outbox_drain();
|
||||||
if (s_st.mqtt != NET_UP || !mlink_tele_due(&s_tele, hal_time_now_ms(), s_cur.tele_ms)) {
|
if (s_st.mqtt != NET_UP || !mlink_tele_due(&s_tele, hal_time_now_ms(), s_cur.tele_ms)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#define PHONERAIL_H
|
#define PHONERAIL_H
|
||||||
|
|
||||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||||
#define PHONERAIL_FW_VERSION "0.1.1-c3"
|
#define PHONERAIL_FW_VERSION "0.1.1-c5"
|
||||||
|
|
||||||
/* 通道数。五路直供,见设计文档 §2。 */
|
/* 通道数。五路直供,见设计文档 §2。 */
|
||||||
#define PHONERAIL_RAIL_COUNT 5
|
#define PHONERAIL_RAIL_COUNT 5
|
||||||
|
|||||||
+15
-3
@@ -122,8 +122,18 @@ static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
|
|||||||
case ACT_ST_HR_PG: {
|
case ACT_ST_HR_PG: {
|
||||||
bool pg = false;
|
bool pg = false;
|
||||||
if (pg_now(rail, &pg) && pg) {
|
if (pg_now(rail, &pg) && pg) {
|
||||||
finish(r, ACT_RES_OK, now);
|
/* 电好了 ⇒ 长按电源键开机(ACT_HR_BOOT_KEY_MS 的注释)。
|
||||||
return ACT_IDLE_WAIT_MS;
|
和 KEY 序列一样:先记 release_mask 再看写没写成 —— 写失败时器件里那个字节
|
||||||
|
是未知的,照样走 CLEANUP 把它显式清一遍。 */
|
||||||
|
const uint8_t mask = key_out_mask(ACT_KEYS_PWR);
|
||||||
|
r->release_mask = mask;
|
||||||
|
r->hold_ms = ACT_HR_BOOT_KEY_MS;
|
||||||
|
if (out_write(rail, mask, mask) != HAL_OK) {
|
||||||
|
finish(r, ACT_RES_IO, now); /* 电是开着的,只是没按上键 */
|
||||||
|
return ACT_CLEANUP_RETRY_MS;
|
||||||
|
}
|
||||||
|
enter(r, ACT_ST_HR_BOOT, now);
|
||||||
|
return ACT_HR_BOOT_KEY_MS;
|
||||||
}
|
}
|
||||||
if (el >= ACT_HR_PG_TIMEOUT_MS) {
|
if (el >= ACT_HR_PG_TIMEOUT_MS) {
|
||||||
/* ⚠️ 电**继续开着**。见 ACT_RES_NO_PG 的注释:把「看不见」当成
|
/* ⚠️ 电**继续开着**。见 ACT_RES_NO_PG 的注释:把「看不见」当成
|
||||||
@@ -135,6 +145,7 @@ static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
|
|||||||
return left < ACT_HR_PG_POLL_MS ? left : ACT_HR_PG_POLL_MS;
|
return left < ACT_HR_PG_POLL_MS ? left : ACT_HR_PG_POLL_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ACT_ST_HR_BOOT:
|
||||||
case ACT_ST_KEY_HOLD:
|
case ACT_ST_KEY_HOLD:
|
||||||
if (el < r->hold_ms) {
|
if (el < r->hold_ms) {
|
||||||
return r->hold_ms - el;
|
return r->hold_ms - el;
|
||||||
@@ -232,7 +243,7 @@ hal_err_t t_actuator_hard_reset(int rail)
|
|||||||
return err; /* 序列没开始 */
|
return err; /* 序列没开始 */
|
||||||
}
|
}
|
||||||
r->seq = ACT_SEQ_HARD_RESET;
|
r->seq = ACT_SEQ_HARD_RESET;
|
||||||
r->release_mask = 0; /* 硬重启在链上没按下任何东西 */
|
r->release_mask = 0; /* 断电这一段没按任何东西;开机那一下在 HR_PG 里才记 */
|
||||||
r->result = ACT_RES_NONE;
|
r->result = ACT_RES_NONE;
|
||||||
enter(r, ACT_ST_HR_OFF, hal_time_now_ms());
|
enter(r, ACT_ST_HR_OFF, hal_time_now_ms());
|
||||||
return HAL_OK;
|
return HAL_OK;
|
||||||
@@ -356,6 +367,7 @@ const char *t_actuator_state_str(act_state_t st)
|
|||||||
case ACT_ST_HR_OFF: return "hr-off";
|
case ACT_ST_HR_OFF: return "hr-off";
|
||||||
case ACT_ST_HR_RAMP: return "hr-ramp";
|
case ACT_ST_HR_RAMP: return "hr-ramp";
|
||||||
case ACT_ST_HR_PG: return "hr-pg";
|
case ACT_ST_HR_PG: return "hr-pg";
|
||||||
|
case ACT_ST_HR_BOOT: return "hr-boot";
|
||||||
case ACT_ST_KEY_HOLD: return "key-hold";
|
case ACT_ST_KEY_HOLD: return "key-hold";
|
||||||
case ACT_ST_EDL_SHORT: return "edl-short";
|
case ACT_ST_EDL_SHORT: return "edl-short";
|
||||||
case ACT_ST_EDL_ENUM: return "edl-enum";
|
case ACT_ST_EDL_ENUM: return "edl-enum";
|
||||||
|
|||||||
+14
-3
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
| 序列 | 时序 | 出处 |
|
| 序列 | 时序 | 出处 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 硬重启 | `EFUSE_EN↓` → 等 3~5 s → `EFUSE_EN↑` → 等 **17 ms** → 等 PG 置位 | 设计文档 §7.4 |
|
| 硬重启 | `EFUSE_EN↓` → 等 3~5 s → `EFUSE_EN↑` → 等 **17 ms** → 等 PG 置位 → 长按电源键 5 s 开机 | 设计文档 §7.4;开机那一步见 `ACT_HR_BOOT_KEY_MS` |
|
||||||
| 按键 | 置位 → 保持 T(100 ms / 2 s / 10 s)→ 清零 | SW-5 / §13.3 |
|
| 按键 | 置位 → 保持 T(100 ms / 2 s / 10 s)→ 清零 | SW-5 / §13.3 |
|
||||||
| EDL | `EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0` | SW-6 / §22.4 |
|
| EDL | `EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0` | SW-6 / §22.4 |
|
||||||
|
|
||||||
@@ -80,9 +80,19 @@ extern "C" {
|
|||||||
会退化成「一律等满 500 ms」,而测试与遥测都看不出区别。 */
|
会退化成「一律等满 500 ms」,而测试与遥测都看不出区别。 */
|
||||||
#define ACT_HR_PG_POLL_MS 5
|
#define ACT_HR_PG_POLL_MS 5
|
||||||
|
|
||||||
/* SW-5 的三档按键时长。 */
|
/* 硬重启的最后一步:等到 PG 之后长按电源键开机。电池端上电的手机**停在关机态**
|
||||||
|
(小米8 实测:重新上电后只漏 ~2 mA,不按键就一直关着),不按这一下的硬重启
|
||||||
|
等于「关机」。时长就是 SW-5 的开机档。
|
||||||
|
⚠️ 只在**等到 PG** 时按:NO_PG 多半是这一片 PCA9555 读不到,键也在同一片上,
|
||||||
|
去按只会让这一路卡在 CLEANUP 里重试。 */
|
||||||
|
#define ACT_HR_BOOT_KEY_MS ACT_KEY_MS_ON
|
||||||
|
|
||||||
|
/* SW-5 的三档按键时长。
|
||||||
|
⚠️ 开机那一档 2026-09-28 小米8 实测:按 2 s 时 PMIC 起来了(415 mA),一松手 3 s 内
|
||||||
|
掉回 ~1 mA —— SoC 还没接住 PS_HOLD。按 5 s 松手后稳在 ~210 mA、约 45 s 进系统。
|
||||||
|
上限是 ACT_KEY_MS_FORCE:按到 10 s 就成了强制重启。 */
|
||||||
#define ACT_KEY_MS_TAP 100 /* 短按:唤醒 / 息屏 */
|
#define ACT_KEY_MS_TAP 100 /* 短按:唤醒 / 息屏 */
|
||||||
#define ACT_KEY_MS_ON 2000 /* 长按:开机 */
|
#define ACT_KEY_MS_ON 5000 /* 长按:开机 */
|
||||||
#define ACT_KEY_MS_FORCE 10000 /* 长按:强制重启(两级重启的「一级(软)」) */
|
#define ACT_KEY_MS_FORCE 10000 /* 长按:强制重启(两级重启的「一级(软)」) */
|
||||||
|
|
||||||
/* 按键时长上限。上层传进来一个 10 分钟,等于把电源键按死 ——
|
/* 按键时长上限。上层传进来一个 10 分钟,等于把电源键按死 ——
|
||||||
@@ -138,6 +148,7 @@ typedef enum {
|
|||||||
ACT_ST_HR_OFF, /* `EFUSE_EN` 已拉低,等 3~5 s(放电由硬件自己做) */
|
ACT_ST_HR_OFF, /* `EFUSE_EN` 已拉低,等 3~5 s(放电由硬件自己做) */
|
||||||
ACT_ST_HR_RAMP, /* `EFUSE_EN` 已拉高,等 17 ms 走完 tON + 斜坡 */
|
ACT_ST_HR_RAMP, /* `EFUSE_EN` 已拉高,等 17 ms 走完 tON + 斜坡 */
|
||||||
ACT_ST_HR_PG, /* 等 PG 置位 */
|
ACT_ST_HR_PG, /* 等 PG 置位 */
|
||||||
|
ACT_ST_HR_BOOT, /* PG 到了,电源键按着,保持 ACT_HR_BOOT_KEY_MS 开机 */
|
||||||
ACT_ST_KEY_HOLD, /* 键已按下,保持 T */
|
ACT_ST_KEY_HOLD, /* 键已按下,保持 T */
|
||||||
ACT_ST_EDL_SHORT, /* `EDL_EN=1`,等短接建立 */
|
ACT_ST_EDL_SHORT, /* `EDL_EN=1`,等短接建立 */
|
||||||
ACT_ST_EDL_ENUM, /* `VBUS_EN=1`,等枚举 */
|
ACT_ST_EDL_ENUM, /* `VBUS_EN=1`,等枚举 */
|
||||||
|
|||||||
+2
-2
@@ -135,8 +135,8 @@ static void judge_locked(safety_snap_t *s, int rail, uint32_t now)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- SR-5:±100 mV 连着 T_SAFETY_DEV_HOLD_CYCLES 圈(= 持续 20 ms)---- */
|
/* ---- SR-5:+100 / −250 mV 连着 T_SAFETY_DEV_HOLD_CYCLES 圈(= 持续 20 ms)---- */
|
||||||
if (dev > T_SAFETY_DEV_MV || dev < -T_SAFETY_DEV_MV) {
|
if (dev > T_SAFETY_DEV_MV || dev < -T_SAFETY_UNDER_MV) {
|
||||||
if (st->dev_n < 0xFF) {
|
if (st->dev_n < 0xFF) {
|
||||||
st->dev_n++;
|
st->dev_n++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ extern "C" {
|
|||||||
SW-4 允许运维单路微调,拿常数比会把一台调到 4.0 V 的机器当成过压。 */
|
SW-4 允许运维单路微调,拿常数比会把一台调到 4.0 V 的机器当成过压。 */
|
||||||
#define T_SAFETY_DEV_MV 100
|
#define T_SAFETY_DEV_MV 100
|
||||||
|
|
||||||
|
/* SR-5 的**欠压**一侧单独放宽到 −250 mV(2026-09-28 用户拍板)。
|
||||||
|
上板实测:小米8 插上屏幕后开机峰值 ~2.1 A,轨 1 读回 3785 mV(设定 3900,−115 mV,
|
||||||
|
≈55 mΩ 压降),−100 mV 的门限把正在开机的手机直接关掉。SR-5 本来就只管记录告警,
|
||||||
|
实时保护在硬件里;过压那一侧不动,仍是 +100 mV。 */
|
||||||
|
#define T_SAFETY_UNDER_MV 250
|
||||||
|
|
||||||
/* SR-5 的「且持续 20 ms」——「持续」在这里数的是**圈**,不是墙钟毫秒:
|
/* SR-5 的「且持续 20 ms」——「持续」在这里数的是**圈**,不是墙钟毫秒:
|
||||||
连着两圈新鲜样本都越界才算,而成立的那一刻就是断电的那一刻。
|
连着两圈新鲜样本都越界才算,而成立的那一刻就是断电的那一刻。
|
||||||
|
|
||||||
|
|||||||
+61
-10
@@ -278,15 +278,30 @@ static void test_hard_reset_walks_the_full_power_up_sequence(void)
|
|||||||
|
|
||||||
run_ms(1);
|
run_ms(1);
|
||||||
TEST_ASSERT_EQUAL_UINT32(ACT_HR_RAMP_MS, hal_time_now_ms() - t_on);
|
TEST_ASSERT_EQUAL_UINT32(ACT_HR_RAMP_MS, hal_time_now_ms() - t_on);
|
||||||
|
|
||||||
|
/* ⭐ PG 到了 ⇒ 当场按下电源键开机。电池端上电的手机停在关机态,
|
||||||
|
不按这一下的硬重启等于「关机」(小米8 实测,t_actuator.h)。 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_BOOT, t_actuator_state(rail));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_VUP));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_VDN));
|
||||||
|
|
||||||
|
run_ms(ACT_HR_BOOT_KEY_MS - 1);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR));
|
||||||
|
TEST_ASSERT_TRUE(t_actuator_busy(rail));
|
||||||
|
|
||||||
|
run_ms(1);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR)); /* 松开 */
|
||||||
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||||
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
|
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
|
||||||
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en));
|
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en));
|
||||||
TEST_ASSERT_EQUAL_UINT32(1, t_actuator_done(rail));
|
TEST_ASSERT_EQUAL_UINT32(1, t_actuator_done(rail));
|
||||||
|
|
||||||
/* ⭐ 硬重启在总线上什么都不该发:键本来就没按着 ⇒ 影子不脏 ⇒ 一个事务都不发。
|
/* ⭐ 总线上只有按下、松开这两次事务,而且只动这一片。断电那一段一个都不发:
|
||||||
R2 里这条比 R1 更要紧 —— 每一次多余的事务都是从巡检嘴里抢锁。 */
|
键本来就没按着 ⇒ 影子不脏。R2 里每一次多余的事务都是从巡检嘴里抢锁。 */
|
||||||
TEST_ASSERT_EQUAL_UINT32(takes0, sim_lock_takes(HAL_LOCK_I2C));
|
TEST_ASSERT_EQUAL_UINT32(takes0 + 2, sim_lock_takes(HAL_LOCK_I2C));
|
||||||
assert_only_this_blade_moved(0, l0, w0); /* 0 = 五片一个都不许动 */
|
assert_only_this_blade_moved(rail, l0, w0);
|
||||||
|
TEST_ASSERT_EQUAL_HEX8(l0[rail - 1], latch(rail)); /* 收工后这一片回到原样 */
|
||||||
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
|
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +316,7 @@ static void test_hard_reset_touches_only_its_own_en_pin(void)
|
|||||||
}
|
}
|
||||||
s_pg_val[rail - 1] = true;
|
s_pg_val[rail - 1] = true;
|
||||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
|
||||||
run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS);
|
run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_BOOT_KEY_MS);
|
||||||
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||||
|
|
||||||
for (int r = 1; r <= N; r++) {
|
for (int r = 1; r <= N; r++) {
|
||||||
@@ -332,6 +347,8 @@ static void test_no_pg_leaves_the_rail_powered(void)
|
|||||||
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||||
TEST_ASSERT_EQUAL_INT(ACT_RES_NO_PG, t_actuator_last_result(rail));
|
TEST_ASSERT_EQUAL_INT(ACT_RES_NO_PG, t_actuator_last_result(rail));
|
||||||
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en)); /* ⭐ 不许再关掉 */
|
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en)); /* ⭐ 不许再关掉 */
|
||||||
|
/* 开机键也不按:键和 PG 在同一片 PCA9555 上,读不到 PG 时去按只会卡在 CLEANUP。 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 读得到、但一直是低(电源确实没好)走的是同一条路:照样不自作主张断电。 */
|
/* 读得到、但一直是低(电源确实没好)走的是同一条路:照样不自作主张断电。 */
|
||||||
@@ -360,6 +377,8 @@ static void test_late_pg_is_still_accepted(void)
|
|||||||
|
|
||||||
s_pg_val[rail - 1] = true;
|
s_pg_val[rail - 1] = true;
|
||||||
run_ms(ACT_HR_PG_POLL_MS);
|
run_ms(ACT_HR_PG_POLL_MS);
|
||||||
|
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_BOOT, t_actuator_state(rail));
|
||||||
|
run_ms(ACT_HR_BOOT_KEY_MS);
|
||||||
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||||
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
|
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
|
||||||
}
|
}
|
||||||
@@ -385,8 +404,8 @@ static void test_the_task_loop_only_sleeps_what_step_asks_for(void)
|
|||||||
|
|
||||||
for (int i = 0; i < 10000; i++) {
|
for (int i = 0; i < 10000; i++) {
|
||||||
const uint32_t w = t_actuator_step(); /* 任务体:它说睡多久就睡多久 */
|
const uint32_t w = t_actuator_step(); /* 任务体:它说睡多久就睡多久 */
|
||||||
if (!t_actuator_busy(rail)) {
|
if (t_actuator_state(rail) == ACT_ST_HR_BOOT) {
|
||||||
total = hal_time_now_ms() - t0; /* 收工的**那一刻**,不是下一次醒来 */
|
total = hal_time_now_ms() - t0; /* 等到 PG 的**那一刻**,不是下一次醒来 */
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
sim_time_advance_ms(w == 0 ? 1 : w);
|
sim_time_advance_ms(w == 0 ? 1 : w);
|
||||||
@@ -396,9 +415,18 @@ static void test_the_task_loop_only_sleeps_what_step_asks_for(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TEST_ASSERT_TRUE(pg_raised);
|
TEST_ASSERT_TRUE(pg_raised);
|
||||||
|
TEST_ASSERT_TRUE(total > 0 && total <= pg_at + 2 * ACT_HR_PG_POLL_MS);
|
||||||
|
|
||||||
|
/* 按键保持期也照样听 step 的:睡到点就松键收工,不多睡一整段。 */
|
||||||
|
for (int i = 0; i < 10000 && t_actuator_busy(rail); i++) {
|
||||||
|
const uint32_t w = t_actuator_step();
|
||||||
|
if (t_actuator_busy(rail)) {
|
||||||
|
sim_time_advance_ms(w == 0 ? 1 : w);
|
||||||
|
}
|
||||||
|
}
|
||||||
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||||
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
|
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
|
||||||
TEST_ASSERT_TRUE(total <= pg_at + 2 * ACT_HR_PG_POLL_MS);
|
TEST_ASSERT_TRUE(hal_time_now_ms() - t0 <= total + ACT_HR_BOOT_KEY_MS + ACT_CLEANUP_RETRY_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 硬重启之前先把这一路的三个键松开:断电重启时有键按着,手机会开进
|
/* 硬重启之前先把这一路的三个键松开:断电重启时有键按着,手机会开进
|
||||||
@@ -544,7 +572,7 @@ static void test_the_key_hold_never_holds_the_i2c_lock(void)
|
|||||||
TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C)); /* 写完当场就放掉 */
|
TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C)); /* 写完当场就放掉 */
|
||||||
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
|
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
|
||||||
|
|
||||||
for (int i = 0; i < 120; i++) {
|
for (int i = 0; i < (int)(ACT_KEY_MS_ON / 20) + 20; i++) {
|
||||||
(void)t_actuator_step();
|
(void)t_actuator_step();
|
||||||
TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C));
|
TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C));
|
||||||
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
|
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
|
||||||
@@ -836,6 +864,26 @@ static void test_abort_during_hard_reset_does_not_restore_power(void)
|
|||||||
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(en)); /* 还是关着的,等上层拿主意 */
|
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(en)); /* 还是关着的,等上层拿主意 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 开机键按着的时候被打断(过压关路走的就是这条):键当场松开,电不动。
|
||||||
|
键留在按下状态的现象是手机再也起不来,而遥测上一切正常。 */
|
||||||
|
static void test_abort_during_boot_press_releases_the_key(void)
|
||||||
|
{
|
||||||
|
const int rail = 2;
|
||||||
|
const int en = rail_cfg(rail)->efuse_en;
|
||||||
|
|
||||||
|
s_pg_val[rail - 1] = true;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
|
||||||
|
run_ms(ACT_HR_OFF_MS + ACT_HR_RAMP_MS + 1000);
|
||||||
|
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_BOOT, t_actuator_state(rail));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, key_pin(rail, PCA9555_KEY_PWR));
|
||||||
|
|
||||||
|
t_actuator_abort(rail);
|
||||||
|
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||||
|
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, key_pin(rail, PCA9555_KEY_PWR));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(en));
|
||||||
|
}
|
||||||
|
|
||||||
/* EDL 跑到「已插 USB」那一段被打断:`EDL_EN` 立刻松(那是本序列按下去的),
|
/* EDL 跑到「已插 USB」那一段被打断:`EDL_EN` 立刻松(那是本序列按下去的),
|
||||||
**`VBUS_EN` 一动不动** —— R2 里它和按键在同一个字节里,一不小心就会被
|
**`VBUS_EN` 一动不动** —— R2 里它和按键在同一个字节里,一不小心就会被
|
||||||
收尾那次读改写顺手清掉,而现象是「手机莫名其妙从 9008 掉线」。 */
|
收尾那次读改写顺手清掉,而现象是「手机莫名其妙从 9008 掉线」。 */
|
||||||
@@ -972,7 +1020,9 @@ static void test_timings_match_the_documents(void)
|
|||||||
TEST_ASSERT_TRUE(ACT_HR_PG_POLL_MS > 0 && ACT_HR_PG_POLL_MS * 10 <= ACT_HR_PG_TIMEOUT_MS);
|
TEST_ASSERT_TRUE(ACT_HR_PG_POLL_MS > 0 && ACT_HR_PG_POLL_MS * 10 <= ACT_HR_PG_TIMEOUT_MS);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_UINT32(100, ACT_KEY_MS_TAP); /* SW-5 三档 */
|
TEST_ASSERT_EQUAL_UINT32(100, ACT_KEY_MS_TAP); /* SW-5 三档 */
|
||||||
TEST_ASSERT_EQUAL_UINT32(2000, ACT_KEY_MS_ON);
|
TEST_ASSERT_EQUAL_UINT32(5000, ACT_KEY_MS_ON); /* 小米8 实测 2 s 不够(t_actuator.h) */
|
||||||
|
TEST_ASSERT_TRUE(ACT_KEY_MS_ON < ACT_KEY_MS_FORCE); /* 开机那一下不能按成强制重启 */
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(ACT_KEY_MS_ON, ACT_HR_BOOT_KEY_MS);
|
||||||
TEST_ASSERT_EQUAL_UINT32(10000, ACT_KEY_MS_FORCE);
|
TEST_ASSERT_EQUAL_UINT32(10000, ACT_KEY_MS_FORCE);
|
||||||
TEST_ASSERT_TRUE(ACT_KEY_MS_FORCE <= ACT_KEY_MAX_MS); /* 最长那一档不能被自己的上限拦掉 */
|
TEST_ASSERT_TRUE(ACT_KEY_MS_FORCE <= ACT_KEY_MAX_MS); /* 最长那一档不能被自己的上限拦掉 */
|
||||||
|
|
||||||
@@ -1075,6 +1125,7 @@ int main(void)
|
|||||||
RUN_TEST(test_a_failed_press_still_goes_through_cleanup);
|
RUN_TEST(test_a_failed_press_still_goes_through_cleanup);
|
||||||
RUN_TEST(test_abort_releases_the_key_and_leaves_the_pins_alone);
|
RUN_TEST(test_abort_releases_the_key_and_leaves_the_pins_alone);
|
||||||
RUN_TEST(test_abort_during_hard_reset_does_not_restore_power);
|
RUN_TEST(test_abort_during_hard_reset_does_not_restore_power);
|
||||||
|
RUN_TEST(test_abort_during_boot_press_releases_the_key);
|
||||||
RUN_TEST(test_abort_during_edl_releases_the_short_but_keeps_vbus);
|
RUN_TEST(test_abort_during_edl_releases_the_short_but_keeps_vbus);
|
||||||
|
|
||||||
RUN_TEST(test_actuator_and_safety_share_one_lock_without_starving);
|
RUN_TEST(test_actuator_and_safety_share_one_lock_without_starving);
|
||||||
|
|||||||
@@ -296,6 +296,7 @@ static void test_thresholds_match_the_documents(void)
|
|||||||
{
|
{
|
||||||
/* SR-5 / SW-2 的两个数,逐字来自需求 §9 那两行。 */
|
/* SR-5 / SW-2 的两个数,逐字来自需求 §9 那两行。 */
|
||||||
TEST_ASSERT_EQUAL_INT(100, T_SAFETY_DEV_MV);
|
TEST_ASSERT_EQUAL_INT(100, T_SAFETY_DEV_MV);
|
||||||
|
TEST_ASSERT_EQUAL_INT(250, T_SAFETY_UNDER_MV); /* 欠压一侧,2026-09-28 放宽 */
|
||||||
TEST_ASSERT_EQUAL_INT(20, T_SAFETY_DEV_HOLD_MS);
|
TEST_ASSERT_EQUAL_INT(20, T_SAFETY_DEV_HOLD_MS);
|
||||||
|
|
||||||
/* 判据数的是**圈**,SR-5 写的是**毫秒** —— 这一条把两者钉在一起。
|
/* 判据数的是**圈**,SR-5 写的是**毫秒** —— 这一条把两者钉在一起。
|
||||||
@@ -321,6 +322,7 @@ static void test_thresholds_match_the_documents(void)
|
|||||||
/* §12.1 注入的是 ±150 mV / −60 mA,两个都必须落在阈值**外侧**,
|
/* §12.1 注入的是 ±150 mV / −60 mA,两个都必须落在阈值**外侧**,
|
||||||
否则验收测的是「阈值刚好卡在注入值上」。 */
|
否则验收测的是「阈值刚好卡在注入值上」。 */
|
||||||
TEST_ASSERT_TRUE(150 > T_SAFETY_DEV_MV);
|
TEST_ASSERT_TRUE(150 > T_SAFETY_DEV_MV);
|
||||||
|
TEST_ASSERT_TRUE(300 > T_SAFETY_UNDER_MV); /* 欠压注入 −300 mV */
|
||||||
TEST_ASSERT_TRUE(-60000 < T_SAFETY_REV_UA);
|
TEST_ASSERT_TRUE(-60000 < T_SAFETY_REV_UA);
|
||||||
|
|
||||||
/* ⭐ R2 新增:mux 连续选不中多少次才算失联。
|
/* ⭐ R2 新增:mux 连续选不中多少次才算失联。
|
||||||
@@ -365,7 +367,7 @@ static void test_the_cut_lands_exactly_one_hold_window_after_first_sight(void)
|
|||||||
{
|
{
|
||||||
sim_i2c_set_bus_time(false);
|
sim_i2c_set_bus_time(false);
|
||||||
power_up(2);
|
power_up(2);
|
||||||
inject(2, NOMINAL_MV - 150, 1000000);
|
inject(2, NOMINAL_MV - 300, 1000000);
|
||||||
|
|
||||||
const uint32_t t_first_sight = hal_time_now_ms();
|
const uint32_t t_first_sight = hal_time_now_ms();
|
||||||
beat();
|
beat();
|
||||||
@@ -409,7 +411,7 @@ static void test_the_cut_is_never_late_because_of_sub_millisecond_jitter(void)
|
|||||||
static void test_undervoltage_cuts_the_rail_too(void)
|
static void test_undervoltage_cuts_the_rail_too(void)
|
||||||
{
|
{
|
||||||
power_up(3);
|
power_up(3);
|
||||||
inject(3, NOMINAL_MV - 150, 1000000);
|
inject(3, NOMINAL_MV - 300, 1000000);
|
||||||
beats(2);
|
beats(2);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_INT(0, en_level(3));
|
TEST_ASSERT_EQUAL_INT(0, en_level(3));
|
||||||
@@ -441,12 +443,28 @@ static void test_exactly_one_hundred_millivolts_is_still_in_range(void)
|
|||||||
beats(10);
|
beats(10);
|
||||||
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
||||||
|
|
||||||
inject(1, NOMINAL_MV - T_SAFETY_DEV_MV, 1000000);
|
inject(1, NOMINAL_MV - T_SAFETY_UNDER_MV, 1000000);
|
||||||
beats(10);
|
beats(10);
|
||||||
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
||||||
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ⭐ 上板实测那一幕:手机开机峰值 2.1 A 时读回 −115 mV。放宽之前这里会关路,
|
||||||
|
把正在开机的手机直接断电。过压那一侧同样的偏差照旧要关。 */
|
||||||
|
static void test_boot_current_droop_does_not_cut_the_rail(void)
|
||||||
|
{
|
||||||
|
power_up(1);
|
||||||
|
inject(1, NOMINAL_MV - 115, 2136000);
|
||||||
|
beats(10);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
||||||
|
|
||||||
|
inject(1, NOMINAL_MV + 115, 1000000);
|
||||||
|
beats(3);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, en_level(1));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_OVER_VOLT, g_ev[0].type);
|
||||||
|
}
|
||||||
|
|
||||||
/* 比的是**该路 DAC 的设定值**,不是 3900 这个常数。SW-4 允许单路微调,
|
/* 比的是**该路 DAC 的设定值**,不是 3900 这个常数。SW-4 允许单路微调,
|
||||||
拿常数比的话,一台被调到 4.010 V 的手机会被当成过压关掉 —— 而且只有那一路。 */
|
拿常数比的话,一台被调到 4.010 V 的手机会被当成过压关掉 —— 而且只有那一路。 */
|
||||||
static void test_the_reference_is_the_setpoint_not_a_constant(void)
|
static void test_the_reference_is_the_setpoint_not_a_constant(void)
|
||||||
@@ -460,11 +478,13 @@ static void test_the_reference_is_the_setpoint_not_a_constant(void)
|
|||||||
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
||||||
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
||||||
|
|
||||||
/* 回到 3900:现在这才是「偏低 110 mV」。 */
|
/* 调到最低档,实测仍是 3900:拿设定值比才是「偏高」。
|
||||||
|
(原来是调到最高档再回 3900 判偏低 110 mV —— 欠压放宽到 −250 mV 之后那已经不越界了。) */
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_set_mv(1, RAIL_MV_MIN));
|
||||||
inject(1, NOMINAL_MV, 1000000);
|
inject(1, NOMINAL_MV, 1000000);
|
||||||
beats(2);
|
beats(2);
|
||||||
TEST_ASSERT_EQUAL_INT(0, en_level(1));
|
TEST_ASSERT_EQUAL_INT(0, en_level(1));
|
||||||
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_UNDER_VOLT, g_ev[0].type);
|
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_OVER_VOLT, g_ev[0].type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 判据什么时候**不**武装(三道闸,每一道都是一次真事故的预防) -------- */
|
/* ---- 判据什么时候**不**武装(三道闸,每一道都是一次真事故的预防) -------- */
|
||||||
@@ -507,7 +527,7 @@ static void test_a_blind_rail_keeps_its_power(void)
|
|||||||
/* ⚠️ 顺序是有讲究的:**先让坏值真的读进快照一次**,再让器件失联。
|
/* ⚠️ 顺序是有讲究的:**先让坏值真的读进快照一次**,再让器件失联。
|
||||||
反过来写(先失联再改寄存器)的话快照里躺着的是个正常值,
|
反过来写(先失联再改寄存器)的话快照里躺着的是个正常值,
|
||||||
「盲跑那一路照样判据」这个错就一点痕迹都不会留下 —— 测试永远绿。 */
|
「盲跑那一路照样判据」这个错就一点痕迹都不会留下 —— 测试永远绿。 */
|
||||||
inject(4, NOMINAL_MV - 150, 1000000);
|
inject(4, NOMINAL_MV - 300, 1000000);
|
||||||
beat();
|
beat();
|
||||||
TEST_ASSERT_EQUAL_INT(1, en_level(4)); /* 第一个样本只起表 */
|
TEST_ASSERT_EQUAL_INT(1, en_level(4)); /* 第一个样本只起表 */
|
||||||
sim_i2c_set_present_ch(ch_of(4), rail_cfg(4)->ina_addr, false);
|
sim_i2c_set_present_ch(ch_of(4), rail_cfg(4)->ina_addr, false);
|
||||||
@@ -517,7 +537,7 @@ static void test_a_blind_rail_keeps_its_power(void)
|
|||||||
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
||||||
TEST_ASSERT_FALSE(g_snap.rail_ok[3]);
|
TEST_ASSERT_FALSE(g_snap.rail_ok[3]);
|
||||||
/* 快照里躺着的确实是那个越界的旧值 —— 这是上面那句「顺序有讲究」的凭据。 */
|
/* 快照里躺着的确实是那个越界的旧值 —— 这是上面那句「顺序有讲究」的凭据。 */
|
||||||
TEST_ASSERT_EQUAL_INT32(NOMINAL_MV - 150, g_snap.rail[3].bus_mv);
|
TEST_ASSERT_EQUAL_INT32(NOMINAL_MV - 300, g_snap.rail[3].bus_mv);
|
||||||
|
|
||||||
/* 器件回来了,值还是坏的 —— 这时才该关,而且要重新数满 20 ms。 */
|
/* 器件回来了,值还是坏的 —— 这时才该关,而且要重新数满 20 ms。 */
|
||||||
sim_i2c_set_present_ch(ch_of(4), rail_cfg(4)->ina_addr, true);
|
sim_i2c_set_present_ch(ch_of(4), rail_cfg(4)->ina_addr, true);
|
||||||
@@ -545,7 +565,7 @@ static void test_the_cut_drops_vbus_inside_the_lock_on_this_rails_channel(void)
|
|||||||
|
|
||||||
const uint32_t en_seq_before = sim_gpio_write_seq(rail_cfg(rail)->efuse_en);
|
const uint32_t en_seq_before = sim_gpio_write_seq(rail_cfg(rail)->efuse_en);
|
||||||
|
|
||||||
inject(rail, NOMINAL_MV - 150, 1000000); /* 欠压,不是反灌 */
|
inject(rail, NOMINAL_MV - 300, 1000000); /* 欠压,不是反灌 */
|
||||||
beat(); /* 第一圈只起表 */
|
beat(); /* 第一圈只起表 */
|
||||||
TEST_ASSERT_EQUAL_UINT32(0, g_w[rail - 1].n);
|
TEST_ASSERT_EQUAL_UINT32(0, g_w[rail - 1].n);
|
||||||
|
|
||||||
@@ -1177,6 +1197,7 @@ int main(void)
|
|||||||
RUN_TEST(test_undervoltage_cuts_the_rail_too);
|
RUN_TEST(test_undervoltage_cuts_the_rail_too);
|
||||||
RUN_TEST(test_a_one_cycle_glitch_does_not_cut_anything);
|
RUN_TEST(test_a_one_cycle_glitch_does_not_cut_anything);
|
||||||
RUN_TEST(test_exactly_one_hundred_millivolts_is_still_in_range);
|
RUN_TEST(test_exactly_one_hundred_millivolts_is_still_in_range);
|
||||||
|
RUN_TEST(test_boot_current_droop_does_not_cut_the_rail);
|
||||||
RUN_TEST(test_the_reference_is_the_setpoint_not_a_constant);
|
RUN_TEST(test_the_reference_is_the_setpoint_not_a_constant);
|
||||||
|
|
||||||
RUN_TEST(test_a_rail_that_is_off_is_never_judged);
|
RUN_TEST(test_a_rail_that_is_off_is_never_judged);
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ USB 通断」这些原本要人手做的动作全部电气化,交给板上 MCU
|
|||||||
| **SR-2** | **第一层 · 拓扑限幅**:分压网络与 DAC 量程在物理上把输出限死在 3.42 ~ 4.12 V(含最坏容差),全部落在正常锂电电压内。防固件 bug / I²C 误码 / MCU 跑飞 | §4 |
|
| **SR-2** | **第一层 · 拓扑限幅**:分压网络与 DAC 量程在物理上把输出限死在 3.42 ~ 4.12 V(含最坏容差),全部落在正常锂电电压内。防固件 bug / I²C 误码 / MCU 跑飞 | §4 |
|
||||||
| **SR-3** | **第二层 · eFuse 独立过压断开**(每路一颗,**最后一道硬件防线**):OVP 4.36 V 标称、**断开而非钳位**、响应 2 µs;限流 4.45 A 恒流、不依赖 MCU | §5 |
|
| **SR-3** | **第二层 · eFuse 独立过压断开**(每路一颗,**最后一道硬件防线**):OVP 4.36 V 标称、**断开而非钳位**、响应 2 µs;限流 4.45 A 恒流、不依赖 MCU | §5 |
|
||||||
| **SR-4** | eFuse 的过压响应**必须来得及**:按最坏值(Buck 高侧限流 14 A、负载 2200 µF)核算,余量 ≥ 10 倍 | §5 |
|
| **SR-4** | eFuse 的过压响应**必须来得及**:按最坏值(Buck 高侧限流 14 A、负载 2200 µF)核算,余量 ≥ 10 倍 | §5 |
|
||||||
| **SR-5** | **第三层 · 软件监控**:电压偏离设定 >±100 mV 且持续 20 ms → 拉低该路 EN 并上报。**仅承担记录与告警,不承担实时保护** | §5 |
|
| **SR-5** | **第三层 · 软件监控**:电压高于设定 >100 mV 或低于设定 >250 mV(欠压一侧 2026-09-28 放宽:小米8 带屏开机峰值 2.1 A 时实测 −115 mV),且持续 20 ms → 拉低该路 EN 并上报。**仅承担记录与告警,不承担实时保护** | §5 |
|
||||||
| **SR-6** | **复位安全**:ESP32 **非计划**复位(上电、崩溃、看门狗、按 EN、esptool)期间,五路输出全关、五路 VBUS 全断、15 路按键全不按、5 路 EDL 全不触发、风扇不转。靠**上下拉电阻**保证,不靠固件。<br>**例外(2026-09-27 用户定)**:固件自己发起的**计划内重启**(OTA 换固件、自证超时回滚)锁住 `EFUSE_EN`、新固件接管,手机不断电;按键 / EDL 仍全松,VBUS 保持。见 §10.9「重启不断电」 | §8.1 / §22.4 |
|
| **SR-6** | **复位安全**:ESP32 **非计划**复位(上电、崩溃、看门狗、按 EN、esptool)期间,五路输出全关、五路 VBUS 全断、15 路按键全不按、5 路 EDL 全不触发、风扇不转。靠**上下拉电阻**保证,不靠固件。<br>**例外(2026-09-27 用户定)**:固件自己发起的**计划内重启**(OTA 换固件、自证超时回滚)锁住 `EFUSE_EN`、新固件接管,手机不断电;按键 / EDL 仍全松,VBUS 保持。见 §10.9「重启不断电」 | §8.1 / §22.4 |
|
||||||
| **SR-7** | 放电 MOS 与 eFuse EN **硬件反相联锁**,物理上不可能同时导通 | §7.3 |
|
| **SR-7** | 放电 MOS 与 eFuse EN **硬件反相联锁**,物理上不可能同时导通 | §7.3 |
|
||||||
| **SR-8** | **输入保护**:保险丝 + TVS + P-MOS 反接保护 | §2 |
|
| **SR-8** | **输入保护**:保险丝 + TVS + P-MOS 反接保护 | §2 |
|
||||||
@@ -277,10 +277,10 @@ USB 通断」这些原本要人手做的动作全部电气化,交给板上 MCU
|
|||||||
| ID | 需求 |
|
| ID | 需求 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **SW-1** | 崩溃检测 + 两级自动重启(§4.1)—— **整块板子最核心的价值** |
|
| **SW-1** | 崩溃检测 + 两级自动重启(§4.1)—— **整块板子最核心的价值** |
|
||||||
| **SW-2** | 20 ms 周期巡检六路 INA226,电压偏离 >±100 mV 持续 20 ms 即关路告警(SR-5) |
|
| **SW-2** | 20 ms 周期巡检六路 INA226,电压偏离 >+100 / −250 mV 持续 20 ms 即关路告警(SR-5) |
|
||||||
| **SW-3** | 过流 / 故障事件记录:路号、时刻、电流值 —— 排查排线接触不良的唯一线索 |
|
| **SW-3** | 过流 / 故障事件记录:路号、时刻、电流值 —— 排查排线接触不良的唯一线索 |
|
||||||
| **SW-4** | 单路电压微调接口(跑不稳的手机提到 4.0 V,或反向降压降温),**上限受 SR-13 约束** |
|
| **SW-4** | 单路电压微调接口(跑不稳的手机提到 4.0 V,或反向降压降温),**上限受 SR-13 约束** |
|
||||||
| **SW-5** | 侧键序列:短按 100 ms 唤醒 / 长按 2 s 开机 / 长按 10 s 强制重启 / VOL−+PWR 进 Fastboot / VOL++PWR 进 Recovery |
|
| **SW-5** | 侧键序列:短按 100 ms 唤醒 / 长按 5 s 开机(小米8 实测 2 s 不够,2026-09-28)/ 长按 10 s 强制重启 / VOL−+PWR 进 Fastboot / VOL++PWR 进 Recovery |
|
||||||
| **SW-6** | EDL 时序**固定为** `EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0`(先短接再插 USB) |
|
| **SW-6** | EDL 时序**固定为** `EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0`(先短接再插 USB) |
|
||||||
| **SW-7** | 开机脚本执行「关充电」(写 `input_suspend` 类节点),路径需在首台机上确认后写死 |
|
| **SW-7** | 开机脚本执行「关充电」(写 `input_suspend` 类节点),路径需在首台机上确认后写死 |
|
||||||
| **SW-8** | USB 反灌保护:检测到该路电流转负或母线上升,**200 ms 内**断开该路 VBUS |
|
| **SW-8** | USB 反灌保护:检测到该路电流转负或母线上升,**200 ms 内**断开该路 VBUS |
|
||||||
@@ -1010,7 +1010,7 @@ FR-12 是「中(可选)」,建议**首版先不做**,把接口留出来
|
|||||||
| PR-4 / PR-5 | 扫 DAC 全量程 | 输出扫过 3.52 ~ 4.01 V,与公式误差 <1%;**任何码值都到不了 4.2 V** |
|
| PR-4 / PR-5 | 扫 DAC 全量程 | 输出扫过 3.52 ~ 4.01 V,与公式误差 <1%;**任何码值都到不了 4.2 V** |
|
||||||
| SR-2 | 固件写越界值 | 输出被拓扑限死,不越过上限 |
|
| SR-2 | 固件写越界值 | 输出被拓扑限死,不越过上限 |
|
||||||
| SR-3 / SR-4 | 人为断开反馈下电阻 | eFuse 断开,输出侧尖峰不超过 4.7 V |
|
| SR-3 / SR-4 | 人为断开反馈下电阻 | eFuse 断开,输出侧尖峰不超过 4.7 V |
|
||||||
| SR-5 | 注入 ±150 mV 偏差 | 20 ms 内关路并产生告警记录 |
|
| SR-5 | 注入 +150 / −300 mV 偏差 | 20 ms 内关路并产生告警记录 |
|
||||||
| SR-6 | 手动复位 ESP32 | 示波器确认五路立即关断、15 路按键无一误触发、EDL 无触发 |
|
| SR-6 | 手动复位 ESP32 | 示波器确认五路立即关断、15 路按键无一误触发、EDL 无触发 |
|
||||||
| SR-7 | 固件同时置位 EN 与放电 | 硬件互斥,10 Ω 上无持续功耗 |
|
| SR-7 | 固件同时置位 EN 与放电 | 硬件互斥,10 Ω 上无持续功耗 |
|
||||||
| SR-9 | 可调电源把母线升到 13.5 V | 泄放导通并拉回 13.2 V 以下 |
|
| SR-9 | 可调电源把母线升到 13.5 V | 泄放导通并拉回 13.2 V 以下 |
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户