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