固件: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]>
这个提交包含在:
@@ -1,11 +1,13 @@
|
||||
# 板上器件驱动(ESP-IDF 侧)。宿主机侧由 host/CMakeLists.txt 的 PHONERAIL_DRV_SRCS 收,
|
||||
# 新增一个驱动两边都要加 —— 漏一处 = 「宿主机绿了但上板编不过」,反之亦然。
|
||||
#
|
||||
# 驱动只准依赖 phonerail_hal,不准直接包含 IDF 的头。
|
||||
idf_component_register(
|
||||
SRCS "ina226.c"
|
||||
"dac63204.c"
|
||||
"sr595.c"
|
||||
"pca9555.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES phonerail_hal)
|
||||
# 板上器件驱动(ESP-IDF 侧)。宿主机侧由 host/CMakeLists.txt 的 PHONERAIL_DRV_SRCS 收,
|
||||
# 新增一个驱动两边都要加 —— 漏一处 = 「宿主机绿了但上板编不过」,反之亦然。
|
||||
#
|
||||
# 驱动只准依赖 phonerail_hal,不准直接包含 IDF 的头。
|
||||
idf_component_register(
|
||||
SRCS "ina226.c"
|
||||
"dac63204.c"
|
||||
"sr595.c"
|
||||
"pca9555.c"
|
||||
"tca9548a.c"
|
||||
"ch224a.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES phonerail_hal)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/* CH224A 驱动(只读)。只经 hal_i2c_*,不碰任何平台头文件。契约见 ch224a.h。
|
||||
⚠️ 本文件里**没有任何写寄存器的函数**,那是有意的(头文件那个框)。 */
|
||||
#include "ch224a.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "hal_i2c.h"
|
||||
|
||||
/* 0x50 的单位是 50 mA/LSB(R2 §36.2)。 */
|
||||
uint32_t ch224a_ma_from_reg(uint8_t reg) { return (uint32_t)reg * 50u; }
|
||||
|
||||
uint32_t ch224a_power_limit_mw(uint32_t ma)
|
||||
{
|
||||
/* 20 V × ma(mA) = mW。20000 mV × mA / 1000 = 20 × mA。 */
|
||||
return (uint32_t)((uint64_t)CH224A_FIXED_MV * ma / 1000u);
|
||||
}
|
||||
|
||||
bool ch224a_addr_ok(uint8_t addr)
|
||||
{
|
||||
/* 主干上的器件 + 刀片支路上的两个。任何一个撞上都会让「选到通道 5」
|
||||
之后的读写打到错的器件上。 */
|
||||
static const uint8_t others[] = { 0x20, 0x40, 0x43, 0x4A, 0x4B, 0x70 };
|
||||
|
||||
if (addr > 0x7f) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(others); i++) {
|
||||
if (others[i] == addr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static hal_err_t read_reg(uint8_t addr, uint8_t reg, uint8_t *val)
|
||||
{
|
||||
return hal_i2c_write_read(addr, ®, 1, val, 1);
|
||||
}
|
||||
|
||||
hal_err_t ch224a_probe(uint8_t addr)
|
||||
{
|
||||
uint8_t st = 0;
|
||||
return read_reg(addr, CH224A_REG_STATUS, &st);
|
||||
}
|
||||
|
||||
hal_err_t ch224a_read_status(uint8_t addr, uint8_t *status)
|
||||
{
|
||||
if (status == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
return read_reg(addr, CH224A_REG_STATUS, status);
|
||||
}
|
||||
|
||||
hal_err_t ch224a_read_max_ma(uint8_t addr, uint32_t *ma)
|
||||
{
|
||||
if (ma == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
uint8_t reg = 0;
|
||||
const hal_err_t err = read_reg(addr, CH224A_REG_CURRENT, ®);
|
||||
if (err != HAL_OK) {
|
||||
return err; /* 读失败时不动调用方的值:旧的上限比一个 0 可信 */
|
||||
}
|
||||
*ma = ch224a_ma_from_reg(reg);
|
||||
return HAL_OK;
|
||||
}
|
||||
+48
-19
@@ -6,10 +6,14 @@
|
||||
|
||||
#include "hal_i2c.h"
|
||||
|
||||
/* 设计文档 §8.2:CH1..CH5。⚠️ 不是 0x40 起顺排,CH3/CH4/CH5 三路都不是。 */
|
||||
static const uint8_t s_rail_addr[INA226_RAIL_COUNT] = { 0x40, 0x41, 0x44, 0x45, 0x42 };
|
||||
/* R2:五片刀片完全同构 ⇒ 五路同址。仍然写成表而不是直接 `return 0x40`,
|
||||
理由和 mux 通道那张表一样:**同址是当下的事实,不是不变量**。
|
||||
哪天某一片改了 strap,改这里一处,`rail_selftest()` 会立刻发现两边分家。 */
|
||||
static const uint8_t s_rail_addr[INA226_RAIL_COUNT] = {
|
||||
INA226_ADDR_RAIL, INA226_ADDR_RAIL, INA226_ADDR_RAIL, INA226_ADDR_RAIL, INA226_ADDR_RAIL,
|
||||
};
|
||||
|
||||
const uint8_t ina226_all_addr[INA226_COUNT] = { 0x40, 0x41, 0x44, 0x45, 0x42, INA226_ADDR_INPUT };
|
||||
const uint8_t ina226_addr_list[INA226_ADDR_COUNT] = { INA226_ADDR_RAIL, INA226_ADDR_INPUT };
|
||||
|
||||
uint8_t ina226_rail_addr(int rail)
|
||||
{
|
||||
@@ -19,27 +23,28 @@ uint8_t ina226_rail_addr(int rail)
|
||||
|
||||
bool ina226_addr_table_ok(void)
|
||||
{
|
||||
/* 同一条 I²C 上的其它器件(§8.2):撞上任何一个都是硬件表被改坏了。 */
|
||||
static const uint8_t others[] = { 0x4A, 0x4B, 0x20, 0x48 };
|
||||
/* 会与之撞车的其它器件:主干上的 DAC63204 / TCA9548A,刀片支路上的 PCA9555。
|
||||
⚠️ 0x48(R1 载板上的 LM75)**不在这张表里** —— R2 没有载板,那片不存在。 */
|
||||
static const uint8_t others[] = { 0x4A, 0x4B, 0x70, 0x20 };
|
||||
|
||||
for (int i = 0; i < INA226_COUNT; i++) {
|
||||
if (ina226_all_addr[i] > 0x7f) {
|
||||
for (int i = 0; i < INA226_ADDR_COUNT; i++) {
|
||||
if (ina226_addr_list[i] > 0x7f) {
|
||||
return false;
|
||||
}
|
||||
for (int j = i + 1; j < INA226_COUNT; j++) {
|
||||
if (ina226_all_addr[i] == ina226_all_addr[j]) {
|
||||
return false;
|
||||
for (int j = i + 1; j < INA226_ADDR_COUNT; j++) {
|
||||
if (ina226_addr_list[i] == ina226_addr_list[j]) {
|
||||
return false; /* 刀片那片与输入侧那片必须不同址:它们同时可见 */
|
||||
}
|
||||
}
|
||||
for (size_t k = 0; k < sizeof(others); k++) {
|
||||
if (ina226_all_addr[i] == others[k]) {
|
||||
if (ina226_addr_list[i] == others[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 前五个必须和 rail 表逐个一致 —— 两张表分家的那天就是错一路的那天。 */
|
||||
/* 五路那张表里的每一个都必须在扫描表里 —— 分家的那天就是错一路的那天。 */
|
||||
for (int i = 0; i < INA226_RAIL_COUNT; i++) {
|
||||
if (ina226_all_addr[i] != s_rail_addr[i]) {
|
||||
if (s_rail_addr[i] != ina226_addr_list[0]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -133,7 +138,30 @@ hal_err_t ina226_check(uint8_t addr)
|
||||
return cal == INA226_CALIB ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
#define READ_ONE(fn, conv, reg) \
|
||||
/* ---- Kelvin 对调接的片子 --------------------------------------------------
|
||||
IN+ / IN− 在板上对调接(IN+ 接分流电阻下游)的片子,分流电压 / 电流读出来是负的。
|
||||
驱动在这里取反,调用方看到的永远是「正 = 正常供电方向」—— SW-8 反灌判据靠这个符号。
|
||||
母线电压、功率寄存器是无符号量,不受影响。
|
||||
R1 全部正接(默认);R2 底板的输入片 U13(0x43)为了让两条 Kelvin 在 PCB 上不交叉
|
||||
而对调(2026-09-11),R2 固件启动时要 ina226_set_reversed(INA226_ADDR_INPUT, true)。 */
|
||||
static uint16_t s_reversed; /* bit (addr − 0x40);INA226 的地址只有 0x40~0x4F */
|
||||
|
||||
void ina226_set_reversed(uint8_t addr, bool reversed)
|
||||
{
|
||||
if (addr < 0x40 || addr > 0x4F) {
|
||||
return;
|
||||
}
|
||||
uint16_t bit = (uint16_t)(1u << (addr - 0x40));
|
||||
s_reversed = reversed ? (uint16_t)(s_reversed | bit) : (uint16_t)(s_reversed & ~bit);
|
||||
}
|
||||
|
||||
bool ina226_is_reversed(uint8_t addr)
|
||||
{
|
||||
return addr >= 0x40 && addr <= 0x4F && (s_reversed & (1u << (addr - 0x40))) != 0;
|
||||
}
|
||||
|
||||
/* pol_ = 1:有符号量(分流电压 / 电流),按登记取反 */
|
||||
#define READ_ONE(fn, conv, reg, pol_) \
|
||||
hal_err_t fn(uint8_t addr, int32_t *out) \
|
||||
{ \
|
||||
if (out == NULL) { \
|
||||
@@ -144,14 +172,15 @@ hal_err_t ina226_check(uint8_t addr)
|
||||
if (err != HAL_OK) { \
|
||||
return err; \
|
||||
} \
|
||||
*out = conv(raw); \
|
||||
int32_t v = conv(raw); \
|
||||
*out = ((pol_) && ina226_is_reversed(addr)) ? -v : v; \
|
||||
return HAL_OK; \
|
||||
}
|
||||
|
||||
READ_ONE(ina226_read_bus_mv, bus_mv_of, INA226_REG_BUS_V)
|
||||
READ_ONE(ina226_read_shunt_uv, shunt_uv_of, INA226_REG_SHUNT_V)
|
||||
READ_ONE(ina226_read_current_ua, current_ua_of, INA226_REG_CURRENT)
|
||||
READ_ONE(ina226_read_power_mw, power_mw_of, INA226_REG_POWER)
|
||||
READ_ONE(ina226_read_bus_mv, bus_mv_of, INA226_REG_BUS_V, 0)
|
||||
READ_ONE(ina226_read_shunt_uv, shunt_uv_of, INA226_REG_SHUNT_V, 1)
|
||||
READ_ONE(ina226_read_current_ua, current_ua_of, INA226_REG_CURRENT, 1)
|
||||
READ_ONE(ina226_read_power_mw, power_mw_of, INA226_REG_POWER, 0)
|
||||
|
||||
hal_err_t ina226_read_vi(uint8_t addr, ina226_vi_t *out)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/* CH224A —— 底板的 USB-PD 受电芯片(`U36`,R2 方案 D16)。
|
||||
|
||||
板上它只干一件事:用 `R7` 120 k 这一颗电阻**硬件**请求 20 V,
|
||||
降压 `U37` 再把 20 V 变成 12 V 母线。固件**不参与协商**,只做一件事:
|
||||
**读出当前这个充电器在 20 V 档最多能给多少电流**,换算成整板功率上限。
|
||||
|
||||
---- 为什么需要这个上限(计划 §5.9) ------------------------------------
|
||||
R2 的输入从 DC 头改成了 USB-C PD。单口上限 20 V × 5 A = 100 W(5 A 还要
|
||||
e-marker 线;普通 3 A 线只有 60 W),而整板最坏工况约 108 W。
|
||||
常态 10~25 W 怎么都够,但**最坏工况要靠固件限功率**(R1 §8.3 的降载)。
|
||||
充电器插的是哪一个,只有运行时才知道 —— 就是读这颗芯片。
|
||||
|
||||
┌─ ⛔ 一条禁令 ────────────────────────────────────────────────────────────┐
|
||||
│ **本驱动不提供写 `0x0A` 的接口,一个都不提供。** │
|
||||
│ `0x0A = 5` 是「请求 28 V EPR」。板上 `D3` 是 SMBJ22A(VRWM 22 V), │
|
||||
│ `U37` 的 EN 分压按 20 V 设计 —— 28 V 会把 TVS 打到雪崩。 │
|
||||
│ 这跟 `sr595.h` 不提供「抬 OE#」、`pca9555.h` 不提供「把接地脚配成输出」 │
|
||||
│ 是同一条规矩:**能烧板子的动作不给接口,而不是给了再叮嘱别用。** │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
挂在 TCA9548A 的**通道 5**(`TCA9548A_CH_PD`)上,不在主干。 */
|
||||
#ifndef CH224A_H
|
||||
#define CH224A_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ⚠️⚠️ **这个地址还没有被实物证实**(计划 §11 的待定项):
|
||||
R2 §36.2 只写了寄存器号,没写从机地址。所以:
|
||||
· 所有接口都**收一个 addr 参数**,不写死;
|
||||
· B1 上板扫描时把通道 5 上扫到的那个地址填回这里,并同步改计划 §5.2。
|
||||
在那之前,用这个值跑出来的「扫不到」是**预期结果**,不是固件 bug。 */
|
||||
#define CH224A_ADDR_DEFAULT 0x23
|
||||
|
||||
/* 寄存器(R2 §36.2)。 */
|
||||
#define CH224A_REG_STATUS 0x09 /* 协商状态 / 当前档位 */
|
||||
#define CH224A_REG_CURRENT 0x50 /* 当前档位的最大电流,**单位 50 mA** */
|
||||
|
||||
/* 板上硬件固定请求的档位。`R7` 120 k 一颗电阻定死,固件改不了也不该改。 */
|
||||
#define CH224A_FIXED_MV 20000
|
||||
|
||||
/* 0x50 那个字节 → mA。 */
|
||||
uint32_t ch224a_ma_from_reg(uint8_t reg);
|
||||
|
||||
/* mA → 整板可用功率(mW),按 20 V 档算。
|
||||
60 W / 65 W / 100 W 三档在这里分别是 3000 / 3250 / 5000 mA。 */
|
||||
uint32_t ch224a_power_limit_mw(uint32_t ma);
|
||||
|
||||
/* 地址不与主干上的器件撞车。⚠️ 它挂在通道 5 上,与刀片支路的 0x40 / 0x20
|
||||
本来就不冲突,这里查的是「万一它和主干某个器件同址」——
|
||||
那种情况下选到通道 5 时主干那位会被它盖住。 */
|
||||
bool ch224a_addr_ok(uint8_t addr);
|
||||
|
||||
/* 认件:读状态寄存器。不应答 → HAL_ERR_NAK。
|
||||
⚠️ **不校验内容**:这颗芯片没有 ID 寄存器,状态字的每一个值都是合法的。 */
|
||||
hal_err_t ch224a_probe(uint8_t addr);
|
||||
|
||||
hal_err_t ch224a_read_status(uint8_t addr, uint8_t *status);
|
||||
|
||||
/* 当前档位最大电流(mA)。读失败时 `*ma` 不动。 */
|
||||
hal_err_t ch224a_read_max_ma(uint8_t addr, uint32_t *ma);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* CH224A_H */
|
||||
@@ -47,20 +47,48 @@ extern "C" {
|
||||
不是任何一种错误。tasks.c 的表自检拿它和 20 ms 对一次。 */
|
||||
#define INA226_CONV_US 8800
|
||||
|
||||
/* ---- 地址表(设计文档 §8.2) ------------------------------------------- */
|
||||
/* ---- 地址表(**R2**:五片同址 + 输入侧一片,计划 §5.2) -----------------
|
||||
|
||||
#define INA226_RAIL_COUNT 5
|
||||
#define INA226_COUNT 6
|
||||
#define INA226_ADDR_INPUT 0x43 /* U13,输入侧 V+I,整板功率预算用 */
|
||||
⚠️⚠️ **R1 的「六片六个地址」在 R2 不成立了。** R2 的五片 INA226 各在一片刀片
|
||||
上(`U3`),五片刀片完全同构 ⇒ **地址全是 0x40**,靠底板 TCA9548A 的通道区分
|
||||
(方案 D8)。板上出处:`.claude/r2/_blade_pcb_nets.json` 的 `U3` pin1/pin2
|
||||
(A1/A0)都接 GND。
|
||||
|
||||
/* CH1~CH5 → U8~U12 的地址。rail 是 1~5,越界直接 abort。
|
||||
⚠️ **`0x40 + ch` 是错的**,CH3/CH4/CH5 三路全错(0x44 / 0x45 / 0x42)。
|
||||
地址按布线难度重排过(§8.2 那张 A1/A0 表),永远查表,不要算。 */
|
||||
于是「查表不要算」这条规矩换了对象:**要查的不再是地址,而是 mux 通道**
|
||||
(`rail_cfg.h` 的 `mux_ch`)。选错通道读到的是另一台手机的电压电流,
|
||||
而量纲、范围、变化趋势全都正常 —— 比 R1 的「地址算错」更难发现。 */
|
||||
|
||||
#define INA226_RAIL_COUNT 5
|
||||
#define INA226_COUNT 6 /* 物理片数:五片刀片 + 一片输入侧 */
|
||||
|
||||
/* 五片刀片上那一片的地址(**五片相同**)。 */
|
||||
#define INA226_ADDR_RAIL 0x40
|
||||
|
||||
/* U13,底板输入侧 V+I,整板功率预算与 PD 限功率用(计划 §5.9)。
|
||||
挂**主干**,不在任何 mux 通道后面。R2 底板上 Kelvin 对调接,见 ina226_set_reversed()。 */
|
||||
#define INA226_ADDR_INPUT 0x43
|
||||
|
||||
/* 扫描用:总线上出现过的 INA226 地址只有这两个(不是六个)。 */
|
||||
#define INA226_ADDR_COUNT 2
|
||||
|
||||
/* 登记「这片的 IN+ / IN− 在板上对调接了」(IN+ 接分流电阻下游):之后驱动对它的
|
||||
分流电压 / 电流取反,调用方看到的永远是「正 = 正常供电方向」。母线电压 / 功率不受影响。
|
||||
默认全部正接(R1)。R2 底板的 U13(0x43)为了让两条 Kelvin 在 PCB 上不交叉而对调
|
||||
(2026-09-11),R2 固件启动时必须调 ina226_set_reversed(INA226_ADDR_INPUT, true),
|
||||
否则 SW-8 反灌判据会把正常供电当成反灌。地址不在 0x40~0x4F 的调用被忽略。 */
|
||||
void ina226_set_reversed(uint8_t addr, bool reversed);
|
||||
bool ina226_is_reversed(uint8_t addr);
|
||||
|
||||
/* CH1~CH5 那一片的地址。rail 是 1~5,越界直接 abort。
|
||||
⚠️ **R2 里它对五路返回同一个值(0x40)**,这不是 bug。
|
||||
真正区分是哪一路的是 `rail_cfg(rail)->mux_ch` —— 光有地址是选不中的,
|
||||
所以任何「拿到地址就直接读」的代码在 R2 上都读的是**上一次选中的那一路**。 */
|
||||
uint8_t ina226_rail_addr(int rail);
|
||||
|
||||
extern const uint8_t ina226_all_addr[INA226_COUNT]; /* CH1..CH5 + 输入侧,扫描用 */
|
||||
extern const uint8_t ina226_addr_list[INA226_ADDR_COUNT]; /* {0x40, 0x43},扫描用 */
|
||||
|
||||
/* 六个地址互不重复、且不与 DAC63204(0x4A/0x4B) / PCA9555(0x20) / LM75(0x48) 撞车。
|
||||
/* 两个地址不重复、都合法、且不与主干上的 DAC63204(0x4A/0x4B) / TCA9548A(0x70)
|
||||
撞车,也不与刀片支路上的 PCA9555(0x20) 撞车。
|
||||
启动自检调一次;返回 false 说明表被改坏了。 */
|
||||
bool ina226_addr_table_ok(void);
|
||||
|
||||
@@ -68,7 +96,7 @@ bool ina226_addr_table_ok(void);
|
||||
|
||||
typedef struct {
|
||||
int32_t bus_mv; /* 母线电压,1.25 mV/LSB */
|
||||
int32_t current_ua; /* **有符号** —— 反灌时为负(SW-8 靠这个判据) */
|
||||
int32_t current_ua; /* **有符号** —— 反灌时为负(SW-8 靠这个判据);对调接的片子已由驱动取反 */
|
||||
} ina226_vi_t;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
/* PCA9555(U23,地址 0x20)—— 板上唯一的 I/O 扩展器,**16 个脚全部是输入**。
|
||||
/* PCA9555 —— **每片刀片上一颗**(`U4`,五片全是 0x20),R2 D9 取代 R1 的
|
||||
74HC595 链。五片靠底板 TCA9548A 分通道区分(D8),所以从驱动看它们**完全一样**。
|
||||
|
||||
挂的是五路 eFuse 的 FLT#/PGOOD、Buck 的线或 PWRGD、六片 INA226 的线或 ALERT,
|
||||
共 12 个信号 + 4 个直接接地的备用脚(设计文档 §17.2)。
|
||||
⚠️⚠️ **R1 那版驱动「16 个脚全是输入」的前提在 R2 不成立了。**
|
||||
R2 的 P0.0~P0.4 是**输出**:三个侧键 + EDL + VBUS 开关。
|
||||
拿 R1 的记忆改这个文件会在板子上冒烟,原因见下面那个框。
|
||||
|
||||
⚠️ **本驱动不提供「把某个脚配成输出」的接口,一个都不提供。**
|
||||
四个备用脚 P0_7 / P1_5~7 在板上**直接接地、不串限流电阻**(§17.2 的注,
|
||||
图纸上已标)。而 PCA9555 的 Output Port 寄存器**复位值是 0xFF**——
|
||||
所以只要把其中一个配成输出,它下一刻就输出高电平,**直接对地短路**。
|
||||
配错既不会报错也不会 NAK,只会发烫。所以方向字一律走 pca9555_dir_safe(),
|
||||
不安全直接 abort。这跟 sr595.h 不提供「抬 OE#」是同一条规矩。
|
||||
位分配从 `.claude/r2/_blade_pcb_nets.json` 的 `U4` 逐脚读回(2026-09-16):
|
||||
|
||||
§10.10 的降级行为:这片不应答 = 失去 FAULT / PG 可见性,**不关输出**,
|
||||
改用 INA226 的电压判据兜底并持续告警。所以本驱动的读失败一律只是返回错误码,
|
||||
不做任何动作。
|
||||
Port0 bit0 KEY_PWR bit1 KEY_VUP bit2 KEY_VDN bit3 EDL_EN bit4 VBUS_EN
|
||||
bit5~7 **直接接地**
|
||||
Port1 bit0 EFUSE_FLT# bit1 BUCK_PG bit2 INA_ALERT bit3 EFUSE_PG
|
||||
bit4~7 **直接接地**
|
||||
|
||||
本驱动只经 hal_i2c_*,宿主机上由 sim 的假 PCA9555(`sim_pca9555_*`)全部测完。 */
|
||||
┌─ ⛔⛔ 初始化顺序是这块板子上最危险的一处 ──────────────────────────────┐
|
||||
│ PCA9555 的 **Output Port 复位值是 0xFF**,Config 复位值也是 0xFF(全输入)。│
|
||||
│ 所以**必须先把输出锁存器写成 0,最后才改方向**: │
|
||||
│ OUTPUT0=0 → OUTPUT1=0 → POLINV=0 → CONFIG1=0xFF → CONFIG0=0xE0 │
|
||||
│ 顺序反过来的那一刻,P0.0~P0.4 一起输出 1 │
|
||||
│ = 三个键全按下 + EDL 短接 + VBUS 上电,五台手机一起被按住电源键。 │
|
||||
│ 而 P0.5~7 / P1.4~7 七个脚在刀片上**直接接地、不串限流电阻** —— │
|
||||
│ 方向写错一位就是对地短路:不报错、不 NAK,只会发烫。 │
|
||||
│ ⇒ `pca9555_dir_safe()` 是硬闸,走到 false 直接 abort。 │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
§10.10 的降级行为:某片不应答 = 失去那一路的 FAULT / PG 可见性,**不关输出**,
|
||||
改用 INA226 的电压判据兜底并持续告警。所以本驱动的读失败一律只返回错误码。
|
||||
⚠️ R2 新增的代价:这片不应答时**那一路的 VBUS 也控制不了了**(计划 §5.4)。
|
||||
|
||||
本驱动只经 hal_i2c_*,**不碰 mux**:通道由调用方(`slot.c`)先选好。 */
|
||||
#ifndef PCA9555_H
|
||||
#define PCA9555_H
|
||||
|
||||
@@ -27,17 +40,13 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- 地址与引脚 ---------------------------------------------------------- */
|
||||
/* ---- 地址与实例 ---------------------------------------------------------- */
|
||||
|
||||
/* A0 = A1 = A2 = GND(U23 的 pin21 / pin2 / pin3,已按 PCB 焊盘网络核过)。 */
|
||||
/* A0 = A1 = A2 = GND(`U4` 的 pin21 / pin2 / pin3,已按 PCB 焊盘网络核过)。
|
||||
**五片同址** —— 这是 D8 的全部意义,不是笔误。 */
|
||||
#define PCA9555_ADDR 0x20
|
||||
|
||||
/* `IOEXP_INT#`:开漏、低有效、板上 R608 10 kΩ 上拉,直连 ESP32 IO38(§8.1)。
|
||||
⚠️ **中断只是加速路径,不是主路径**:§10.3 的 `t_safety` 每 20 ms 无条件轮询
|
||||
读一次两个输入端口。原因是 PCA9555 的 INT 要**读走输入端口才会释放**,而且
|
||||
输入自己变回原值时它也会释放 —— 只靠中断的话,漏掉一次读就再也等不到下一个沿。
|
||||
A6 只留这个宏,读它是 A8/A10 的活。 */
|
||||
#define PCA9555_PIN_INT_N 38
|
||||
#define PCA9555_SLOT_COUNT 5
|
||||
|
||||
/* ---- 寄存器(全部 8 位) ------------------------------------------------- */
|
||||
|
||||
@@ -50,86 +59,124 @@ extern "C" {
|
||||
#define PCA9555_REG_CONFIG0 0x06
|
||||
#define PCA9555_REG_CONFIG1 0x07
|
||||
|
||||
/* 方向寄存器:**1 = 输入**(复位值 0xFF)。本板全输入,所以只会写这一个值。 */
|
||||
/* 方向寄存器:**1 = 输入**(复位值 0xFF)。 */
|
||||
#define PCA9555_DIR_ALL_INPUT 0xFFu
|
||||
|
||||
/* 极性反转寄存器必须是 0。写成非 0 的现象是「五路同时报故障」或者更糟的
|
||||
「五路故障全都看不见」,而 I²C 一切正常 —— init 会写 0 并回读校验。 */
|
||||
/* 本板的两个方向字。Port0 只有 bit0~4 是输出。 */
|
||||
#define PCA9555_CONFIG0_INIT 0xE0u
|
||||
#define PCA9555_CONFIG1_INIT 0xFFu
|
||||
|
||||
/* 极性反转必须是 0。写成非 0 的现象是「故障全都看不见」,而 I²C 一切正常。 */
|
||||
#define PCA9555_POLINV_NONE 0x00u
|
||||
|
||||
/* ---- 位分配(从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核对) -------
|
||||
/* ---- Port0:五个输出位 --------------------------------------------------- */
|
||||
|
||||
Port0 bit0~4 = EFUSE_FLT1~5 bit5 = BUCK_PG bit6 = INA_ALERT bit7 = 接地
|
||||
Port1 bit0~4 = EFUSE_PG1~5 bit5~7 = 接地
|
||||
/* 三个侧键的逻辑键位。**上层一律用这个枚举,不许写 `1 << 0`。**
|
||||
(R1 时这个枚举在 `sr595.h` 里,链退役之后搬到这里。) */
|
||||
typedef enum {
|
||||
PCA9555_KEY_PWR = 0,
|
||||
PCA9555_KEY_VUP,
|
||||
PCA9555_KEY_VDN,
|
||||
PCA9555_KEY_COUNT,
|
||||
} pca9555_key_t;
|
||||
|
||||
这一次两张表**确实是顺排的**(`bit = rail - 1`),和 INA226 地址、595 位号
|
||||
那两张表不一样。所以这里最容易写出来的错不是「算式」,而是
|
||||
**把 FLT 和 PG 的端口搞反** —— 两个端口的 bit0~4 长得一模一样,
|
||||
搞反之后「有故障」会被读成「电源良好」,方向正好相反。
|
||||
所以取位号一律走下面两个函数,不要在别处写 `1 << (rail - 1)`。 */
|
||||
#define PCA9555_BIT_KEY_PWR 0
|
||||
#define PCA9555_BIT_KEY_VUP 1
|
||||
#define PCA9555_BIT_KEY_VDN 2
|
||||
#define PCA9555_BIT_EDL_EN 3
|
||||
#define PCA9555_BIT_VBUS_EN 4
|
||||
|
||||
#define PCA9555_RAIL_COUNT 5
|
||||
#define PCA9555_P0_OUT_MASK 0x1Fu /* bit0~4:唯一允许配成输出的五位 */
|
||||
#define PCA9555_P0_GND_MASK 0xE0u /* bit5~7 焊死在地上 */
|
||||
|
||||
#define PCA9555_BIT_BUCK_PG 5 /* Port0,五路 Buck 的 PWRGD 线或(都是开漏) */
|
||||
#define PCA9555_BIT_INA_ALERT 6 /* Port0,六片 INA226 的 ALERT 线或 */
|
||||
/* ---- Port1:四个输入位(全是开漏 + 上拉) -------------------------------- */
|
||||
|
||||
/* 接地的备用脚。**永远、永远是输入。** */
|
||||
#define PCA9555_P0_GND_MASK 0x80u /* P0_7 */
|
||||
#define PCA9555_P1_GND_MASK 0xE0u /* P1_5 P1_6 P1_7 */
|
||||
#define PCA9555_BIT_EFUSE_FLT 0
|
||||
#define PCA9555_BIT_BUCK_PG 1
|
||||
#define PCA9555_BIT_INA_ALERT 2
|
||||
#define PCA9555_BIT_EFUSE_PG 3
|
||||
|
||||
/* rail 是 **1~5**,越界 abort。返回的是**位号**(0~4),不是掩码。 */
|
||||
int pca9555_flt_bit(int rail); /* Port0 */
|
||||
int pca9555_pg_bit(int rail); /* Port1 */
|
||||
#define PCA9555_P1_IN_MASK 0x0Fu
|
||||
#define PCA9555_P1_GND_MASK 0xF0u /* bit4~7 焊死在地上 */
|
||||
|
||||
/* 12 个信号位互不重复、都没落到接地脚上、FLT 与 PG 分属两个端口。
|
||||
启动自检调一次(A7,和 ina226_addr_table_ok() / sr595_map_ok() 一起)。 */
|
||||
/* 键 → Port0 位号。**不带路号**:五片刀片同构,位号与路号无关(计划 §5.7)。
|
||||
越界 abort。 */
|
||||
int pca9555_key_bit(pca9555_key_t key);
|
||||
|
||||
/* 一路上三个键的掩码(Port0)。`t_actuator` 的「防御性松键」用它。 */
|
||||
uint8_t pca9555_key_mask(void);
|
||||
|
||||
/* 位分配自检:五个输出位互不重复、四个输入位互不重复、
|
||||
**一个信号都没落到接地脚上**、输出位与输入位分属两个端口。
|
||||
启动自检调一次(和 `ina226_addr_table_ok()` 一起)。 */
|
||||
bool pca9555_map_ok(void);
|
||||
|
||||
/* 0x20 不与 INA226(0x40/41/44/45/42/43) / DAC63204(0x4A/0x4B) / LM75(0x48) 撞车。 */
|
||||
/* 0x20 不与主干上的 TCA9548A(0x70) / INA226(0x43) / DAC63204(0x4A/0x4B) 撞车。
|
||||
⚠️ 刀片上的 INA226 也是 0x40,与 0x20 本来就不同,两者同在一条支路上没问题。 */
|
||||
bool pca9555_addr_ok(void);
|
||||
|
||||
/* dir 里 1 = 输入。返回 false = 这个方向字会把某个**接地脚**配成输出。
|
||||
/* `dir` 里 1 = 输入。返回 false = 这个方向字会把某个**接地脚**配成输出,
|
||||
或者把 Port1 上那四个开漏信号源配成输出(与 eFuse 的 FLT# 打架)。
|
||||
驱动内部走到 false 直接 abort:悄悄放过去的后果是板子发烫,不是功能不好用。 */
|
||||
bool pca9555_dir_safe(int port, uint8_t dir);
|
||||
|
||||
/* ---- 采样 ---------------------------------------------------------------- */
|
||||
/* ---- 器件操作(当前 mux 通道上那一片) ----------------------------------- */
|
||||
|
||||
/* 认件:读回两个方向寄存器,全 0xFF(复位值)才算是它。
|
||||
不应答 → HAL_ERR_NAK;应答了但方向寄存器不是复位值 → HAL_ERR_BUS。
|
||||
⚠️ 已经被 `pca9555_init()` 配过的片子会返回 HAL_ERR_BUS —— 这是对的:
|
||||
probe 的语义是「这是一片刚上电的 PCA9555 吗」。 */
|
||||
hal_err_t pca9555_probe(void);
|
||||
|
||||
/* 按文件头那个框里的顺序初始化,并**逐个回读校验**。
|
||||
`slot` 是 0~`PCA9555_SLOT_COUNT-1`,**只用来记影子**,不参与总线操作 ——
|
||||
通道必须由调用方先选好(本驱动不碰 mux)。 */
|
||||
hal_err_t pca9555_init(int slot);
|
||||
|
||||
/* 巡检主路径:**一次事务**读两个输入端口(PCA9555 的读指针在 0x00 / 0x01 之间
|
||||
toggle,这两个字节是 datasheet 保证的)。 */
|
||||
typedef struct {
|
||||
uint8_t port0;
|
||||
uint8_t port1;
|
||||
} pca9555_in_t;
|
||||
|
||||
/* 认件:读回两个方向寄存器,全 0xFF(复位值)才算是它。
|
||||
不应答 → HAL_ERR_NAK;应答了但方向寄存器不是复位值 → HAL_ERR_BUS
|
||||
(这个地址上有东西,但不是一片刚上电的 PCA9555)。 */
|
||||
hal_err_t pca9555_probe(void);
|
||||
|
||||
/* 写方向(全输入)与极性反转(全 0)并**逐个回读校验**,回读不一致 → HAL_ERR_BUS。
|
||||
⚠️ Output Port 一个字节都不写:写它没有意义(没有输出脚),而写错方向的
|
||||
后果见文件头。 */
|
||||
hal_err_t pca9555_init(void);
|
||||
|
||||
/* 巡检主路径:**一次事务**读两个输入端口(§10.3 的 0.12 ms 预算就是按一次算的;
|
||||
PCA9555 的读指针在 0x00 / 0x01 之间 toggle,这两个字节是 datasheet 保证的)。 */
|
||||
hal_err_t pca9555_read_inputs(pca9555_in_t *out);
|
||||
|
||||
/* ---- 语义判读 ------------------------------------------------------------
|
||||
四个信号**三种极性**,这是本文件第二个容易写反的地方。极性只在下面四个函数里
|
||||
各出现一次(和 sr595_set_hub_reset() 里那个反相同一个道理:多一处就多一处写反
|
||||
的机会)。上层一律问「有没有故障」,不许自己去 & 位。 */
|
||||
/* ---- Port0 输出:影子 + 唯一写入点 --------------------------------------- */
|
||||
|
||||
/* TPS25940A 的 `FLT#`:开漏、板上 R113 47 kΩ 上拉 ⇒ **低 = 有故障**。 */
|
||||
bool pca9555_rail_fault(const pca9555_in_t *in, int rail);
|
||||
/* 改 `mask` 指定的那几位为 `value` 里的值,其余位保持影子里的值,整字节写下去。
|
||||
组合键的两位落在**同一次写**里(§10.6:分两次写会先单按一个键)。
|
||||
|
||||
/* TPS25940A 的 `PGOOD`:开漏、R115 47 kΩ 上拉,输出 2.97 V 以上才释放
|
||||
⇒ **高 = 电源良好**。 */
|
||||
bool pca9555_rail_pg(const pca9555_in_t *in, int rail);
|
||||
⚠️ **`slot` 必须与当前 mux 通道对应的那一路一致。** 驱动没法自己验证这件事,
|
||||
传错的后果是影子记在了另一片上 —— 下一次写会把这一片的键"顺手"改掉。
|
||||
这就是 `slot.c` 存在的理由:别在 `slot.c` 之外调这个函数。
|
||||
|
||||
/* 五路 Buck 的 `PWRGD` 线或(开漏)⇒ **高 = 五路都正常**;低只说明「有某一路
|
||||
不正常」,看不出是哪一路(§16.1)。 */
|
||||
`mask` 里出现输出位以外的位 → HAL_ERR_ARG(不是 abort:那可能是上层算错了
|
||||
掩码,返回错误码能让它记一条事件)。写失败时**影子不更新**。 */
|
||||
hal_err_t pca9555_write_out0(int slot, uint8_t mask, uint8_t value);
|
||||
|
||||
uint8_t pca9555_shadow(int slot); /* 该片 Port0 输出锁存器的影子(init 后是 0) */
|
||||
|
||||
/* 把影子全部清成 0,**不碰总线**。给测试的 setUp() 与 A15 的降级路径用
|
||||
(「现在生效的是上电默认值」)。 */
|
||||
void pca9555_shadow_reset(void);
|
||||
|
||||
/* ---- 语义判读(Port1) ---------------------------------------------------
|
||||
四个信号**两种极性**,这是本文件第二个容易写反的地方。极性只在下面四个函数里
|
||||
各出现一次。上层一律问「有没有故障」,不许自己去 & 位。 */
|
||||
|
||||
/* TPS25940A 的 `FLT#`:开漏、上拉 ⇒ **低 = 有故障**。 */
|
||||
bool pca9555_efuse_fault(const pca9555_in_t *in);
|
||||
|
||||
/* TPS25940A 的 `PGOOD`:开漏、上拉,输出过 2.97 V 才释放 ⇒ **高 = 电源良好**。 */
|
||||
bool pca9555_efuse_pg(const pca9555_in_t *in);
|
||||
|
||||
/* 本路 Buck 的 `PWRGD` ⇒ 高 = 正常。
|
||||
⚠️ R1 里这一位是**五路线或**,R2 里它只代表**本路**(一片刀片一个 Buck)。 */
|
||||
bool pca9555_buck_pg(const pca9555_in_t *in);
|
||||
|
||||
/* 六片 INA226 的 `ALERT` 线或:开漏、低有效 ⇒ **低 = 有片子在告警**。 */
|
||||
/* 本片 INA226 的 `ALERT`:开漏、低有效 ⇒ **低 = 在告警**。
|
||||
⚠️ R1 里是六片线或,R2 里刀片上的 ALERT 不跨板,只进本片的 P1.2。 */
|
||||
bool pca9555_ina_alert(const pca9555_in_t *in);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
+145
-133
@@ -1,133 +1,145 @@
|
||||
/* 74HC595 ×3 —— 一条 24 位移位链,带全固件唯一的影子寄存器(需求 §10.6)。
|
||||
|
||||
链上挂着 **15 路侧键 + 5 路 EDL_EN + HUB_RST**,共 21 位有效、3 位空脚。
|
||||
写任何一位都要把整条链重发一遍,所以:
|
||||
|
||||
⚠️ **内存里只有一份 24 位影子值,只有本文件的 `sr595_flush()` 会碰移位链。**
|
||||
任何任务改按键 / EDL 都是「`sr595_modify()` 改影子 → `sr595_flush()` 请求刷新」。
|
||||
多处直接操作移位链的症状是**另一台手机的按键被顺手清掉**,而且要两个动作
|
||||
正好撞在一起才复现 —— 属于会拖很久的那类 bug。
|
||||
|
||||
`RCLK` 打点之前输出不变 ⇒ 整帧刷新是原子的,组合键(VOL− + PWR)必须
|
||||
**一次 `sr595_write()` 两位一起给**,不能先后两次。
|
||||
|
||||
⚠️ **不许用拉高 `OE#` 的方式「临时全松开」**(需求 §10.6):那会让 15 个栅极
|
||||
同时进高阻,靠的是每个栅极自己的 100 kΩ 下拉 —— 那是**复位路径**,不是正常
|
||||
控制路径。所以本头文件根本不提供「抬 OE#」的接口。
|
||||
|
||||
本驱动只经 hal_gpio_*,宿主机上由 sim 的移位链观测器(`sim_sr595_*`)全部测完。 */
|
||||
#ifndef SR595_H
|
||||
#define SR595_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- 四根线(ESP32-S3 引脚,设计文档 §8.1 的表,§26.18 重排后) --------- */
|
||||
|
||||
#define SR595_PIN_SER 13 /* IO13 模组脚 21 → U21.DS(14) */
|
||||
#define SR595_PIN_SRCLK 12 /* IO12 模组脚 20 → 三片的 SRCLK(11) */
|
||||
#define SR595_PIN_RCLK 9 /* IO9 模组脚 17 → 三片的 RCLK(12) */
|
||||
#define SR595_PIN_OE_N 21 /* IO21 模组脚 23 → 三片的 OE#(13),板上另有 R609 100 kΩ 上拉 */
|
||||
|
||||
/* `MR#`(10) 三片都直接接 +3V3 —— 没有复位线,上电后移位寄存器内容是**不确定**的。
|
||||
所以 `sr595_init()` 必须先把安全影子锁存进去、**再**驱动 `OE#`。 */
|
||||
|
||||
/* ---- 24 位的编号 --------------------------------------------------------- */
|
||||
|
||||
/* 位号 = **链上的物理位置**:0 = U21.QA(离 ESP32 最近的第一级),23 = U33.QH(链尾)。
|
||||
一次移位 = 每一级往编号大的方向走一格,所以**先移入的位停在编号最大处**。 */
|
||||
#define SR595_CHIP_COUNT 3
|
||||
#define SR595_BITS 24
|
||||
#define SR595_MASK_ALL ((uint32_t)0x00FFFFFFu)
|
||||
|
||||
#define SR595_CHIP_U21 0 /* 影子的 bit0~7 ;P3B 页,板上 y≈50(中)*/
|
||||
#define SR595_CHIP_U22 1 /* 影子的 bit8~15 ;P3B 页,板上 y≈35(南)*/
|
||||
#define SR595_CHIP_U33 2 /* 影子的 bit16~23;**P6 页**,板上 y≈70(北)*/
|
||||
|
||||
/* 位分配(**2026-09-06 按 PCB 几何重排**,设计文档 §26.18 C;
|
||||
此处的值是从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核对出来的):
|
||||
|
||||
U21 QA=EDL_EN4 QB=K1_PWR QC=K1_VUP QD=K1_VDN QE=空 QF=EDL_EN5 QG=EDL_EN3 QH=EDL_EN2
|
||||
U22 QA=空 QB=K4_PWR QC=K4_VUP QD=K4_VDN QE=空 QF=K5_VDN QG=K5_VUP QH=K5_PWR
|
||||
U33 QA=EDL_EN1 QB=K2_VUP QC=K2_VDN QD=K2_PWR QE=K3_VDN QF=K3_VUP QG=K3_PWR QH=HUB_RST
|
||||
|
||||
⚠️ **「K1/K2/K3 顺排在 U21、U33 是 EDL 专用片」是旧版**(设计文档 §17.2 的
|
||||
09-04 那张表),15 路里有 12 路对不上。三片本来就是一条链,谁在哪一位只是移位
|
||||
的字节序 ⇒ **搞错了板子照样正常工作,只是按 CH1 的电源键按到了 CH4 的音量键**,
|
||||
功能测试完全测不出来。查表,不要算。 */
|
||||
#define SR595_BIT_HUB_RST 23
|
||||
|
||||
/* 空脚(U21.QE / U22.QA / U22.QE)。写它们没有意义,`sr595_modify()` 直接 abort。 */
|
||||
#define SR595_MASK_UNUSED (((uint32_t)1u << 4) | ((uint32_t)1u << 8) | ((uint32_t)1u << 12))
|
||||
#define SR595_MASK_VALID (SR595_MASK_ALL & ~SR595_MASK_UNUSED)
|
||||
|
||||
typedef enum {
|
||||
SR595_KEY_PWR = 0,
|
||||
SR595_KEY_VUP,
|
||||
SR595_KEY_VDN,
|
||||
SR595_KEY_COUNT,
|
||||
} sr595_key_t;
|
||||
|
||||
/* rail 是 **1~5**,key 越界或 rail 越界一律 abort(那是调用方算错了位号)。 */
|
||||
int sr595_key_bit(int rail, sr595_key_t key);
|
||||
uint32_t sr595_key_mask(int rail); /* 该路三键的掩码,「一路全松开」用得上 */
|
||||
int sr595_edl_bit(int rail);
|
||||
|
||||
/* 15 路侧键的总掩码。安全巡检与「全松开」用。 */
|
||||
uint32_t sr595_all_keys_mask(void);
|
||||
|
||||
/* 21 个有效位互不重复、都落在 SR595_MASK_VALID 里、空脚一个都没被占。
|
||||
启动自检调一次(A7);返回 false = 位表被改坏了。 */
|
||||
bool sr595_map_ok(void);
|
||||
|
||||
/* ---- 上电安全影子 -------------------------------------------------------- */
|
||||
|
||||
/* ⚠️ **`HUB_RST` 是低有效**(USB2517I 的 `RESET_N`(43),设计文档 §21.7,
|
||||
板上 R716 10 kΩ 上拉)。影子位 = 1 才是「Hub 正常跑」,= 0 是**按住复位**。
|
||||
全 0 的影子会把 Hub 一直摁在复位里 —— 现象是「插上枚举不出来」,
|
||||
而五路手机的电压电流全都正常,看起来一点毛病没有。
|
||||
所以上电安全值不是 0,是这个: */
|
||||
#define SR595_SHADOW_SAFE ((uint32_t)1u << SR595_BIT_HUB_RST)
|
||||
|
||||
/* 复位是低有效,反相只在 `sr595_set_hub_reset()` 里出现这一次
|
||||
(和 `rail_mv_to_code()` 里那个减法同一个道理:多一处就多一处写反的机会)。 */
|
||||
void sr595_set_hub_reset(bool in_reset);
|
||||
|
||||
/* ---- 器件操作 ------------------------------------------------------------ */
|
||||
|
||||
/* 三步,顺序不能换(上电时移位寄存器内容不确定,`MR#` 又接死在 +3V3):
|
||||
① SER / SRCLK / RCLK 配成输出并置低;**`OE#` 这时还是输入**,
|
||||
靠板上 R609 上拉着 ⇒ 三片输出保持高阻,15 个栅极被各自的 100 kΩ 拉低
|
||||
② 把 `SR595_SHADOW_SAFE` 移进去并锁存
|
||||
③ 这才把 `OE#` 配成输出并拉低,输出使能
|
||||
②③ 换个个儿 = 上电瞬间把移位寄存器里的随机值放到 15 个栅极上,
|
||||
也就是**五台手机的电源键可能一起被按下去**。 */
|
||||
hal_err_t sr595_init(void);
|
||||
|
||||
uint32_t sr595_shadow(void); /* 当前影子(未必已经刷下去,看 sr595_dirty()) */
|
||||
bool sr595_dirty(void); /* 影子和链上的值不一致 */
|
||||
|
||||
/* 影子 = (影子 & ~mask) | (value & mask),并置脏。**不碰总线**。
|
||||
mask 越出 SR595_MASK_VALID(含碰到空脚)直接 abort。 */
|
||||
void sr595_modify(uint32_t mask, uint32_t value);
|
||||
|
||||
/* 把影子整帧发下去:U33 字节 → U22 字节 → U21 字节,每个字节 QH 先发,
|
||||
最后一个 RCLK 上升沿统一生效。不脏就直接返回 HAL_OK(不空发一帧)。
|
||||
没 init 过 → HAL_ERR_STATE。 */
|
||||
hal_err_t sr595_flush(void);
|
||||
|
||||
/* `sr595_modify()` + `sr595_flush()`。**组合键必须走它**——
|
||||
VOL− + PWR 要落在同一帧里,分两次写会先单按一个键。 */
|
||||
hal_err_t sr595_write(uint32_t mask, uint32_t value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SR595_H */
|
||||
/* ⛔⛔ **R2 已退役:这条 24 位链在现在的板子上不存在。**
|
||||
(2026-09-16。R2 方案 D9 用「每片刀片一颗 PCA9555」取代了它,
|
||||
少 4 根跨板信号,而且复位态天然安全 —— PCA9555 上电是高阻输入,
|
||||
配栅极 100 k 下拉 ⇒ 按键必然松开;595 那套要靠 `KEY_OE#` 上拉才做得到同一件事。)
|
||||
|
||||
文件和测试**保留**(它自洽、测试全绿、不碰任何板级事实),但:
|
||||
· 没有任何生产代码 include 它,`rail_cfg` / `t_actuator` 都已经改走 `pca9555.h`;
|
||||
· 下面那些 `SR595_PIN_*` 的 GPIO 号是 **R1 底板**的,与 R2 的 `board_pins.h` 冲突
|
||||
(IO9/12/13/21 在 R2 上分别是 EFUSE_EN1/EN4/EN5 与 FAN_TACH_IN)——
|
||||
**别拿它们去查重、也别照着改 board_pins.h**;
|
||||
· 三个逻辑键位(PWR/VUP/VDN)已经搬到 `pca9555.h` 的 `pca9555_key_t`。
|
||||
要改 R2 的按键,去 `pca9555.h` 与 `main/slot.h`,不要动这个文件。 */
|
||||
/* 74HC595 ×3 —— 一条 24 位移位链,带全固件唯一的影子寄存器(需求 §10.6)。
|
||||
|
||||
链上挂着 **15 路侧键 + 5 路 EDL_EN + HUB_RST**,共 21 位有效、3 位空脚。
|
||||
写任何一位都要把整条链重发一遍,所以:
|
||||
|
||||
⚠️ **内存里只有一份 24 位影子值,只有本文件的 `sr595_flush()` 会碰移位链。**
|
||||
任何任务改按键 / EDL 都是「`sr595_modify()` 改影子 → `sr595_flush()` 请求刷新」。
|
||||
多处直接操作移位链的症状是**另一台手机的按键被顺手清掉**,而且要两个动作
|
||||
正好撞在一起才复现 —— 属于会拖很久的那类 bug。
|
||||
|
||||
`RCLK` 打点之前输出不变 ⇒ 整帧刷新是原子的,组合键(VOL− + PWR)必须
|
||||
**一次 `sr595_write()` 两位一起给**,不能先后两次。
|
||||
|
||||
⚠️ **不许用拉高 `OE#` 的方式「临时全松开」**(需求 §10.6):那会让 15 个栅极
|
||||
同时进高阻,靠的是每个栅极自己的 100 kΩ 下拉 —— 那是**复位路径**,不是正常
|
||||
控制路径。所以本头文件根本不提供「抬 OE#」的接口。
|
||||
|
||||
本驱动只经 hal_gpio_*,宿主机上由 sim 的移位链观测器(`sim_sr595_*`)全部测完。 */
|
||||
#ifndef SR595_H
|
||||
#define SR595_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- 四根线(ESP32-S3 引脚,设计文档 §8.1 的表,§26.18 重排后) --------- */
|
||||
|
||||
#define SR595_PIN_SER 13 /* IO13 模组脚 21 → U21.DS(14) */
|
||||
#define SR595_PIN_SRCLK 12 /* IO12 模组脚 20 → 三片的 SRCLK(11) */
|
||||
#define SR595_PIN_RCLK 9 /* IO9 模组脚 17 → 三片的 RCLK(12) */
|
||||
#define SR595_PIN_OE_N 21 /* IO21 模组脚 23 → 三片的 OE#(13),板上另有 R609 100 kΩ 上拉 */
|
||||
|
||||
/* `MR#`(10) 三片都直接接 +3V3 —— 没有复位线,上电后移位寄存器内容是**不确定**的。
|
||||
所以 `sr595_init()` 必须先把安全影子锁存进去、**再**驱动 `OE#`。 */
|
||||
|
||||
/* ---- 24 位的编号 --------------------------------------------------------- */
|
||||
|
||||
/* 位号 = **链上的物理位置**:0 = U21.QA(离 ESP32 最近的第一级),23 = U33.QH(链尾)。
|
||||
一次移位 = 每一级往编号大的方向走一格,所以**先移入的位停在编号最大处**。 */
|
||||
#define SR595_CHIP_COUNT 3
|
||||
#define SR595_BITS 24
|
||||
#define SR595_MASK_ALL ((uint32_t)0x00FFFFFFu)
|
||||
|
||||
#define SR595_CHIP_U21 0 /* 影子的 bit0~7 ;P3B 页,板上 y≈50(中)*/
|
||||
#define SR595_CHIP_U22 1 /* 影子的 bit8~15 ;P3B 页,板上 y≈35(南)*/
|
||||
#define SR595_CHIP_U33 2 /* 影子的 bit16~23;**P6 页**,板上 y≈70(北)*/
|
||||
|
||||
/* 位分配(**2026-09-06 按 PCB 几何重排**,设计文档 §26.18 C;
|
||||
此处的值是从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核对出来的):
|
||||
|
||||
U21 QA=EDL_EN4 QB=K1_PWR QC=K1_VUP QD=K1_VDN QE=空 QF=EDL_EN5 QG=EDL_EN3 QH=EDL_EN2
|
||||
U22 QA=空 QB=K4_PWR QC=K4_VUP QD=K4_VDN QE=空 QF=K5_VDN QG=K5_VUP QH=K5_PWR
|
||||
U33 QA=EDL_EN1 QB=K2_VUP QC=K2_VDN QD=K2_PWR QE=K3_VDN QF=K3_VUP QG=K3_PWR QH=HUB_RST
|
||||
|
||||
⚠️ **「K1/K2/K3 顺排在 U21、U33 是 EDL 专用片」是旧版**(设计文档 §17.2 的
|
||||
09-04 那张表),15 路里有 12 路对不上。三片本来就是一条链,谁在哪一位只是移位
|
||||
的字节序 ⇒ **搞错了板子照样正常工作,只是按 CH1 的电源键按到了 CH4 的音量键**,
|
||||
功能测试完全测不出来。查表,不要算。 */
|
||||
#define SR595_BIT_HUB_RST 23
|
||||
|
||||
/* 空脚(U21.QE / U22.QA / U22.QE)。写它们没有意义,`sr595_modify()` 直接 abort。 */
|
||||
#define SR595_MASK_UNUSED (((uint32_t)1u << 4) | ((uint32_t)1u << 8) | ((uint32_t)1u << 12))
|
||||
#define SR595_MASK_VALID (SR595_MASK_ALL & ~SR595_MASK_UNUSED)
|
||||
|
||||
typedef enum {
|
||||
SR595_KEY_PWR = 0,
|
||||
SR595_KEY_VUP,
|
||||
SR595_KEY_VDN,
|
||||
SR595_KEY_COUNT,
|
||||
} sr595_key_t;
|
||||
|
||||
/* rail 是 **1~5**,key 越界或 rail 越界一律 abort(那是调用方算错了位号)。 */
|
||||
int sr595_key_bit(int rail, sr595_key_t key);
|
||||
uint32_t sr595_key_mask(int rail); /* 该路三键的掩码,「一路全松开」用得上 */
|
||||
int sr595_edl_bit(int rail);
|
||||
|
||||
/* 15 路侧键的总掩码。安全巡检与「全松开」用。 */
|
||||
uint32_t sr595_all_keys_mask(void);
|
||||
|
||||
/* 21 个有效位互不重复、都落在 SR595_MASK_VALID 里、空脚一个都没被占。
|
||||
启动自检调一次(A7);返回 false = 位表被改坏了。 */
|
||||
bool sr595_map_ok(void);
|
||||
|
||||
/* ---- 上电安全影子 -------------------------------------------------------- */
|
||||
|
||||
/* ⚠️ **`HUB_RST` 是低有效**(USB2517I 的 `RESET_N`(43),设计文档 §21.7,
|
||||
板上 R716 10 kΩ 上拉)。影子位 = 1 才是「Hub 正常跑」,= 0 是**按住复位**。
|
||||
全 0 的影子会把 Hub 一直摁在复位里 —— 现象是「插上枚举不出来」,
|
||||
而五路手机的电压电流全都正常,看起来一点毛病没有。
|
||||
所以上电安全值不是 0,是这个: */
|
||||
#define SR595_SHADOW_SAFE ((uint32_t)1u << SR595_BIT_HUB_RST)
|
||||
|
||||
/* 复位是低有效,反相只在 `sr595_set_hub_reset()` 里出现这一次
|
||||
(和 `rail_mv_to_code()` 里那个减法同一个道理:多一处就多一处写反的机会)。 */
|
||||
void sr595_set_hub_reset(bool in_reset);
|
||||
|
||||
/* ---- 器件操作 ------------------------------------------------------------ */
|
||||
|
||||
/* 三步,顺序不能换(上电时移位寄存器内容不确定,`MR#` 又接死在 +3V3):
|
||||
① SER / SRCLK / RCLK 配成输出并置低;**`OE#` 这时还是输入**,
|
||||
靠板上 R609 上拉着 ⇒ 三片输出保持高阻,15 个栅极被各自的 100 kΩ 拉低
|
||||
② 把 `SR595_SHADOW_SAFE` 移进去并锁存
|
||||
③ 这才把 `OE#` 配成输出并拉低,输出使能
|
||||
②③ 换个个儿 = 上电瞬间把移位寄存器里的随机值放到 15 个栅极上,
|
||||
也就是**五台手机的电源键可能一起被按下去**。 */
|
||||
hal_err_t sr595_init(void);
|
||||
|
||||
uint32_t sr595_shadow(void); /* 当前影子(未必已经刷下去,看 sr595_dirty()) */
|
||||
bool sr595_dirty(void); /* 影子和链上的值不一致 */
|
||||
|
||||
/* 影子 = (影子 & ~mask) | (value & mask),并置脏。**不碰总线**。
|
||||
mask 越出 SR595_MASK_VALID(含碰到空脚)直接 abort。 */
|
||||
void sr595_modify(uint32_t mask, uint32_t value);
|
||||
|
||||
/* 把影子整帧发下去:U33 字节 → U22 字节 → U21 字节,每个字节 QH 先发,
|
||||
最后一个 RCLK 上升沿统一生效。不脏就直接返回 HAL_OK(不空发一帧)。
|
||||
没 init 过 → HAL_ERR_STATE。 */
|
||||
hal_err_t sr595_flush(void);
|
||||
|
||||
/* `sr595_modify()` + `sr595_flush()`。**组合键必须走它**——
|
||||
VOL− + PWR 要落在同一帧里,分两次写会先单按一个键。 */
|
||||
hal_err_t sr595_write(uint32_t mask, uint32_t value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SR595_H */
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/* TCA9548A —— 底板上的 I²C 八路多路器(U35,0x70),R2 新增(方案 D8)。
|
||||
|
||||
它的存在让五片刀片可以**完全同构**:每片上都是 0x40 的 INA226 + 0x20 的
|
||||
PCA9555,零 strap 电阻、五片可互换。代价是**每一次访问刀片都先得选通道**。
|
||||
|
||||
---- 这颗器件的三件反直觉的事 --------------------------------------------
|
||||
|
||||
1. **它没有寄存器地址。** 整个器件只有一个 8 位控制寄存器,写就是「发一个
|
||||
字节」、读就是「收一个字节」,**不带寄存器指针**。所以这里用的是
|
||||
`hal_i2c_write(addr, &byte, 1)` 而不是常见的「写 reg + 写 val」。
|
||||
按后者写会把通道选成 `reg` 那个字节的值 —— 而且**不会报错**。
|
||||
|
||||
2. **控制字是掩码不是编号。** bit n = 1 表示通道 n 接通,**可以同时开多路**。
|
||||
同时开两路的后果是两片同地址的 INA226 一起应答,读回来的是两片的**线与**
|
||||
(量纲正常、数值偏小、还很稳)。所以本驱动只接受「恰好一路」或「全关」,
|
||||
多路那种用法在这块板上永远是 bug。
|
||||
|
||||
3. **`RESET#` 只复位它自己**(把通道全关),不影响挂在支路上的器件。
|
||||
复位脉冲很短(datasheet 最小 6 ns),但复位之后**通道是全关的** ——
|
||||
所以调用方的通道影子必须一起作废,见 `i2cbus.h`。
|
||||
|
||||
⚠️ 本驱动**不做加锁,也不管影子**。「选通道 + 事务」的原子性是 `i2cbus`
|
||||
那一层的事(计划 §4.3)—— 混在这里会让驱动没法单独测。 */
|
||||
#ifndef TCA9548A_H
|
||||
#define TCA9548A_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* A0 = A1 = A2 = GND(U35 的 pin1 / pin2 / pin21,已按 PCB 焊盘网络核过)。 */
|
||||
#define TCA9548A_ADDR 0x70
|
||||
|
||||
#define TCA9548A_CH_COUNT 8
|
||||
|
||||
/* 全关。`tca9548a_select()` 传这个值 = 断开所有支路,只剩主干可见
|
||||
(主干上是 DAC63204 ×2 + 输入侧 INA226 + CH224A 以外的东西)。 */
|
||||
#define TCA9548A_CH_NONE (-1)
|
||||
|
||||
/* 通道 → 去哪(`.claude/r2/_base_pcb_nets.json` 的 `U35` 逐脚读回,2026-09-16)。
|
||||
⚠️ 槽 1~5 这次**确实是顺排的**,但通道号仍然从 `rail_cfg` 那张表里查 ——
|
||||
顺排是当下的事实,不是不变量。 */
|
||||
#define TCA9548A_CH_SLOT1 0
|
||||
#define TCA9548A_CH_SLOT5 4
|
||||
#define TCA9548A_CH_PD 5 /* SDA6/SCL6 → U36 CH224A(PD 受电芯片) */
|
||||
/* 通道 6 / 7 悬空。 */
|
||||
|
||||
/* 0x70 不与 INA226(0x40/0x43) / DAC63204(0x4A/0x4B) / PCA9555(0x20) 撞车。
|
||||
启动自检调一次。 */
|
||||
bool tca9548a_addr_ok(void);
|
||||
|
||||
/* 选通道。`ch` 是 0~7 或 `TCA9548A_CH_NONE`;其它值 → HAL_ERR_ARG。
|
||||
**一次只开一路**(见文件头第 2 条)。 */
|
||||
hal_err_t tca9548a_select(uint8_t addr, int ch);
|
||||
|
||||
/* 读回控制字(一个字节,不带寄存器指针)。
|
||||
用来验证「选通道真的生效了」—— 写下去不报错但没生效是这颗器件最难查的失效
|
||||
(比如上拉太弱、支路被某个器件拉死)。 */
|
||||
hal_err_t tca9548a_read_mask(uint8_t addr, uint8_t *mask);
|
||||
|
||||
/* 认件:全关之后读回控制字,必须是 0。
|
||||
不应答 → HAL_ERR_NAK;应答了但读回不是 0 → HAL_ERR_BUS(地址上有东西,
|
||||
但它不是一颗听话的 TCA9548A)。 */
|
||||
hal_err_t tca9548a_probe(uint8_t addr);
|
||||
|
||||
/* 掩码 → 通道号。恰好一位才返回 0~7;全 0 返回 `TCA9548A_CH_NONE`;
|
||||
**多于一位返回 -2**(见文件头第 2 条:那是 bug,不是一种用法)。 */
|
||||
int tca9548a_mask_to_ch(uint8_t mask);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* TCA9548A_H */
|
||||
+149
-58
@@ -1,4 +1,5 @@
|
||||
/* PCA9555 驱动。只经 hal_i2c_*,不碰任何平台头文件。 */
|
||||
/* PCA9555 驱动(R2:每片刀片一颗,P0.0~P0.4 是输出)。
|
||||
只经 hal_i2c_*,不碰 mux、不碰任何平台头文件。契约见 pca9555.h。 */
|
||||
#include "pca9555.h"
|
||||
|
||||
#include <assert.h>
|
||||
@@ -6,47 +7,83 @@
|
||||
|
||||
#include "hal_i2c.h"
|
||||
|
||||
/* 位表见 pca9555.h。这两张表**这次确实是顺排的**,但仍然写成表:
|
||||
哪天硬件改一根线,改这里一处就够,而不是满工程找 `1 << (rail - 1)`。 */
|
||||
static const uint8_t s_flt_bit[PCA9555_RAIL_COUNT] = { 0, 1, 2, 3, 4 }; /* Port0 */
|
||||
static const uint8_t s_pg_bit[PCA9555_RAIL_COUNT] = { 0, 1, 2, 3, 4 }; /* Port1 */
|
||||
/* 键 → 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,
|
||||
};
|
||||
|
||||
int pca9555_flt_bit(int rail)
|
||||
/* 五片各自的 Port0 输出影子。**写成功才更新**(见 pca9555_write_out0)。 */
|
||||
static uint8_t s_out0[PCA9555_SLOT_COUNT];
|
||||
|
||||
int pca9555_key_bit(pca9555_key_t key)
|
||||
{
|
||||
assert(rail >= 1 && rail <= PCA9555_RAIL_COUNT); /* 越界是调用方算错了,不许悄悄返回 */
|
||||
return s_flt_bit[rail - 1];
|
||||
assert(key >= 0 && key < PCA9555_KEY_COUNT); /* 越界是调用方算错了 */
|
||||
return s_key_bit[key];
|
||||
}
|
||||
|
||||
int pca9555_pg_bit(int rail)
|
||||
uint8_t pca9555_key_mask(void)
|
||||
{
|
||||
assert(rail >= 1 && rail <= PCA9555_RAIL_COUNT);
|
||||
return s_pg_bit[rail - 1];
|
||||
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, p1 = 0;
|
||||
uint8_t p0 = 0;
|
||||
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
const uint8_t f = (uint8_t)(1u << s_flt_bit[rail - 1]);
|
||||
const uint8_t g = (uint8_t)(1u << s_pg_bit[rail - 1]);
|
||||
if ((p0 & f) != 0 || (p1 & g) != 0) {
|
||||
return false; /* 两路占了同一位 */
|
||||
/* 三个键互不重复。 */
|
||||
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 |= f;
|
||||
p1 |= g;
|
||||
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;
|
||||
}
|
||||
|
||||
const uint8_t buck = (uint8_t)(1u << PCA9555_BIT_BUCK_PG);
|
||||
const uint8_t alrt = (uint8_t)(1u << PCA9555_BIT_INA_ALERT);
|
||||
if ((p0 & buck) != 0 || (p0 & alrt) != 0 || buck == alrt) {
|
||||
return false; /* 线或的两根撞到某路 FLT 上,或者撞到一起 */
|
||||
/* 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;
|
||||
}
|
||||
p0 |= (uint8_t)(buck | alrt);
|
||||
|
||||
/* 一个信号都不许落到接地脚上 —— 落上去就等于永远读到 0(FLT 永远报故障,
|
||||
PG 永远报「不良」),而 I²C 一切正常。 */
|
||||
if ((p0 & PCA9555_P0_GND_MASK) != 0 || (p1 & PCA9555_P1_GND_MASK) != 0) {
|
||||
/* 两个方向字必须与位分配自洽: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;
|
||||
@@ -54,8 +91,8 @@ bool pca9555_map_ok(void)
|
||||
|
||||
bool pca9555_addr_ok(void)
|
||||
{
|
||||
/* 同一条 I²C 上的其它器件(§8.2)。 */
|
||||
static const uint8_t others[] = { 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x48, 0x4A, 0x4B };
|
||||
/* 主干上的器件(计划 §5.2)。刀片支路上另有 0x40 的 INA226,与 0x20 不冲突。 */
|
||||
static const uint8_t others[] = { 0x40, 0x43, 0x4A, 0x4B, 0x70 };
|
||||
|
||||
if (PCA9555_ADDR > 0x7f) {
|
||||
return false;
|
||||
@@ -70,19 +107,22 @@ bool pca9555_addr_ok(void)
|
||||
|
||||
bool pca9555_dir_safe(int port, uint8_t dir)
|
||||
{
|
||||
if (port != 0 && port != 1) {
|
||||
return false;
|
||||
/* 1 = 输入。 */
|
||||
if (port == 0) {
|
||||
/* 接地脚必须是输入,一位都不能少。bit0~4 随意(它们就是要当输出的)。 */
|
||||
return (dir & PCA9555_P0_GND_MASK) == PCA9555_P0_GND_MASK;
|
||||
}
|
||||
/* 1 = 输入。接地脚对应的位必须是 1,一位都不能少。 */
|
||||
const uint8_t gnd = (port == 0) ? PCA9555_P0_GND_MASK : PCA9555_P1_GND_MASK;
|
||||
return (dir & gnd) == gnd;
|
||||
if (port == 1) {
|
||||
/* Port1 **一位都不许输出**:bit0~3 接的是 eFuse / Buck / INA 的开漏输出,
|
||||
配成输出就是两个驱动器对打;bit4~7 焊死在地上。 */
|
||||
return dir == PCA9555_DIR_ALL_INPUT;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---- 寄存器读写 ---------------------------------------------------------
|
||||
PCA9555 的寄存器是 8 位的,命令字节在**成对**的寄存器之间 toggle
|
||||
(0/1、2/3、4/5、6/7),不是无限自增。本驱动只在一对之内连读 2 字节,
|
||||
落在 datasheet 保证的范围里;跨对连读(比如从 0x06 读 4 字节)行为不同,
|
||||
别那么写。 */
|
||||
PCA9555 的命令字节在**成对**的寄存器之间 toggle(0/1、2/3、4/5、6/7),
|
||||
不是无限自增。本驱动只在一对之内连读 2 字节,落在 datasheet 保证的范围里。 */
|
||||
|
||||
static hal_err_t read_reg(uint8_t reg, uint8_t *val)
|
||||
{
|
||||
@@ -121,34 +161,53 @@ hal_err_t pca9555_probe(void)
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
/* 刚上电的 PCA9555 两个方向寄存器都是 0xFF。不是的话,要么这地址上蹲着别的
|
||||
器件,要么它已经被谁配过了 —— 两种都要在启动时就叫出来。 */
|
||||
return (c0 == PCA9555_DIR_ALL_INPUT && c1 == PCA9555_DIR_ALL_INPUT) ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
hal_err_t pca9555_init(void)
|
||||
hal_err_t pca9555_init(int slot)
|
||||
{
|
||||
/* 方向必须先于一切:这一步写错就是四个接地脚被驱动成高电平。
|
||||
全输入是唯一合法值,assert 拦的是「哪天有人顺手改了这个常量」。 */
|
||||
assert(pca9555_dir_safe(0, PCA9555_DIR_ALL_INPUT));
|
||||
assert(pca9555_dir_safe(1, PCA9555_DIR_ALL_INPUT));
|
||||
if (slot < 0 || slot >= PCA9555_SLOT_COUNT) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
/* 方向字的合法性是编译期就能判的事,assert 拦的是「哪天有人顺手改了常量」。 */
|
||||
assert(pca9555_dir_safe(0, PCA9555_CONFIG0_INIT));
|
||||
assert(pca9555_dir_safe(1, PCA9555_CONFIG1_INIT));
|
||||
|
||||
hal_err_t err = write_verify(PCA9555_REG_CONFIG0, PCA9555_DIR_ALL_INPUT);
|
||||
/* ⛔ 顺序就是安全本身(头文件那个框):**输出锁存器先清零,方向最后改**。
|
||||
复位值 0xFF 的输出锁存器 + 先改方向 = 三个键全按下 + VBUS 上电。 */
|
||||
hal_err_t err = write_verify(PCA9555_REG_OUTPUT0, 0x00);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
err = write_verify(PCA9555_REG_CONFIG1, PCA9555_DIR_ALL_INPUT);
|
||||
err = write_verify(PCA9555_REG_OUTPUT1, 0x00);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* 极性反转清零。复位值本来就是 0,但「本来就是」不值得赌 —— 看门狗复位
|
||||
只复位 ESP32,PCA9555 是不掉电的,上一条命的残留值会原样留着。 */
|
||||
只复位 ESP32,刀片上的 PCA9555 是不掉电的,上一条命的残留值会原样留着。 */
|
||||
err = write_verify(PCA9555_REG_POLINV0, PCA9555_POLINV_NONE);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return write_verify(PCA9555_REG_POLINV1, PCA9555_POLINV_NONE);
|
||||
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] = 0;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t pca9555_read_inputs(pca9555_in_t *out)
|
||||
@@ -167,28 +226,60 @@ hal_err_t pca9555_read_inputs(pca9555_in_t *out)
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* ---- 语义判读(极性各出现一次,见头文件) -------------------------------- */
|
||||
|
||||
bool pca9555_rail_fault(const pca9555_in_t *in, int rail)
|
||||
hal_err_t pca9555_write_out0(int slot, uint8_t mask, uint8_t value)
|
||||
{
|
||||
assert(in != NULL);
|
||||
return (in->port0 & (uint8_t)(1u << pca9555_flt_bit(rail))) == 0; /* FLT# 低有效 */
|
||||
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;
|
||||
}
|
||||
|
||||
bool pca9555_rail_pg(const pca9555_in_t *in, int rail)
|
||||
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_pg_bit(rail))) != 0; /* PGOOD 高 = 良好 */
|
||||
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->port0 & (uint8_t)(1u << PCA9555_BIT_BUCK_PG)) != 0; /* 高 = 五路都正常 */
|
||||
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->port0 & (uint8_t)(1u << PCA9555_BIT_INA_ALERT)) == 0; /* ALERT 低有效 */
|
||||
return (in->port1 & (uint8_t)(1u << PCA9555_BIT_INA_ALERT)) == 0; /* 低有效 */
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/* TCA9548A 驱动。只经 hal_i2c_*,不碰任何平台头文件。契约见 tca9548a.h。 */
|
||||
#include "tca9548a.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "hal_i2c.h"
|
||||
|
||||
bool tca9548a_addr_ok(void)
|
||||
{
|
||||
/* 同一条主干上的其它器件。撞上任何一个都是硬件表被改坏了。
|
||||
⚠️ 0x40 / 0x20 **不在**这张表里:它们在 mux 的支路上,与主干不冲突 ——
|
||||
这正是 D8 的全部意义。 */
|
||||
static const uint8_t others[] = { 0x43, 0x4A, 0x4B };
|
||||
|
||||
if (TCA9548A_ADDR > 0x7f) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(others); i++) {
|
||||
if (others[i] == TCA9548A_ADDR) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
hal_err_t tca9548a_select(uint8_t addr, int ch)
|
||||
{
|
||||
if (ch < TCA9548A_CH_NONE || ch >= TCA9548A_CH_COUNT) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
/* 一个字节,**没有寄存器指针**(文件头第 1 条)。 */
|
||||
const uint8_t mask = (ch == TCA9548A_CH_NONE) ? 0u : (uint8_t)(1u << ch);
|
||||
return hal_i2c_write(addr, &mask, 1);
|
||||
}
|
||||
|
||||
hal_err_t tca9548a_read_mask(uint8_t addr, uint8_t *mask)
|
||||
{
|
||||
if (mask == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
return hal_i2c_read(addr, mask, 1);
|
||||
}
|
||||
|
||||
hal_err_t tca9548a_probe(uint8_t addr)
|
||||
{
|
||||
hal_err_t err = tca9548a_select(addr, TCA9548A_CH_NONE);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
uint8_t mask = 0xFF;
|
||||
err = tca9548a_read_mask(addr, &mask);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
/* 刚被写成全关的器件读回来必须是 0。不是的话要么这地址上蹲着别的东西,
|
||||
要么写根本没生效 —— 后者在真板上是「所有刀片都读不到」,
|
||||
而这里能在启动自检时就叫出来。 */
|
||||
return (mask == 0) ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
int tca9548a_mask_to_ch(uint8_t mask)
|
||||
{
|
||||
if (mask == 0) {
|
||||
return TCA9548A_CH_NONE;
|
||||
}
|
||||
if ((mask & (uint8_t)(mask - 1u)) != 0) {
|
||||
return -2; /* 多于一位:见文件头第 2 条 */
|
||||
}
|
||||
for (int ch = 0; ch < TCA9548A_CH_COUNT; ch++) {
|
||||
if (mask == (uint8_t)(1u << ch)) {
|
||||
return ch;
|
||||
}
|
||||
}
|
||||
return -2; /* 到不了 */
|
||||
}
|
||||
@@ -1,82 +1,90 @@
|
||||
/* 互斥锁 —— HAL 的第五样东西,而且**只有两把**(需求 §10.3)。
|
||||
|
||||
计划 §4.1 写着「HAL 只抽象四样:I²C / GPIO / PWM-定时 / 单调时钟,不要抽象得更多」。
|
||||
这里多出来的第五样是有理由的:§10.3 的五个任务共享的资源正好是两个
|
||||
—— I²C 总线(10 个器件在同一条上)和 74HC595 的 24 位链 —— 而「谁在用总线」
|
||||
这件事没法用前四样表达。
|
||||
|
||||
为了不让它长成一个通用的锁库,这里**没有创建接口**:两把锁是枚举出来的,
|
||||
编译期就这么多,也不许再加第三把。要加第三把之前先回头看一眼 §10.3
|
||||
——「共享资源只有两个」是架构结论,不是当时懒得数。
|
||||
|
||||
⚠️ **两把锁不许嵌套持有。** 595 那条链是纯 GPIO(SER/SRCLK/RCLK),
|
||||
和 I²C 没有任何交集,所以「拿着 A 去拿 B」这件事在这块板子上永远是写错了,
|
||||
而不是一个需要小心处理的情况。`hal_lock_take()` 直接把它判成 HAL_ERR_STATE
|
||||
—— 与其定一套加锁顺序然后指望后面每个人都记得,不如让它根本不可能发生。 */
|
||||
#ifndef HAL_LOCK_H
|
||||
#define HAL_LOCK_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
HAL_LOCK_I2C = 0, /* I²C 总线:6×INA226 + 2×DAC63204 + PCA9555(+ 载板 LM75) */
|
||||
HAL_LOCK_SR595, /* 74HC595 的 24 位链,全板共享(§10.6 的影子寄存器) */
|
||||
HAL_LOCK_COUNT,
|
||||
} hal_lock_id_t;
|
||||
|
||||
/* 幂等。任何任务起来之前调一次;没 init 就 take 会返回 HAL_ERR_STATE
|
||||
(FreeRTOS 上那是个空句柄,不拦就是随机崩)。 */
|
||||
hal_err_t hal_lock_init(void);
|
||||
|
||||
/* 拿锁。timeout_ms = HAL_LOCK_FOREVER 表示一直等。
|
||||
HAL_ERR_TIMEOUT —— 等超时了,**调用方必须自己处理**:巡检任务应当跳过这一周期
|
||||
并计一次数,不要死等(死等的现象是 t_safety 整个停摆)。
|
||||
HAL_ERR_STATE —— 没 init,或者违反了「不许嵌套持有两把锁」。
|
||||
同一个任务重复拿同一把锁也是 HAL_ERR_STATE:这两把都不是递归锁。 */
|
||||
#define HAL_LOCK_FOREVER 0xFFFFFFFFu
|
||||
hal_err_t hal_lock_take(hal_lock_id_t id, uint32_t timeout_ms);
|
||||
|
||||
/* 放锁。不是自己拿的、或者根本没人拿,都会 abort ——
|
||||
那是代码写错了,悄悄放掉会让下一个持有者莫名其妙地丢掉互斥。 */
|
||||
void hal_lock_give(hal_lock_id_t id);
|
||||
|
||||
/* **当前任务**是不是正持着这把锁。给断言和 sim 的强制检查用,
|
||||
不要拿它做流程控制(「没拿就拿一下」正是双重加锁的写法)。 */
|
||||
bool hal_lock_held(hal_lock_id_t id);
|
||||
|
||||
/* 累计等锁失败次数(超时)。遥测里报出去,用来看总线是不是排队了。 */
|
||||
uint32_t hal_lock_timeouts(hal_lock_id_t id);
|
||||
|
||||
/* ---- 临界区(A11)——**这不是第三把锁** ----------------------------------
|
||||
|
||||
事件环形缓冲的入队要在几条指令里改完两个下标,而入队方与出队方是
|
||||
优先级两端的两个任务:`t_safety`(最高,而且是在**断电之后**调的)
|
||||
与 `t_log`(最低)。这里不能用互斥锁,理由有两条,都不是风格问题:
|
||||
|
||||
· **不许阻塞。** t_safety.h 写着「接进来的东西不许阻塞」——
|
||||
入队要是可能停在一把 t_log 持着的锁上,巡检的节拍就被最低优先级的任务
|
||||
决定了。优先级继承能救回来,但那是「让阻塞变短」,不是「不阻塞」。
|
||||
· **§10.3 的共享资源只有两个**(I²C 与 595 链),这条架构结论不该被
|
||||
一个环形缓冲的下标改写。
|
||||
|
||||
所以给的是关中断的临界区:不阻塞、没有持有者、不参与「不许嵌套两把锁」
|
||||
那条规则,也不出现在 `hal_lock_id_t` 里。
|
||||
|
||||
⚠️ **里面只许改内存。** 关着中断去等锁、睡、跑 I²C 事务,都是错的
|
||||
—— sim 里 `sim_crit_blocking()` 会计数,`test_evlog.c` 拿它当断言。
|
||||
可以嵌套(按层数计),但没有理由这么写。 */
|
||||
void hal_crit_enter(void);
|
||||
void hal_crit_exit(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HAL_LOCK_H */
|
||||
/* 互斥锁 —— HAL 的第五样东西,而且**只有两把**(需求 §10.3)。
|
||||
|
||||
计划 §4.1 写着「HAL 只抽象四样:I²C / GPIO / PWM-定时 / 单调时钟,不要抽象得更多」。
|
||||
这里多出来的第五样是有理由的:§10.3 的五个任务共享的资源正好是两个
|
||||
—— I²C 总线(10 个器件在同一条上)和 74HC595 的 24 位链 —— 而「谁在用总线」
|
||||
这件事没法用前四样表达。
|
||||
|
||||
为了不让它长成一个通用的锁库,这里**没有创建接口**:两把锁是枚举出来的,
|
||||
编译期就这么多,也不许再加第三把。要加第三把之前先回头看一眼 §10.3
|
||||
——「共享资源只有两个」是架构结论,不是当时懒得数。
|
||||
|
||||
⚠️ **两把锁不许嵌套持有。** 595 那条链是纯 GPIO(SER/SRCLK/RCLK),
|
||||
和 I²C 没有任何交集,所以「拿着 A 去拿 B」这件事在这块板子上永远是写错了,
|
||||
而不是一个需要小心处理的情况。`hal_lock_take()` 直接把它判成 HAL_ERR_STATE
|
||||
—— 与其定一套加锁顺序然后指望后面每个人都记得,不如让它根本不可能发生。 */
|
||||
#ifndef HAL_LOCK_H
|
||||
#define HAL_LOCK_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
/* I²C 总线。**R2 之后这是全系统唯一还在用的一把锁**:
|
||||
TCA9548A + 输入侧 INA226 + 2×DAC63204(主干)、五片刀片各自的
|
||||
INA226 + PCA9555(mux 支路)。按键 / EDL / VBUS 也都在这条线上了。 */
|
||||
HAL_LOCK_I2C = 0,
|
||||
/* ⛔ **R2 已退役**(2026-09-16,方案 D9:74HC595 链换成刀片上的 PCA9555)。
|
||||
枚举留着是因为 `sim_lock_*` 的那套「两把锁不许嵌套」的检查仍要有第二把锁
|
||||
才测得动;生产代码里**没有任何一处再拿它**。
|
||||
R2 只剩一把锁,于是「嵌套」在物理上不可能 —— 这是 D9 顺手带来的好处,
|
||||
代价是执行器与巡检开始争用同一把锁(计划 §5.6)。 */
|
||||
HAL_LOCK_SR595,
|
||||
HAL_LOCK_COUNT,
|
||||
} hal_lock_id_t;
|
||||
|
||||
/* 幂等。任何任务起来之前调一次;没 init 就 take 会返回 HAL_ERR_STATE
|
||||
(FreeRTOS 上那是个空句柄,不拦就是随机崩)。 */
|
||||
hal_err_t hal_lock_init(void);
|
||||
|
||||
/* 拿锁。timeout_ms = HAL_LOCK_FOREVER 表示一直等。
|
||||
HAL_ERR_TIMEOUT —— 等超时了,**调用方必须自己处理**:巡检任务应当跳过这一周期
|
||||
并计一次数,不要死等(死等的现象是 t_safety 整个停摆)。
|
||||
HAL_ERR_STATE —— 没 init,或者违反了「不许嵌套持有两把锁」。
|
||||
同一个任务重复拿同一把锁也是 HAL_ERR_STATE:这两把都不是递归锁。 */
|
||||
#define HAL_LOCK_FOREVER 0xFFFFFFFFu
|
||||
hal_err_t hal_lock_take(hal_lock_id_t id, uint32_t timeout_ms);
|
||||
|
||||
/* 放锁。不是自己拿的、或者根本没人拿,都会 abort ——
|
||||
那是代码写错了,悄悄放掉会让下一个持有者莫名其妙地丢掉互斥。 */
|
||||
void hal_lock_give(hal_lock_id_t id);
|
||||
|
||||
/* **当前任务**是不是正持着这把锁。给断言和 sim 的强制检查用,
|
||||
不要拿它做流程控制(「没拿就拿一下」正是双重加锁的写法)。 */
|
||||
bool hal_lock_held(hal_lock_id_t id);
|
||||
|
||||
/* 累计等锁失败次数(超时)。遥测里报出去,用来看总线是不是排队了。 */
|
||||
uint32_t hal_lock_timeouts(hal_lock_id_t id);
|
||||
|
||||
/* ---- 临界区(A11)——**这不是第三把锁** ----------------------------------
|
||||
|
||||
事件环形缓冲的入队要在几条指令里改完两个下标,而入队方与出队方是
|
||||
优先级两端的两个任务:`t_safety`(最高,而且是在**断电之后**调的)
|
||||
与 `t_log`(最低)。这里不能用互斥锁,理由有两条,都不是风格问题:
|
||||
|
||||
· **不许阻塞。** t_safety.h 写着「接进来的东西不许阻塞」——
|
||||
入队要是可能停在一把 t_log 持着的锁上,巡检的节拍就被最低优先级的任务
|
||||
决定了。优先级继承能救回来,但那是「让阻塞变短」,不是「不阻塞」。
|
||||
· **§10.3 的共享资源只有两个**(I²C 与 595 链),这条架构结论不该被
|
||||
一个环形缓冲的下标改写。
|
||||
|
||||
所以给的是关中断的临界区:不阻塞、没有持有者、不参与「不许嵌套两把锁」
|
||||
那条规则,也不出现在 `hal_lock_id_t` 里。
|
||||
|
||||
⚠️ **里面只许改内存。** 关着中断去等锁、睡、跑 I²C 事务,都是错的
|
||||
—— sim 里 `sim_crit_blocking()` 会计数,`test_evlog.c` 拿它当断言。
|
||||
可以嵌套(按层数计),但没有理由这么写。 */
|
||||
void hal_crit_enter(void);
|
||||
void hal_crit_exit(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HAL_LOCK_H */
|
||||
|
||||
@@ -1,92 +1,98 @@
|
||||
/* GPIO 的假实现。宿主机专用。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
typedef struct {
|
||||
bool configured;
|
||||
hal_gpio_mode_t mode;
|
||||
int level;
|
||||
uint32_t writes;
|
||||
uint32_t seq; /* 最后一次被写时的全局序号,0 = 没写过 */
|
||||
} pin_t;
|
||||
|
||||
static pin_t s_pin[SIM_GPIO_PINS];
|
||||
static uint32_t s_seq; /* 全局写计数,从 1 开始发号 */
|
||||
|
||||
void sim_gpio_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
/* 假 74HC595 链的窃听口(sim_sr595.c)。没 attach 过就是个空动作。
|
||||
放在这里而不是让测试自己数边:移位链要看的是**电平的跳变**,
|
||||
而 hal_gpio_write() 写同一个值也会计一次 writes。 */
|
||||
void sim_sr595_gpio_hook(int pin, int level);
|
||||
|
||||
void sim_gpio_reset_internal(void)
|
||||
{
|
||||
memset(s_pin, 0, sizeof(s_pin));
|
||||
s_seq = 0;
|
||||
}
|
||||
|
||||
static bool valid(int pin) { return pin >= 0 && pin < SIM_GPIO_PINS; }
|
||||
|
||||
hal_err_t hal_gpio_config(int pin, hal_gpio_mode_t mode)
|
||||
{
|
||||
if (!valid(pin)) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
pin_t *p = &s_pin[pin];
|
||||
p->configured = true;
|
||||
p->mode = mode;
|
||||
/* 上拉输入没人驱动时读回高电平 —— 五路 FLT/PG 都是开漏 + 上拉,
|
||||
「没有故障」在假板子上也必须是 1,不然测试会把默认 0 当成一直有故障。 */
|
||||
p->level = (mode == HAL_GPIO_INPUT_PULLUP) ? 1 : 0;
|
||||
/* 配成输出的那一刻引脚就开始驱动了(ESP-IDF 的 gpio_config 也是这个行为,
|
||||
输出寄存器复位值 0)。`KEY_OE#` 正是靠这一条:配成输出 = 输出立刻使能,
|
||||
所以 sr595_init() 必须等安全值锁存完了再配它。 */
|
||||
if (mode == HAL_GPIO_OUTPUT) {
|
||||
sim_sr595_gpio_hook(pin, p->level);
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_gpio_write(int pin, int level)
|
||||
{
|
||||
if (!valid(pin) || !s_pin[pin].configured || s_pin[pin].mode != HAL_GPIO_OUTPUT) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
s_pin[pin].level = level ? 1 : 0;
|
||||
s_pin[pin].writes++;
|
||||
s_pin[pin].seq = ++s_seq;
|
||||
sim_sr595_gpio_hook(pin, s_pin[pin].level);
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_gpio_read(int pin, int *level)
|
||||
{
|
||||
if (!valid(pin) || !s_pin[pin].configured || level == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
*level = s_pin[pin].level;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
void sim_gpio_drive(int pin, int level)
|
||||
{
|
||||
assert(valid(pin));
|
||||
assert(s_pin[pin].configured);
|
||||
assert(s_pin[pin].mode != HAL_GPIO_OUTPUT); /* 外部驱一个输出脚 = 打架,测试写错了 */
|
||||
s_pin[pin].level = level ? 1 : 0;
|
||||
}
|
||||
|
||||
int sim_gpio_level(int pin)
|
||||
{
|
||||
if (!valid(pin) || !s_pin[pin].configured) {
|
||||
return -1;
|
||||
}
|
||||
return s_pin[pin].level;
|
||||
}
|
||||
|
||||
uint32_t sim_gpio_writes(int pin) { return valid(pin) ? s_pin[pin].writes : 0; }
|
||||
|
||||
uint32_t sim_gpio_write_seq(int pin) { return valid(pin) ? s_pin[pin].seq : 0; }
|
||||
/* GPIO 的假实现。宿主机专用。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
typedef struct {
|
||||
bool configured;
|
||||
hal_gpio_mode_t mode;
|
||||
int level;
|
||||
uint32_t writes;
|
||||
uint32_t seq; /* 最后一次被写时的全局序号,0 = 没写过 */
|
||||
} pin_t;
|
||||
|
||||
static pin_t s_pin[SIM_GPIO_PINS];
|
||||
static uint32_t s_seq; /* 全局写计数,从 1 开始发号 */
|
||||
|
||||
void sim_gpio_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
/* 假 74HC595 链的窃听口(sim_sr595.c)。没 attach 过就是个空动作。
|
||||
放在这里而不是让测试自己数边:移位链要看的是**电平的跳变**,
|
||||
而 hal_gpio_write() 写同一个值也会计一次 writes。 */
|
||||
void sim_sr595_gpio_hook(int pin, int level);
|
||||
|
||||
/* 假 TCA9548A 的 `MUX_RST#` 窃听口(hal_i2c_sim.c)。拉低 = 通道全关。
|
||||
没挂 mux 就是个空动作。 */
|
||||
void sim_mux_gpio_hook(int pin, int level);
|
||||
|
||||
void sim_gpio_reset_internal(void)
|
||||
{
|
||||
memset(s_pin, 0, sizeof(s_pin));
|
||||
s_seq = 0;
|
||||
}
|
||||
|
||||
static bool valid(int pin) { return pin >= 0 && pin < SIM_GPIO_PINS; }
|
||||
|
||||
hal_err_t hal_gpio_config(int pin, hal_gpio_mode_t mode)
|
||||
{
|
||||
if (!valid(pin)) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
pin_t *p = &s_pin[pin];
|
||||
p->configured = true;
|
||||
p->mode = mode;
|
||||
/* 上拉输入没人驱动时读回高电平 —— 五路 FLT/PG 都是开漏 + 上拉,
|
||||
「没有故障」在假板子上也必须是 1,不然测试会把默认 0 当成一直有故障。 */
|
||||
p->level = (mode == HAL_GPIO_INPUT_PULLUP) ? 1 : 0;
|
||||
/* 配成输出的那一刻引脚就开始驱动了(ESP-IDF 的 gpio_config 也是这个行为,
|
||||
输出寄存器复位值 0)。`KEY_OE#` 正是靠这一条:配成输出 = 输出立刻使能,
|
||||
所以 sr595_init() 必须等安全值锁存完了再配它。 */
|
||||
if (mode == HAL_GPIO_OUTPUT) {
|
||||
sim_sr595_gpio_hook(pin, p->level);
|
||||
sim_mux_gpio_hook(pin, p->level);
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_gpio_write(int pin, int level)
|
||||
{
|
||||
if (!valid(pin) || !s_pin[pin].configured || s_pin[pin].mode != HAL_GPIO_OUTPUT) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
s_pin[pin].level = level ? 1 : 0;
|
||||
s_pin[pin].writes++;
|
||||
s_pin[pin].seq = ++s_seq;
|
||||
sim_sr595_gpio_hook(pin, s_pin[pin].level);
|
||||
sim_mux_gpio_hook(pin, s_pin[pin].level);
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_gpio_read(int pin, int *level)
|
||||
{
|
||||
if (!valid(pin) || !s_pin[pin].configured || level == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
*level = s_pin[pin].level;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
void sim_gpio_drive(int pin, int level)
|
||||
{
|
||||
assert(valid(pin));
|
||||
assert(s_pin[pin].configured);
|
||||
assert(s_pin[pin].mode != HAL_GPIO_OUTPUT); /* 外部驱一个输出脚 = 打架,测试写错了 */
|
||||
s_pin[pin].level = level ? 1 : 0;
|
||||
}
|
||||
|
||||
int sim_gpio_level(int pin)
|
||||
{
|
||||
if (!valid(pin) || !s_pin[pin].configured) {
|
||||
return -1;
|
||||
}
|
||||
return s_pin[pin].level;
|
||||
}
|
||||
|
||||
uint32_t sim_gpio_writes(int pin) { return valid(pin) ? s_pin[pin].writes : 0; }
|
||||
|
||||
uint32_t sim_gpio_write_seq(int pin) { return valid(pin) ? s_pin[pin].seq : 0; }
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
/* I²C 的假实现:一张寄存器表 + 可注入的 NAK。宿主机专用。 */
|
||||
/* I²C 的假实现:一张寄存器表 + 可注入的 NAK + **一颗假 TCA9548A**。宿主机专用。
|
||||
|
||||
---- R2 为什么要在假总线里做 mux ------------------------------------------
|
||||
R2 的五片刀片**完全同构**:五片上都是 0x40 的 INA226 和 0x20 的 PCA9555。
|
||||
在真板上区分它们的唯一手段是底板那颗 TCA9548A 的通道选择。
|
||||
假总线如果只按地址查表,五片就会塌成一片 —— 于是「选错通道」这一类
|
||||
R2 最危险的错误(计划 §4.3)在宿主机上**一次也复现不出来**。
|
||||
|
||||
所以这里的器件带一个 `bus`:-1 = 主干,0~7 = 挂在 mux 的某个通道后面。
|
||||
只有**当前打开的那个通道**上的器件看得见;关着的通道上的器件一律 NAK
|
||||
(真器件就是这个行为:支路被断开,从机根本收不到地址)。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -9,6 +19,14 @@ static int s_n;
|
||||
static bool s_inited;
|
||||
static bool s_bus_time = true;
|
||||
|
||||
/* 假 mux 的状态。`s_mux_addr < 0` = 板上没挂 mux(A3~A6 那批驱动测试就是这样,
|
||||
它们直接挂器件到主干上)。 */
|
||||
static int s_mux_addr = -1;
|
||||
static int s_mux_rst_pin = -1;
|
||||
static uint8_t s_mux_mask; /* 控制字:bit n = 通道 n 通 */
|
||||
static uint32_t s_mux_resets;
|
||||
static uint32_t s_ambiguous; /* 同一地址在当前可见范围里出现了不止一次 */
|
||||
|
||||
void sim_i2c_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
void sim_i2c_reset_internal(void)
|
||||
@@ -17,33 +35,132 @@ void sim_i2c_reset_internal(void)
|
||||
s_n = 0;
|
||||
s_inited = false;
|
||||
s_bus_time = true;
|
||||
s_mux_addr = -1;
|
||||
s_mux_rst_pin = -1;
|
||||
s_mux_mask = 0;
|
||||
s_mux_resets = 0;
|
||||
s_ambiguous = 0;
|
||||
}
|
||||
|
||||
/* 挂在**主干**上的那一个(bus < 0)。A3~A6 的驱动测试用的就是这个语义。 */
|
||||
sim_i2c_dev_t *sim_i2c_dev(uint8_t addr)
|
||||
{
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_dev[i].addr == addr) {
|
||||
if (s_dev[i].addr == addr && s_dev[i].bus < 0) {
|
||||
return &s_dev[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_add(uint8_t addr, uint8_t reg_width)
|
||||
sim_i2c_dev_t *sim_i2c_dev_ch(int ch, uint8_t addr)
|
||||
{
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_dev[i].addr == addr && s_dev[i].bus == ch) {
|
||||
return &s_dev[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 当前**看得见**的那一个:主干上的永远看得见,支路上的要那个通道开着。
|
||||
同一地址看得见不止一片时记一次 `ambiguous` —— 真板上那是两片一起应答,
|
||||
读回来的是两片的线与(数值正常、还很稳),这里让它可被断言。 */
|
||||
static sim_i2c_dev_t *visible(uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *hit = NULL;
|
||||
int n = 0;
|
||||
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_dev[i].addr != addr) {
|
||||
continue;
|
||||
}
|
||||
const bool on = (s_dev[i].bus < 0) ||
|
||||
(s_mux_addr >= 0 && (s_mux_mask & (uint8_t)(1u << s_dev[i].bus)) != 0);
|
||||
if (!on) {
|
||||
continue;
|
||||
}
|
||||
n++;
|
||||
if (hit == NULL) {
|
||||
hit = &s_dev[i];
|
||||
}
|
||||
}
|
||||
if (n > 1) {
|
||||
s_ambiguous++;
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
static sim_i2c_dev_t *add(int bus, uint8_t addr, uint8_t reg_width)
|
||||
{
|
||||
assert(addr <= 0x7f);
|
||||
assert(reg_width == 1 || reg_width == 2);
|
||||
assert(sim_i2c_dev(addr) == NULL); /* 地址撞车:测试写错了 */
|
||||
/* 地址撞车只在**同一条支路上**才算错 —— 五片刀片同址正是 R2 的设计。 */
|
||||
assert(sim_i2c_dev_ch(bus, addr) == NULL);
|
||||
assert(s_n < SIM_I2C_MAX_DEV);
|
||||
|
||||
sim_i2c_dev_t *d = &s_dev[s_n++];
|
||||
memset(d, 0, sizeof(*d));
|
||||
d->addr = addr;
|
||||
d->bus = bus;
|
||||
d->reg_width = reg_width;
|
||||
d->present = true;
|
||||
return d;
|
||||
}
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_add(uint8_t addr, uint8_t reg_width) { return add(-1, addr, reg_width); }
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_add_ch(int ch, uint8_t addr, uint8_t reg_width)
|
||||
{
|
||||
assert(ch >= 0 && ch < SIM_I2C_MUX_CH);
|
||||
return add(ch, addr, reg_width);
|
||||
}
|
||||
|
||||
void sim_i2c_add_mux(uint8_t addr, int rst_pin)
|
||||
{
|
||||
assert(s_mux_addr < 0); /* 板上只有一颗 */
|
||||
sim_i2c_dev_t *d = add(-1, addr, 1);
|
||||
d->is_mux = true;
|
||||
s_mux_addr = addr;
|
||||
s_mux_rst_pin = rst_pin;
|
||||
s_mux_mask = 0; /* 上电全关 */
|
||||
}
|
||||
|
||||
/* 当前打开的是哪一路:全关 -1,恰好一路 0~7,**多于一路 -2**(那是 bug
|
||||
而不是一种用法,见 tca9548a.h 文件头第 2 条)。 */
|
||||
int sim_i2c_mux_ch(void)
|
||||
{
|
||||
if (s_mux_mask == 0) {
|
||||
return -1;
|
||||
}
|
||||
if ((s_mux_mask & (uint8_t)(s_mux_mask - 1u)) != 0) {
|
||||
return -2;
|
||||
}
|
||||
for (int ch = 0; ch < SIM_I2C_MUX_CH; ch++) {
|
||||
if (s_mux_mask == (uint8_t)(1u << ch)) {
|
||||
return ch;
|
||||
}
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
uint8_t sim_i2c_mux_mask(void) { return s_mux_mask; }
|
||||
uint32_t sim_i2c_mux_resets(void) { return s_mux_resets; }
|
||||
uint32_t sim_i2c_ambiguous(void) { return s_ambiguous; }
|
||||
|
||||
/* `MUX_RST#` 的窃听口(低有效)。真器件复位之后**通道全关** ——
|
||||
固件那一侧的通道影子必须跟着作废,否则下一次事务会发到断开的支路上。 */
|
||||
void sim_mux_gpio_hook(int pin, int level)
|
||||
{
|
||||
if (s_mux_addr < 0 || pin != s_mux_rst_pin) {
|
||||
return;
|
||||
}
|
||||
if (level == 0) {
|
||||
s_mux_mask = 0;
|
||||
s_mux_resets++;
|
||||
}
|
||||
}
|
||||
|
||||
static sim_i2c_dev_t *must(uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(addr);
|
||||
@@ -51,14 +168,39 @@ static sim_i2c_dev_t *must(uint8_t addr)
|
||||
return d;
|
||||
}
|
||||
|
||||
static sim_i2c_dev_t *must_ch(int ch, uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *d = sim_i2c_dev_ch(ch, addr);
|
||||
assert(d != NULL);
|
||||
return d;
|
||||
}
|
||||
|
||||
void sim_i2c_set_reg(uint8_t addr, uint8_t reg, uint16_t val) { must(addr)->regs[reg] = val; }
|
||||
uint16_t sim_i2c_get_reg(uint8_t addr, uint8_t reg) { return must(addr)->regs[reg]; }
|
||||
void sim_i2c_inject_nak(uint8_t addr, int count) { must(addr)->nak_left = count; }
|
||||
void sim_i2c_set_present(uint8_t addr, bool present) { must(addr)->present = present; }
|
||||
|
||||
void sim_i2c_set_reg_ch(int ch, uint8_t addr, uint8_t reg, uint16_t val)
|
||||
{
|
||||
must_ch(ch, addr)->regs[reg] = val;
|
||||
}
|
||||
uint16_t sim_i2c_get_reg_ch(int ch, uint8_t addr, uint8_t reg)
|
||||
{
|
||||
return must_ch(ch, addr)->regs[reg];
|
||||
}
|
||||
void sim_i2c_inject_nak_ch(int ch, uint8_t addr, int count)
|
||||
{
|
||||
must_ch(ch, addr)->nak_left = count;
|
||||
}
|
||||
void sim_i2c_set_present_ch(int ch, uint8_t addr, bool present)
|
||||
{
|
||||
must_ch(ch, addr)->present = present;
|
||||
}
|
||||
|
||||
void sim_i2c_set_bus_time(bool on) { s_bus_time = on; }
|
||||
|
||||
/* 每次事务(成功或 NAK)都记账并把虚拟时钟花掉。NAK 也占总线:地址字节已经发出去了。
|
||||
同时查一次加锁纪律 —— I²C 是 §10.3 两个共享资源之一。 */
|
||||
同时查一次加锁纪律 —— I²C 是 §10.3 的共享资源。 */
|
||||
static void spend(size_t wr_len, size_t rd_len)
|
||||
{
|
||||
sim_lock_check(HAL_LOCK_I2C);
|
||||
@@ -78,7 +220,7 @@ hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg)
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* 取器件并把「这次事务要不要 NAK」判掉。 */
|
||||
/* 取**当前看得见的**器件并把「这次事务要不要 NAK」判掉。 */
|
||||
static hal_err_t take(uint8_t addr, sim_i2c_dev_t **out)
|
||||
{
|
||||
if (!s_inited) {
|
||||
@@ -87,9 +229,9 @@ static hal_err_t take(uint8_t addr, sim_i2c_dev_t **out)
|
||||
if (addr > 0x7f) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(addr);
|
||||
sim_i2c_dev_t *d = visible(addr);
|
||||
if (d == NULL || !d->present) {
|
||||
return HAL_ERR_NAK;
|
||||
return HAL_ERR_NAK; /* 不存在,或者它那条支路现在是断开的 */
|
||||
}
|
||||
if (d->nak_left != 0) {
|
||||
if (d->nak_left > 0) {
|
||||
@@ -101,12 +243,26 @@ static hal_err_t take(uint8_t addr, sim_i2c_dev_t **out)
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* data[0] 是寄存器指针,其后按 reg_width 一格一格写,指针自增(uint8_t 自然回绕)。 */
|
||||
/* data[0] 是寄存器指针,其后按 reg_width 一格一格写,指针自增(uint8_t 自然回绕)。
|
||||
⚠️ **mux 除外**:它整颗器件只有一个控制寄存器,写就是一个裸字节
|
||||
(tca9548a.h 文件头第 1 条)。按「寄存器 + 值」写它会把通道选成寄存器号,
|
||||
而且不会报错 —— 这里如实照做,让那种写法在宿主机上就现形。 */
|
||||
static hal_err_t do_write(sim_i2c_dev_t *d, const uint8_t *data, size_t len)
|
||||
{
|
||||
if (len != 0 && data == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (d->is_mux) {
|
||||
d->n_write++;
|
||||
if (len == 0) {
|
||||
return HAL_OK; /* 只发地址 = 探测 */
|
||||
}
|
||||
d->last_write_len = 1;
|
||||
d->last_write[0] = data[0];
|
||||
s_mux_mask = data[0]; /* 裸字节就是控制字 */
|
||||
d->regs[0] = data[0];
|
||||
return HAL_OK;
|
||||
}
|
||||
if (len > 1 && ((len - 1) % d->reg_width) != 0) {
|
||||
return HAL_ERR_ARG; /* 半个寄存器:真器件上是未定义行为,这里直接报错 */
|
||||
}
|
||||
@@ -137,6 +293,13 @@ static hal_err_t do_read(sim_i2c_dev_t *d, uint8_t *data, size_t len)
|
||||
if (len != 0 && data == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (d->is_mux) {
|
||||
d->n_read++;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
data[i] = s_mux_mask; /* 读它永远只回控制字 */
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
if ((len % d->reg_width) != 0) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
|
||||
@@ -12,17 +12,139 @@ typedef struct {
|
||||
|
||||
static ch_t s_ch[HAL_PWM_CH_MAX];
|
||||
|
||||
/* ---- 假风扇(模型说明见 sim_hal.h)--------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
bool attached;
|
||||
int pwm_ch;
|
||||
int en_pin;
|
||||
uint32_t rpm_full;
|
||||
uint32_t rpm_min;
|
||||
uint8_t ppr;
|
||||
bool gated; /* 关断期不吐脉冲 */
|
||||
bool stalled;
|
||||
uint64_t last_us; /* 上一次结算的时刻 */
|
||||
uint64_t frac; /* 边沿的小数部分,按 1e6 缩放(整数积分,不引浮点) */
|
||||
uint32_t edges;
|
||||
} fan_t;
|
||||
|
||||
static fan_t s_fan[SIM_FAN_MAX];
|
||||
|
||||
static void fan_sync(void);
|
||||
|
||||
void sim_pwm_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
void sim_pwm_reset_internal(void) { memset(s_ch, 0, sizeof(s_ch)); }
|
||||
void sim_pwm_reset_internal(void)
|
||||
{
|
||||
memset(s_ch, 0, sizeof(s_ch));
|
||||
memset(s_fan, 0, sizeof(s_fan)); /* 换了一块新板子 ⇒ 风扇也是新的 */
|
||||
}
|
||||
|
||||
static bool valid(int ch) { return ch >= 0 && ch < HAL_PWM_CH_MAX; }
|
||||
|
||||
static bool fan_valid(int idx) { return idx >= 0 && idx < SIM_FAN_MAX && s_fan[idx].attached; }
|
||||
|
||||
/* 这一刻这只风扇的转速。三个把它变成 0 的条件各对应一条真实故障。 */
|
||||
static uint32_t fan_rpm_now(const fan_t *f)
|
||||
{
|
||||
if (f->stalled) {
|
||||
return 0; /* 堵转 / 风扇线没插 */
|
||||
}
|
||||
if (f->en_pin >= 0 && sim_gpio_level(f->en_pin) != 1) {
|
||||
return 0; /* 12V 总开关没开 —— 复位态就是这个样子 */
|
||||
}
|
||||
int g = sim_pwm_permille(f->pwm_ch);
|
||||
if (g < 0) {
|
||||
/* PWM 还没配:板上 R734 把 Q6 栅极拉低 ⇒ 风扇 PWM 线被风扇自己拉高
|
||||
⇒ **满速**。这是安全侧,也是固件还没接管时的真实行为。 */
|
||||
g = 0;
|
||||
}
|
||||
const uint32_t fan_duty = 1000u - (uint32_t)g; /* Q6 反相 */
|
||||
return f->rpm_min + (f->rpm_full - f->rpm_min) * fan_duty / 1000u;
|
||||
}
|
||||
|
||||
/* 把「上次结算到现在」这段时间的边沿积出来。 */
|
||||
static void fan_sync(void)
|
||||
{
|
||||
const uint64_t now = hal_time_now_us();
|
||||
for (int i = 0; i < SIM_FAN_MAX; i++) {
|
||||
fan_t *f = &s_fan[i];
|
||||
if (!f->attached) {
|
||||
continue;
|
||||
}
|
||||
if (now <= f->last_us) {
|
||||
f->last_us = now;
|
||||
continue;
|
||||
}
|
||||
uint64_t dt = now - f->last_us;
|
||||
f->last_us = now;
|
||||
|
||||
const uint32_t rpm = fan_rpm_now(f);
|
||||
if (rpm == 0) {
|
||||
continue;
|
||||
}
|
||||
if (f->gated) {
|
||||
/* 关断期没有回流 ⇒ 脉冲只出现在导通的那一部分时间里。
|
||||
⚠️ 这一行就是 TR-6 那条误报的来源:占空比越低,同样一秒里
|
||||
吐出来的脉冲越少,而风扇转得好好的。 */
|
||||
int g = sim_pwm_permille(f->pwm_ch);
|
||||
const uint32_t fan_duty = (g < 0) ? 1000u : (1000u - (uint32_t)g);
|
||||
dt = dt * fan_duty / 1000u;
|
||||
}
|
||||
/* edges = rpm/60 × ppr × dt[s],按 1e6 缩放累加。 */
|
||||
f->frac += (uint64_t)rpm * f->ppr * dt / 60u;
|
||||
f->edges += (uint32_t)(f->frac / 1000000u);
|
||||
f->frac %= 1000000u;
|
||||
}
|
||||
}
|
||||
|
||||
void sim_fan_attach(int idx, int pwm_ch, int en_pin,
|
||||
uint32_t rpm_full, uint32_t rpm_min, uint8_t ppr)
|
||||
{
|
||||
if (idx < 0 || idx >= SIM_FAN_MAX || ppr == 0) {
|
||||
return;
|
||||
}
|
||||
s_fan[idx] = (fan_t){
|
||||
.attached = true, .pwm_ch = pwm_ch, .en_pin = en_pin,
|
||||
.rpm_full = rpm_full, .rpm_min = rpm_min, .ppr = ppr,
|
||||
.gated = true, .last_us = hal_time_now_us(),
|
||||
};
|
||||
}
|
||||
|
||||
void sim_fan_set_gated(int idx, bool on)
|
||||
{
|
||||
fan_sync();
|
||||
if (fan_valid(idx)) {
|
||||
s_fan[idx].gated = on;
|
||||
}
|
||||
}
|
||||
|
||||
void sim_fan_set_stalled(int idx, bool on)
|
||||
{
|
||||
fan_sync();
|
||||
if (fan_valid(idx)) {
|
||||
s_fan[idx].stalled = on;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t sim_fan_edges(int idx)
|
||||
{
|
||||
fan_sync();
|
||||
return fan_valid(idx) ? s_fan[idx].edges : 0;
|
||||
}
|
||||
|
||||
uint32_t sim_fan_rpm(int idx)
|
||||
{
|
||||
fan_sync();
|
||||
return fan_valid(idx) ? fan_rpm_now(&s_fan[idx]) : 0;
|
||||
}
|
||||
|
||||
hal_err_t hal_pwm_config(int ch, int pin, uint32_t freq_hz)
|
||||
{
|
||||
if (!valid(ch) || pin < 0 || pin >= SIM_GPIO_PINS || freq_hz == 0) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
fan_sync(); /* 配置之前那段时间按「还没配 ⇒ 满速」结算(见 fan_rpm_now) */
|
||||
s_ch[ch] = (ch_t){ .configured = true, .pin = pin, .freq_hz = freq_hz, .permille = 0 };
|
||||
return HAL_OK;
|
||||
}
|
||||
@@ -32,6 +154,9 @@ hal_err_t hal_pwm_set_permille(int ch, uint16_t permille)
|
||||
if (!valid(ch) || !s_ch[ch].configured || permille > 1000) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
/* 先按**旧**占空比把已经过去的那段时间结算掉,再换新值 ——
|
||||
不然改占空比的那一刻会把过去一整秒的脉冲按新占空比重算一遍。 */
|
||||
fan_sync();
|
||||
s_ch[ch].permille = permille;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
@@ -33,9 +33,10 @@ void sim_hal_reboot(void);
|
||||
|
||||
/* ---------------- I²C ---------------- */
|
||||
|
||||
#define SIM_I2C_MAX_DEV 16
|
||||
#define SIM_I2C_MAX_DEV 24 /* R2:主干 4 + 五片刀片各 2 = 14,留些余量 */
|
||||
#define SIM_I2C_REGS 256
|
||||
#define SIM_I2C_LOG_BYTES 8
|
||||
#define SIM_I2C_MUX_CH 8 /* TCA9548A 的通道数 */
|
||||
|
||||
/* 假器件的窃听事件。通用寄存器表模拟不了「配成输出就对地短路」这种事,
|
||||
所以按 datasheet 建模的假器件(sim_pca9555)挂一个钩子进来。 */
|
||||
@@ -49,7 +50,11 @@ typedef void (*sim_i2c_hook_t)(struct sim_i2c_dev *d, sim_i2c_ev_t ev);
|
||||
|
||||
typedef struct sim_i2c_dev {
|
||||
uint8_t addr;
|
||||
uint8_t reg_width; /* 1 或 2 字节。2 字节按**大端**——INA226 / DAC63204 / LM75 都是 */
|
||||
/* **R2**:-1 = 挂在主干上;0~7 = 挂在 mux 的那个通道后面。
|
||||
通道关着的时候它看不见(事务一律 NAK),这正是「选错通道」能被测出来的原因。 */
|
||||
int bus;
|
||||
bool is_mux; /* 这颗就是 TCA9548A 自己:写它是**一个裸字节**,不带寄存器号 */
|
||||
uint8_t reg_width; /* 1 或 2 字节。2 字节按**大端**——INA226 / DAC63204 都是 */
|
||||
bool present; /* false = 器件失联(A15 降级矩阵用),事务一律 NAK */
|
||||
int nak_left; /* >0:接下来 n 次事务 NAK 后自动恢复;<0:一直 NAK;0:正常 */
|
||||
uint16_t regs[SIM_I2C_REGS];
|
||||
@@ -61,17 +66,38 @@ typedef struct sim_i2c_dev {
|
||||
sim_i2c_hook_t hook; /* NULL = 就是一张寄存器表 */
|
||||
} sim_i2c_dev_t;
|
||||
|
||||
/* 往假总线上挂一个器件。地址重复或超出 16 个会 abort(测试写错了,不该悄悄过)。 */
|
||||
/* 往假总线的**主干**上挂一个器件。同一条支路上地址重复会 abort(测试写错了)。 */
|
||||
sim_i2c_dev_t *sim_i2c_add(uint8_t addr, uint8_t reg_width);
|
||||
sim_i2c_dev_t *sim_i2c_dev(uint8_t addr); /* 没挂就返回 NULL */
|
||||
sim_i2c_dev_t *sim_i2c_dev(uint8_t addr); /* 主干上的那一个,没挂返回 NULL */
|
||||
|
||||
/* 往 mux 的某个通道后面挂一个器件(**R2**:五片刀片可以用同一个地址)。 */
|
||||
sim_i2c_dev_t *sim_i2c_add_ch(int ch, uint8_t addr, uint8_t reg_width);
|
||||
sim_i2c_dev_t *sim_i2c_dev_ch(int ch, uint8_t addr);
|
||||
|
||||
/* 挂那颗假 TCA9548A。`rst_pin` 是 `MUX_RST#` 接的 GPIO(低有效)——
|
||||
固件拉低它的那一刻,假器件的通道**全部关掉**,和真器件一样。 */
|
||||
void sim_i2c_add_mux(uint8_t addr, int rst_pin);
|
||||
|
||||
int sim_i2c_mux_ch(void); /* 当前通道:-1 全关 / 0~7 / **-2 同时开了多路** */
|
||||
uint8_t sim_i2c_mux_mask(void); /* 控制字原样 */
|
||||
uint32_t sim_i2c_mux_resets(void);
|
||||
|
||||
/* 同一个地址在**当前可见范围**里出现了不止一次的次数。
|
||||
真板上那是两片一起应答、读回来是线与(数值正常、还很稳),
|
||||
所以这个计数必须恒为 0 —— 非 0 就是通道开重了。 */
|
||||
uint32_t sim_i2c_ambiguous(void);
|
||||
|
||||
/* 预置 / 回读寄存器。地址没挂过会 abort。 */
|
||||
void sim_i2c_set_reg(uint8_t addr, uint8_t reg, uint16_t val);
|
||||
uint16_t sim_i2c_get_reg(uint8_t addr, uint8_t reg);
|
||||
void sim_i2c_set_reg_ch(int ch, uint8_t addr, uint8_t reg, uint16_t val);
|
||||
uint16_t sim_i2c_get_reg_ch(int ch, uint8_t addr, uint8_t reg);
|
||||
|
||||
/* 故障注入。count>0:接下来 count 次事务 NAK;count<0:一直 NAK;0:清除。 */
|
||||
void sim_i2c_inject_nak(uint8_t addr, int count);
|
||||
void sim_i2c_set_present(uint8_t addr, bool present);
|
||||
void sim_i2c_inject_nak_ch(int ch, uint8_t addr, int count);
|
||||
void sim_i2c_set_present_ch(int ch, uint8_t addr, bool present);
|
||||
|
||||
/* **假总线上的事务是要花时间的**(默认开):每次事务把虚拟时钟往前拨
|
||||
`hal_i2c_txn_bits() / 频率` 那么多微秒。没有这一条,t_safety 的一个周期在 sim 里
|
||||
@@ -126,31 +152,47 @@ uint32_t sim_sr595_enable_outputs(void);
|
||||
/* 某一位在栅极上的实际电平;OE# 为高时返回 -1(高阻,不是 0)。 */
|
||||
int sim_sr595_pin(int bit);
|
||||
|
||||
/* ---------------- PCA9555(按 datasheet 建模的假器件) ---------------- */
|
||||
/* ---------------- PCA9555 ×5(按 datasheet 建模的假器件) ---------------- */
|
||||
|
||||
/* 通用寄存器表证明不了 A6 的核心判据。这个模型只多做三件真器件会做的事:
|
||||
/* 通用寄存器表证明不了这一层的核心判据。这个模型只多做三件真器件会做的事:
|
||||
① **Output Port 复位值 0xFF** —— 所以把一个脚配成输出,它下一刻就输出高;
|
||||
② 输入寄存器按位合成:配成输入的位读**外部电平**,配成输出的位读**自己的输出**;
|
||||
③ 四个备用脚 P0_7 / P1_5~7 **焊死在地上** —— 读永远是 0,被驱动成高就是短路。
|
||||
③ 是这块板子上「配错方向」唯一会留下痕迹的地方:真板子上它既不报错也不 NAK。 */
|
||||
void sim_pca9555_attach(uint8_t addr);
|
||||
③ 备用脚 P0.5~7 / P1.4~7 **焊死在地上** —— 读永远是 0,被驱动成高就是短路。
|
||||
③ 是这块板子上「配错方向」唯一会留下痕迹的地方:真板子上它既不报错也不 NAK。
|
||||
|
||||
**R2**:五片刀片各一颗,同地址、不同 mux 通道。`idx` 是 0~4,
|
||||
与 `rail - 1` 对应 —— 但那是测试自己的约定,假器件只认 attach 的顺序。 */
|
||||
#define SIM_PCA9555_MAX 5
|
||||
|
||||
void sim_pca9555_attach_ch(int idx, int ch, uint8_t addr);
|
||||
|
||||
/* 外部世界把某个端口的 8 根线驱动成 level(接地脚不受影响,恒 0)。
|
||||
FLT#/PGOOD 都是开漏上拉,所以「什么都没发生」的静息值是 0xFF。 */
|
||||
void sim_pca9555_drive(int port, uint8_t level);
|
||||
void sim_pca9555_drive(int idx, int port, uint8_t level);
|
||||
|
||||
uint8_t sim_pca9555_config(int port); /* 方向寄存器现值,1 = 输入 */
|
||||
uint8_t sim_pca9555_output(int port); /* 输出锁存器现值(复位 0xFF) */
|
||||
uint8_t sim_pca9555_polinv(int port);
|
||||
uint8_t sim_pca9555_config(int idx, int port); /* 方向寄存器现值,1 = 输入 */
|
||||
uint8_t sim_pca9555_output(int idx, int port); /* 输出锁存器现值(复位 0xFF) */
|
||||
uint8_t sim_pca9555_polinv(int idx, int port);
|
||||
|
||||
/* 接地脚被配成输出过几次(不管输出高低,配了就算 —— 那已经是在对地灌电流)。 */
|
||||
/* 某个输出脚**在栅极上的实际电平**:配成输入时返回 -1(高阻),
|
||||
配成输出时返回锁存器里那一位。断言「键真的被按下了」用这个,
|
||||
不要去读驱动自己的影子(那是自己证明自己)。 */
|
||||
int sim_pca9555_pin(int idx, int port, int bit);
|
||||
|
||||
/* 接地脚被配成输出过几次(不管输出高低,配了就算 —— 那已经是在对地灌电流)。
|
||||
**五片合计**:这个数必须恒为 0,是哪一片犯的都一样严重。 */
|
||||
uint32_t sim_pca9555_gnd_driven(void);
|
||||
/* 其中真的输出了高电平、也就是**直接对地短路**过几次。 */
|
||||
uint32_t sim_pca9555_shorted(void);
|
||||
|
||||
/* ⭐ 「方向先于输出」那条规矩的拦网:某一片在**输出锁存器还是 0xFF 的时候**
|
||||
就把 Port0 配成了输出的次数。真板上那一刻五路的键全被按下 + VBUS 全部上电。
|
||||
必须恒为 0。 */
|
||||
uint32_t sim_pca9555_unsafe_enable(void);
|
||||
|
||||
/* INT# 现在是不是被拉低(低有效)。输入变化拉低,**读走输入端口才释放**。 */
|
||||
bool sim_pca9555_int_asserted(void);
|
||||
uint32_t sim_pca9555_int_asserts(void); /* 累计拉低次数 */
|
||||
bool sim_pca9555_int_asserted(int idx);
|
||||
uint32_t sim_pca9555_int_asserts(int idx); /* 累计拉低次数 */
|
||||
|
||||
/* ---------------- PWM ---------------- */
|
||||
|
||||
@@ -158,6 +200,32 @@ int sim_pwm_pin(int ch); /* 未配置返回 -1 */
|
||||
uint32_t sim_pwm_freq(int ch); /* 未配置返回 0 */
|
||||
int sim_pwm_permille(int ch); /* 未配置返回 -1 */
|
||||
|
||||
/* ---------------- 假风扇 ×2(A16 / TR-6 的 TACH 采样窗口) ---------------- */
|
||||
|
||||
/* 按板子的接法建模的假风扇:**一路 PWM 喂两只**、一个 12V 总开关、两路独立测速。
|
||||
它比「一个转速变量」多做四件真板子会做的事,每一件都是一条判据的靠山:
|
||||
① `FAN_EN` 是低电平就**一圈都不转**(复位态就是这样,固件不主动开就没风);
|
||||
② PWM 反相(板上 Q6 低边管):GPIO 占空比 d ⇒ 风扇侧 `1000 − d`
|
||||
—— 固件把反相写反时,这里的转速会随温度**下降**;
|
||||
③ **可选的「关断期无脉冲」**(`sim_fan_set_gated`,三线低边调速的行为):
|
||||
占空比 40 % 就只有 40 % 的时间在吐脉冲。TR-6 那条误报全靠它才摆得出来;
|
||||
④ 0 % PWM 仍转 `rpm_min`(设计文档 §22.6:四线风扇 0 % 下仍 3300 RPM)。
|
||||
|
||||
边沿数是按**虚拟时钟**积分出来的,积分在读 `sim_fan_edges()/sim_fan_rpm()`、
|
||||
写 `hal_pwm_set_permille()`、以及 `sim_fan_set_*()` 时结算。
|
||||
⚠️ 改 `FAN_EN` 引脚之前请先读一次 `sim_fan_edges()`:模型不监视 GPIO 的写,
|
||||
那一段时间会整段按新的 EN 状态算。 */
|
||||
#define SIM_FAN_MAX 2
|
||||
|
||||
void sim_fan_attach(int idx, int pwm_ch, int en_pin,
|
||||
uint32_t rpm_full, uint32_t rpm_min, uint8_t ppr);
|
||||
|
||||
void sim_fan_set_gated(int idx, bool on); /* 默认 true = 关断期不吐脉冲 */
|
||||
void sim_fan_set_stalled(int idx, bool on); /* 堵转 / 拔线:一个脉冲也没有 */
|
||||
|
||||
uint32_t sim_fan_edges(int idx); /* 累计 TACH 边沿数(没 attach 返回 0) */
|
||||
uint32_t sim_fan_rpm(int idx); /* 此刻的真实转速,给测试自查用 */
|
||||
|
||||
/* ---------------- 两把互斥锁 ---------------- */
|
||||
|
||||
/* 扮演第几号任务(默认 0)。单线程 sim 里「另一个任务持着锁」就靠它模拟。 */
|
||||
|
||||
@@ -1,77 +1,114 @@
|
||||
/* 按 datasheet 建模的假 PCA9555。宿主机专用。
|
||||
/* 按 datasheet 建模的假 PCA9555 ×5(R2:每片刀片一颗)。宿主机专用。
|
||||
|
||||
通用寄存器表(sim_i2c)能测「写下去的字节对不对」,但测不出 A6 真正要拦的那件事:
|
||||
**把一个焊死在地上的脚配成输出**。真板子上这既不报错也不 NAK,只是发烫。
|
||||
所以这里把三件真器件会做的事补上(见 sim_hal.h 那一节)。
|
||||
通用寄存器表(sim_i2c)能测「写下去的字节对不对」,但测不出这一层真正要拦的
|
||||
两件事:
|
||||
① **把一个焊死在地上的脚配成输出**(P0.5~7 / P1.4~7)——
|
||||
真板子上这既不报错也不 NAK,只是发烫;
|
||||
② **在输出锁存器还是 0xFF 的时候就把 Port0 配成输出** ——
|
||||
那一刻三个键全被按下 + EDL 短接 + VBUS 上电(pca9555.h 那个框)。
|
||||
② 在真板上的现象是「五台手机一起进了 Recovery」,而 I²C 从头到尾一切正常。
|
||||
|
||||
⚠️ 这个模型是用来证明**代码逻辑**对的。别拿它推翻设计文档 §17.2 的任何一条。 */
|
||||
⚠️ 这个模型是用来证明**代码逻辑**对的。别拿它推翻硬件设计的任何一条。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "pca9555.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
/* 焊死在地上的备用脚(设计文档 §17.2 的注)。 */
|
||||
/* 焊死在地上的备用脚(`_blade_pcb_nets.json` 的 `U4` pin9~11 / pin17~20)。 */
|
||||
static const uint8_t s_gnd_mask[2] = { PCA9555_P0_GND_MASK, PCA9555_P1_GND_MASK };
|
||||
|
||||
typedef struct {
|
||||
bool attached;
|
||||
int ch; /* 挂在 mux 的哪个通道后面 */
|
||||
uint8_t addr;
|
||||
uint8_t ext[2]; /* 外部世界驱动的电平;开漏上拉,静息 0xFF */
|
||||
uint8_t int_latch[2];/* 上一次「被读走」时的输入值,用来判 INT# */
|
||||
bool int_low;
|
||||
uint32_t int_asserts;
|
||||
uint32_t gnd_driven;
|
||||
uint32_t shorted;
|
||||
} sim_pca9555_t;
|
||||
} sim_pca_t;
|
||||
|
||||
static sim_pca9555_t s;
|
||||
static sim_pca_t s[SIM_PCA9555_MAX];
|
||||
static uint32_t s_gnd_driven; /* 五片合计 */
|
||||
static uint32_t s_shorted;
|
||||
static uint32_t s_unsafe_enable;
|
||||
|
||||
void sim_pca9555_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
void sim_pca9555_reset_internal(void)
|
||||
{
|
||||
memset(&s, 0, sizeof(s));
|
||||
memset(s, 0, sizeof(s));
|
||||
s_gnd_driven = 0;
|
||||
s_shorted = 0;
|
||||
s_unsafe_enable = 0;
|
||||
}
|
||||
|
||||
static sim_pca_t *inst(int idx)
|
||||
{
|
||||
assert(idx >= 0 && idx < SIM_PCA9555_MAX);
|
||||
assert(s[idx].attached); /* 先 sim_pca9555_attach_ch() */
|
||||
return &s[idx];
|
||||
}
|
||||
|
||||
/* 从 dev 反查是哪一片:地址相同,靠通道区分(真板上也是这么区分的)。 */
|
||||
static sim_pca_t *from_dev(const sim_i2c_dev_t *d)
|
||||
{
|
||||
for (int i = 0; i < SIM_PCA9555_MAX; i++) {
|
||||
if (s[i].attached && s[i].addr == d->addr && s[i].ch == d->bus) {
|
||||
return &s[i];
|
||||
}
|
||||
}
|
||||
assert(0 && "hook 挂在了一个没登记的假 PCA9555 上");
|
||||
return &s[0];
|
||||
}
|
||||
|
||||
/* 输入寄存器 = 逐位选择:配成输入的位取外部电平,配成输出的位取自己的输出锁存器。
|
||||
接地脚无论配成什么,线上都是 0(它焊在地上,赢的永远是铜)。 */
|
||||
static uint8_t input_value(const sim_i2c_dev_t *d, int port)
|
||||
static uint8_t input_value(const sim_i2c_dev_t *d, const sim_pca_t *p, int port)
|
||||
{
|
||||
const uint8_t cfg = (uint8_t)d->regs[PCA9555_REG_CONFIG0 + port];
|
||||
const uint8_t out = (uint8_t)d->regs[PCA9555_REG_OUTPUT0 + port];
|
||||
const uint8_t pol = (uint8_t)d->regs[PCA9555_REG_POLINV0 + port];
|
||||
|
||||
uint8_t v = (uint8_t)((s.ext[port] & cfg) | (out & (uint8_t)~cfg));
|
||||
uint8_t v = (uint8_t)((p->ext[port] & cfg) | (out & (uint8_t)~cfg));
|
||||
v &= (uint8_t)~s_gnd_mask[port]; /* 接地脚读回来永远是 0 */
|
||||
return (uint8_t)(v ^ pol); /* 极性反转寄存器作用在**输入**上 */
|
||||
}
|
||||
|
||||
/* 方向寄存器一变就查一次:有没有把接地脚配成输出,输出的又是不是高电平。 */
|
||||
static void check_grounded_pins(const sim_i2c_dev_t *d)
|
||||
/* 方向寄存器一变就查两件事(见文件头 ① ②)。 */
|
||||
static void check_dangerous(const sim_i2c_dev_t *d)
|
||||
{
|
||||
for (int port = 0; port < 2; port++) {
|
||||
const uint8_t cfg = (uint8_t)d->regs[PCA9555_REG_CONFIG0 + port];
|
||||
const uint8_t out = (uint8_t)d->regs[PCA9555_REG_OUTPUT0 + port];
|
||||
const uint8_t driven = (uint8_t)(s_gnd_mask[port] & (uint8_t)~cfg); /* cfg 0 = 输出 */
|
||||
if (driven == 0) {
|
||||
continue;
|
||||
}
|
||||
s.gnd_driven++;
|
||||
if ((driven & out) != 0) {
|
||||
s.shorted++; /* 真板子上这一刻开始冒烟 */
|
||||
if (driven != 0) {
|
||||
s_gnd_driven++;
|
||||
if ((driven & out) != 0) {
|
||||
s_shorted++; /* 真板子上这一刻开始冒烟 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ② Port0 上被配成输出、而锁存器里是 1 的位:那一刻它就开始输出高电平。
|
||||
本板 Port0 的五个输出(三个键 / EDL / VBUS)全都是**高有效**,
|
||||
所以这等于「键被按下 + VBUS 上电」。 */
|
||||
const uint8_t cfg0 = (uint8_t)d->regs[PCA9555_REG_CONFIG0];
|
||||
const uint8_t out0 = (uint8_t)d->regs[PCA9555_REG_OUTPUT0];
|
||||
const uint8_t as_output = (uint8_t)(PCA9555_P0_OUT_MASK & (uint8_t)~cfg0);
|
||||
if ((as_output & out0) != 0) {
|
||||
s_unsafe_enable++;
|
||||
}
|
||||
}
|
||||
|
||||
/* INT#:任何输入位相对「上次被读走的那一帧」发生变化就拉低,读走输入端口才释放。 */
|
||||
static void update_int(const sim_i2c_dev_t *d)
|
||||
static void update_int(const sim_i2c_dev_t *d, sim_pca_t *p)
|
||||
{
|
||||
for (int port = 0; port < 2; port++) {
|
||||
if (input_value(d, port) != s.int_latch[port]) {
|
||||
if (!s.int_low) {
|
||||
s.int_low = true;
|
||||
s.int_asserts++;
|
||||
if (input_value(d, p, port) != p->int_latch[port]) {
|
||||
if (!p->int_low) {
|
||||
p->int_low = true;
|
||||
p->int_asserts++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -80,34 +117,39 @@ static void update_int(const sim_i2c_dev_t *d)
|
||||
|
||||
static void hook(sim_i2c_dev_t *d, sim_i2c_ev_t ev)
|
||||
{
|
||||
sim_pca_t *p = from_dev(d);
|
||||
|
||||
if (ev == SIM_I2C_EV_POST_WRITE) {
|
||||
check_grounded_pins(d);
|
||||
update_int(d);
|
||||
check_dangerous(d);
|
||||
update_int(d, p);
|
||||
return;
|
||||
}
|
||||
|
||||
/* PRE_READ:把两个输入寄存器合成好再让总线取走。 */
|
||||
for (int port = 0; port < 2; port++) {
|
||||
d->regs[PCA9555_REG_INPUT0 + port] = input_value(d, port);
|
||||
d->regs[PCA9555_REG_INPUT0 + port] = input_value(d, p, port);
|
||||
}
|
||||
/* 读输入端口 = 确认中断。读别的寄存器不清 INT#(真器件就是这个行为,
|
||||
也是「只靠中断会永远收不到下一个沿」的根源)。 */
|
||||
if (d->ptr == PCA9555_REG_INPUT0 || d->ptr == PCA9555_REG_INPUT1) {
|
||||
s.int_latch[0] = (uint8_t)d->regs[PCA9555_REG_INPUT0];
|
||||
s.int_latch[1] = (uint8_t)d->regs[PCA9555_REG_INPUT1];
|
||||
s.int_low = false;
|
||||
p->int_latch[0] = (uint8_t)d->regs[PCA9555_REG_INPUT0];
|
||||
p->int_latch[1] = (uint8_t)d->regs[PCA9555_REG_INPUT1];
|
||||
p->int_low = false;
|
||||
}
|
||||
}
|
||||
|
||||
void sim_pca9555_attach(uint8_t addr)
|
||||
void sim_pca9555_attach_ch(int idx, int ch, uint8_t addr)
|
||||
{
|
||||
assert(!s.attached); /* 板上只有一片 */
|
||||
sim_i2c_dev_t *d = sim_i2c_add(addr, 1);
|
||||
assert(idx >= 0 && idx < SIM_PCA9555_MAX);
|
||||
assert(!s[idx].attached);
|
||||
sim_i2c_dev_t *d = sim_i2c_add_ch(ch, addr, 1);
|
||||
|
||||
s.attached = true;
|
||||
s.addr = addr;
|
||||
s.ext[0] = 0xFF; /* FLT# / PGOOD 都是开漏上拉,静息是高 */
|
||||
s.ext[1] = 0xFF;
|
||||
sim_pca_t *p = &s[idx];
|
||||
p->attached = true;
|
||||
p->ch = ch;
|
||||
p->addr = addr;
|
||||
p->ext[0] = 0xFF; /* 开漏上拉,静息是高 */
|
||||
p->ext[1] = 0xFF;
|
||||
|
||||
/* 上电复位值(datasheet 表 6):方向全输入 0xFF,**输出锁存器 0xFF**,极性 0。
|
||||
输出的 0xFF 是本器件最阴的一条:配成输出的那一刻就输出高电平。 */
|
||||
@@ -118,45 +160,61 @@ void sim_pca9555_attach(uint8_t addr)
|
||||
d->regs[PCA9555_REG_POLINV0] = 0x00;
|
||||
d->regs[PCA9555_REG_POLINV1] = 0x00;
|
||||
|
||||
s.int_latch[0] = input_value(d, 0);
|
||||
s.int_latch[1] = input_value(d, 1);
|
||||
p->int_latch[0] = input_value(d, p, 0);
|
||||
p->int_latch[1] = input_value(d, p, 1);
|
||||
d->hook = hook;
|
||||
}
|
||||
|
||||
static sim_i2c_dev_t *must_dev(void)
|
||||
static sim_i2c_dev_t *dev_of(int idx)
|
||||
{
|
||||
assert(s.attached); /* 先 sim_pca9555_attach() */
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(s.addr);
|
||||
sim_pca_t *p = inst(idx);
|
||||
sim_i2c_dev_t *d = sim_i2c_dev_ch(p->ch, p->addr);
|
||||
assert(d != NULL);
|
||||
return d;
|
||||
}
|
||||
|
||||
void sim_pca9555_drive(int port, uint8_t level)
|
||||
void sim_pca9555_drive(int idx, int port, uint8_t level)
|
||||
{
|
||||
assert(port == 0 || port == 1);
|
||||
s.ext[port] = level;
|
||||
update_int(must_dev());
|
||||
sim_pca_t *p = inst(idx);
|
||||
p->ext[port] = level;
|
||||
update_int(dev_of(idx), p);
|
||||
}
|
||||
|
||||
uint8_t sim_pca9555_config(int port)
|
||||
uint8_t sim_pca9555_config(int idx, int port)
|
||||
{
|
||||
assert(port == 0 || port == 1);
|
||||
return (uint8_t)must_dev()->regs[PCA9555_REG_CONFIG0 + port];
|
||||
return (uint8_t)dev_of(idx)->regs[PCA9555_REG_CONFIG0 + port];
|
||||
}
|
||||
|
||||
uint8_t sim_pca9555_output(int port)
|
||||
uint8_t sim_pca9555_output(int idx, int port)
|
||||
{
|
||||
assert(port == 0 || port == 1);
|
||||
return (uint8_t)must_dev()->regs[PCA9555_REG_OUTPUT0 + port];
|
||||
return (uint8_t)dev_of(idx)->regs[PCA9555_REG_OUTPUT0 + port];
|
||||
}
|
||||
|
||||
uint8_t sim_pca9555_polinv(int port)
|
||||
uint8_t sim_pca9555_polinv(int idx, int port)
|
||||
{
|
||||
assert(port == 0 || port == 1);
|
||||
return (uint8_t)must_dev()->regs[PCA9555_REG_POLINV0 + port];
|
||||
return (uint8_t)dev_of(idx)->regs[PCA9555_REG_POLINV0 + port];
|
||||
}
|
||||
|
||||
uint32_t sim_pca9555_gnd_driven(void) { return s.gnd_driven; }
|
||||
uint32_t sim_pca9555_shorted(void) { return s.shorted; }
|
||||
bool sim_pca9555_int_asserted(void) { return s.int_low; }
|
||||
uint32_t sim_pca9555_int_asserts(void) { return s.int_asserts; }
|
||||
int sim_pca9555_pin(int idx, int port, int bit)
|
||||
{
|
||||
assert(port == 0 || port == 1);
|
||||
assert(bit >= 0 && bit < 8);
|
||||
const sim_i2c_dev_t *d = dev_of(idx);
|
||||
const uint8_t cfg = (uint8_t)d->regs[PCA9555_REG_CONFIG0 + port];
|
||||
const uint8_t out = (uint8_t)d->regs[PCA9555_REG_OUTPUT0 + port];
|
||||
|
||||
if ((cfg & (uint8_t)(1u << bit)) != 0) {
|
||||
return -1; /* 配成输入 = 高阻,不是 0 */
|
||||
}
|
||||
return (out & (uint8_t)(1u << bit)) != 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
uint32_t sim_pca9555_gnd_driven(void) { return s_gnd_driven; }
|
||||
uint32_t sim_pca9555_shorted(void) { return s_shorted; }
|
||||
uint32_t sim_pca9555_unsafe_enable(void) { return s_unsafe_enable; }
|
||||
bool sim_pca9555_int_asserted(int idx) { return inst(idx)->int_low; }
|
||||
uint32_t sim_pca9555_int_asserts(int idx) { return inst(idx)->int_asserts; }
|
||||
|
||||
在新工单中引用
屏蔽一个用户