A2:HAL 接口(i2c/gpio/pwm/time)+ sim 与 ESP 两套实现

接口在 components/phonerail_hal/include/,宿主机链 sim/、ESP32-S3 链 esp/
(IDF v5.4 新版 i2c_master / gpio / LEDC / esp_timer)。

sim 提供 A3 起要用的假板子控制面:假 I²C 器件(寄存器表 + 大端 + 指针自增)、
可注入 NAK / 器件失联、GPIO 外部驱动与写计数、虚拟时钟。

组件名不叫 hal —— ESP-IDF 自己有 components/hal(driver 依赖它),同名会盖掉。

验收:bash host/test.sh → 3 条 ctest 全过;build_idf.ps1 → 编过且零警告。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
这个提交包含在:
zhou yang
2026-09-06 18:33:26 +08:00
共同撰写人 Claude Opus 5
父节点 1b5bd159ec
当前提交 cac6ce47ce
共修改 25 个文件,包含 1174 行新增和 40 行删除
+36 -33
查看文件
@@ -1,50 +1,53 @@
# 交接
更新:2026-09-06 完成任务包 **A1**(工程骨架)
更新:2026-09-06 完成任务包 **A2**(HAL 接口 + sim 实现)
## 一句话现状
`firmware/` 骨架立起来了:同一份 `main/` 源码在**宿主机(cygwin gcc + CTest + Unity)**
和 **ESP-IDF v5.4 / esp32s3** 两个目标下都编得过,两条验收命令都是绿的。
四路 HAL(i2c / gpio / pwm / time)的接口定死了,两套实现都在:
宿主机链 `sim/`(假 I²C 器件 + 虚拟时钟 + 故障注入),ESP32-S3 链 `esp/`(IDF v5.4 新版
`i2c_master` / gpio / LEDC / esp_timer)。3 条 ctest(16 个用例)全绿,`idf.py build` 零警告。
## 下一步
**A2**:HAL 接口(i2c / gpio / pwm / time)+ `sim` 实现。
新增的 `.c` 要**同时**登记到 `main/CMakeLists.txt` 的 `SRCS` 和
`host/CMakeLists.txt` 的 `PHONERAIL_CORE_SRCS`(两处,漏一处就是「宿主机绿了但上板编不过」)。
**A3**:INA226 驱动 + 六片地址表(`0x40/41/44/45/42/43`,设计文档 §8.2)。
驱动只准通过 `hal_i2c_*` 访问总线;测试用 `sim_i2c_add(addr, 2)` 挂假器件、
`sim_i2c_set_reg()` 预置值。**16 位寄存器是大端**,sim 已按大端实现。
## 本次新增/修改的文件
- `CMakeLists.txt`、`sdkconfig.defaults` — IDF 工程根,目标 esp32s3 / 8 MB flash
- `main/phonerail.h`、`main/version.c` — 版本号与通道数,两个目标共享
- `main/app_main.c` — ESP 专用入口(宿主机构建**不含**此文件)
- `main/CMakeLists.txt` — IDF main 组件
- `host/CMakeLists.txt` — 宿主机根工程:`phonerail_core` + `unity` + `test/test_*.c` 自动成 ctest
- `host/test.sh` — 宿主机验收一条命令
- `build_idf.ps1` — 目标验收一条命令(钉死 v5.4、关 ccache)
- `test/test_version.c` — A1 的假测试(A2 起有真逻辑后可删)
- `third_party/unity/` — 从 IDF v5.4 原样复制的 Unity(上板测试同一套框架)
- `../固件开发计划.md` §10 — 钉死了 IDF 版本与测试框架两个待定项
- `components/phonerail_hal/include/hal_{err,i2c,gpio,pwm,time}.h` — 接口(唯一的对外契约)
- `components/phonerail_hal/hal_err.c` — 两个目标共用的错误码字符串
- `components/phonerail_hal/esp/hal_{i2c,gpio,pwm,time}_esp.c` — ESP-IDF 实现
- `components/phonerail_hal/sim/sim_hal.h` — **假板子的控制面**(应用代码不许包含)
- `components/phonerail_hal/sim/{sim_hal,hal_i2c_sim,hal_gpio_sim,hal_pwm_sim,hal_time_sim}.c`
- `components/phonerail_hal/CMakeLists.txt` — IDF 组件(只收 `esp/`,不收 `sim/`)
- `test/test_hal_i2c_sim.c`、`test/test_hal_sim.c` — 本包的测试
- `host/CMakeLists.txt` — 加 `PHONERAIL_HAL_SIM_SRCS` 与两个 include 目录
- `main/CMakeLists.txt` — `REQUIRES phonerail_hal`;`main/app_main.c` 调一次 HAL 保证链接
- `../固件开发计划.md` §4.1 — 记下组件重名那个坑
## 验收命令与结果
```
$ bash host/test.sh
100% tests passed, 0 tests failed out of 1
100% tests passed, 0 tests failed out of 3 # test_hal_i2c_sim / test_hal_sim / test_version
$ .\build_idf.ps1 # PowerShell
phonerail.bin binary size 0x32520 bytes. ... 0xcdae0 bytes (80%) free.
phonerail.bin binary size 0x32550 bytes. ... 0xcdab0 bytes (80%) free.
Project build complete.
```
## 踩到的坑(只写会再犯的)
- **宿主机必须用 cygwin 那套 cmake/ctest/gcc**(`/c/cygwin64/bin/`)。
Windows 版 CMake(`C:\Program Files\CMake`)配 cygwin gcc 会认错平台。
- **cygwin cmake 只认 `/cygdrive/...`**:喂它 `D:/...` 会报
`Found relative path while evaluating include directories`。
所以 `host/test.sh` 先 `cd` 再全用相对路径,不传绝对路径。
同理 `-DCMAKE_C_COMPILER=/c/...` 会被 Git Bash 的路径转换搞坏,改用 `export CC=gcc`。
- **`IDF_CCACHE_ENABLE=0`**:开着 ccache 时 IDF 自带的 `esp_lcd_panel_rgb.c`
编出过一次编译器 segfault(ICE),关掉就过。已写进 `build_idf.ps1`。
- **PowerShell 5.1 + `$ErrorActionPreference="Stop"` 会把 `export.ps1` 的正常 stderr
当成终止错误**("Activating ESP-IDF 5.4" 就报红)。`build_idf.ps1` 已去掉它,改用 `exit $LASTEXITCODE`。
- 机器上有四个 IDF(v5.3.1 / **v5.4** / v5.5 / v5.5.1 / v6.0-rc1)。
PATH 上的是 `D:\Users\35138\esp\v5.4`,本工程就钉这个。
- **git 仓库已建**(`D:\DCDC`,分支 `main`,初始提交 `7a85df9`)。
`.gitattributes` 默认 `* -text`(不重写行尾),只有 `*.sh` 强制 LF。
`build/`、`host/build/`、`sdkconfig` 已被忽略,别硬 `git add -f` 进去。
- **组件名不能叫 `hal`**:ESP-IDF 自己有 `components/hal`(`driver` 依赖它),
工程里同名会把它盖掉。所以叫 `phonerail_hal`。头文件名 `hal_*.h` 不冲突。
- **新增 HAL 实现要改两处 CMake**:`components/phonerail_hal/CMakeLists.txt`(esp 那份)
和 `host/CMakeLists.txt` 的 `PHONERAIL_HAL_SIM_SRCS`(sim 那份)。漏一处 =
「宿主机绿了但上板编不过」,反之亦然。`main/` 里的 .c 仍是老规矩的另外两处。
- **IDF 组件的 `SRCS` 是显式列举**,所以 `sim/` 放在组件目录里也不会被 IDF 编到。
## HAL 的几条约定(写驱动前看一眼,省得回头改)
- 16 位 I²C 寄存器一律**大端**;写事务的第一个字节是寄存器指针,指针**跨事务保持**并自增。
- 读写长度不是 `reg_width` 整数倍 → `HAL_ERR_ARG`(真器件上是未定义行为,不许悄悄凑合)。
- 器件不在 / `present=false` / 注入 NAK → `HAL_ERR_NAK`;**没 `hal_i2c_init()` 就用 → `HAL_ERR_STATE`**。
- **写一个没配成输出的 GPIO 返回 `HAL_ERR_ARG`**(板子上 `gpio_set_level()` 是静默无效的)。
`HAL_GPIO_INPUT_PULLUP` 配完默认读回 **1** —— 五路 FLT/PG 是开漏 + 上拉,默认 0 会被当成一直有故障。
- PWM 占空比单位是**千分比 0~1000**,应用层不需要知道 LEDC 是几位。
- 时刻比较一律用 `hal_time_elapsed_ms()`,不要写 `now - then > x`(32 位毫秒 49.7 天回绕)。
- sim 的时钟是虚拟的:`hal_time_delay_ms()` 只把时间往前拨,测试零耗时。
每个测试 `setUp()` 里调 `sim_hal_reset()`。
@@ -0,0 +1,12 @@
# 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)
@@ -0,0 +1,47 @@
/* GPIO 的 ESP-IDF 实现。 */
#include <string.h>
#include "driver/gpio.h"
#include "hal_gpio.h"
#define PIN_MAX 48 /* ESP32-S3 的 GPIO 编号上限,与 sim 保持一致 */
/* 记住方向,好让「写一个输入脚」在板子上也返回 HAL_ERR_ARG ——
ESP-IDF 的 gpio_set_level() 对输入脚是静默无效的,那正是最难查的一类 bug。 */
static uint8_t s_mode[PIN_MAX]; /* 0 = 没配过;否则 mode+1 */
hal_err_t hal_gpio_config(int pin, hal_gpio_mode_t mode)
{
if (pin < 0 || pin >= PIN_MAX) {
return HAL_ERR_ARG;
}
gpio_config_t cfg = {
.pin_bit_mask = 1ULL << pin,
.mode = (mode == HAL_GPIO_OUTPUT) ? GPIO_MODE_OUTPUT : GPIO_MODE_INPUT,
.pull_up_en = (mode == HAL_GPIO_INPUT_PULLUP) ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
if (gpio_config(&cfg) != ESP_OK) {
return HAL_ERR_ARG;
}
s_mode[pin] = (uint8_t)(mode + 1);
return HAL_OK;
}
hal_err_t hal_gpio_write(int pin, int level)
{
if (pin < 0 || pin >= PIN_MAX || s_mode[pin] != (uint8_t)(HAL_GPIO_OUTPUT + 1)) {
return HAL_ERR_ARG;
}
return gpio_set_level(pin, level ? 1 : 0) == ESP_OK ? HAL_OK : HAL_ERR_ARG;
}
hal_err_t hal_gpio_read(int pin, int *level)
{
if (pin < 0 || pin >= PIN_MAX || s_mode[pin] == 0 || level == NULL) {
return HAL_ERR_ARG;
}
*level = gpio_get_level(pin);
return HAL_OK;
}
@@ -0,0 +1,114 @@
/* 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));
}
@@ -0,0 +1,66 @@
/* PWM 的 ESP-IDF 实现(LEDC 低速组)。本项目只有风扇一路。 */
#include "driver/ledc.h"
#include "hal_pwm.h"
#define PWM_SPEED_MODE LEDC_LOW_SPEED_MODE
#define PWM_RES_BITS LEDC_TIMER_10_BIT
#define PWM_DUTY_FULL 1023u /* 2^10 - 1 */
static bool s_ready[HAL_PWM_CH_MAX];
static uint16_t s_permille[HAL_PWM_CH_MAX];
hal_err_t hal_pwm_config(int ch, int pin, uint32_t freq_hz)
{
if (ch < 0 || ch >= HAL_PWM_CH_MAX || pin < 0 || freq_hz == 0) {
return HAL_ERR_ARG;
}
/* 一个通道一个定时器:风扇 25 kHz 与将来别的用途未必同频。 */
ledc_timer_config_t tc = {
.speed_mode = PWM_SPEED_MODE,
.duty_resolution = PWM_RES_BITS,
.timer_num = (ledc_timer_t)ch,
.freq_hz = freq_hz,
.clk_cfg = LEDC_AUTO_CLK,
};
if (ledc_timer_config(&tc) != ESP_OK) {
return HAL_ERR_ARG;
}
ledc_channel_config_t cc = {
.gpio_num = pin,
.speed_mode = PWM_SPEED_MODE,
.channel = (ledc_channel_t)ch,
.timer_sel = (ledc_timer_t)ch,
.duty = 0,
.hpoint = 0,
};
if (ledc_channel_config(&cc) != ESP_OK) {
return HAL_ERR_ARG;
}
s_ready[ch] = true;
s_permille[ch] = 0;
return HAL_OK;
}
hal_err_t hal_pwm_set_permille(int ch, uint16_t permille)
{
if (ch < 0 || ch >= HAL_PWM_CH_MAX || !s_ready[ch] || permille > 1000) {
return HAL_ERR_ARG;
}
/* +500 是四舍五入:1000 permille 必须正好是满占空比,别差一个码值。 */
uint32_t duty = ((uint32_t)permille * PWM_DUTY_FULL + 500u) / 1000u;
if (ledc_set_duty(PWM_SPEED_MODE, (ledc_channel_t)ch, duty) != ESP_OK ||
ledc_update_duty(PWM_SPEED_MODE, (ledc_channel_t)ch) != ESP_OK) {
return HAL_ERR_BUS;
}
s_permille[ch] = permille;
return HAL_OK;
}
hal_err_t hal_pwm_get_permille(int ch, uint16_t *permille)
{
if (ch < 0 || ch >= HAL_PWM_CH_MAX || !s_ready[ch] || permille == NULL) {
return HAL_ERR_ARG;
}
*permille = s_permille[ch];
return HAL_OK;
}
@@ -0,0 +1,25 @@
/* 时钟的 ESP-IDF 实现。 */
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "hal_time.h"
uint64_t hal_time_now_us(void) { return (uint64_t)esp_timer_get_time(); }
uint32_t hal_time_now_ms(void) { return (uint32_t)(hal_time_now_us() / 1000u); }
uint32_t hal_time_elapsed_ms(uint32_t since_ms)
{
return hal_time_now_ms() - since_ms; /* 无符号减法,49.7 天回绕自然正确 */
}
void hal_time_delay_ms(uint32_t ms)
{
if (ms == 0) {
return;
}
/* 至少睡一个 tick:pdMS_TO_TICKS(1) 在 100 Hz tick 下会算成 0,
那样就变成忙等而不是让出 CPU。 */
TickType_t ticks = pdMS_TO_TICKS(ms);
vTaskDelay(ticks != 0 ? ticks : 1);
}
@@ -0,0 +1,14 @@
#include "hal_err.h"
const char *hal_err_str(hal_err_t err)
{
switch (err) {
case HAL_OK: return "OK";
case HAL_ERR_ARG: return "ARG";
case HAL_ERR_NAK: return "NAK";
case HAL_ERR_TIMEOUT: return "TIMEOUT";
case HAL_ERR_BUS: return "BUS";
case HAL_ERR_STATE: return "STATE";
}
return "?";
}
@@ -0,0 +1,25 @@
/* HAL 的统一错误码。四路 HAL(i2c/gpio/pwm/time)共用这一套。 */
#ifndef HAL_ERR_H
#define HAL_ERR_H
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
HAL_OK = 0,
HAL_ERR_ARG, /* 参数不对:引脚越界、没配就用、长度不是寄存器宽度的整数倍 */
HAL_ERR_NAK, /* 器件不应答:地址错、器件失联、总线上根本没这个东西 */
HAL_ERR_TIMEOUT, /* 事务超时(时钟被拉死、从机时钟延展过长) */
HAL_ERR_BUS, /* 仲裁丢失等其它总线级故障 */
HAL_ERR_STATE, /* 没 init 就调 */
} hal_err_t;
/* 只用于日志与测试失败信息。返回的是静态字符串,不会是 NULL。 */
const char *hal_err_str(hal_err_t err);
#ifdef __cplusplus
}
#endif
#endif /* HAL_ERR_H */
@@ -0,0 +1,31 @@
/* 单根 GPIO 的方向与电平。74HC595 的三根线、五路 PG/FLT、风扇 TACH 都走这里。 */
#ifndef HAL_GPIO_H
#define HAL_GPIO_H
#include "hal_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
HAL_GPIO_INPUT = 0,
HAL_GPIO_INPUT_PULLUP,
HAL_GPIO_OUTPUT,
} hal_gpio_mode_t;
hal_err_t hal_gpio_config(int pin, hal_gpio_mode_t mode);
/* 写一个没配成输出的脚返回 HAL_ERR_ARG,不是悄悄成功 ——
「把输入脚当输出用」是 PCA9555 那条禁令(计划 A6)的 GPIO 版本,
在宿主机测试里就要响。 */
hal_err_t hal_gpio_write(int pin, int level);
/* level 用出参,是为了把「读到低电平」和「读失败」分开。 */
hal_err_t hal_gpio_read(int pin, int *level);
#ifdef __cplusplus
}
#endif
#endif /* HAL_GPIO_H */
@@ -0,0 +1,44 @@
/* I²C 主机事务。板上挂着 6 片 INA226 + 2 片 DAC63204 + PCA9555(+ 载板 LM75),
见设计文档 §8.2 的地址表。
刻意只抽象「事务」这一层:不暴露 start/stop/ack,也不暴露时钟延展 ——
驱动层需要的全部就是「往这个地址写这几个字节 / 从这个地址读这几个字节」。 */
#ifndef HAL_I2C_H
#define HAL_I2C_H
#include <stddef.h>
#include <stdint.h>
#include "hal_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
int sda_gpio;
int scl_gpio;
uint32_t freq_hz; /* 0 = 用默认 400 kHz */
} hal_i2c_cfg_t;
/* 幂等:重复调用直接返回 HAL_OK,不会重建总线。
sim 实现忽略 cfg 的内容,但仍然要求先 init —— 让「忘了 init」在宿主机上就暴露。 */
hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg);
/* 地址一律是 7 位(0x00~0x7F),不含读写位。 */
hal_err_t hal_i2c_write(uint8_t addr, const uint8_t *data, size_t len);
hal_err_t hal_i2c_read(uint8_t addr, uint8_t *data, size_t len);
/* 写后重复起始再读 —— INA226 / LM75 / PCA9555 读寄存器都是这个形状。
拆成 write + read 两次事务在多主机总线上会被插进来,所以单列一个接口。 */
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);
/* 只发地址看有没有 ACK。需求 §12.1 步 2「I²C 扫到 10 个器件」用它。 */
hal_err_t hal_i2c_probe(uint8_t addr);
#ifdef __cplusplus
}
#endif
#endif /* HAL_I2C_H */
@@ -0,0 +1,28 @@
/* PWM 输出。本项目只有风扇一路(计划 A16 的闭环),留 4 个通道够用。
占空比单位是**千分比**(0~1000),不是硬件码值 ——
应用层不该知道 LEDC 配了几位分辨率。 */
#ifndef HAL_PWM_H
#define HAL_PWM_H
#include <stdint.h>
#include "hal_err.h"
#ifdef __cplusplus
extern "C" {
#endif
#define HAL_PWM_CH_MAX 4
hal_err_t hal_pwm_config(int ch, int pin, uint32_t freq_hz);
hal_err_t hal_pwm_set_permille(int ch, uint16_t permille);
/* 回读的是**固件设过的值**,不是量出来的波形。风扇实际转不转看 TACH。 */
hal_err_t hal_pwm_get_permille(int ch, uint16_t *permille);
#ifdef __cplusplus
}
#endif
#endif /* HAL_PWM_H */
@@ -0,0 +1,28 @@
/* 单调时钟与延时。sim 下是一个**虚拟时钟**:不真的睡,只把时间往前拨,
所以状态机的时序测试(A9 的 17 ms 上电时序等)跑起来是零耗时的。 */
#ifndef HAL_TIME_H
#define HAL_TIME_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 单调递增,不受对时影响。上电起算。 */
uint64_t hal_time_now_us(void);
/* 32 位毫秒会在 49.7 天回绕 —— 比较两个时刻一律用 hal_time_elapsed_ms(),
不要写 `now - then > x`(农场是长稳运行的,回绕一定会发生)。 */
uint32_t hal_time_now_ms(void);
/* 从 since_ms 到现在过了多久,回绕安全。 */
uint32_t hal_time_elapsed_ms(uint32_t since_ms);
void hal_time_delay_ms(uint32_t ms);
#ifdef __cplusplus
}
#endif
#endif /* HAL_TIME_H */
@@ -0,0 +1,71 @@
/* 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;
} pin_t;
static pin_t s_pin[SIM_GPIO_PINS];
void sim_gpio_reset_internal(void); /* 只给 sim_hal_reset() 用 */
void sim_gpio_reset_internal(void) { memset(s_pin, 0, sizeof(s_pin)); }
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;
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++;
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; }
@@ -0,0 +1,168 @@
/* 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;
}
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++;
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);
}
@@ -0,0 +1,50 @@
/* PWM 的假实现:只记住配置与占空比。宿主机专用。 */
#include <string.h>
#include "sim_hal.h"
typedef struct {
bool configured;
int pin;
uint32_t freq_hz;
uint16_t permille;
} ch_t;
static ch_t s_ch[HAL_PWM_CH_MAX];
void sim_pwm_reset_internal(void); /* 只给 sim_hal_reset() 用 */
void sim_pwm_reset_internal(void) { memset(s_ch, 0, sizeof(s_ch)); }
static bool valid(int ch) { return ch >= 0 && ch < HAL_PWM_CH_MAX; }
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;
}
s_ch[ch] = (ch_t){ .configured = true, .pin = pin, .freq_hz = freq_hz, .permille = 0 };
return HAL_OK;
}
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;
}
s_ch[ch].permille = permille;
return HAL_OK;
}
hal_err_t hal_pwm_get_permille(int ch, uint16_t *permille)
{
if (!valid(ch) || !s_ch[ch].configured || permille == NULL) {
return HAL_ERR_ARG;
}
*permille = s_ch[ch].permille;
return HAL_OK;
}
int sim_pwm_pin(int ch) { return valid(ch) && s_ch[ch].configured ? s_ch[ch].pin : -1; }
uint32_t sim_pwm_freq(int ch) { return valid(ch) && s_ch[ch].configured ? s_ch[ch].freq_hz : 0; }
int sim_pwm_permille(int ch) { return valid(ch) && s_ch[ch].configured ? (int)s_ch[ch].permille : -1; }
@@ -0,0 +1,35 @@
/* 时钟的假实现:虚拟时钟,delay 只是把时间往前拨,不真的睡。
所以 A9 那种「17 ms 上电时序」的场景测试跑起来是零耗时的。 */
#include "sim_hal.h"
static uint64_t s_now_us;
static uint32_t s_delay_calls;
void sim_time_reset_internal(void); /* 只给 sim_hal_reset() 用 */
void sim_time_reset_internal(void)
{
s_now_us = 0;
s_delay_calls = 0;
}
uint64_t hal_time_now_us(void) { return s_now_us; }
uint32_t hal_time_now_ms(void) { return (uint32_t)(s_now_us / 1000u); }
uint32_t hal_time_elapsed_ms(uint32_t since_ms)
{
/* 无符号减法自然回绕,49.7 天那个坎不用特判。 */
return hal_time_now_ms() - since_ms;
}
void hal_time_delay_ms(uint32_t ms)
{
s_delay_calls++;
s_now_us += (uint64_t)ms * 1000u;
}
void sim_time_set_us(uint64_t us) { s_now_us = us; }
void sim_time_advance_us(uint64_t us) { s_now_us += us; }
void sim_time_advance_ms(uint32_t ms) { s_now_us += (uint64_t)ms * 1000u; }
uint32_t sim_time_delay_calls(void) { return s_delay_calls; }
@@ -0,0 +1,16 @@
/* sim 的总闸:一次把四路都恢复出厂。每个测试的 setUp() 调它。
各路的清理函数是内部符号,不进 sim_hal.h —— 测试没有理由单独重置某一路。 */
#include "sim_hal.h"
void sim_i2c_reset_internal(void);
void sim_gpio_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_pwm_reset_internal();
sim_time_reset_internal();
}
@@ -0,0 +1,86 @@
/* 假板子的控制面 —— 只有宿主机测试与 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
typedef struct {
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_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 ---------------- */
#define SIM_GPIO_PINS 48 /* ESP32-S3 的 GPIO 编号上限 */
/* 外部世界把某个**输入**脚拉成 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);
/* ---------------- 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 */
+19 -2
查看文件
@@ -5,6 +5,9 @@
# 与 IDF 目标共享的源码只有 ../main 里的与硬件无关部分。
# 新增一个既要上板又要被测的 .c,必须同时加进:
# ../main/CMakeLists.txt 的 SRCS 和 下面的 PHONERAIL_CORE_SRCS
# HAL 里新增一路实现则要同时加进:
# ../components/phonerail_hal/CMakeLists.txt 的 SRCS(esp 那份)
# 和下面的 PHONERAIL_HAL_SIM_SRCS(sim 那份)
cmake_minimum_required(VERSION 3.16)
project(phonerail_host C)
@@ -19,8 +22,22 @@ set(PHONERAIL_CORE_SRCS
${FW_ROOT}/main/version.c
)
add_library(phonerail_core STATIC ${PHONERAIL_CORE_SRCS})
target_include_directories(phonerail_core PUBLIC ${FW_ROOT}/main)
# HAL:接口在 include/,宿主机链 sim/ 的实现(esp/ 那份只有 IDF 目标编)。
set(PHONERAIL_HAL_DIR ${FW_ROOT}/components/phonerail_hal)
set(PHONERAIL_HAL_SIM_SRCS
${PHONERAIL_HAL_DIR}/hal_err.c
${PHONERAIL_HAL_DIR}/sim/sim_hal.c
${PHONERAIL_HAL_DIR}/sim/hal_i2c_sim.c
${PHONERAIL_HAL_DIR}/sim/hal_gpio_sim.c
${PHONERAIL_HAL_DIR}/sim/hal_pwm_sim.c
${PHONERAIL_HAL_DIR}/sim/hal_time_sim.c
)
add_library(phonerail_core STATIC ${PHONERAIL_CORE_SRCS} ${PHONERAIL_HAL_SIM_SRCS})
target_include_directories(phonerail_core PUBLIC
${FW_ROOT}/main
${PHONERAIL_HAL_DIR}/include
${PHONERAIL_HAL_DIR}/sim)
# ---- 测试框架 Unity(从 ESP-IDF v5.4 原样复制到 third_party/,与上板测试同一套)
add_library(unity STATIC ${FW_ROOT}/third_party/unity/unity.c)
+2 -1
查看文件
@@ -4,4 +4,5 @@
idf_component_register(
SRCS "app_main.c"
"version.c"
INCLUDE_DIRS ".")
INCLUDE_DIRS "."
REQUIRES phonerail_hal)
+4 -2
查看文件
@@ -1,11 +1,13 @@
/* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */
#include "esp_log.h"
#include "hal_time.h"
#include "phonerail.h"
static const char *TAG = "phonerail";
void app_main(void)
{
ESP_LOGI(TAG, "PhoneRail %s boot, %d rails", phonerail_version(),
PHONERAIL_RAIL_COUNT);
/* 引脚表与外设初始化留到 A7/A8;这里只确认 HAL 真的被链进来了。 */
ESP_LOGI(TAG, "PhoneRail %s boot, %d rails, t=%u ms", phonerail_version(),
PHONERAIL_RAIL_COUNT, (unsigned)hal_time_now_ms());
}
+1 -1
查看文件
@@ -3,7 +3,7 @@
#define PHONERAIL_H
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
#define PHONERAIL_FW_VERSION "0.1.0-a1"
#define PHONERAIL_FW_VERSION "0.1.0-a2"
/* 通道数。五路直供,见设计文档 §2。 */
#define PHONERAIL_RAIL_COUNT 5
+136
查看文件
@@ -0,0 +1,136 @@
/* A2 验收主项:能在假总线上挂一个器件并读回预置值。
顺带把「器件失联 / 偶发 NAK」这两种后面 A15 要用的故障注入也测掉。 */
#include <string.h>
#include "sim_hal.h"
#include "unity.h"
#define INA226_CH1 0x40 /* 设计文档 §8.2 的地址表;A3 才会真的写驱动 */
#define PCA9555 0x20
void setUp(void) { sim_hal_reset(); }
void tearDown(void) {}
static void init_bus(void)
{
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL));
}
static void test_use_before_init_is_state_error(void)
{
sim_i2c_add(INA226_CH1, 2);
uint8_t rd[2];
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, hal_i2c_read(INA226_CH1, rd, sizeof(rd)));
}
/* ⭐ A2 的验收判据本身 */
static void test_read_back_preset_register(void)
{
init_bus();
sim_i2c_add(INA226_CH1, 2);
sim_i2c_set_reg(INA226_CH1, 0x02, 0x1234); /* INA226 的 Bus Voltage 寄存器 */
const uint8_t ptr = 0x02;
uint8_t rd[2] = { 0, 0 };
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write_read(INA226_CH1, &ptr, 1, rd, sizeof(rd)));
TEST_ASSERT_EQUAL_HEX8(0x12, rd[0]); /* 大端 */
TEST_ASSERT_EQUAL_HEX8(0x34, rd[1]);
TEST_ASSERT_EQUAL_UINT32(1, sim_i2c_dev(INA226_CH1)->n_read);
}
static void test_write_stores_big_endian_and_logs_bytes(void)
{
init_bus();
sim_i2c_add(INA226_CH1, 2);
const uint8_t wr[3] = { 0x05, 0x12, 0x34 };
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(INA226_CH1, wr, sizeof(wr)));
TEST_ASSERT_EQUAL_HEX16(0x1234, sim_i2c_get_reg(INA226_CH1, 0x05));
const sim_i2c_dev_t *d = sim_i2c_dev(INA226_CH1);
TEST_ASSERT_EQUAL_UINT8(3, d->last_write_len);
TEST_ASSERT_EQUAL_HEX8_ARRAY(wr, d->last_write, 3);
}
/* 寄存器指针自增:一次事务连读两个寄存器,真器件就是这个行为。 */
static void test_pointer_auto_increments(void)
{
init_bus();
sim_i2c_add(INA226_CH1, 2);
sim_i2c_set_reg(INA226_CH1, 0x01, 0xAAAA);
sim_i2c_set_reg(INA226_CH1, 0x02, 0xBBBB);
const uint8_t ptr = 0x01;
uint8_t rd[4];
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write_read(INA226_CH1, &ptr, 1, rd, sizeof(rd)));
TEST_ASSERT_EQUAL_HEX8(0xAA, rd[0]);
TEST_ASSERT_EQUAL_HEX8(0xBB, rd[2]);
}
/* PCA9555 是 8 位寄存器;同一套 HAL 要能同时伺候两种宽度。 */
static void test_byte_wide_device(void)
{
init_bus();
sim_i2c_add(PCA9555, 1);
const uint8_t wr[3] = { 0x06, 0xFF, 0x0F }; /* 配置寄存器 Port0/Port1 */
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(PCA9555, wr, sizeof(wr)));
TEST_ASSERT_EQUAL_HEX16(0x00FF, sim_i2c_get_reg(PCA9555, 0x06));
TEST_ASSERT_EQUAL_HEX16(0x000F, sim_i2c_get_reg(PCA9555, 0x07));
}
static void test_absent_device_naks(void)
{
init_bus();
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, hal_i2c_probe(0x77)); /* 总线上没这个地址 */
sim_i2c_add(INA226_CH1, 2);
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(INA226_CH1));
sim_i2c_set_present(INA226_CH1, false); /* 器件失联(A15 降级矩阵) */
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, hal_i2c_probe(INA226_CH1));
}
static void test_transient_nak_then_recovers(void)
{
init_bus();
sim_i2c_add(INA226_CH1, 2);
sim_i2c_inject_nak(INA226_CH1, 2); /* 接下来两次事务失败 */
uint8_t rd[2];
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, hal_i2c_read(INA226_CH1, rd, sizeof(rd)));
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, hal_i2c_read(INA226_CH1, rd, sizeof(rd)));
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_read(INA226_CH1, rd, sizeof(rd)));
/* NAK 掉的事务不该被算成一次成功访问 */
TEST_ASSERT_EQUAL_UINT32(1, sim_i2c_dev(INA226_CH1)->n_read);
}
/* 半个寄存器的长度:真器件上是未定义行为,这里必须当场报错而不是悄悄凑合。 */
static void test_partial_register_length_rejected(void)
{
init_bus();
sim_i2c_add(INA226_CH1, 2);
uint8_t rd[1];
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_i2c_read(INA226_CH1, rd, 1));
const uint8_t wr[2] = { 0x05, 0x12 };
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_i2c_write(INA226_CH1, wr, sizeof(wr)));
}
static void test_address_out_of_range(void)
{
init_bus();
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_i2c_probe(0x80));
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_use_before_init_is_state_error);
RUN_TEST(test_read_back_preset_register);
RUN_TEST(test_write_stores_big_endian_and_logs_bytes);
RUN_TEST(test_pointer_auto_increments);
RUN_TEST(test_byte_wide_device);
RUN_TEST(test_absent_device_naks);
RUN_TEST(test_transient_nak_then_recovers);
RUN_TEST(test_partial_register_length_rejected);
RUN_TEST(test_address_out_of_range);
return UNITY_END();
}
+110
查看文件
@@ -0,0 +1,110 @@
/* GPIO / PWM / 时钟三路 sim 的行为约定。 */
#include "sim_hal.h"
#include "unity.h"
void setUp(void) { sim_hal_reset(); }
void tearDown(void) {}
/* ---- GPIO ---- */
static void test_output_write_and_readback(void)
{
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(5, HAL_GPIO_OUTPUT));
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(5)); /* 配完默认低 */
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_write(5, 1));
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(5));
int level = -1;
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_read(5, &level));
TEST_ASSERT_EQUAL_INT(1, level);
}
/* 「把输入脚当输出写」在板子上是静默无效的,最难查 —— sim 里必须报错。 */
static void test_write_to_input_pin_is_error(void)
{
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(6, HAL_GPIO_INPUT));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_gpio_write(6, 1));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_gpio_write(7, 1)); /* 压根没配过 */
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_gpio_config(SIM_GPIO_PINS, HAL_GPIO_OUTPUT));
}
/* 五路 FLT/PG 是开漏 + 上拉:没有故障时必须读回 1,不能默认 0。 */
static void test_pullup_input_idles_high(void)
{
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(8, HAL_GPIO_INPUT_PULLUP));
int level = -1;
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_read(8, &level));
TEST_ASSERT_EQUAL_INT(1, level);
sim_gpio_drive(8, 0); /* 器件报故障,把脚拉低 */
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_read(8, &level));
TEST_ASSERT_EQUAL_INT(0, level);
}
/* 写计数是给 A5「改 CH1 不动 CH2~5」那条断言准备的。 */
static void test_write_counter_is_per_pin(void)
{
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(10, HAL_GPIO_OUTPUT));
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_gpio_config(11, HAL_GPIO_OUTPUT));
hal_gpio_write(10, 1);
hal_gpio_write(10, 0);
TEST_ASSERT_EQUAL_UINT32(2, sim_gpio_writes(10));
TEST_ASSERT_EQUAL_UINT32(0, sim_gpio_writes(11));
}
/* ---- PWM ---- */
static void test_pwm_config_and_duty(void)
{
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_pwm_set_permille(0, 500)); /* 没配就用 */
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_pwm_config(0, 21, 25000)); /* 风扇 25 kHz */
TEST_ASSERT_EQUAL_INT(21, sim_pwm_pin(0));
TEST_ASSERT_EQUAL_UINT32(25000, sim_pwm_freq(0));
TEST_ASSERT_EQUAL_INT(0, sim_pwm_permille(0)); /* 配完默认停转 */
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_pwm_set_permille(0, 400));
uint16_t d = 0;
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_pwm_get_permille(0, &d));
TEST_ASSERT_EQUAL_UINT16(400, d);
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_pwm_set_permille(0, 1001)); /* 千分比上限 */
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_pwm_config(HAL_PWM_CH_MAX, 21, 25000));
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_pwm_config(1, 21, 0));
}
/* ---- 时钟 ---- */
static void test_virtual_clock_advances_only_on_demand(void)
{
TEST_ASSERT_EQUAL_UINT64(0, hal_time_now_us());
hal_time_delay_ms(17); /* A9 的上电时序就是这个量级 */
TEST_ASSERT_EQUAL_UINT32(17, hal_time_now_ms());
TEST_ASSERT_EQUAL_UINT32(1, sim_time_delay_calls());
sim_time_advance_us(500);
TEST_ASSERT_EQUAL_UINT64(17500, hal_time_now_us());
TEST_ASSERT_EQUAL_UINT32(17, hal_time_now_ms()); /* 不满 1 ms 不进位 */
}
/* 49.7 天回绕:农场是长稳运行的,这一条迟早会被踩到。 */
static void test_elapsed_survives_wraparound(void)
{
sim_time_set_us(0xFFFFFFF0ull * 1000ull);
uint32_t t0 = hal_time_now_ms();
sim_time_advance_ms(32); /* 跨过 32 位回绕点 */
TEST_ASSERT_TRUE(hal_time_now_ms() < t0);
TEST_ASSERT_EQUAL_UINT32(32, hal_time_elapsed_ms(t0));
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_output_write_and_readback);
RUN_TEST(test_write_to_input_pin_is_error);
RUN_TEST(test_pullup_input_idles_high);
RUN_TEST(test_write_counter_is_per_pin);
RUN_TEST(test_pwm_config_and_duty);
RUN_TEST(test_virtual_clock_advances_only_on_demand);
RUN_TEST(test_elapsed_survives_wraparound);
return UNITY_END();
}