固件: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,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;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户