固件: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]>
这个提交包含在:
+105
-92
@@ -1,4 +1,4 @@
|
||||
/* 五路配置表 + 启动自检 + 两根 EN 线。契约与来历见 rail_cfg.h。 */
|
||||
/* 五路配置表 + 启动自检 + `EFUSE_EN`。契约与来历见 rail_cfg.h。 */
|
||||
#include "rail_cfg.h"
|
||||
|
||||
#include <assert.h>
|
||||
@@ -9,27 +9,31 @@
|
||||
#include "hal_gpio.h"
|
||||
#include "ina226.h"
|
||||
#include "pca9555.h"
|
||||
#include "sr595.h"
|
||||
#include "slot.h"
|
||||
#include "tca9548a.h"
|
||||
|
||||
/* ---- 那张表 --------------------------------------------------------------
|
||||
|
||||
六列的出处:INA 地址 §8.2;DAC 片/通道 §8.2 + §26.18;按键位与 EDL 位
|
||||
§17.2 + §26.18 C(从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核过);
|
||||
两根 EN 的 GPIO §8.1(同样按 U20 的焊盘网络核过,2026-09-07)。
|
||||
六列的出处(全部 2026-09-16 从板上缓存逐格核过):
|
||||
· INA 地址 —— `.claude/r2/_blade_pcb_nets.json` 的 `U3` pin1/pin2 都接 GND ⇒ 0x40
|
||||
· mux 通道 —— `.claude/r2/_base_pcb_nets.json` 的 `U35`:SD0/SC0=SDA1/SCL1 → 槽 1,
|
||||
一路到 SD4/SC4=SDA5/SCL5 → 槽 5(通道 5 给 CH224A,通道 6/7 悬空)
|
||||
· DAC 片/通道 —— R1 §8.2 + §26.18,**D11b 之后一个字没改**
|
||||
· `EFUSE_EN` / `PRSNT#` —— `_base_pcb_nets.json` 的 `U20`(模组脚 → GPIO 见 board_pins.h)
|
||||
|
||||
⚠️ **一列都不许改成算式。** 六列里没有一列是顺排的。 */
|
||||
⚠️ 顺排的那几列也不许改成算式,`PRSNT#` 那一列就是现成的反例。 */
|
||||
static const rail_cfg_t k_cfg[PHONERAIL_RAIL_COUNT] = {
|
||||
/* INA DAC片 ch EFUSE VBUS PWR VUP VDN EDL FLT PG */
|
||||
/* CH1 */ { 0x40, 0x4A, 2, 39, 40, { 1, 2, 3 }, 16, 0, 0 },
|
||||
/* CH2 */ { 0x41, 0x4A, 0, 48, 10, { 19, 17, 18 }, 7, 1, 1 },
|
||||
/* CH3 */ { 0x44, 0x4B, 0, 6, 11, { 22, 21, 20 }, 6, 2, 2 },
|
||||
/* CH4 */ { 0x45, 0x4A, 1, 41, 42, { 9, 10, 11 }, 0, 3, 3 },
|
||||
/* CH5 */ { 0x42, 0x4B, 1, 8, 17, { 15, 14, 13 }, 5, 4, 4 },
|
||||
/* INA mux DAC片 ch EFUSE_EN PRSNT# */
|
||||
/* CH1 */ { INA226_ADDR_RAIL, 0, 0x4A, 2, 9, 15 },
|
||||
/* CH2 */ { INA226_ADDR_RAIL, 1, 0x4A, 0, 10, 16 },
|
||||
/* CH3 */ { INA226_ADDR_RAIL, 2, 0x4B, 0, 11, 17 },
|
||||
/* CH4 */ { INA226_ADDR_RAIL, 3, 0x4A, 1, 12, 18 },
|
||||
/* CH5 */ { INA226_ADDR_RAIL, 4, 0x4B, 1, 13, 7 }, /* ⚠️ PRSNT5# 不接着顺排 */
|
||||
};
|
||||
|
||||
_Static_assert(PHONERAIL_RAIL_COUNT == INA226_RAIL_COUNT, "五路数对不上 INA226");
|
||||
_Static_assert(PHONERAIL_RAIL_COUNT == DAC63204_RAIL_COUNT, "五路数对不上 DAC63204");
|
||||
_Static_assert(PHONERAIL_RAIL_COUNT == PCA9555_RAIL_COUNT, "五路数对不上 PCA9555");
|
||||
_Static_assert(PHONERAIL_RAIL_COUNT == PCA9555_SLOT_COUNT, "五路数对不上 PCA9555 实例数");
|
||||
|
||||
const rail_cfg_t *rail_cfg(int rail)
|
||||
{
|
||||
@@ -37,9 +41,8 @@ const rail_cfg_t *rail_cfg(int rail)
|
||||
return &k_cfg[rail - 1];
|
||||
}
|
||||
|
||||
/* ---- 自检之一:本表与四个驱动的表逐格对账 --------------------------------
|
||||
分家的现象是「命令发给了另一路」,功能测试测不出来,所以这里是**逐格**比,
|
||||
不是抽查。四个驱动的取值函数越界会 abort,但这里的 rail 恒在 1~5。 */
|
||||
/* ---- 自检之一:本表与驱动的表逐格对账 ------------------------------------
|
||||
分家的现象是「命令发给了另一路」,功能测试测不出来,所以这里是**逐格**比。 */
|
||||
static bool table_matches_drivers(const rail_cfg_t *t)
|
||||
{
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
@@ -51,27 +54,36 @@ static bool table_matches_drivers(const rail_cfg_t *t)
|
||||
if (c->dac_addr != dac63204_rail_addr(rail) || c->dac_ch != dac63204_rail_ch(rail)) {
|
||||
return false;
|
||||
}
|
||||
for (int k = 0; k < SR595_KEY_COUNT; k++) {
|
||||
if (c->key_bit[k] != (uint8_t)sr595_key_bit(rail, (sr595_key_t)k)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- 自检之二:mux 通道(R2 新增) ---------------------------------------
|
||||
三条:都在 0~7 之内、五路互不重复、**都不等于 PD 那一路**。
|
||||
第三条不是洁癖:通道 5 上是 CH224A,选到它再去读 0x40 会读不到任何东西,
|
||||
而现象是「这一路的电压电流突然全是 0」—— 看上去像刀片拔了。 */
|
||||
static bool mux_ok(const rail_cfg_t *t)
|
||||
{
|
||||
for (int i = 0; i < PHONERAIL_RAIL_COUNT; i++) {
|
||||
const uint8_t ch = t[i].mux_ch;
|
||||
if (ch >= TCA9548A_CH_COUNT) {
|
||||
return false;
|
||||
}
|
||||
if (ch == TCA9548A_CH_PD) {
|
||||
return false;
|
||||
}
|
||||
for (int j = i + 1; j < PHONERAIL_RAIL_COUNT; j++) {
|
||||
if (t[j].mux_ch == ch) {
|
||||
return false; /* 两路指向同一片刀片 */
|
||||
}
|
||||
}
|
||||
if (c->edl_bit != (uint8_t)sr595_edl_bit(rail)) {
|
||||
return false;
|
||||
}
|
||||
if (c->flt_bit != (uint8_t)pca9555_flt_bit(rail)) {
|
||||
return false;
|
||||
}
|
||||
if (c->pg_bit != (uint8_t)pca9555_pg_bit(rail)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- 自检之二:引脚无重复、无越界 ----------------------------------------
|
||||
/* ---- 自检之三:引脚无重复、无越界 ----------------------------------------
|
||||
查的是**全板**,不只是五路那十个脚:重复的两个脚一定分属两个模块,
|
||||
只查自己那十个的话,「`VBUS_EN2` 写成了 IO14」这种错(撞上 SDA)查不出来。 */
|
||||
只查自己那十个的话,「`PRSNT3#` 写成了 IO41」这种错(撞上 SDA)查不出来。 */
|
||||
|
||||
static bool pin_in(const int *list, size_t n, int pin)
|
||||
{
|
||||
@@ -88,6 +100,7 @@ static bool pin_usable(int pin)
|
||||
static const int absent[] = BOARD_PINS_ABSENT;
|
||||
static const int strapping[] = BOARD_PINS_STRAPPING;
|
||||
static const int reserved[] = BOARD_PINS_RESERVED;
|
||||
static const int uart0[] = BOARD_PINS_UART0;
|
||||
|
||||
if (pin < 0 || pin > BOARD_PIN_NUM_MAX) {
|
||||
return false;
|
||||
@@ -99,7 +112,10 @@ static bool pin_usable(int pin)
|
||||
return false;
|
||||
}
|
||||
if (pin_in(reserved, sizeof(reserved) / sizeof(reserved[0]), pin)) {
|
||||
return false; /* R8 版八线 PSRAM 占着,留空以便换料 */
|
||||
return false; /* N16R8 的八线 PSRAM 占着 */
|
||||
}
|
||||
if (pin_in(uart0, sizeof(uart0) / sizeof(uart0[0]), pin)) {
|
||||
return false; /* J33 应急口:Hub 挂掉之后唯一进得来的路(R2 D13) */
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -107,28 +123,24 @@ static bool pin_usable(int pin)
|
||||
static bool pins_ok(const rail_cfg_t *t)
|
||||
{
|
||||
/* 全板用掉的每一个脚。顺序无所谓,这里只查「都能用」和「不重复」。 */
|
||||
int used[2 * PHONERAIL_RAIL_COUNT + 14];
|
||||
int used[2 * PHONERAIL_RAIL_COUNT + 11];
|
||||
size_t n = 0;
|
||||
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
used[n++] = t[rail - 1].efuse_en;
|
||||
used[n++] = t[rail - 1].vbus_en;
|
||||
used[n++] = t[rail - 1].prsnt_n;
|
||||
}
|
||||
used[n++] = BOARD_PIN_I2C_SDA;
|
||||
used[n++] = BOARD_PIN_I2C_SCL;
|
||||
used[n++] = BOARD_PIN_FAN_TACH;
|
||||
used[n++] = BOARD_PIN_FAN_PWM;
|
||||
used[n++] = BOARD_PIN_MUX_RST_N;
|
||||
used[n++] = BOARD_PIN_IOEXP_INT_N;
|
||||
used[n++] = BOARD_PIN_INA_ALERT_N;
|
||||
used[n++] = BOARD_PIN_HUB_RST_N;
|
||||
used[n++] = BOARD_PIN_UP_VBUS_DET;
|
||||
used[n++] = BOARD_PIN_NTC1;
|
||||
used[n++] = BOARD_PIN_NTC2;
|
||||
used[n++] = BOARD_PIN_USB_DM;
|
||||
used[n++] = BOARD_PIN_USB_DP;
|
||||
/* 四根移位链的线与 PCA9555 的中断脚:**引用驱动里的宏,不重抄一遍数字**。 */
|
||||
used[n++] = SR595_PIN_SER;
|
||||
used[n++] = SR595_PIN_SRCLK;
|
||||
used[n++] = SR595_PIN_RCLK;
|
||||
used[n++] = SR595_PIN_OE_N;
|
||||
used[n++] = PCA9555_PIN_INT_N;
|
||||
used[n++] = BOARD_PIN_FAN_PWM;
|
||||
used[n++] = BOARD_PIN_FAN_EN;
|
||||
assert(n == sizeof(used) / sizeof(used[0]));
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
@@ -142,6 +154,29 @@ static bool pins_ok(const rail_cfg_t *t)
|
||||
}
|
||||
}
|
||||
|
||||
/* 两路 TACH 与 USB 的 D± 单独查:TACH 会与上面那些撞,而 D± 是硬连线,
|
||||
它们只要不和五路那十个脚撞就行。 */
|
||||
const int more[] = { BOARD_PIN_FAN_TACH_IN, BOARD_PIN_FAN_TACH_OUT };
|
||||
for (size_t k = 0; k < sizeof(more) / sizeof(more[0]); k++) {
|
||||
if (!pin_usable(more[k])) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (used[i] == more[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (BOARD_PIN_FAN_TACH_IN == BOARD_PIN_FAN_TACH_OUT) {
|
||||
return false;
|
||||
}
|
||||
/* 原生 USB 的两个脚是硬连线(IO19/IO20),谁也不能占。 */
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (used[i] == BOARD_PIN_USB_DM || used[i] == BOARD_PIN_USB_DP) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* 两路 NTC 必须在 ADC1(IO1~IO10)上:ADC2 在 WiFi 开着的时候读不了,
|
||||
而本板 WiFi 是常开的(§10.8 传输 A)。挪到 ADC2 上不会报错,
|
||||
只会在连上 WiFi 之后开始读失败 —— 台架上很难复现。 */
|
||||
@@ -158,7 +193,7 @@ uint32_t rail_selftest_table(const rail_cfg_t *t)
|
||||
uint32_t bad = 0;
|
||||
|
||||
if (t == NULL) {
|
||||
return RAIL_SELFTEST_TABLE | RAIL_SELFTEST_PINS;
|
||||
return RAIL_SELFTEST_TABLE | RAIL_SELFTEST_PINS | RAIL_SELFTEST_MUX;
|
||||
}
|
||||
if (!table_matches_drivers(t)) {
|
||||
bad |= RAIL_SELFTEST_TABLE;
|
||||
@@ -166,6 +201,9 @@ uint32_t rail_selftest_table(const rail_cfg_t *t)
|
||||
if (!pins_ok(t)) {
|
||||
bad |= RAIL_SELFTEST_PINS;
|
||||
}
|
||||
if (!mux_ok(t)) {
|
||||
bad |= RAIL_SELFTEST_MUX;
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
@@ -173,17 +211,14 @@ uint32_t rail_selftest(void)
|
||||
{
|
||||
uint32_t bad = 0;
|
||||
|
||||
/* 四个驱动各自的表(A3~A6 都留了这个谓词,A7 是它们的归宿)。 */
|
||||
/* 三个驱动各自的表(A3/A4/R-2 都留了这个谓词)。 */
|
||||
if (!ina226_addr_table_ok()) {
|
||||
bad |= RAIL_SELFTEST_INA;
|
||||
}
|
||||
if (!dac63204_map_ok()) {
|
||||
bad |= RAIL_SELFTEST_DAC;
|
||||
}
|
||||
if (!sr595_map_ok()) {
|
||||
bad |= RAIL_SELFTEST_SR595;
|
||||
}
|
||||
if (!pca9555_map_ok() || !pca9555_addr_ok()) {
|
||||
if (!pca9555_map_ok() || !pca9555_addr_ok() || !tca9548a_addr_ok()) {
|
||||
bad |= RAIL_SELFTEST_PCA9555;
|
||||
}
|
||||
return bad | rail_selftest_table(k_cfg);
|
||||
@@ -196,10 +231,10 @@ const char *rail_selftest_str(uint32_t bit)
|
||||
return "INA226 地址表";
|
||||
case RAIL_SELFTEST_DAC:
|
||||
return "DAC63204 映射表";
|
||||
case RAIL_SELFTEST_SR595:
|
||||
return "74HC595 位表";
|
||||
case RAIL_SELFTEST_MUX:
|
||||
return "mux 通道重复或越界";
|
||||
case RAIL_SELFTEST_PCA9555:
|
||||
return "PCA9555 位表/地址";
|
||||
return "PCA9555/TCA9548A 位表或地址";
|
||||
case RAIL_SELFTEST_TABLE:
|
||||
return "五路表与驱动分家";
|
||||
case RAIL_SELFTEST_PINS:
|
||||
@@ -209,50 +244,39 @@ const char *rail_selftest_str(uint32_t bit)
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 两根 EN 线 ---------------------------------------------------------- */
|
||||
/* ---- `EFUSE_EN` ---------------------------------------------------------- */
|
||||
|
||||
/* 高有效。**全固件唯一一处极性**(和 `sr595_set_hub_reset()` 里那个反相、
|
||||
`rail_mv_to_code()` 里那个减法同一个道理:多一处就多一处写反的机会)。 */
|
||||
/* 高有效。**全固件唯一一处极性**(和 `rail_mv_to_code()` 里那个减法、
|
||||
`slot.c` 里 VBUS 那一处同一个道理:多一处就多一处写反的机会)。 */
|
||||
static hal_err_t en_write(int pin, bool on)
|
||||
{
|
||||
return hal_gpio_write(pin, on ? 1 : 0);
|
||||
}
|
||||
|
||||
/* 两根线的影子(契约见 rail_cfg.h)。写失败时**不更新** —— 那一次的线上电平
|
||||
是未知的,而「保留上一次成功的意图」在两个方向上都倒向安全的那一侧。 */
|
||||
/* 影子(契约见 rail_cfg.h)。写失败时**不更新**。 */
|
||||
static bool s_efuse_on[PHONERAIL_RAIL_COUNT];
|
||||
static bool s_vbus_on[PHONERAIL_RAIL_COUNT];
|
||||
|
||||
bool rail_efuse_is_on(int rail)
|
||||
{
|
||||
return rail >= 1 && rail <= PHONERAIL_RAIL_COUNT && s_efuse_on[rail - 1];
|
||||
}
|
||||
|
||||
bool rail_vbus_is_on(int rail)
|
||||
{
|
||||
return rail >= 1 && rail <= PHONERAIL_RAIL_COUNT && s_vbus_on[rail - 1];
|
||||
}
|
||||
|
||||
hal_err_t rail_en_init(void)
|
||||
{
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
const rail_cfg_t *c = &k_cfg[rail - 1];
|
||||
const int pins[2] = { c->efuse_en, c->vbus_en };
|
||||
const int pin = k_cfg[rail - 1].efuse_en;
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
hal_err_t err = hal_gpio_config(pins[i], HAL_GPIO_OUTPUT);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
/* 配成输出的那一刻两边的 HAL 都已经在输出 0 了,这一句是把「关」
|
||||
写成明文 —— 别让「五路上电即全关」依赖某个默认值。 */
|
||||
err = en_write(pins[i], false);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
hal_err_t err = hal_gpio_config(pin, HAL_GPIO_OUTPUT);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
/* 配成输出的那一刻两边的 HAL 都已经在输出 0 了,这一句是把「关」
|
||||
写成明文 —— 别让「五路上电即全关」依赖某个默认值。 */
|
||||
err = en_write(pin, false);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
s_efuse_on[rail - 1] = false;
|
||||
s_vbus_on[rail - 1] = false;
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
@@ -269,26 +293,15 @@ hal_err_t rail_efuse_enable(int rail, bool on)
|
||||
return err;
|
||||
}
|
||||
|
||||
hal_err_t rail_vbus_enable(int rail, bool on)
|
||||
{
|
||||
if (rail < 1 || rail > PHONERAIL_RAIL_COUNT) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
const hal_err_t err = en_write(k_cfg[rail - 1].vbus_en, on);
|
||||
if (err == HAL_OK) {
|
||||
s_vbus_on[rail - 1] = on;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
hal_err_t rail_all_off(void)
|
||||
{
|
||||
hal_err_t first = HAL_OK;
|
||||
|
||||
/* 先断五路 VBUS,再关五路 eFuse:反过来的话中间有一小段「手机只靠 USB
|
||||
供着电」,那既不是开也不是关。十个都要试,不许中途 return。 */
|
||||
/* 先断五路 VBUS(走 I²C,可能失败),再关五路 eFuse(GPIO,永远成功)。
|
||||
反过来的话中间有一小段「手机只靠 USB 供着电」,那既不是开也不是关。
|
||||
十次都要试,不许中途 return —— 关电路径上「剩下的也别关了」是最坏的选择。 */
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
hal_err_t err = rail_vbus_enable(rail, false);
|
||||
hal_err_t err = slot_vbus_enable(rail, false);
|
||||
if (err != HAL_OK && first == HAL_OK) {
|
||||
first = err;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户