A8:任务骨架与调度(5 个任务、两把锁)
需求 §10.3 落地。四件事: 1. 五任务表(tasks.c):周期与优先级写在一张 const 表里,开机自检查的是 相对顺序(safety 唯一最高、log 唯一最低、thermal 与 actuator 同级), 外加「巡检周期必须长于 INA226 一帧转换 8.8 ms」。不过就 abort。 2. 节拍器(sched.c):记下一次该醒的绝对时刻,回绕安全(int32_t 差判先后)。 过载时跳过错过的整周期、保住相位,不补跑。 3. 两把锁(hal_lock):I²C 与 74HC595,禁止嵌套持有 —— 这块板上两者没有交集, 「拿着 A 去拿 B」永远是写错了。ESP 侧用 mutex 取优先级继承。 4. t_safety 的一个周期:拿锁 → 6 片 INA226 的 V+I + PCA9555 两端口 → 放锁。 不死等锁(5 ms 超时就跳过这圈),一片读失败不中断整圈、保留上一圈的值。 I²C 占用由 HAL 按线上真实字节数记账(两个目标同一份代码),静态预算与实测 必须相等:一次寄存器读 48 位,一圈 13 次事务 = 1560 µs / 20 ms = 7.8 %, 与 §10.3 手算的「约 1.5 ms / 约 8 %」相符(本节的 46 位没数 STOP 和重复 START)。 sim 侧配套两样:假 I²C 事务真的把虚拟时钟花掉(否则时序测试是空话), 以及可强制的加锁纪律检查(配一条反向测试,防止检查本身失灵)。 11 条 ctest(161 例)全绿,idf.py build 通过;16 个注错实验全部变红。 Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
@@ -1,12 +1,14 @@
|
||||
# PhoneRail 的硬件抽象层(ESP-IDF 侧)。宿主机侧编的是 sim/,由 host/CMakeLists.txt 收。
|
||||
#
|
||||
# ⚠️ 组件名不能叫 `hal` —— ESP-IDF 自己有一个 components/hal(driver 依赖它),
|
||||
# 重名会把 IDF 的那个盖掉。所以目录叫 phonerail_hal。
|
||||
idf_component_register(
|
||||
SRCS "hal_err.c"
|
||||
"esp/hal_i2c_esp.c"
|
||||
"esp/hal_gpio_esp.c"
|
||||
"esp/hal_pwm_esp.c"
|
||||
"esp/hal_time_esp.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_i2c esp_driver_gpio esp_driver_ledc esp_timer freertos)
|
||||
# PhoneRail 的硬件抽象层(ESP-IDF 侧)。宿主机侧编的是 sim/,由 host/CMakeLists.txt 收。
|
||||
#
|
||||
# ⚠️ 组件名不能叫 `hal` —— ESP-IDF 自己有一个 components/hal(driver 依赖它),
|
||||
# 重名会把 IDF 的那个盖掉。所以目录叫 phonerail_hal。
|
||||
idf_component_register(
|
||||
SRCS "hal_err.c"
|
||||
"hal_i2c_acct.c"
|
||||
"esp/hal_lock_esp.c"
|
||||
"esp/hal_i2c_esp.c"
|
||||
"esp/hal_gpio_esp.c"
|
||||
"esp/hal_pwm_esp.c"
|
||||
"esp/hal_time_esp.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_i2c esp_driver_gpio esp_driver_ledc esp_timer freertos)
|
||||
|
||||
@@ -1,114 +1,120 @@
|
||||
/* I²C 的 ESP-IDF v5.4 实现(新版 i2c_master 驱动,不是已废弃的 driver/i2c.h)。 */
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "hal_i2c.h"
|
||||
|
||||
#define I2C_XFER_TIMEOUT_MS 50
|
||||
#define I2C_DEFAULT_FREQ_HZ 400000
|
||||
|
||||
static i2c_master_bus_handle_t s_bus;
|
||||
static i2c_master_dev_handle_t s_devs[128]; /* 按 7 位地址索引,用到才建 */
|
||||
static uint32_t s_freq_hz;
|
||||
|
||||
static hal_err_t map_err(esp_err_t e)
|
||||
{
|
||||
switch (e) {
|
||||
case ESP_OK: return HAL_OK;
|
||||
case ESP_ERR_INVALID_ARG: return HAL_ERR_ARG;
|
||||
case ESP_ERR_INVALID_STATE: return HAL_ERR_STATE;
|
||||
case ESP_ERR_TIMEOUT: return HAL_ERR_TIMEOUT;
|
||||
case ESP_ERR_NOT_FOUND: return HAL_ERR_NAK;
|
||||
case ESP_FAIL: return HAL_ERR_NAK; /* 从机没 ACK 走这条 */
|
||||
default: return HAL_ERR_BUS;
|
||||
}
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg)
|
||||
{
|
||||
if (cfg == NULL || cfg->sda_gpio < 0 || cfg->scl_gpio < 0) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (s_bus != NULL) {
|
||||
return HAL_OK; /* 幂等 */
|
||||
}
|
||||
i2c_master_bus_config_t bc = {
|
||||
.i2c_port = -1, /* 自动选口 */
|
||||
.sda_io_num = (gpio_num_t)cfg->sda_gpio,
|
||||
.scl_io_num = (gpio_num_t)cfg->scl_gpio,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
/* 板上已有 4.7 kΩ 外部上拉(设计文档 §8.2),内部上拉 45 kΩ 太弱,
|
||||
开着只会掩盖「上拉电阻漏焊」这类问题。 */
|
||||
.flags.enable_internal_pullup = false,
|
||||
};
|
||||
esp_err_t e = i2c_new_master_bus(&bc, &s_bus);
|
||||
if (e != ESP_OK) {
|
||||
return map_err(e);
|
||||
}
|
||||
s_freq_hz = cfg->freq_hz != 0 ? cfg->freq_hz : I2C_DEFAULT_FREQ_HZ;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
static hal_err_t dev_of(uint8_t addr, i2c_master_dev_handle_t *out)
|
||||
{
|
||||
if (s_bus == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (addr > 0x7f) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (s_devs[addr] == NULL) {
|
||||
i2c_device_config_t dc = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = addr,
|
||||
.scl_speed_hz = s_freq_hz,
|
||||
};
|
||||
esp_err_t e = i2c_master_bus_add_device(s_bus, &dc, &s_devs[addr]);
|
||||
if (e != ESP_OK) {
|
||||
return map_err(e);
|
||||
}
|
||||
}
|
||||
*out = s_devs[addr];
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write(uint8_t addr, const uint8_t *data, size_t len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return map_err(i2c_master_transmit(dev, data, len, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_read(uint8_t addr, uint8_t *data, size_t len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return map_err(i2c_master_receive(dev, data, len, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write_read(uint8_t addr, const uint8_t *wr, size_t wr_len,
|
||||
uint8_t *rd, size_t rd_len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return map_err(i2c_master_transmit_receive(dev, wr, wr_len, rd, rd_len,
|
||||
I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_probe(uint8_t addr)
|
||||
{
|
||||
if (s_bus == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
return map_err(i2c_master_probe(s_bus, addr, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
/* I²C 的 ESP-IDF v5.4 实现(新版 i2c_master 驱动,不是已废弃的 driver/i2c.h)。 */
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_i2c_acct.h"
|
||||
|
||||
#define I2C_XFER_TIMEOUT_MS 50
|
||||
#define I2C_DEFAULT_FREQ_HZ 400000
|
||||
|
||||
static i2c_master_bus_handle_t s_bus;
|
||||
static i2c_master_dev_handle_t s_devs[128]; /* 按 7 位地址索引,用到才建 */
|
||||
static uint32_t s_freq_hz;
|
||||
|
||||
static hal_err_t map_err(esp_err_t e)
|
||||
{
|
||||
switch (e) {
|
||||
case ESP_OK: return HAL_OK;
|
||||
case ESP_ERR_INVALID_ARG: return HAL_ERR_ARG;
|
||||
case ESP_ERR_INVALID_STATE: return HAL_ERR_STATE;
|
||||
case ESP_ERR_TIMEOUT: return HAL_ERR_TIMEOUT;
|
||||
case ESP_ERR_NOT_FOUND: return HAL_ERR_NAK;
|
||||
case ESP_FAIL: return HAL_ERR_NAK; /* 从机没 ACK 走这条 */
|
||||
default: return HAL_ERR_BUS;
|
||||
}
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg)
|
||||
{
|
||||
if (cfg == NULL || cfg->sda_gpio < 0 || cfg->scl_gpio < 0) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (s_bus != NULL) {
|
||||
return HAL_OK; /* 幂等 */
|
||||
}
|
||||
i2c_master_bus_config_t bc = {
|
||||
.i2c_port = -1, /* 自动选口 */
|
||||
.sda_io_num = (gpio_num_t)cfg->sda_gpio,
|
||||
.scl_io_num = (gpio_num_t)cfg->scl_gpio,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
/* 板上已有 4.7 kΩ 外部上拉(设计文档 §8.2),内部上拉 45 kΩ 太弱,
|
||||
开着只会掩盖「上拉电阻漏焊」这类问题。 */
|
||||
.flags.enable_internal_pullup = false,
|
||||
};
|
||||
esp_err_t e = i2c_new_master_bus(&bc, &s_bus);
|
||||
if (e != ESP_OK) {
|
||||
return map_err(e);
|
||||
}
|
||||
s_freq_hz = cfg->freq_hz != 0 ? cfg->freq_hz : I2C_DEFAULT_FREQ_HZ;
|
||||
hal_i2c_acct_set_freq(s_freq_hz); /* 占用核算按真实速率折算,见 hal_i2c_acct.h */
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
static hal_err_t dev_of(uint8_t addr, i2c_master_dev_handle_t *out)
|
||||
{
|
||||
if (s_bus == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (addr > 0x7f) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (s_devs[addr] == NULL) {
|
||||
i2c_device_config_t dc = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = addr,
|
||||
.scl_speed_hz = s_freq_hz,
|
||||
};
|
||||
esp_err_t e = i2c_master_bus_add_device(s_bus, &dc, &s_devs[addr]);
|
||||
if (e != ESP_OK) {
|
||||
return map_err(e);
|
||||
}
|
||||
}
|
||||
*out = s_devs[addr];
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write(uint8_t addr, const uint8_t *data, size_t len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
hal_i2c_acct_add(len, 0);
|
||||
return map_err(i2c_master_transmit(dev, data, len, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_read(uint8_t addr, uint8_t *data, size_t len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
hal_i2c_acct_add(0, len);
|
||||
return map_err(i2c_master_receive(dev, data, len, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write_read(uint8_t addr, const uint8_t *wr, size_t wr_len,
|
||||
uint8_t *rd, size_t rd_len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
hal_i2c_acct_add(wr_len, rd_len);
|
||||
return map_err(i2c_master_transmit_receive(dev, wr, wr_len, rd, rd_len,
|
||||
I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_probe(uint8_t addr)
|
||||
{
|
||||
if (s_bus == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
hal_i2c_acct_add(0, 0);
|
||||
return map_err(i2c_master_probe(s_bus, addr, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/* 两把互斥锁的 FreeRTOS 实现。静态分配(`xSemaphoreCreateMutexStatic`)——
|
||||
两把锁的数量是编译期定死的,没有理由去堆上要内存,也就没有「创建失败」这条路径。
|
||||
|
||||
⚠️ 用的是 mutex 不是 binary semaphore:mutex 有**优先级继承**。
|
||||
t_safety 是最高优先级且每 20 ms 就要总线,低优先级的 t_link 拿着 I²C 锁去读遥测时
|
||||
如果没有继承,t_safety 会被一个中优先级任务无限期挡在门外(经典优先级反转)。 */
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
#include "hal_lock.h"
|
||||
|
||||
static StaticSemaphore_t s_buf[HAL_LOCK_COUNT];
|
||||
static SemaphoreHandle_t s_mtx[HAL_LOCK_COUNT];
|
||||
static uint32_t s_timeouts[HAL_LOCK_COUNT];
|
||||
|
||||
hal_err_t hal_lock_init(void)
|
||||
{
|
||||
for (int i = 0; i < HAL_LOCK_COUNT; i++) {
|
||||
if (s_mtx[i] == NULL) {
|
||||
s_mtx[i] = xSemaphoreCreateMutexStatic(&s_buf[i]);
|
||||
s_timeouts[i] = 0;
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
bool hal_lock_held(hal_lock_id_t id)
|
||||
{
|
||||
if (id < 0 || id >= HAL_LOCK_COUNT || s_mtx[id] == NULL) {
|
||||
return false;
|
||||
}
|
||||
return xSemaphoreGetMutexHolder(s_mtx[id]) == xTaskGetCurrentTaskHandle();
|
||||
}
|
||||
|
||||
hal_err_t hal_lock_take(hal_lock_id_t id, uint32_t timeout_ms)
|
||||
{
|
||||
if (id < 0 || id >= HAL_LOCK_COUNT || s_mtx[id] == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
/* 已经持有 = 重复加锁;持有另一把 = 嵌套。两者都是写错了,见 hal_lock.h。 */
|
||||
for (int i = 0; i < HAL_LOCK_COUNT; i++) {
|
||||
if (hal_lock_held((hal_lock_id_t)i)) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
}
|
||||
const TickType_t ticks =
|
||||
(timeout_ms == HAL_LOCK_FOREVER) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
|
||||
if (xSemaphoreTake(s_mtx[id], ticks) != pdTRUE) {
|
||||
s_timeouts[id]++;
|
||||
return HAL_ERR_TIMEOUT;
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
void hal_lock_give(hal_lock_id_t id)
|
||||
{
|
||||
configASSERT(id >= 0 && id < HAL_LOCK_COUNT && s_mtx[id] != NULL);
|
||||
configASSERT(hal_lock_held(id)); /* 不是自己拿的就别放 */
|
||||
(void)xSemaphoreGive(s_mtx[id]);
|
||||
}
|
||||
|
||||
uint32_t hal_lock_timeouts(hal_lock_id_t id)
|
||||
{
|
||||
return (id >= 0 && id < HAL_LOCK_COUNT) ? s_timeouts[id] : 0;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/* I²C 占用核算的实现。**两个目标编的是同一份**(和 hal_err.c 一样放在组件根下),
|
||||
所以宿主机上算出来的占用率就是上板会打印的那个数。 */
|
||||
#include "hal_i2c_acct.h"
|
||||
|
||||
static uint64_t s_bits;
|
||||
static uint32_t s_txns;
|
||||
static uint32_t s_freq_hz = HAL_I2C_FREQ_HZ_DEFAULT;
|
||||
|
||||
uint32_t hal_i2c_txn_bits(size_t wr_len, size_t rd_len)
|
||||
{
|
||||
if (wr_len == 0 && rd_len == 0) {
|
||||
return 1u + 9u + 1u; /* 探测:START + 地址字节 + STOP */
|
||||
}
|
||||
|
||||
uint32_t bits = 1u; /* START */
|
||||
if (wr_len > 0) {
|
||||
bits += 9u * (uint32_t)(1u + wr_len); /* 地址 + 载荷 */
|
||||
}
|
||||
if (rd_len > 0) {
|
||||
if (wr_len > 0) {
|
||||
bits += 1u; /* 重复 START */
|
||||
}
|
||||
bits += 9u * (uint32_t)(1u + rd_len);
|
||||
}
|
||||
bits += 1u; /* STOP */
|
||||
return bits;
|
||||
}
|
||||
|
||||
void hal_i2c_acct_add(size_t wr_len, size_t rd_len)
|
||||
{
|
||||
s_bits += hal_i2c_txn_bits(wr_len, rd_len);
|
||||
s_txns++;
|
||||
}
|
||||
|
||||
uint64_t hal_i2c_acct_bits(void) { return s_bits; }
|
||||
uint32_t hal_i2c_acct_txns(void) { return s_txns; }
|
||||
|
||||
void hal_i2c_acct_set_freq(uint32_t hz)
|
||||
{
|
||||
s_freq_hz = (hz == 0) ? HAL_I2C_FREQ_HZ_DEFAULT : hz;
|
||||
}
|
||||
|
||||
uint32_t hal_i2c_acct_freq(void) { return s_freq_hz; }
|
||||
|
||||
uint64_t hal_i2c_bits_to_us(uint64_t bits)
|
||||
{
|
||||
/* 先乘后除,别先算「每位多少 µs」—— 400 kHz 下那是 2.5 µs,整数除法会抹平。 */
|
||||
return (bits * 1000000ull) / (uint64_t)s_freq_hz;
|
||||
}
|
||||
|
||||
uint32_t hal_i2c_acct_window_us(uint64_t *cursor_bits)
|
||||
{
|
||||
const uint64_t now = s_bits;
|
||||
const uint64_t was = (cursor_bits != NULL) ? *cursor_bits : 0;
|
||||
if (cursor_bits != NULL) {
|
||||
*cursor_bits = now;
|
||||
}
|
||||
return (uint32_t)hal_i2c_bits_to_us(now - was);
|
||||
}
|
||||
|
||||
void hal_i2c_acct_reset(void)
|
||||
{
|
||||
s_bits = 0;
|
||||
s_txns = 0;
|
||||
s_freq_hz = HAL_I2C_FREQ_HZ_DEFAULT;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* I²C 总线占用核算 —— 需求 §10.3 那 8 % 的可执行版本。
|
||||
|
||||
§10.3 手算了一遍「t_safety 每 20 ms 要占总线多久」,结论是约 1.5 ms、约 8 %,
|
||||
并在末尾要求「首板上电时用逻辑分析仪实测复核一次」。这里做的是中间那一档:
|
||||
让固件**自己按线上真实发生的事务**把这个数算出来,两个目标(sim 与 ESP)
|
||||
用的是同一份计数代码,所以宿主机上跑出来的数就是上板会打印出来的数。
|
||||
|
||||
为什么放在 HAL 里而不是应用层:只有 HAL 知道每次事务真正过了多少字节。
|
||||
放在应用层就变成「按计划中的事务数手算」,那是把 §10.3 的算式抄第二遍
|
||||
—— 少读一次寄存器、多一次重试,手算的那份都不会变。
|
||||
|
||||
⚠️ 这是**理想时序**的下限:不含时钟延展、不含 NAK 重试、不含事务之间
|
||||
固件自己的开销。它能证明「预算没被写崩」,不能替代 §10.3 要求的实测。 */
|
||||
#ifndef HAL_I2C_ACCT_H
|
||||
#define HAL_I2C_ACCT_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HAL_I2C_FREQ_HZ_DEFAULT 400000u
|
||||
|
||||
/* 一次事务在总线上占的**位周期数**。
|
||||
写: START + (1+wr_len) 个字节 × 9 位 + STOP
|
||||
写后读: START + (1+wr_len) 字节 + 重复 START + (1+rd_len) 字节 + STOP
|
||||
每字节 9 位 = 8 数据 + 1 应答;START / 重复 START / STOP 各按 1 个位周期计。
|
||||
rd_len == 0 就是纯写,wr_len == 0 就是纯读(探测是 wr_len = rd_len = 0)。
|
||||
|
||||
对一次 INA226 寄存器读(wr 1 字节、rd 2 字节)它给出 48 位 = 120 µs @400 kHz。
|
||||
§10.3 手算的是「≈46 个位周期 ≈115 µs」—— 差的两位是 STOP 和一个 START,
|
||||
§10.3 没数进去。整周期的结论不变(1.56 ms / 7.8 %,见 §10.3 的「约 1.5 ms / 约 8 %」)。 */
|
||||
uint32_t hal_i2c_txn_bits(size_t wr_len, size_t rd_len);
|
||||
|
||||
/* 累计位周期数。两个 HAL 实现都在事务成功**与失败**之后调它 ——
|
||||
NAK 掉的事务也占了总线时间(地址字节已经发出去了)。 */
|
||||
void hal_i2c_acct_add(size_t wr_len, size_t rd_len);
|
||||
uint64_t hal_i2c_acct_bits(void);
|
||||
uint32_t hal_i2c_acct_txns(void);
|
||||
|
||||
/* 位周期 → µs,按当前总线频率。hal_i2c_init() 会把频率填进来(0 视为默认 400 kHz)。 */
|
||||
void hal_i2c_acct_set_freq(uint32_t hz);
|
||||
uint32_t hal_i2c_acct_freq(void);
|
||||
uint64_t hal_i2c_bits_to_us(uint64_t bits);
|
||||
|
||||
/* 取「从上一次取样到现在」占了多少 µs,并把取样点推到现在。
|
||||
t_safety 每个周期调一次,拿到的就是这一周期的总线占用。 */
|
||||
uint32_t hal_i2c_acct_window_us(uint64_t *cursor_bits);
|
||||
|
||||
void hal_i2c_acct_reset(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HAL_I2C_ACCT_H */
|
||||
@@ -0,0 +1,61 @@
|
||||
/* 互斥锁 —— 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);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HAL_LOCK_H */
|
||||
@@ -1,174 +1,193 @@
|
||||
/* I²C 的假实现:一张寄存器表 + 可注入的 NAK。宿主机专用。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
static sim_i2c_dev_t s_dev[SIM_I2C_MAX_DEV];
|
||||
static int s_n;
|
||||
static bool s_inited;
|
||||
|
||||
void sim_i2c_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
void sim_i2c_reset_internal(void)
|
||||
{
|
||||
memset(s_dev, 0, sizeof(s_dev));
|
||||
s_n = 0;
|
||||
s_inited = false;
|
||||
}
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_dev(uint8_t addr)
|
||||
{
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_dev[i].addr == addr) {
|
||||
return &s_dev[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_add(uint8_t addr, uint8_t reg_width)
|
||||
{
|
||||
assert(addr <= 0x7f);
|
||||
assert(reg_width == 1 || reg_width == 2);
|
||||
assert(sim_i2c_dev(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->reg_width = reg_width;
|
||||
d->present = true;
|
||||
return d;
|
||||
}
|
||||
|
||||
static sim_i2c_dev_t *must(uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(addr);
|
||||
assert(d != NULL); /* 先 sim_i2c_add() 再预置寄存器 */
|
||||
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; }
|
||||
|
||||
/* ---- HAL 实现 ---------------------------------------------------------- */
|
||||
|
||||
hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg)
|
||||
{
|
||||
(void)cfg; /* sim 不关心引脚与速率 */
|
||||
s_inited = true;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* 取器件并把「这次事务要不要 NAK」判掉。 */
|
||||
static hal_err_t take(uint8_t addr, sim_i2c_dev_t **out)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (addr > 0x7f) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(addr);
|
||||
if (d == NULL || !d->present) {
|
||||
return HAL_ERR_NAK;
|
||||
}
|
||||
if (d->nak_left != 0) {
|
||||
if (d->nak_left > 0) {
|
||||
d->nak_left--;
|
||||
}
|
||||
return HAL_ERR_NAK;
|
||||
}
|
||||
*out = d;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* data[0] 是寄存器指针,其后按 reg_width 一格一格写,指针自增(uint8_t 自然回绕)。 */
|
||||
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 (len > 1 && ((len - 1) % d->reg_width) != 0) {
|
||||
return HAL_ERR_ARG; /* 半个寄存器:真器件上是未定义行为,这里直接报错 */
|
||||
}
|
||||
d->n_write++;
|
||||
d->last_write_len = (uint8_t)(len < SIM_I2C_LOG_BYTES ? len : SIM_I2C_LOG_BYTES);
|
||||
if (d->last_write_len != 0) {
|
||||
memcpy(d->last_write, data, d->last_write_len);
|
||||
}
|
||||
if (len == 0) {
|
||||
return HAL_OK; /* 只发地址 = 探测 */
|
||||
}
|
||||
d->ptr = data[0];
|
||||
for (size_t i = 1; i < len; i += d->reg_width) {
|
||||
uint16_t v = data[i];
|
||||
if (d->reg_width == 2) {
|
||||
v = (uint16_t)((v << 8) | data[i + 1]);
|
||||
}
|
||||
d->regs[d->ptr++] = v;
|
||||
}
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_POST_WRITE);
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
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 ((len % d->reg_width) != 0) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
d->n_read++;
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_PRE_READ); /* 按 datasheet 建模的器件在这里合成输入寄存器 */
|
||||
}
|
||||
for (size_t i = 0; i < len; i += d->reg_width) {
|
||||
uint16_t v = d->regs[d->ptr++];
|
||||
if (d->reg_width == 2) {
|
||||
data[i] = (uint8_t)(v >> 8);
|
||||
data[i + 1] = (uint8_t)(v & 0xff);
|
||||
} else {
|
||||
data[i] = (uint8_t)v;
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write(uint8_t addr, const uint8_t *data, size_t len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
hal_err_t err = take(addr, &d);
|
||||
return err != HAL_OK ? err : do_write(d, data, len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_read(uint8_t addr, uint8_t *data, size_t len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
hal_err_t err = take(addr, &d);
|
||||
return err != HAL_OK ? err : do_read(d, data, len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write_read(uint8_t addr, const uint8_t *wr, size_t wr_len,
|
||||
uint8_t *rd, size_t rd_len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
hal_err_t err = take(addr, &d);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
err = do_write(d, wr, wr_len);
|
||||
return err != HAL_OK ? err : do_read(d, rd, rd_len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_probe(uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
return take(addr, &d);
|
||||
}
|
||||
/* I²C 的假实现:一张寄存器表 + 可注入的 NAK。宿主机专用。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
static sim_i2c_dev_t s_dev[SIM_I2C_MAX_DEV];
|
||||
static int s_n;
|
||||
static bool s_inited;
|
||||
static bool s_bus_time = true;
|
||||
|
||||
void sim_i2c_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
void sim_i2c_reset_internal(void)
|
||||
{
|
||||
memset(s_dev, 0, sizeof(s_dev));
|
||||
s_n = 0;
|
||||
s_inited = false;
|
||||
s_bus_time = true;
|
||||
}
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_dev(uint8_t addr)
|
||||
{
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_dev[i].addr == addr) {
|
||||
return &s_dev[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_add(uint8_t addr, uint8_t reg_width)
|
||||
{
|
||||
assert(addr <= 0x7f);
|
||||
assert(reg_width == 1 || reg_width == 2);
|
||||
assert(sim_i2c_dev(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->reg_width = reg_width;
|
||||
d->present = true;
|
||||
return d;
|
||||
}
|
||||
|
||||
static sim_i2c_dev_t *must(uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(addr);
|
||||
assert(d != NULL); /* 先 sim_i2c_add() 再预置寄存器 */
|
||||
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_bus_time(bool on) { s_bus_time = on; }
|
||||
|
||||
/* 每次事务(成功或 NAK)都记账并把虚拟时钟花掉。NAK 也占总线:地址字节已经发出去了。
|
||||
同时查一次加锁纪律 —— I²C 是 §10.3 两个共享资源之一。 */
|
||||
static void spend(size_t wr_len, size_t rd_len)
|
||||
{
|
||||
sim_lock_check(HAL_LOCK_I2C);
|
||||
hal_i2c_acct_add(wr_len, rd_len);
|
||||
if (s_bus_time) {
|
||||
sim_time_advance_us(hal_i2c_bits_to_us(hal_i2c_txn_bits(wr_len, rd_len)));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- HAL 实现 ---------------------------------------------------------- */
|
||||
|
||||
hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg)
|
||||
{
|
||||
(void)cfg; /* sim 不关心引脚 —— 但速率要告诉占用核算,它按频率折算 µs */
|
||||
hal_i2c_acct_set_freq(cfg != NULL ? cfg->freq_hz : 0);
|
||||
s_inited = true;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* 取器件并把「这次事务要不要 NAK」判掉。 */
|
||||
static hal_err_t take(uint8_t addr, sim_i2c_dev_t **out)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (addr > 0x7f) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(addr);
|
||||
if (d == NULL || !d->present) {
|
||||
return HAL_ERR_NAK;
|
||||
}
|
||||
if (d->nak_left != 0) {
|
||||
if (d->nak_left > 0) {
|
||||
d->nak_left--;
|
||||
}
|
||||
return HAL_ERR_NAK;
|
||||
}
|
||||
*out = d;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* data[0] 是寄存器指针,其后按 reg_width 一格一格写,指针自增(uint8_t 自然回绕)。 */
|
||||
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 (len > 1 && ((len - 1) % d->reg_width) != 0) {
|
||||
return HAL_ERR_ARG; /* 半个寄存器:真器件上是未定义行为,这里直接报错 */
|
||||
}
|
||||
d->n_write++;
|
||||
d->last_write_len = (uint8_t)(len < SIM_I2C_LOG_BYTES ? len : SIM_I2C_LOG_BYTES);
|
||||
if (d->last_write_len != 0) {
|
||||
memcpy(d->last_write, data, d->last_write_len);
|
||||
}
|
||||
if (len == 0) {
|
||||
return HAL_OK; /* 只发地址 = 探测 */
|
||||
}
|
||||
d->ptr = data[0];
|
||||
for (size_t i = 1; i < len; i += d->reg_width) {
|
||||
uint16_t v = data[i];
|
||||
if (d->reg_width == 2) {
|
||||
v = (uint16_t)((v << 8) | data[i + 1]);
|
||||
}
|
||||
d->regs[d->ptr++] = v;
|
||||
}
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_POST_WRITE);
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
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 ((len % d->reg_width) != 0) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
d->n_read++;
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_PRE_READ); /* 按 datasheet 建模的器件在这里合成输入寄存器 */
|
||||
}
|
||||
for (size_t i = 0; i < len; i += d->reg_width) {
|
||||
uint16_t v = d->regs[d->ptr++];
|
||||
if (d->reg_width == 2) {
|
||||
data[i] = (uint8_t)(v >> 8);
|
||||
data[i + 1] = (uint8_t)(v & 0xff);
|
||||
} else {
|
||||
data[i] = (uint8_t)v;
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write(uint8_t addr, const uint8_t *data, size_t len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
spend(len, 0);
|
||||
hal_err_t err = take(addr, &d);
|
||||
return err != HAL_OK ? err : do_write(d, data, len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_read(uint8_t addr, uint8_t *data, size_t len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
spend(0, len);
|
||||
hal_err_t err = take(addr, &d);
|
||||
return err != HAL_OK ? err : do_read(d, data, len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write_read(uint8_t addr, const uint8_t *wr, size_t wr_len,
|
||||
uint8_t *rd, size_t rd_len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
spend(wr_len, rd_len);
|
||||
hal_err_t err = take(addr, &d);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
err = do_write(d, wr, wr_len);
|
||||
return err != HAL_OK ? err : do_read(d, rd, rd_len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_probe(uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
spend(0, 0);
|
||||
return take(addr, &d);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/* 两把互斥锁的假实现。宿主机测试是**单线程**的,所以这里模拟的不是并发本身,
|
||||
而是「加锁纪律」—— 谁拿了、放没放、有没有嵌套、该拿的时候拿了没有。
|
||||
|
||||
⚠️ 单线程 sim 里「等一把别人持有的锁」永远等不到,所以:
|
||||
· 有限超时 → 把虚拟时钟往前拨满 timeout_ms,计一次超时,返回 HAL_ERR_TIMEOUT
|
||||
(这正是真板上等不到锁时该发生的事,调用方的处理路径因此测得到);
|
||||
· HAL_LOCK_FOREVER 且被别人持有 → 直接 abort。那在真板上是死锁,
|
||||
在这里悄悄返回一个错误码只会把它藏起来。
|
||||
|
||||
「谁」是一个假任务号(`sim_lock_set_task()`),默认 0。测试用它扮演第二个任务。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_lock.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
#define NO_OWNER (-1)
|
||||
|
||||
static int s_owner[HAL_LOCK_COUNT];
|
||||
static uint32_t s_takes[HAL_LOCK_COUNT];
|
||||
static uint32_t s_timeouts[HAL_LOCK_COUNT];
|
||||
static uint32_t s_violations[HAL_LOCK_COUNT];
|
||||
static bool s_require[HAL_LOCK_COUNT];
|
||||
static int s_task;
|
||||
static bool s_inited;
|
||||
|
||||
void sim_lock_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
void sim_lock_reset_internal(void)
|
||||
{
|
||||
for (int i = 0; i < HAL_LOCK_COUNT; i++) {
|
||||
s_owner[i] = NO_OWNER;
|
||||
}
|
||||
memset(s_takes, 0, sizeof(s_takes));
|
||||
memset(s_timeouts, 0, sizeof(s_timeouts));
|
||||
memset(s_violations, 0, sizeof(s_violations));
|
||||
memset(s_require, 0, sizeof(s_require));
|
||||
s_task = 0;
|
||||
s_inited = false;
|
||||
}
|
||||
|
||||
static bool valid(hal_lock_id_t id) { return id >= 0 && id < HAL_LOCK_COUNT; }
|
||||
|
||||
hal_err_t hal_lock_init(void)
|
||||
{
|
||||
s_inited = true;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
bool hal_lock_held(hal_lock_id_t id)
|
||||
{
|
||||
return valid(id) && s_owner[id] == s_task;
|
||||
}
|
||||
|
||||
hal_err_t hal_lock_take(hal_lock_id_t id, uint32_t timeout_ms)
|
||||
{
|
||||
if (!valid(id) || !s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
/* 已经持有 = 重复加锁(这两把都不是递归锁);持有另一把 = 嵌套,见 hal_lock.h。 */
|
||||
for (int i = 0; i < HAL_LOCK_COUNT; i++) {
|
||||
if (s_owner[i] == s_task) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
}
|
||||
if (s_owner[id] != NO_OWNER) {
|
||||
assert(timeout_ms != HAL_LOCK_FOREVER); /* 单线程里死等 = 死锁 */
|
||||
s_timeouts[id]++;
|
||||
hal_time_delay_ms(timeout_ms); /* 真板上这段时间是等掉的,虚拟时钟也要花掉 */
|
||||
return HAL_ERR_TIMEOUT;
|
||||
}
|
||||
s_owner[id] = s_task;
|
||||
s_takes[id]++;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
void hal_lock_give(hal_lock_id_t id)
|
||||
{
|
||||
assert(valid(id));
|
||||
assert(s_owner[id] == s_task); /* 不是自己拿的 / 根本没人拿 */
|
||||
s_owner[id] = NO_OWNER;
|
||||
}
|
||||
|
||||
uint32_t hal_lock_timeouts(hal_lock_id_t id) { return valid(id) ? s_timeouts[id] : 0; }
|
||||
|
||||
/* ---- 假板子这一侧的控制面 ------------------------------------------------ */
|
||||
|
||||
void sim_lock_set_task(int task) { s_task = task; }
|
||||
int sim_lock_task(void) { return s_task; }
|
||||
int sim_lock_owner(hal_lock_id_t id) { return valid(id) ? s_owner[id] : NO_OWNER; }
|
||||
uint32_t sim_lock_takes(hal_lock_id_t id) { return valid(id) ? s_takes[id] : 0; }
|
||||
|
||||
void sim_lock_require(hal_lock_id_t id, bool on)
|
||||
{
|
||||
if (valid(id)) {
|
||||
s_require[id] = on;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t sim_lock_violations(hal_lock_id_t id) { return valid(id) ? s_violations[id] : 0; }
|
||||
|
||||
void sim_lock_check(hal_lock_id_t id)
|
||||
{
|
||||
if (valid(id) && s_require[id] && !hal_lock_held(id)) {
|
||||
s_violations[id]++;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
/* sim 的总闸:一次把四路都恢复出厂。每个测试的 setUp() 调它。
|
||||
各路的清理函数是内部符号,不进 sim_hal.h —— 测试没有理由单独重置某一路。 */
|
||||
#include "sim_hal.h"
|
||||
|
||||
void sim_i2c_reset_internal(void);
|
||||
void sim_gpio_reset_internal(void);
|
||||
void sim_sr595_reset_internal(void);
|
||||
void sim_pca9555_reset_internal(void);
|
||||
void sim_pwm_reset_internal(void);
|
||||
void sim_time_reset_internal(void);
|
||||
|
||||
void sim_hal_reset(void)
|
||||
{
|
||||
sim_i2c_reset_internal();
|
||||
sim_gpio_reset_internal();
|
||||
sim_sr595_reset_internal();
|
||||
sim_pca9555_reset_internal();
|
||||
sim_pwm_reset_internal();
|
||||
sim_time_reset_internal();
|
||||
}
|
||||
/* sim 的总闸:一次把四路都恢复出厂。每个测试的 setUp() 调它。
|
||||
各路的清理函数是内部符号,不进 sim_hal.h —— 测试没有理由单独重置某一路。 */
|
||||
#include "hal_i2c_acct.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
void sim_i2c_reset_internal(void);
|
||||
void sim_gpio_reset_internal(void);
|
||||
void sim_sr595_reset_internal(void);
|
||||
void sim_pca9555_reset_internal(void);
|
||||
void sim_pwm_reset_internal(void);
|
||||
void sim_time_reset_internal(void);
|
||||
void sim_lock_reset_internal(void);
|
||||
|
||||
void sim_hal_reset(void)
|
||||
{
|
||||
sim_i2c_reset_internal();
|
||||
sim_gpio_reset_internal();
|
||||
sim_sr595_reset_internal();
|
||||
sim_pca9555_reset_internal();
|
||||
sim_pwm_reset_internal();
|
||||
sim_time_reset_internal();
|
||||
sim_lock_reset_internal();
|
||||
hal_i2c_acct_reset();
|
||||
}
|
||||
|
||||
@@ -1,156 +1,186 @@
|
||||
/* 假板子的控制面 —— 只有宿主机测试与 sim/ 自己会包含它,应用代码一律不许包含。
|
||||
(包含了就编不过:ESP 目标根本不编译 sim/。)
|
||||
|
||||
⚠️ 模拟器是用来证明**代码逻辑**对的,不是用来证明**设计**对的。
|
||||
电压、余量、时序裕量的结论全部来自设计文档的计算与首板实测。见计划 §4.2。 */
|
||||
#ifndef SIM_HAL_H
|
||||
#define SIM_HAL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_gpio.h"
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_pwm.h"
|
||||
#include "hal_time.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 四路 sim 全部恢复出厂:器件表清空、引脚未配置、PWM 未配置、时钟归零、
|
||||
init 标志清掉。每个测试的 setUp() 里调它。 */
|
||||
void sim_hal_reset(void);
|
||||
|
||||
/* ---------------- I²C ---------------- */
|
||||
|
||||
#define SIM_I2C_MAX_DEV 16
|
||||
#define SIM_I2C_REGS 256
|
||||
#define SIM_I2C_LOG_BYTES 8
|
||||
|
||||
/* 假器件的窃听事件。通用寄存器表模拟不了「配成输出就对地短路」这种事,
|
||||
所以按 datasheet 建模的假器件(sim_pca9555)挂一个钩子进来。 */
|
||||
typedef enum {
|
||||
SIM_I2C_EV_POST_WRITE = 0, /* 一次写事务刚落进寄存器表 */
|
||||
SIM_I2C_EV_PRE_READ, /* 一次读事务即将取寄存器表 —— 在这里合成输入 */
|
||||
} sim_i2c_ev_t;
|
||||
|
||||
struct sim_i2c_dev;
|
||||
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 都是 */
|
||||
bool present; /* false = 器件失联(A15 降级矩阵用),事务一律 NAK */
|
||||
int nak_left; /* >0:接下来 n 次事务 NAK 后自动恢复;<0:一直 NAK;0:正常 */
|
||||
uint16_t regs[SIM_I2C_REGS];
|
||||
uint8_t ptr; /* 器件内部的寄存器指针,跨事务保持(真器件就是这个行为) */
|
||||
uint32_t n_write; /* 事务计数,可用来断言「驱动真的访问过它 / 没多访问」 */
|
||||
uint32_t n_read;
|
||||
uint8_t last_write[SIM_I2C_LOG_BYTES]; /* 最后一次写进去的原始字节 */
|
||||
uint8_t last_write_len;
|
||||
sim_i2c_hook_t hook; /* NULL = 就是一张寄存器表 */
|
||||
} sim_i2c_dev_t;
|
||||
|
||||
/* 往假总线上挂一个器件。地址重复或超出 16 个会 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 */
|
||||
|
||||
/* 预置 / 回读寄存器。地址没挂过会 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);
|
||||
|
||||
/* 故障注入。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);
|
||||
|
||||
/* ---------------- GPIO ---------------- */
|
||||
|
||||
/* ESP32-S3 的 GPIO 编号 **0~48**,所以数组要 49 个 —— `EFUSE_EN2` 就在 IO48 上。
|
||||
A7 之前这里是 48,于是 CH2 的 eFuse 既开不了也**关不掉**(`hal_gpio_config()`
|
||||
直接 HAL_ERR_ARG)。边界写成「个数」而不是「上限」,就是这么错的。 */
|
||||
#define SIM_GPIO_PINS 49
|
||||
|
||||
/* 外部世界把某个**输入**脚拉成 level。对输出脚调用会 abort(那是总线打架)。 */
|
||||
void sim_gpio_drive(int pin, int level);
|
||||
|
||||
/* 该脚当前电平:输出脚 = 固件驱动的值,输入脚 = 外部驱动的值。没配过返回 -1。 */
|
||||
int sim_gpio_level(int pin);
|
||||
|
||||
/* 被 hal_gpio_write() 写成功过几次。断言「改 CH1 没动 CH2~5」用得上(A5)。 */
|
||||
uint32_t sim_gpio_writes(int pin);
|
||||
|
||||
/* 全局递增的写序号:每成功写一个脚 +1,本函数返回**该脚最后一次**被写时的序号
|
||||
(从没被写过返回 0)。用来断言顺序 —— 「先断 VBUS 再关 eFuse」(A7)、
|
||||
「17 ms 上电时序」(A9)这类事,光看每个脚的终值和次数是看不出来的。 */
|
||||
uint32_t sim_gpio_write_seq(int pin);
|
||||
|
||||
/* ---------------- 74HC595 ×3 的 24 位链 ---------------- */
|
||||
|
||||
/* 按 datasheet 建模的假移位链,**只看线上真实发生的边**(SRCLK / RCLK 上升沿)。
|
||||
对着驱动自己的影子变量断言等于自己证明自己 —— 字节顺序发反、每帧多打一个时钟、
|
||||
RCLK 打早了,影子变量一律看不出来。位号与 sr595.h 一致:0 = U21.QA,23 = U33.QH。
|
||||
|
||||
attach 之后移位寄存器与输出锁存器被塞成 0xA5A5A5:真板子上电时它们的内容
|
||||
**不确定**(`MR#` 接死在 +3V3,没有复位线),驱动必须先锁存安全值再使能 OE#。 */
|
||||
void sim_sr595_attach(int ser_pin, int srclk_pin, int rclk_pin, int oe_pin);
|
||||
|
||||
uint32_t sim_sr595_outputs(void); /* 输出锁存器(不管 OE# 是高是低) */
|
||||
uint32_t sim_sr595_shift(void); /* 移位寄存器当前内容(还没锁存的那一份) */
|
||||
uint32_t sim_sr595_clocks(void); /* SRCLK 上升沿总数:一帧应当正好 +24 */
|
||||
uint32_t sim_sr595_latches(void); /* RCLK 上升沿总数:一帧应当正好 +1 */
|
||||
int sim_sr595_oe(void); /* OE# 当前电平。初值 1 —— 板上 R609 拉着 */
|
||||
|
||||
/* OE# 第一次被拉低那一刻的现场。断言「输出使能之前已经锁存过安全值」用:
|
||||
驱动要是把顺序换了,enable_latches 会是 0、enable_outputs 会是那坨 0xA5A5A5。 */
|
||||
bool sim_sr595_enabled(void);
|
||||
uint32_t sim_sr595_enable_latches(void);
|
||||
uint32_t sim_sr595_enable_outputs(void);
|
||||
|
||||
/* 某一位在栅极上的实际电平;OE# 为高时返回 -1(高阻,不是 0)。 */
|
||||
int sim_sr595_pin(int bit);
|
||||
|
||||
/* ---------------- PCA9555(按 datasheet 建模的假器件) ---------------- */
|
||||
|
||||
/* 通用寄存器表证明不了 A6 的核心判据。这个模型只多做三件真器件会做的事:
|
||||
① **Output Port 复位值 0xFF** —— 所以把一个脚配成输出,它下一刻就输出高;
|
||||
② 输入寄存器按位合成:配成输入的位读**外部电平**,配成输出的位读**自己的输出**;
|
||||
③ 四个备用脚 P0_7 / P1_5~7 **焊死在地上** —— 读永远是 0,被驱动成高就是短路。
|
||||
③ 是这块板子上「配错方向」唯一会留下痕迹的地方:真板子上它既不报错也不 NAK。 */
|
||||
void sim_pca9555_attach(uint8_t addr);
|
||||
|
||||
/* 外部世界把某个端口的 8 根线驱动成 level(接地脚不受影响,恒 0)。
|
||||
FLT#/PGOOD 都是开漏上拉,所以「什么都没发生」的静息值是 0xFF。 */
|
||||
void sim_pca9555_drive(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);
|
||||
|
||||
/* 接地脚被配成输出过几次(不管输出高低,配了就算 —— 那已经是在对地灌电流)。 */
|
||||
uint32_t sim_pca9555_gnd_driven(void);
|
||||
/* 其中真的输出了高电平、也就是**直接对地短路**过几次。 */
|
||||
uint32_t sim_pca9555_shorted(void);
|
||||
|
||||
/* INT# 现在是不是被拉低(低有效)。输入变化拉低,**读走输入端口才释放**。 */
|
||||
bool sim_pca9555_int_asserted(void);
|
||||
uint32_t sim_pca9555_int_asserts(void); /* 累计拉低次数 */
|
||||
|
||||
/* ---------------- PWM ---------------- */
|
||||
|
||||
int sim_pwm_pin(int ch); /* 未配置返回 -1 */
|
||||
uint32_t sim_pwm_freq(int ch); /* 未配置返回 0 */
|
||||
int sim_pwm_permille(int ch); /* 未配置返回 -1 */
|
||||
|
||||
/* ---------------- 时钟 ---------------- */
|
||||
|
||||
void sim_time_set_us(uint64_t us);
|
||||
void sim_time_advance_us(uint64_t us);
|
||||
void sim_time_advance_ms(uint32_t ms);
|
||||
uint32_t sim_time_delay_calls(void); /* hal_time_delay_ms() 被调了几次 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SIM_HAL_H */
|
||||
/* 假板子的控制面 —— 只有宿主机测试与 sim/ 自己会包含它,应用代码一律不许包含。
|
||||
(包含了就编不过:ESP 目标根本不编译 sim/。)
|
||||
|
||||
⚠️ 模拟器是用来证明**代码逻辑**对的,不是用来证明**设计**对的。
|
||||
电压、余量、时序裕量的结论全部来自设计文档的计算与首板实测。见计划 §4.2。 */
|
||||
#ifndef SIM_HAL_H
|
||||
#define SIM_HAL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_gpio.h"
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_i2c_acct.h"
|
||||
#include "hal_lock.h"
|
||||
#include "hal_pwm.h"
|
||||
#include "hal_time.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 四路 sim 全部恢复出厂:器件表清空、引脚未配置、PWM 未配置、时钟归零、
|
||||
init 标志清掉。每个测试的 setUp() 里调它。 */
|
||||
void sim_hal_reset(void);
|
||||
|
||||
/* ---------------- I²C ---------------- */
|
||||
|
||||
#define SIM_I2C_MAX_DEV 16
|
||||
#define SIM_I2C_REGS 256
|
||||
#define SIM_I2C_LOG_BYTES 8
|
||||
|
||||
/* 假器件的窃听事件。通用寄存器表模拟不了「配成输出就对地短路」这种事,
|
||||
所以按 datasheet 建模的假器件(sim_pca9555)挂一个钩子进来。 */
|
||||
typedef enum {
|
||||
SIM_I2C_EV_POST_WRITE = 0, /* 一次写事务刚落进寄存器表 */
|
||||
SIM_I2C_EV_PRE_READ, /* 一次读事务即将取寄存器表 —— 在这里合成输入 */
|
||||
} sim_i2c_ev_t;
|
||||
|
||||
struct sim_i2c_dev;
|
||||
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 都是 */
|
||||
bool present; /* false = 器件失联(A15 降级矩阵用),事务一律 NAK */
|
||||
int nak_left; /* >0:接下来 n 次事务 NAK 后自动恢复;<0:一直 NAK;0:正常 */
|
||||
uint16_t regs[SIM_I2C_REGS];
|
||||
uint8_t ptr; /* 器件内部的寄存器指针,跨事务保持(真器件就是这个行为) */
|
||||
uint32_t n_write; /* 事务计数,可用来断言「驱动真的访问过它 / 没多访问」 */
|
||||
uint32_t n_read;
|
||||
uint8_t last_write[SIM_I2C_LOG_BYTES]; /* 最后一次写进去的原始字节 */
|
||||
uint8_t last_write_len;
|
||||
sim_i2c_hook_t hook; /* NULL = 就是一张寄存器表 */
|
||||
} sim_i2c_dev_t;
|
||||
|
||||
/* 往假总线上挂一个器件。地址重复或超出 16 个会 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 */
|
||||
|
||||
/* 预置 / 回读寄存器。地址没挂过会 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);
|
||||
|
||||
/* 故障注入。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);
|
||||
|
||||
/* **假总线上的事务是要花时间的**(默认开):每次事务把虚拟时钟往前拨
|
||||
`hal_i2c_txn_bits() / 频率` 那么多微秒。没有这一条,t_safety 的一个周期在 sim 里
|
||||
耗时为零,「20 ms 周期」的测试就只是在测「delay 传了 20」——
|
||||
而真正会出事的是「巡检自己就占掉了 1.5 ms、周期还准不准」。
|
||||
关掉它只有一个正当理由:某个测试要拿虚拟时钟当纯计数器用。 */
|
||||
void sim_i2c_set_bus_time(bool on);
|
||||
|
||||
/* ---------------- GPIO ---------------- */
|
||||
|
||||
/* ESP32-S3 的 GPIO 编号 **0~48**,所以数组要 49 个 —— `EFUSE_EN2` 就在 IO48 上。
|
||||
A7 之前这里是 48,于是 CH2 的 eFuse 既开不了也**关不掉**(`hal_gpio_config()`
|
||||
直接 HAL_ERR_ARG)。边界写成「个数」而不是「上限」,就是这么错的。 */
|
||||
#define SIM_GPIO_PINS 49
|
||||
|
||||
/* 外部世界把某个**输入**脚拉成 level。对输出脚调用会 abort(那是总线打架)。 */
|
||||
void sim_gpio_drive(int pin, int level);
|
||||
|
||||
/* 该脚当前电平:输出脚 = 固件驱动的值,输入脚 = 外部驱动的值。没配过返回 -1。 */
|
||||
int sim_gpio_level(int pin);
|
||||
|
||||
/* 被 hal_gpio_write() 写成功过几次。断言「改 CH1 没动 CH2~5」用得上(A5)。 */
|
||||
uint32_t sim_gpio_writes(int pin);
|
||||
|
||||
/* 全局递增的写序号:每成功写一个脚 +1,本函数返回**该脚最后一次**被写时的序号
|
||||
(从没被写过返回 0)。用来断言顺序 —— 「先断 VBUS 再关 eFuse」(A7)、
|
||||
「17 ms 上电时序」(A9)这类事,光看每个脚的终值和次数是看不出来的。 */
|
||||
uint32_t sim_gpio_write_seq(int pin);
|
||||
|
||||
/* ---------------- 74HC595 ×3 的 24 位链 ---------------- */
|
||||
|
||||
/* 按 datasheet 建模的假移位链,**只看线上真实发生的边**(SRCLK / RCLK 上升沿)。
|
||||
对着驱动自己的影子变量断言等于自己证明自己 —— 字节顺序发反、每帧多打一个时钟、
|
||||
RCLK 打早了,影子变量一律看不出来。位号与 sr595.h 一致:0 = U21.QA,23 = U33.QH。
|
||||
|
||||
attach 之后移位寄存器与输出锁存器被塞成 0xA5A5A5:真板子上电时它们的内容
|
||||
**不确定**(`MR#` 接死在 +3V3,没有复位线),驱动必须先锁存安全值再使能 OE#。 */
|
||||
void sim_sr595_attach(int ser_pin, int srclk_pin, int rclk_pin, int oe_pin);
|
||||
|
||||
uint32_t sim_sr595_outputs(void); /* 输出锁存器(不管 OE# 是高是低) */
|
||||
uint32_t sim_sr595_shift(void); /* 移位寄存器当前内容(还没锁存的那一份) */
|
||||
uint32_t sim_sr595_clocks(void); /* SRCLK 上升沿总数:一帧应当正好 +24 */
|
||||
uint32_t sim_sr595_latches(void); /* RCLK 上升沿总数:一帧应当正好 +1 */
|
||||
int sim_sr595_oe(void); /* OE# 当前电平。初值 1 —— 板上 R609 拉着 */
|
||||
|
||||
/* OE# 第一次被拉低那一刻的现场。断言「输出使能之前已经锁存过安全值」用:
|
||||
驱动要是把顺序换了,enable_latches 会是 0、enable_outputs 会是那坨 0xA5A5A5。 */
|
||||
bool sim_sr595_enabled(void);
|
||||
uint32_t sim_sr595_enable_latches(void);
|
||||
uint32_t sim_sr595_enable_outputs(void);
|
||||
|
||||
/* 某一位在栅极上的实际电平;OE# 为高时返回 -1(高阻,不是 0)。 */
|
||||
int sim_sr595_pin(int bit);
|
||||
|
||||
/* ---------------- PCA9555(按 datasheet 建模的假器件) ---------------- */
|
||||
|
||||
/* 通用寄存器表证明不了 A6 的核心判据。这个模型只多做三件真器件会做的事:
|
||||
① **Output Port 复位值 0xFF** —— 所以把一个脚配成输出,它下一刻就输出高;
|
||||
② 输入寄存器按位合成:配成输入的位读**外部电平**,配成输出的位读**自己的输出**;
|
||||
③ 四个备用脚 P0_7 / P1_5~7 **焊死在地上** —— 读永远是 0,被驱动成高就是短路。
|
||||
③ 是这块板子上「配错方向」唯一会留下痕迹的地方:真板子上它既不报错也不 NAK。 */
|
||||
void sim_pca9555_attach(uint8_t addr);
|
||||
|
||||
/* 外部世界把某个端口的 8 根线驱动成 level(接地脚不受影响,恒 0)。
|
||||
FLT#/PGOOD 都是开漏上拉,所以「什么都没发生」的静息值是 0xFF。 */
|
||||
void sim_pca9555_drive(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);
|
||||
|
||||
/* 接地脚被配成输出过几次(不管输出高低,配了就算 —— 那已经是在对地灌电流)。 */
|
||||
uint32_t sim_pca9555_gnd_driven(void);
|
||||
/* 其中真的输出了高电平、也就是**直接对地短路**过几次。 */
|
||||
uint32_t sim_pca9555_shorted(void);
|
||||
|
||||
/* INT# 现在是不是被拉低(低有效)。输入变化拉低,**读走输入端口才释放**。 */
|
||||
bool sim_pca9555_int_asserted(void);
|
||||
uint32_t sim_pca9555_int_asserts(void); /* 累计拉低次数 */
|
||||
|
||||
/* ---------------- PWM ---------------- */
|
||||
|
||||
int sim_pwm_pin(int ch); /* 未配置返回 -1 */
|
||||
uint32_t sim_pwm_freq(int ch); /* 未配置返回 0 */
|
||||
int sim_pwm_permille(int ch); /* 未配置返回 -1 */
|
||||
|
||||
/* ---------------- 两把互斥锁 ---------------- */
|
||||
|
||||
/* 扮演第几号任务(默认 0)。单线程 sim 里「另一个任务持着锁」就靠它模拟。 */
|
||||
void sim_lock_set_task(int task);
|
||||
int sim_lock_task(void);
|
||||
|
||||
/* 现在被几号任务持着;没人持着返回 -1。 */
|
||||
int sim_lock_owner(hal_lock_id_t id);
|
||||
uint32_t sim_lock_takes(hal_lock_id_t id);
|
||||
|
||||
/* **强制加锁纪律**:打开之后,动这个共享资源(I²C 的任何事务 / 595 那四根线)
|
||||
却没持着对应的锁,就记一次违规。默认关 —— A3~A6 的驱动测试是直接调驱动的,
|
||||
它们不该被要求先拿锁。
|
||||
|
||||
⚠️ 违规只计数、不 abort:这条检查的用处是**证明上层的加锁路径是通的**
|
||||
(t_safety 跑一圈违规为 0),以及证明这条检查本身不是句空话
|
||||
(不拿锁直接读一片 INA226,违规必须 +1)。 */
|
||||
void sim_lock_require(hal_lock_id_t id, bool on);
|
||||
uint32_t sim_lock_violations(hal_lock_id_t id);
|
||||
void sim_lock_check(hal_lock_id_t id); /* 给 sim 各路自己调,测试一般用不到 */
|
||||
|
||||
/* ---------------- 时钟 ---------------- */
|
||||
|
||||
void sim_time_set_us(uint64_t us);
|
||||
void sim_time_advance_us(uint64_t us);
|
||||
void sim_time_advance_ms(uint32_t ms);
|
||||
uint32_t sim_time_delay_calls(void); /* hal_time_delay_ms() 被调了几次 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SIM_HAL_H */
|
||||
|
||||
@@ -1,98 +1,105 @@
|
||||
/* 假的 74HC595 ×3 移位链 —— 挂在 GPIO sim 上,只看**线上真实发生的边**。
|
||||
|
||||
为什么要这么一个东西:A5 的两条验收断言(改一路不动别路、字节顺序 U33→U22→U21)
|
||||
如果只对着驱动自己的影子变量断言,那是**自己证明自己** —— 字节顺序发反了、
|
||||
每帧多打一个时钟、RCLK 在数据发完之前就打了,影子变量一律看不出来。
|
||||
所以这里按 datasheet 建模:SRCLK 上升沿移一格,RCLK 上升沿把移位寄存器抄进输出锁存器。
|
||||
|
||||
24 级的编号与 sr595.h 一致:0 = U21.QA(DS 进来的第一级),23 = U33.QH(链尾)。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
typedef struct {
|
||||
bool attached;
|
||||
int ser_pin, srclk_pin, rclk_pin, oe_pin;
|
||||
int ser, srclk, rclk, oe; /* 当前电平;oe 初值 1 = 板上 R609 拉着 */
|
||||
uint32_t shift; /* 移位寄存器(未锁存) */
|
||||
uint32_t out; /* 输出锁存器 */
|
||||
uint32_t clocks; /* SRCLK 上升沿总数 */
|
||||
uint32_t latches; /* RCLK 上升沿总数 */
|
||||
bool enabled_once;
|
||||
uint32_t enable_latches; /* OE# 第一次被拉低时,已经锁存过几次 */
|
||||
uint32_t enable_out; /* OE# 第一次被拉低那一刻,输出锁存器里是什么 */
|
||||
} chain_t;
|
||||
|
||||
static chain_t s_ch;
|
||||
|
||||
void sim_sr595_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
void sim_sr595_gpio_hook(int pin, int level);
|
||||
|
||||
void sim_sr595_reset_internal(void) { memset(&s_ch, 0, sizeof(s_ch)); }
|
||||
|
||||
void sim_sr595_attach(int ser_pin, int srclk_pin, int rclk_pin, int oe_pin)
|
||||
{
|
||||
memset(&s_ch, 0, sizeof(s_ch));
|
||||
s_ch.attached = true;
|
||||
s_ch.ser_pin = ser_pin;
|
||||
s_ch.srclk_pin = srclk_pin;
|
||||
s_ch.rclk_pin = rclk_pin;
|
||||
s_ch.oe_pin = oe_pin;
|
||||
/* `OE#` 上电就被 R609 拉高 —— 高阻,不是 0。 */
|
||||
s_ch.oe = 1;
|
||||
/* 上电后移位寄存器与输出锁存器的内容**不确定**(`MR#` 接死在 +3V3,没有复位线)。
|
||||
这里塞一个显眼的非零值:驱动要是先使能 OE# 再锁存安全值,
|
||||
`sim_sr595_enable_outputs()` 就会把这坨脏东西原样报出来。 */
|
||||
s_ch.shift = 0x00A5A5A5u & 0x00FFFFFFu;
|
||||
s_ch.out = s_ch.shift;
|
||||
}
|
||||
|
||||
void sim_sr595_gpio_hook(int pin, int level)
|
||||
{
|
||||
if (!s_ch.attached) {
|
||||
return;
|
||||
}
|
||||
level = level ? 1 : 0;
|
||||
|
||||
if (pin == s_ch.ser_pin) {
|
||||
s_ch.ser = level;
|
||||
} else if (pin == s_ch.srclk_pin) {
|
||||
if (level == 1 && s_ch.srclk == 0) { /* 上升沿:整条链往编号大的方向挪一格 */
|
||||
s_ch.shift = ((s_ch.shift << 1) | (uint32_t)s_ch.ser) & 0x00FFFFFFu;
|
||||
s_ch.clocks++;
|
||||
}
|
||||
s_ch.srclk = level;
|
||||
} else if (pin == s_ch.rclk_pin) {
|
||||
if (level == 1 && s_ch.rclk == 0) {
|
||||
s_ch.out = s_ch.shift;
|
||||
s_ch.latches++;
|
||||
}
|
||||
s_ch.rclk = level;
|
||||
} else if (pin == s_ch.oe_pin) {
|
||||
if (level == 0 && s_ch.oe != 0 && !s_ch.enabled_once) {
|
||||
s_ch.enabled_once = true;
|
||||
s_ch.enable_latches = s_ch.latches;
|
||||
s_ch.enable_out = s_ch.out;
|
||||
}
|
||||
s_ch.oe = level;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t sim_sr595_outputs(void) { return s_ch.out; }
|
||||
uint32_t sim_sr595_shift(void) { return s_ch.shift; }
|
||||
uint32_t sim_sr595_clocks(void) { return s_ch.clocks; }
|
||||
uint32_t sim_sr595_latches(void) { return s_ch.latches; }
|
||||
int sim_sr595_oe(void) { return s_ch.oe; }
|
||||
bool sim_sr595_enabled(void) { return s_ch.enabled_once; }
|
||||
uint32_t sim_sr595_enable_latches(void) { return s_ch.enable_latches; }
|
||||
uint32_t sim_sr595_enable_outputs(void) { return s_ch.enable_out; }
|
||||
|
||||
int sim_sr595_pin(int bit)
|
||||
{
|
||||
assert(bit >= 0 && bit < 24);
|
||||
if (s_ch.oe) {
|
||||
return -1; /* 高阻。栅极侧靠自己的 100 kΩ 下拉,HUB_RST 侧靠 R716 上拉 */
|
||||
}
|
||||
return (int)((s_ch.out >> bit) & 1u);
|
||||
}
|
||||
/* 假的 74HC595 ×3 移位链 —— 挂在 GPIO sim 上,只看**线上真实发生的边**。
|
||||
|
||||
为什么要这么一个东西:A5 的两条验收断言(改一路不动别路、字节顺序 U33→U22→U21)
|
||||
如果只对着驱动自己的影子变量断言,那是**自己证明自己** —— 字节顺序发反了、
|
||||
每帧多打一个时钟、RCLK 在数据发完之前就打了,影子变量一律看不出来。
|
||||
所以这里按 datasheet 建模:SRCLK 上升沿移一格,RCLK 上升沿把移位寄存器抄进输出锁存器。
|
||||
|
||||
24 级的编号与 sr595.h 一致:0 = U21.QA(DS 进来的第一级),23 = U33.QH(链尾)。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
typedef struct {
|
||||
bool attached;
|
||||
int ser_pin, srclk_pin, rclk_pin, oe_pin;
|
||||
int ser, srclk, rclk, oe; /* 当前电平;oe 初值 1 = 板上 R609 拉着 */
|
||||
uint32_t shift; /* 移位寄存器(未锁存) */
|
||||
uint32_t out; /* 输出锁存器 */
|
||||
uint32_t clocks; /* SRCLK 上升沿总数 */
|
||||
uint32_t latches; /* RCLK 上升沿总数 */
|
||||
bool enabled_once;
|
||||
uint32_t enable_latches; /* OE# 第一次被拉低时,已经锁存过几次 */
|
||||
uint32_t enable_out; /* OE# 第一次被拉低那一刻,输出锁存器里是什么 */
|
||||
} chain_t;
|
||||
|
||||
static chain_t s_ch;
|
||||
|
||||
void sim_sr595_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
void sim_sr595_gpio_hook(int pin, int level);
|
||||
|
||||
void sim_sr595_reset_internal(void) { memset(&s_ch, 0, sizeof(s_ch)); }
|
||||
|
||||
void sim_sr595_attach(int ser_pin, int srclk_pin, int rclk_pin, int oe_pin)
|
||||
{
|
||||
memset(&s_ch, 0, sizeof(s_ch));
|
||||
s_ch.attached = true;
|
||||
s_ch.ser_pin = ser_pin;
|
||||
s_ch.srclk_pin = srclk_pin;
|
||||
s_ch.rclk_pin = rclk_pin;
|
||||
s_ch.oe_pin = oe_pin;
|
||||
/* `OE#` 上电就被 R609 拉高 —— 高阻,不是 0。 */
|
||||
s_ch.oe = 1;
|
||||
/* 上电后移位寄存器与输出锁存器的内容**不确定**(`MR#` 接死在 +3V3,没有复位线)。
|
||||
这里塞一个显眼的非零值:驱动要是先使能 OE# 再锁存安全值,
|
||||
`sim_sr595_enable_outputs()` 就会把这坨脏东西原样报出来。 */
|
||||
s_ch.shift = 0x00A5A5A5u & 0x00FFFFFFu;
|
||||
s_ch.out = s_ch.shift;
|
||||
}
|
||||
|
||||
void sim_sr595_gpio_hook(int pin, int level)
|
||||
{
|
||||
if (!s_ch.attached) {
|
||||
return;
|
||||
}
|
||||
level = level ? 1 : 0;
|
||||
|
||||
/* 动这条链的四根线之一 = 在用那个共享资源,此刻必须持着 595 那把锁
|
||||
(只在 sim_lock_require(HAL_LOCK_SR595, true) 之后才计数,见 hal_lock_sim.c)。 */
|
||||
if (pin == s_ch.ser_pin || pin == s_ch.srclk_pin || pin == s_ch.rclk_pin ||
|
||||
pin == s_ch.oe_pin) {
|
||||
sim_lock_check(HAL_LOCK_SR595);
|
||||
}
|
||||
|
||||
if (pin == s_ch.ser_pin) {
|
||||
s_ch.ser = level;
|
||||
} else if (pin == s_ch.srclk_pin) {
|
||||
if (level == 1 && s_ch.srclk == 0) { /* 上升沿:整条链往编号大的方向挪一格 */
|
||||
s_ch.shift = ((s_ch.shift << 1) | (uint32_t)s_ch.ser) & 0x00FFFFFFu;
|
||||
s_ch.clocks++;
|
||||
}
|
||||
s_ch.srclk = level;
|
||||
} else if (pin == s_ch.rclk_pin) {
|
||||
if (level == 1 && s_ch.rclk == 0) {
|
||||
s_ch.out = s_ch.shift;
|
||||
s_ch.latches++;
|
||||
}
|
||||
s_ch.rclk = level;
|
||||
} else if (pin == s_ch.oe_pin) {
|
||||
if (level == 0 && s_ch.oe != 0 && !s_ch.enabled_once) {
|
||||
s_ch.enabled_once = true;
|
||||
s_ch.enable_latches = s_ch.latches;
|
||||
s_ch.enable_out = s_ch.out;
|
||||
}
|
||||
s_ch.oe = level;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t sim_sr595_outputs(void) { return s_ch.out; }
|
||||
uint32_t sim_sr595_shift(void) { return s_ch.shift; }
|
||||
uint32_t sim_sr595_clocks(void) { return s_ch.clocks; }
|
||||
uint32_t sim_sr595_latches(void) { return s_ch.latches; }
|
||||
int sim_sr595_oe(void) { return s_ch.oe; }
|
||||
bool sim_sr595_enabled(void) { return s_ch.enabled_once; }
|
||||
uint32_t sim_sr595_enable_latches(void) { return s_ch.enable_latches; }
|
||||
uint32_t sim_sr595_enable_outputs(void) { return s_ch.enable_out; }
|
||||
|
||||
int sim_sr595_pin(int bit)
|
||||
{
|
||||
assert(bit >= 0 && bit < 24);
|
||||
if (s_ch.oe) {
|
||||
return -1; /* 高阻。栅极侧靠自己的 100 kΩ 下拉,HUB_RST 侧靠 R716 上拉 */
|
||||
}
|
||||
return (int)((s_ch.out >> bit) & 1u);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户