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
这个提交包含在:
+36
-33
@@ -1,50 +1,53 @@
|
|||||||
# 交接
|
# 交接
|
||||||
更新:2026-09-06 完成任务包 **A1**(工程骨架)
|
更新:2026-09-06 完成任务包 **A2**(HAL 接口 + sim 实现)
|
||||||
|
|
||||||
## 一句话现状
|
## 一句话现状
|
||||||
`firmware/` 骨架立起来了:同一份 `main/` 源码在**宿主机(cygwin gcc + CTest + Unity)**
|
四路 HAL(i2c / gpio / pwm / time)的接口定死了,两套实现都在:
|
||||||
和 **ESP-IDF v5.4 / esp32s3** 两个目标下都编得过,两条验收命令都是绿的。
|
宿主机链 `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` 实现。
|
**A3**:INA226 驱动 + 六片地址表(`0x40/41/44/45/42/43`,设计文档 §8.2)。
|
||||||
新增的 `.c` 要**同时**登记到 `main/CMakeLists.txt` 的 `SRCS` 和
|
驱动只准通过 `hal_i2c_*` 访问总线;测试用 `sim_i2c_add(addr, 2)` 挂假器件、
|
||||||
`host/CMakeLists.txt` 的 `PHONERAIL_CORE_SRCS`(两处,漏一处就是「宿主机绿了但上板编不过」)。
|
`sim_i2c_set_reg()` 预置值。**16 位寄存器是大端**,sim 已按大端实现。
|
||||||
|
|
||||||
## 本次新增/修改的文件
|
## 本次新增/修改的文件
|
||||||
- `CMakeLists.txt`、`sdkconfig.defaults` — IDF 工程根,目标 esp32s3 / 8 MB flash
|
- `components/phonerail_hal/include/hal_{err,i2c,gpio,pwm,time}.h` — 接口(唯一的对外契约)
|
||||||
- `main/phonerail.h`、`main/version.c` — 版本号与通道数,两个目标共享
|
- `components/phonerail_hal/hal_err.c` — 两个目标共用的错误码字符串
|
||||||
- `main/app_main.c` — ESP 专用入口(宿主机构建**不含**此文件)
|
- `components/phonerail_hal/esp/hal_{i2c,gpio,pwm,time}_esp.c` — ESP-IDF 实现
|
||||||
- `main/CMakeLists.txt` — IDF main 组件
|
- `components/phonerail_hal/sim/sim_hal.h` — **假板子的控制面**(应用代码不许包含)
|
||||||
- `host/CMakeLists.txt` — 宿主机根工程:`phonerail_core` + `unity` + `test/test_*.c` 自动成 ctest
|
- `components/phonerail_hal/sim/{sim_hal,hal_i2c_sim,hal_gpio_sim,hal_pwm_sim,hal_time_sim}.c`
|
||||||
- `host/test.sh` — 宿主机验收一条命令
|
- `components/phonerail_hal/CMakeLists.txt` — IDF 组件(只收 `esp/`,不收 `sim/`)
|
||||||
- `build_idf.ps1` — 目标验收一条命令(钉死 v5.4、关 ccache)
|
- `test/test_hal_i2c_sim.c`、`test/test_hal_sim.c` — 本包的测试
|
||||||
- `test/test_version.c` — A1 的假测试(A2 起有真逻辑后可删)
|
- `host/CMakeLists.txt` — 加 `PHONERAIL_HAL_SIM_SRCS` 与两个 include 目录
|
||||||
- `third_party/unity/` — 从 IDF v5.4 原样复制的 Unity(上板测试同一套框架)
|
- `main/CMakeLists.txt` — `REQUIRES phonerail_hal`;`main/app_main.c` 调一次 HAL 保证链接
|
||||||
- `../固件开发计划.md` §10 — 钉死了 IDF 版本与测试框架两个待定项
|
- `../固件开发计划.md` §4.1 — 记下组件重名那个坑
|
||||||
|
|
||||||
## 验收命令与结果
|
## 验收命令与结果
|
||||||
```
|
```
|
||||||
$ bash host/test.sh
|
$ 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
|
$ .\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.
|
Project build complete.
|
||||||
```
|
```
|
||||||
|
|
||||||
## 踩到的坑(只写会再犯的)
|
## 踩到的坑(只写会再犯的)
|
||||||
- **宿主机必须用 cygwin 那套 cmake/ctest/gcc**(`/c/cygwin64/bin/`)。
|
- **组件名不能叫 `hal`**:ESP-IDF 自己有 `components/hal`(`driver` 依赖它),
|
||||||
Windows 版 CMake(`C:\Program Files\CMake`)配 cygwin gcc 会认错平台。
|
工程里同名会把它盖掉。所以叫 `phonerail_hal`。头文件名 `hal_*.h` 不冲突。
|
||||||
- **cygwin cmake 只认 `/cygdrive/...`**:喂它 `D:/...` 会报
|
- **新增 HAL 实现要改两处 CMake**:`components/phonerail_hal/CMakeLists.txt`(esp 那份)
|
||||||
`Found relative path while evaluating include directories`。
|
和 `host/CMakeLists.txt` 的 `PHONERAIL_HAL_SIM_SRCS`(sim 那份)。漏一处 =
|
||||||
所以 `host/test.sh` 先 `cd` 再全用相对路径,不传绝对路径。
|
「宿主机绿了但上板编不过」,反之亦然。`main/` 里的 .c 仍是老规矩的另外两处。
|
||||||
同理 `-DCMAKE_C_COMPILER=/c/...` 会被 Git Bash 的路径转换搞坏,改用 `export CC=gcc`。
|
- **IDF 组件的 `SRCS` 是显式列举**,所以 `sim/` 放在组件目录里也不会被 IDF 编到。
|
||||||
- **`IDF_CCACHE_ENABLE=0`**:开着 ccache 时 IDF 自带的 `esp_lcd_panel_rgb.c`
|
|
||||||
编出过一次编译器 segfault(ICE),关掉就过。已写进 `build_idf.ps1`。
|
## HAL 的几条约定(写驱动前看一眼,省得回头改)
|
||||||
- **PowerShell 5.1 + `$ErrorActionPreference="Stop"` 会把 `export.ps1` 的正常 stderr
|
- 16 位 I²C 寄存器一律**大端**;写事务的第一个字节是寄存器指针,指针**跨事务保持**并自增。
|
||||||
当成终止错误**("Activating ESP-IDF 5.4" 就报红)。`build_idf.ps1` 已去掉它,改用 `exit $LASTEXITCODE`。
|
- 读写长度不是 `reg_width` 整数倍 → `HAL_ERR_ARG`(真器件上是未定义行为,不许悄悄凑合)。
|
||||||
- 机器上有四个 IDF(v5.3.1 / **v5.4** / v5.5 / v5.5.1 / v6.0-rc1)。
|
- 器件不在 / `present=false` / 注入 NAK → `HAL_ERR_NAK`;**没 `hal_i2c_init()` 就用 → `HAL_ERR_STATE`**。
|
||||||
PATH 上的是 `D:\Users\35138\esp\v5.4`,本工程就钉这个。
|
- **写一个没配成输出的 GPIO 返回 `HAL_ERR_ARG`**(板子上 `gpio_set_level()` 是静默无效的)。
|
||||||
- **git 仓库已建**(`D:\DCDC`,分支 `main`,初始提交 `7a85df9`)。
|
`HAL_GPIO_INPUT_PULLUP` 配完默认读回 **1** —— 五路 FLT/PG 是开漏 + 上拉,默认 0 会被当成一直有故障。
|
||||||
`.gitattributes` 默认 `* -text`(不重写行尾),只有 `*.sh` 强制 LF。
|
- PWM 占空比单位是**千分比 0~1000**,应用层不需要知道 LEDC 是几位。
|
||||||
`build/`、`host/build/`、`sdkconfig` 已被忽略,别硬 `git add -f` 进去。
|
- 时刻比较一律用 `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 里的与硬件无关部分。
|
# 与 IDF 目标共享的源码只有 ../main 里的与硬件无关部分。
|
||||||
# 新增一个既要上板又要被测的 .c,必须同时加进:
|
# 新增一个既要上板又要被测的 .c,必须同时加进:
|
||||||
# ../main/CMakeLists.txt 的 SRCS 和 下面的 PHONERAIL_CORE_SRCS
|
# ../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)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(phonerail_host C)
|
project(phonerail_host C)
|
||||||
|
|
||||||
@@ -19,8 +22,22 @@ set(PHONERAIL_CORE_SRCS
|
|||||||
${FW_ROOT}/main/version.c
|
${FW_ROOT}/main/version.c
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(phonerail_core STATIC ${PHONERAIL_CORE_SRCS})
|
# HAL:接口在 include/,宿主机链 sim/ 的实现(esp/ 那份只有 IDF 目标编)。
|
||||||
target_include_directories(phonerail_core PUBLIC ${FW_ROOT}/main)
|
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/,与上板测试同一套)
|
# ---- 测试框架 Unity(从 ESP-IDF v5.4 原样复制到 third_party/,与上板测试同一套)
|
||||||
add_library(unity STATIC ${FW_ROOT}/third_party/unity/unity.c)
|
add_library(unity STATIC ${FW_ROOT}/third_party/unity/unity.c)
|
||||||
|
|||||||
@@ -4,4 +4,5 @@
|
|||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "app_main.c"
|
SRCS "app_main.c"
|
||||||
"version.c"
|
"version.c"
|
||||||
INCLUDE_DIRS ".")
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES phonerail_hal)
|
||||||
|
|||||||
+4
-2
@@ -1,11 +1,13 @@
|
|||||||
/* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */
|
/* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include "hal_time.h"
|
||||||
#include "phonerail.h"
|
#include "phonerail.h"
|
||||||
|
|
||||||
static const char *TAG = "phonerail";
|
static const char *TAG = "phonerail";
|
||||||
|
|
||||||
void app_main(void)
|
void app_main(void)
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "PhoneRail %s boot, %d rails", phonerail_version(),
|
/* 引脚表与外设初始化留到 A7/A8;这里只确认 HAL 真的被链进来了。 */
|
||||||
PHONERAIL_RAIL_COUNT);
|
ESP_LOGI(TAG, "PhoneRail %s boot, %d rails, t=%u ms", phonerail_version(),
|
||||||
|
PHONERAIL_RAIL_COUNT, (unsigned)hal_time_now_ms());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#define PHONERAIL_H
|
#define PHONERAIL_H
|
||||||
|
|
||||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||||
#define PHONERAIL_FW_VERSION "0.1.0-a1"
|
#define PHONERAIL_FW_VERSION "0.1.0-a2"
|
||||||
|
|
||||||
/* 通道数。五路直供,见设计文档 §2。 */
|
/* 通道数。五路直供,见设计文档 §2。 */
|
||||||
#define PHONERAIL_RAIL_COUNT 5
|
#define PHONERAIL_RAIL_COUNT 5
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -133,7 +133,7 @@ firmware/
|
|||||||
│ ├── evlog.c 事件环形缓冲(§10.7)
|
│ ├── evlog.c 事件环形缓冲(§10.7)
|
||||||
│ └── thermal.c 风扇闭环(§10.3)
|
│ └── thermal.c 风扇闭环(§10.3)
|
||||||
├── components/
|
├── components/
|
||||||
│ ├── hal/ i2c / gpio / pwm / time 的**接口**
|
│ ├── phonerail_hal/ i2c / gpio / pwm / time 的**接口**(`include/`)
|
||||||
│ │ ├── esp/ 真实实现(ESP-IDF)
|
│ │ ├── esp/ 真实实现(ESP-IDF)
|
||||||
│ │ └── sim/ 模拟实现(宿主机)
|
│ │ └── sim/ 模拟实现(宿主机)
|
||||||
│ └── drv/ ina226 / dac63204 / pca9555 / hc595 / lm75
|
│ └── drv/ ina226 / dac63204 / pca9555 / hc595 / lm75
|
||||||
@@ -144,6 +144,11 @@ firmware/
|
|||||||
**HAL 只抽象四样东西:I²C 事务、GPIO 电平、PWM/定时、单调时钟。**
|
**HAL 只抽象四样东西:I²C 事务、GPIO 电平、PWM/定时、单调时钟。**
|
||||||
不要抽象得更多 —— 抽象层每多一个概念,后面每次会话都要多读一遍。
|
不要抽象得更多 —— 抽象层每多一个概念,后面每次会话都要多读一遍。
|
||||||
|
|
||||||
|
> ⚠️ **组件名不能叫 `hal`**(A2 实测):ESP-IDF 自己有一个 `components/hal`,
|
||||||
|
> `driver` 依赖它,同名会被工程里的这个盖掉。所以目录是 `phonerail_hal`。
|
||||||
|
> 头文件名 `hal_i2c.h` / `hal_gpio.h` / `hal_pwm.h` / `hal_time.h` 不冲突
|
||||||
|
> (IDF 那个的头都在 `hal/` 前缀下)。
|
||||||
|
|
||||||
### 4.2 决定二:先写模拟器(A2 + A3 之前)
|
### 4.2 决定二:先写模拟器(A2 + A3 之前)
|
||||||
|
|
||||||
没有模拟器,每个任务包的「验收」都只能写成「等板子到了再说」,
|
没有模拟器,每个任务包的「验收」都只能写成「等板子到了再说」,
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户