固件: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]>
这个提交包含在:
zhou yang
2026-09-27 10:26:24 +08:00
共同撰写人 Claude Opus 5.5
父节点 5a80d8a872
当前提交 b8ad69681f
共修改 51 个文件,包含 10882 行新增和 4068 行删除
+27 -32
查看文件
@@ -6,10 +6,9 @@
#include <stdio.h>
#include <string.h>
#include "hal_lock.h"
#include "hal_time.h"
#include "rail_cfg.h"
#include "sr595.h"
#include "slot.h"
typedef struct {
act_state_t state;
@@ -17,7 +16,7 @@ typedef struct {
uint32_t t0_ms; /* 进入当前状态的时刻(绝对,回绕安全) */
uint32_t keys; /* KEY 序列:逻辑键位(ACT_KEYS_*) */
uint32_t hold_ms;
uint32_t release_mask; /* 收尾时要在链上清掉的位。0 = 这个序列没按下任何东西 */
uint8_t release_mask; /* 收尾时要在 Port0 上清掉的位。0 = 这个序列没按下任何东西 */
act_result_t result;
uint32_t done;
uint32_t cleanup_retries;
@@ -38,30 +37,26 @@ static act_rail_t *at(int rail)
return &s_rail[rail - 1];
}
/* 逻辑键位 → 链上位号。查 `sr595_key_bit()`,**不算**(sr595.h 的禁令)。 */
static uint32_t key_chain_mask(int rail, uint32_t keys)
/* 逻辑键位 → PCA9555 Port0 位号。查 `pca9555_key_bit()`,**不算**。
⚠️ R2 里位号与路号无关(五片刀片同构),所以这个函数不收 rail。 */
static uint8_t key_out_mask(uint32_t keys)
{
uint32_t m = 0;
for (int k = 0; k < SR595_KEY_COUNT; k++) {
uint8_t m = 0;
for (int k = 0; k < PCA9555_KEY_COUNT; k++) {
if ((keys & ((uint32_t)1u << k)) != 0) {
m |= (uint32_t)1u << sr595_key_bit(rail, (sr595_key_t)k);
m |= (uint8_t)(1u << pca9555_key_bit((pca9555_key_t)k));
}
}
return m;
}
/* 动 24 位链的**唯一**入口:拿 595 那把锁 → 改影子 + 整帧发下去 → 放锁。
一帧原子(RCLK 之前输出不变)⇒ 组合键的两位天然落在同一帧里。
⚠️ 这里绝不会去拿 I²C 那把锁,两把锁因此永远不会嵌套(hal_lock.h)。 */
static hal_err_t chain_write(uint32_t mask, uint32_t value)
/* 动这一路 Port0 的**唯一**入口:`slot_out0_write()` 自己开一次 `i2cbus_xact`
(拿锁 → 选通道 → 读改写一个字节 → 放锁)。
一次写就是一个字节 ⇒ 组合键的两位天然落在同一次写里(§10.6)。
⚠️ 这一步会拿 I²C 锁,所以**不许**在持锁的上下文里调它(R2 的新前提,见头文件)。 */
static hal_err_t out_write(int rail, uint8_t mask, uint8_t value)
{
hal_err_t err = hal_lock_take(HAL_LOCK_SR595, ACT_LOCK_TIMEOUT_MS);
if (err != HAL_OK) {
return err; /* 影子没动过,下一拍重来 */
}
err = sr595_write(mask, value);
hal_lock_give(HAL_LOCK_SR595);
return err;
return slot_out0_write(rail, mask, value);
}
static bool pg_now(int rail, bool *pg)
@@ -152,7 +147,7 @@ static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
if (el < ACT_EDL_SHORT_MS) {
return ACT_EDL_SHORT_MS - el;
}
if (rail_vbus_enable(rail, true) != HAL_OK) {
if (slot_vbus_enable(rail, true) != HAL_OK) {
finish(r, ACT_RES_IO, now); /* CLEANUP 会把 EDL_EN 清掉 */
return ACT_CLEANUP_RETRY_MS;
}
@@ -168,7 +163,7 @@ static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
return ACT_CLEANUP_RETRY_MS;
case ACT_ST_CLEANUP:
if (chain_write(r->release_mask, 0) == HAL_OK) {
if (out_write(rail, r->release_mask, 0) == HAL_OK) {
r->release_mask = 0;
r->seq = ACT_SEQ_NONE;
enter(r, ACT_ST_IDLE, now);
@@ -227,9 +222,9 @@ hal_err_t t_actuator_hard_reset(int rail)
Fastboot 而不是正常起来。正常情况下这里一定已经是 0(有序列在跑的话
上面那一句就把请求挡了),所以影子不变 ⇒ 不脏 ⇒ 一个时钟都不会发。
只有上一次收尾没落地时才真的发一帧。 */
const uint32_t km = sr595_key_mask(rail);
if ((sr595_shadow() & km) != 0) {
(void)chain_write(km, 0); /* 失败也照断电:断电永远比不断电安全 */
const uint8_t km = pca9555_key_mask();
if ((slot_shadow(rail) & km) != 0) {
(void)out_write(rail, km, 0); /* 失败也照断电:断电永远比不断电安全 */
}
const hal_err_t err = rail_efuse_enable(rail, false);
@@ -259,11 +254,11 @@ hal_err_t t_actuator_key(int rail, uint32_t keys, uint32_t hold_ms)
return HAL_ERR_STATE;
}
const uint32_t mask = key_chain_mask(rail, keys);
const uint8_t mask = key_out_mask(keys);
/* 组合键的两位在**同一次 `chain_write()`** 里 ⇒ 同一帧 ⇒ 同一个 RCLK 沿。
/* 组合键的两位在**同一次写**里 ⇒ 输出锁存器一次更新。
分两次写会先单按一个键(§10.6)。 */
const hal_err_t err = chain_write(mask, mask);
const hal_err_t err = out_write(rail, mask, mask);
r->seq = ACT_SEQ_KEY;
r->keys = keys;
@@ -272,9 +267,9 @@ hal_err_t t_actuator_key(int rail, uint32_t keys, uint32_t hold_ms)
r->release_mask = mask;
if (err != HAL_OK) {
/* 影子可能已经改成「按下」了(拿到锁但整帧没发出去)。直接返回错误
就等于把一个「按下」的脏影子留在那儿,谁下一次 flush 谁就替我们把键
按下去 —— 而没有人会来松开它。所以照样进 CLEANUP 把它撤回来。 */
/* 写失败时驱动**不更新影子**,但器件里那个字节的状态是未知的
(事务可能发出去一半)。所以照样进 CLEANUP 把这几位显式清一遍 ——
「按下去的东西一定要松开」这条规矩不看我们以为发生了什么。 */
finish(r, ACT_RES_IO, hal_time_now_ms());
return err;
}
@@ -296,8 +291,8 @@ hal_err_t t_actuator_edl(int rail)
return HAL_ERR_STATE;
}
const uint32_t mask = (uint32_t)1u << sr595_edl_bit(rail); /* 查表,不算 */
const hal_err_t err = chain_write(mask, mask);
const uint8_t mask = (uint8_t)(1u << PCA9555_BIT_EDL_EN);
const hal_err_t err = out_write(rail, mask, mask);
r->seq = ACT_SEQ_EDL;
r->result = ACT_RES_NONE;