固件 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]>
这个提交包含在:
+64
-15
@@ -7,10 +7,14 @@
|
||||
| WiFi / IP 事件(掉线、拿到地址) | 系统事件循环 |
|
||||
| MQTT 事件(连上、收到命令) | esp-mqtt 自己的任务,优先级压到与 `t_link` 相同(§10.3:链路最低) |
|
||||
| **MQTT 客户端的创建 / 销毁、重配置、遥测节拍** | `t_link`(`mlink_esp_poll()`)—— 生命周期只在一个任务里管 |
|
||||
| 回应 / 遥测的发布 | 执行器任务(cmd_queue 的回调)—— 只用 `esp_mqtt_client_enqueue()`,**不阻塞** |
|
||||
| 回应 / 遥测的发布 | 执行器任务只把一份拷贝塞进 `s_outbox`(不等);`t_link` 取出来 `esp_mqtt_client_enqueue()` |
|
||||
|
||||
`esp_mqtt_client_publish()` 在网络慢的时候会阻塞到超时,放在执行器任务里就是让
|
||||
「按键该松了」等 TCP。enqueue 只进发件箱,由 esp-mqtt 自己的任务发。
|
||||
⚠️ **执行器任务一律不碰 esp-mqtt 的任何 API**,连 `esp_mqtt_client_enqueue()` 也不行:
|
||||
它要拿客户端的 `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) -----------------------------------------
|
||||
WiFi 断、broker 断,本文件只做一件事:**重连**(WiFi 指数退避,MQTT 由 esp-mqtt 每 10 s 重试)。
|
||||
@@ -25,6 +29,7 @@
|
||||
#include "esp_timer.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "hal_time.h"
|
||||
#include "mlink.h"
|
||||
@@ -65,6 +70,20 @@ static mlink_tele_t s_tele;
|
||||
static volatile bool s_tele_inflight;
|
||||
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,命令处理器随时读得到一份完整的 ------------ */
|
||||
|
||||
/* 可变参数:语句里带逗号(snprintf 的实参)也能整个传进来。 */
|
||||
@@ -119,39 +138,66 @@ static void on_cfg_changed(void *ctx)
|
||||
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) {
|
||||
return;
|
||||
/* 1.5 KB 不放栈上。执行器和 esp-mqtt 任务(link_error)都会进来 ⇒ s_out_mtx 护着。
|
||||
这把锁持有期间只有 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) {
|
||||
(void)esp_mqtt_client_enqueue(s_mqtt, mlink_topic(t), data, (int)len, qos, retain, true);
|
||||
/* 没连上就不攒(上线后 on_mqtt 会先补一条遥测)。读一个枚举,不拿 s_mtx。 */
|
||||
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)
|
||||
{
|
||||
(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)
|
||||
{
|
||||
(void)ctx;
|
||||
enqueue(MLINK_T_TELE, line, len, 0, 0);
|
||||
ST_EDIT(s_st.tele_sent++);
|
||||
const bool ok = enqueue(MLINK_T_TELE, line, len, 0);
|
||||
ST_EDIT(if (ok) { s_st.tele_sent++; } else { s_st.tele_skipped++; });
|
||||
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)
|
||||
{
|
||||
char out[96];
|
||||
const size_t n = ulink_link_error(payload, err, out, sizeof(out));
|
||||
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)
|
||||
{
|
||||
s_mtx = xSemaphoreCreateMutex();
|
||||
s_out_mtx = xSemaphoreCreateMutex();
|
||||
s_outbox = xQueueCreate(OUTBOX_DEPTH, sizeof(out_msg_t));
|
||||
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(); /* NVS 分区格式不认:清掉重来(里面只有本模块的配置与 WiFi 校准) */
|
||||
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));
|
||||
netcfg_init(NULL, NULL, NULL, NULL);
|
||||
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') {
|
||||
mqtt_start();
|
||||
}
|
||||
outbox_drain();
|
||||
if (s_st.mqtt != NET_UP || !mlink_tele_due(&s_tele, hal_time_now_ms(), s_cur.tele_ms)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.1-c3"
|
||||
#define PHONERAIL_FW_VERSION "0.1.1-c5"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §2。 */
|
||||
#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: {
|
||||
bool pg = false;
|
||||
if (pg_now(rail, &pg) && pg) {
|
||||
finish(r, ACT_RES_OK, now);
|
||||
return ACT_IDLE_WAIT_MS;
|
||||
/* 电好了 ⇒ 长按电源键开机(ACT_HR_BOOT_KEY_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) {
|
||||
/* ⚠️ 电**继续开着**。见 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;
|
||||
}
|
||||
|
||||
case ACT_ST_HR_BOOT:
|
||||
case ACT_ST_KEY_HOLD:
|
||||
if (el < r->hold_ms) {
|
||||
return r->hold_ms - el;
|
||||
@@ -232,7 +243,7 @@ hal_err_t t_actuator_hard_reset(int rail)
|
||||
return err; /* 序列没开始 */
|
||||
}
|
||||
r->seq = ACT_SEQ_HARD_RESET;
|
||||
r->release_mask = 0; /* 硬重启在链上没按下任何东西 */
|
||||
r->release_mask = 0; /* 断电这一段没按任何东西;开机那一下在 HR_PG 里才记 */
|
||||
r->result = ACT_RES_NONE;
|
||||
enter(r, ACT_ST_HR_OFF, hal_time_now_ms());
|
||||
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_RAMP: return "hr-ramp";
|
||||
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_EDL_SHORT: return "edl-short";
|
||||
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 |
|
||||
| EDL | `EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0` | SW-6 / §22.4 |
|
||||
|
||||
@@ -80,9 +80,19 @@ extern "C" {
|
||||
会退化成「一律等满 500 ms」,而测试与遥测都看不出区别。 */
|
||||
#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_ON 2000 /* 长按:开机 */
|
||||
#define ACT_KEY_MS_ON 5000 /* 长按:开机 */
|
||||
#define ACT_KEY_MS_FORCE 10000 /* 长按:强制重启(两级重启的「一级(软)」) */
|
||||
|
||||
/* 按键时长上限。上层传进来一个 10 分钟,等于把电源键按死 ——
|
||||
@@ -138,6 +148,7 @@ typedef enum {
|
||||
ACT_ST_HR_OFF, /* `EFUSE_EN` 已拉低,等 3~5 s(放电由硬件自己做) */
|
||||
ACT_ST_HR_RAMP, /* `EFUSE_EN` 已拉高,等 17 ms 走完 tON + 斜坡 */
|
||||
ACT_ST_HR_PG, /* 等 PG 置位 */
|
||||
ACT_ST_HR_BOOT, /* PG 到了,电源键按着,保持 ACT_HR_BOOT_KEY_MS 开机 */
|
||||
ACT_ST_KEY_HOLD, /* 键已按下,保持 T */
|
||||
ACT_ST_EDL_SHORT, /* `EDL_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)---- */
|
||||
if (dev > T_SAFETY_DEV_MV || dev < -T_SAFETY_DEV_MV) {
|
||||
/* ---- SR-5:+100 / −250 mV 连着 T_SAFETY_DEV_HOLD_CYCLES 圈(= 持续 20 ms)---- */
|
||||
if (dev > T_SAFETY_DEV_MV || dev < -T_SAFETY_UNDER_MV) {
|
||||
if (st->dev_n < 0xFF) {
|
||||
st->dev_n++;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,12 @@ extern "C" {
|
||||
SW-4 允许运维单路微调,拿常数比会把一台调到 4.0 V 的机器当成过压。 */
|
||||
#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」——「持续」在这里数的是**圈**,不是墙钟毫秒:
|
||||
连着两圈新鲜样本都越界才算,而成立的那一刻就是断电的那一刻。
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户