- warm_esp.{h,c}:warm_restart() 记下开着的路(RTC_NOINIT,魔数 + 掩码 + 反码)→ gpio_hold 锁五根 EN → 重启;
目标镜像没有能力标记(第 288 字节 PRCAPS1,.rodata_custom_desc)就不锁 —— 老固件写不动锁住的 EN。
warm_take():软件复位 + 标记对得上才接管,一次性;再读 EN 真电平核对
- bringup:adopt 掩码;rail_en_init_keep() 接管的路写高、五根一律放开 hold(崩溃时旧锁可能还在);
刀片不在 / mux 失联的接管路立刻关(SLOT_EMPTY arg 1);第二段已开着的路不重开、不等 PG
- pca9555_init_keep():方向寄存器是本固件写的才留 VBUS(掉过电的片子锁存器是 0xFF),键 / EDL 清零
- HAL:hal_gpio_hold();ESP 输出脚配 INPUT_OUTPUT(读得到真电平);sim 给 hold 建模
- 事件 LOG_EV_OTA arg 7 = 接管;兜底定时器回滚故意不走接管
- 顶层 CMake:phonerail.h 进 CMAKE_CONFIGURE_DEPENDS(否则镜像头版本停在旧值);-u g_phonerail_caps 防 gc
- test_bringup 加 6 例(接管 / 非计划复位全关 / 空槽 / mux 失联 / PCA 掉过电 / hold 冻结),全工程 448 例全绿
- rack:固件库识别能力标记,推送对话框按「支持 / 不支持重启不断电」给不同提示(已部署)
- 需求文档:SR-6 改为「非计划复位全关」+ 计划内重启例外;§10.9 新增「重启不断电」
- 板 B:台架镜像(空槽强制 CH1 在位,未提交)开 CH1 → OTA 带重启 → 事件 18/7 mv=1,CH1 一直开着;
现跑 0.1.0-c3。未测:示波器看重启期间 EN / 输出毛刺(要插刀片)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
313 行
9.5 KiB
C
313 行
9.5 KiB
C
/* PCA9555 驱动(R2:每片刀片一颗,P0.0~P0.4 是输出)。
|
||
只经 hal_i2c_*,不碰 mux、不碰任何平台头文件。契约见 pca9555.h。 */
|
||
#include "pca9555.h"
|
||
|
||
#include <assert.h>
|
||
#include <stddef.h>
|
||
|
||
#include "hal_i2c.h"
|
||
|
||
/* 键 → Port0 位号。写成表而不是 `key + 0`:哪天硬件改一根线,改这里一处就够。 */
|
||
static const uint8_t s_key_bit[PCA9555_KEY_COUNT] = {
|
||
PCA9555_BIT_KEY_PWR,
|
||
PCA9555_BIT_KEY_VUP,
|
||
PCA9555_BIT_KEY_VDN,
|
||
};
|
||
|
||
/* 五片各自的 Port0 输出影子。**写成功才更新**(见 pca9555_write_out0)。 */
|
||
static uint8_t s_out0[PCA9555_SLOT_COUNT];
|
||
|
||
int pca9555_key_bit(pca9555_key_t key)
|
||
{
|
||
assert(key >= 0 && key < PCA9555_KEY_COUNT); /* 越界是调用方算错了 */
|
||
return s_key_bit[key];
|
||
}
|
||
|
||
uint8_t pca9555_key_mask(void)
|
||
{
|
||
uint8_t m = 0;
|
||
for (int k = 0; k < PCA9555_KEY_COUNT; k++) {
|
||
m |= (uint8_t)(1u << s_key_bit[k]);
|
||
}
|
||
return m;
|
||
}
|
||
|
||
bool pca9555_map_ok(void)
|
||
{
|
||
uint8_t p0 = 0;
|
||
|
||
/* 三个键互不重复。 */
|
||
for (int k = 0; k < PCA9555_KEY_COUNT; k++) {
|
||
const uint8_t b = (uint8_t)(1u << s_key_bit[k]);
|
||
if ((p0 & b) != 0) {
|
||
return false;
|
||
}
|
||
p0 |= b;
|
||
}
|
||
/* EDL 与 VBUS 也在 Port0,同样不许撞。 */
|
||
const uint8_t edl = (uint8_t)(1u << PCA9555_BIT_EDL_EN);
|
||
const uint8_t vbus = (uint8_t)(1u << PCA9555_BIT_VBUS_EN);
|
||
if ((p0 & edl) != 0 || (p0 & vbus) != 0 || edl == vbus) {
|
||
return false;
|
||
}
|
||
p0 |= (uint8_t)(edl | vbus);
|
||
|
||
/* 五个输出位必须正好等于「允许配成输出」的那五位:
|
||
少一位 = 有个输出永远发不出去;多一位 = 方向字会把接地脚驱起来。 */
|
||
if (p0 != PCA9555_P0_OUT_MASK) {
|
||
return false;
|
||
}
|
||
if ((p0 & PCA9555_P0_GND_MASK) != 0) {
|
||
return false;
|
||
}
|
||
|
||
/* Port1 的四个输入位互不重复、都不落在接地脚上。 */
|
||
const uint8_t in_bits[] = {
|
||
(uint8_t)(1u << PCA9555_BIT_EFUSE_FLT),
|
||
(uint8_t)(1u << PCA9555_BIT_BUCK_PG),
|
||
(uint8_t)(1u << PCA9555_BIT_INA_ALERT),
|
||
(uint8_t)(1u << PCA9555_BIT_EFUSE_PG),
|
||
};
|
||
uint8_t p1 = 0;
|
||
for (size_t i = 0; i < sizeof(in_bits); i++) {
|
||
if ((p1 & in_bits[i]) != 0) {
|
||
return false;
|
||
}
|
||
p1 |= in_bits[i];
|
||
}
|
||
if (p1 != PCA9555_P1_IN_MASK || (p1 & PCA9555_P1_GND_MASK) != 0) {
|
||
return false;
|
||
}
|
||
|
||
/* 两个方向字必须与位分配自洽:Port0 上「不是输入」的正好是那五个输出位。 */
|
||
if ((uint8_t)~PCA9555_CONFIG0_INIT != PCA9555_P0_OUT_MASK) {
|
||
return false;
|
||
}
|
||
if (PCA9555_CONFIG1_INIT != PCA9555_DIR_ALL_INPUT) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool pca9555_addr_ok(void)
|
||
{
|
||
/* 主干上的器件(计划 §5.2)。刀片支路上另有 0x40 的 INA226,与 0x20 不冲突。 */
|
||
static const uint8_t others[] = { 0x40, 0x43, 0x4A, 0x4B, 0x70 };
|
||
|
||
if (PCA9555_ADDR > 0x7f) {
|
||
return false;
|
||
}
|
||
for (size_t i = 0; i < sizeof(others); i++) {
|
||
if (others[i] == PCA9555_ADDR) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool pca9555_dir_safe(int port, uint8_t dir)
|
||
{
|
||
/* 1 = 输入。 */
|
||
if (port == 0) {
|
||
/* 接地脚必须是输入,一位都不能少。bit0~4 随意(它们就是要当输出的)。 */
|
||
return (dir & PCA9555_P0_GND_MASK) == PCA9555_P0_GND_MASK;
|
||
}
|
||
if (port == 1) {
|
||
/* Port1 **一位都不许输出**:bit0~3 接的是 eFuse / Buck / INA 的开漏输出,
|
||
配成输出就是两个驱动器对打;bit4~7 焊死在地上。 */
|
||
return dir == PCA9555_DIR_ALL_INPUT;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/* ---- 寄存器读写 ---------------------------------------------------------
|
||
PCA9555 的命令字节在**成对**的寄存器之间 toggle(0/1、2/3、4/5、6/7),
|
||
不是无限自增。本驱动只在一对之内连读 2 字节,落在 datasheet 保证的范围里。 */
|
||
|
||
static hal_err_t read_reg(uint8_t reg, uint8_t *val)
|
||
{
|
||
return hal_i2c_write_read(PCA9555_ADDR, ®, 1, val, 1);
|
||
}
|
||
|
||
static hal_err_t write_reg(uint8_t reg, uint8_t val)
|
||
{
|
||
const uint8_t wr[2] = { reg, val };
|
||
return hal_i2c_write(PCA9555_ADDR, wr, sizeof(wr));
|
||
}
|
||
|
||
/* 写完立刻回读比对。回读不一致 = 总线上有东西但不是我们以为的那片。 */
|
||
static hal_err_t write_verify(uint8_t reg, uint8_t val)
|
||
{
|
||
hal_err_t err = write_reg(reg, val);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
uint8_t rb = 0;
|
||
err = read_reg(reg, &rb);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
return (rb == val) ? HAL_OK : HAL_ERR_BUS;
|
||
}
|
||
|
||
hal_err_t pca9555_probe(void)
|
||
{
|
||
uint8_t c0 = 0, c1 = 0;
|
||
hal_err_t err = read_reg(PCA9555_REG_CONFIG0, &c0);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
err = read_reg(PCA9555_REG_CONFIG1, &c1);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
return (c0 == PCA9555_DIR_ALL_INPUT && c1 == PCA9555_DIR_ALL_INPUT) ? HAL_OK : HAL_ERR_BUS;
|
||
}
|
||
|
||
static hal_err_t init_with(int slot, uint8_t out0);
|
||
|
||
hal_err_t pca9555_init(int slot)
|
||
{
|
||
if (slot < 0 || slot >= PCA9555_SLOT_COUNT) {
|
||
return HAL_ERR_ARG;
|
||
}
|
||
return init_with(slot, 0x00);
|
||
}
|
||
|
||
hal_err_t pca9555_init_keep(int slot, uint8_t keep_mask)
|
||
{
|
||
if (slot < 0 || slot >= PCA9555_SLOT_COUNT || (keep_mask & (uint8_t)~PCA9555_P0_OUT_MASK) != 0) {
|
||
return HAL_ERR_ARG;
|
||
}
|
||
uint8_t cfg0 = 0;
|
||
uint8_t out0 = 0;
|
||
hal_err_t err = read_reg(PCA9555_REG_CONFIG0, &cfg0);
|
||
if (err == HAL_OK) {
|
||
err = read_reg(PCA9555_REG_OUTPUT0, &out0);
|
||
}
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
/* ⭐ 只有「这片已经被上一条命配过」才留:方向字不是我们写的那个 = 它掉过电(复位值
|
||
0xFF 的输出锁存器里 VBUS 位是 1),照留就是凭空把 VBUS 打开。 */
|
||
const uint8_t keep = (cfg0 == PCA9555_CONFIG0_INIT) ? (uint8_t)(out0 & keep_mask) : 0u;
|
||
return init_with(slot, keep);
|
||
}
|
||
|
||
static hal_err_t init_with(int slot, uint8_t out0)
|
||
{
|
||
/* 方向字的合法性是编译期就能判的事,assert 拦的是「哪天有人顺手改了常量」。 */
|
||
assert(pca9555_dir_safe(0, PCA9555_CONFIG0_INIT));
|
||
assert(pca9555_dir_safe(1, PCA9555_CONFIG1_INIT));
|
||
|
||
/* ⛔ 顺序就是安全本身(头文件那个框):**输出锁存器先清零,方向最后改**。
|
||
复位值 0xFF 的输出锁存器 + 先改方向 = 三个键全按下 + VBUS 上电。 */
|
||
hal_err_t err = write_verify(PCA9555_REG_OUTPUT0, out0);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
err = write_verify(PCA9555_REG_OUTPUT1, 0x00);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
|
||
/* 极性反转清零。复位值本来就是 0,但「本来就是」不值得赌 —— 看门狗复位
|
||
只复位 ESP32,刀片上的 PCA9555 是不掉电的,上一条命的残留值会原样留着。 */
|
||
err = write_verify(PCA9555_REG_POLINV0, PCA9555_POLINV_NONE);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
err = write_verify(PCA9555_REG_POLINV1, PCA9555_POLINV_NONE);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
|
||
/* Port1 先(它本来就该保持全输入),Port0 最后 —— 这一句之后五个输出脚
|
||
才真的开始驱动,而它们的锁存器在上面已经是 0 了。 */
|
||
err = write_verify(PCA9555_REG_CONFIG1, PCA9555_CONFIG1_INIT);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
err = write_verify(PCA9555_REG_CONFIG0, PCA9555_CONFIG0_INIT);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
|
||
s_out0[slot] = out0;
|
||
return HAL_OK;
|
||
}
|
||
|
||
hal_err_t pca9555_read_inputs(pca9555_in_t *out)
|
||
{
|
||
if (out == NULL) {
|
||
return HAL_ERR_ARG;
|
||
}
|
||
const uint8_t reg = PCA9555_REG_INPUT0;
|
||
uint8_t rd[2] = { 0, 0 };
|
||
hal_err_t err = hal_i2c_write_read(PCA9555_ADDR, ®, 1, rd, sizeof(rd));
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
out->port0 = rd[0];
|
||
out->port1 = rd[1];
|
||
return HAL_OK;
|
||
}
|
||
|
||
hal_err_t pca9555_write_out0(int slot, uint8_t mask, uint8_t value)
|
||
{
|
||
if (slot < 0 || slot >= PCA9555_SLOT_COUNT) {
|
||
return HAL_ERR_ARG;
|
||
}
|
||
if ((mask & ~PCA9555_P0_OUT_MASK) != 0) {
|
||
/* 掩码碰到了接地脚或者别的端口。不 abort:这多半是上层算错了掩码,
|
||
返回错误码能让它记一条事件;真正会冒烟的是**方向**,那条走 abort。 */
|
||
return HAL_ERR_ARG;
|
||
}
|
||
|
||
const uint8_t next = (uint8_t)((s_out0[slot] & ~mask) | (value & mask));
|
||
const hal_err_t err = write_reg(PCA9555_REG_OUTPUT0, next);
|
||
if (err == HAL_OK) {
|
||
s_out0[slot] = next; /* 影子只在写成功之后更新 */
|
||
}
|
||
return err;
|
||
}
|
||
|
||
uint8_t pca9555_shadow(int slot)
|
||
{
|
||
assert(slot >= 0 && slot < PCA9555_SLOT_COUNT);
|
||
return s_out0[slot];
|
||
}
|
||
|
||
void pca9555_shadow_reset(void)
|
||
{
|
||
for (int i = 0; i < PCA9555_SLOT_COUNT; i++) {
|
||
s_out0[i] = 0;
|
||
}
|
||
}
|
||
|
||
/* ---- 语义判读(极性各出现一次,见头文件) -------------------------------- */
|
||
|
||
bool pca9555_efuse_fault(const pca9555_in_t *in)
|
||
{
|
||
assert(in != NULL);
|
||
return (in->port1 & (uint8_t)(1u << PCA9555_BIT_EFUSE_FLT)) == 0; /* FLT# 低有效 */
|
||
}
|
||
|
||
bool pca9555_efuse_pg(const pca9555_in_t *in)
|
||
{
|
||
assert(in != NULL);
|
||
return (in->port1 & (uint8_t)(1u << PCA9555_BIT_EFUSE_PG)) != 0; /* 高 = 良好 */
|
||
}
|
||
|
||
bool pca9555_buck_pg(const pca9555_in_t *in)
|
||
{
|
||
assert(in != NULL);
|
||
return (in->port1 & (uint8_t)(1u << PCA9555_BIT_BUCK_PG)) != 0; /* 高 = 正常 */
|
||
}
|
||
|
||
bool pca9555_ina_alert(const pca9555_in_t *in)
|
||
{
|
||
assert(in != NULL);
|
||
return (in->port1 & (uint8_t)(1u << PCA9555_BIT_INA_ALERT)) == 0; /* 低有效 */
|
||
}
|