固件:R2 迁移 R-0 ~ R-6(底板 + 5 片同构刀片)
2026-09-16 完成、当时未提交,这里补提交(原样,不含 09-27 上板的修改)。 - 器件层 / 拓扑层整体从 R1 换到 R2:TCA9548A 分通道、五路同址 INA226 0x40 + 输入侧 0x43、 刀片 PCA9555(VBUS_EN 挪到 P0.4)、CH224A 只读驱动、风扇闭环 t_thermal - 新增 i2cbus(mux 原子事务)、slot(刀片 I/O 门面)、host/qtest.sh - 74HC595 链退役(代码与测试保留) - 判据阈值与时间常量未动;19 个 ctest 全绿,idf.py build 通过 Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+120
-12
@@ -6,12 +6,17 @@
|
||||
#include "evlog.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.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 "phonerail.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "sched.h"
|
||||
#include "slot.h"
|
||||
#include "t_actuator.h"
|
||||
#include "t_safety.h"
|
||||
#include "tasks.h"
|
||||
@@ -64,10 +69,19 @@ static safety_snap_t s_snap;
|
||||
static bool act_pg_from_safety(void *ctx, int rail, bool *pg)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!s_snap.io_ok) {
|
||||
/* ⭐ R2:PCA9555 变成**每路一片**(五片同构、同地址 0x20,靠 mux 分开),
|
||||
所以快照里的输入也是每路一份 —— R1 那时全板只有一片,只有一份。
|
||||
⚠️ 这里的 `rail - 1` 是**数组下标**,不是由路号算出来的器件编号
|
||||
(rail_cfg.h 禁的是后者)。器件是哪一片由 `slot_*` 那一层查表决定。 */
|
||||
if (rail < 1 || rail > PHONERAIL_RAIL_COUNT) {
|
||||
return false;
|
||||
}
|
||||
*pg = pca9555_rail_pg(&s_snap.io, rail); /* 极性只在驱动里出现一次 */
|
||||
if (!s_snap.io_ok[rail - 1]) {
|
||||
return false;
|
||||
}
|
||||
/* 极性只在驱动里出现一次。⚠️ R2 的判读函数**不带 rail** —— 五片逐格相同,
|
||||
位号是板级常量(pca9555.h),传路号进去反而会让人以为五片位表不一样。 */
|
||||
*pg = pca9555_efuse_pg(&s_snap.io[rail - 1]);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -118,7 +132,10 @@ static void task_thermal(void *arg)
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, task_desc(TASK_THERMAL)->period_ms);
|
||||
for (;;) {
|
||||
/* A16:两路 NTC + 载板 LM75 → 风扇 PWM 闭环(TR-6 的 TACH 采样窗口)。 */
|
||||
/* A16:两路 NTC(IO4/IO5)→ 风扇 PWM 闭环(TR-6 的 TACH 采样窗口)。
|
||||
⚠️ R2 没有 LM75 了(载板整个不存在,计划 §5.10)。
|
||||
真正的任务体在 `main/t_thermal.c` 里,由 A16 那一包填;
|
||||
**这里不 include 它**,免得两个人同时改到同一处。 */
|
||||
sched_periodic_wait(&tick);
|
||||
}
|
||||
}
|
||||
@@ -174,6 +191,33 @@ static void task_log(void *arg)
|
||||
}
|
||||
}
|
||||
|
||||
/* USB2517I 的复位脉冲 —— **整个固件里唯一碰 `HUB_RST#` 的地方,而且只在启动调一次**。
|
||||
|
||||
低有效(板上 R716 上拉到 +3V3_HUB),给 1 ms 足够:手册要的是几十微秒量级。
|
||||
|
||||
⛔⛔ **运行期一个字都不许再写它**(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:失联 ≠ 断电)。 */
|
||||
static void hub_reset_pulse_once(void)
|
||||
{
|
||||
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;
|
||||
}
|
||||
(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)
|
||||
{
|
||||
const task_desc_t *d = task_desc(id);
|
||||
@@ -191,21 +235,32 @@ void app_main(void)
|
||||
|
||||
selftest_or_die();
|
||||
|
||||
/* 十根 EN 线配成输出并全部拉低。**先于一切外设**:DAC / eFuse 一上电就有
|
||||
输出,这两根线这时必须已经确定是低的。 */
|
||||
/* ⭐ R2:**五根** `EFUSE_EN` 配成输出并全部拉低(R1 是十根:另外五根 `VBUS_EN`
|
||||
已经挪到刀片的 PCA9555 上了)。**先于一切外设**:DAC / eFuse 一上电就有
|
||||
输出,这五根线这时必须已经确定是低的。 */
|
||||
hal_err_t err = rail_en_init();
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "EN 引脚初始化失败:%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
ESP_LOGI(TAG, "五路输出与 VBUS 全部关闭");
|
||||
/* ⚠️ 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,
|
||||
.scl_gpio = BOARD_PIN_I2C_SCL,
|
||||
.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);
|
||||
@@ -214,9 +269,62 @@ void app_main(void)
|
||||
abort();
|
||||
}
|
||||
|
||||
/* ⚠️ 器件初始化(INA226 的 Config/Calibration、DAC、PCA9555 方向、595 安全值)
|
||||
与**降级矩阵**是 A15 的启动顺序那一包。在那之前 t_safety 读回来的电流恒为 0
|
||||
(Calibration 还没写),这是预期的,不是故障。 */
|
||||
/* ⭐ 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 还没写),
|
||||
这是预期的,不是故障。 */
|
||||
|
||||
/* 执行器:PG 从巡检的快照里取,本任务自己不碰 I²C(t_actuator.h 开头那一段)。
|
||||
⚠️ 必须在 `spawn(TASK_SAFETY, ...)` 之前:判据要关路时会先 `t_actuator_abort()`,
|
||||
|
||||
在新工单中引用
屏蔽一个用户