diff --git a/firmware/HANDOFF.md b/firmware/HANDOFF.md index 80c00ef..631e94c 100644 --- a/firmware/HANDOFF.md +++ b/firmware/HANDOFF.md @@ -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()`。 diff --git a/firmware/components/phonerail_hal/CMakeLists.txt b/firmware/components/phonerail_hal/CMakeLists.txt new file mode 100644 index 0000000..bc5d92c --- /dev/null +++ b/firmware/components/phonerail_hal/CMakeLists.txt @@ -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) diff --git a/firmware/components/phonerail_hal/esp/hal_gpio_esp.c b/firmware/components/phonerail_hal/esp/hal_gpio_esp.c new file mode 100644 index 0000000..336a508 --- /dev/null +++ b/firmware/components/phonerail_hal/esp/hal_gpio_esp.c @@ -0,0 +1,47 @@ +/* GPIO 的 ESP-IDF 实现。 */ +#include + +#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; +} diff --git a/firmware/components/phonerail_hal/esp/hal_i2c_esp.c b/firmware/components/phonerail_hal/esp/hal_i2c_esp.c new file mode 100644 index 0000000..c76878f --- /dev/null +++ b/firmware/components/phonerail_hal/esp/hal_i2c_esp.c @@ -0,0 +1,114 @@ +/* I²C 的 ESP-IDF v5.4 实现(新版 i2c_master 驱动,不是已废弃的 driver/i2c.h)。 */ +#include + +#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)); +} diff --git a/firmware/components/phonerail_hal/esp/hal_pwm_esp.c b/firmware/components/phonerail_hal/esp/hal_pwm_esp.c new file mode 100644 index 0000000..f27ea6a --- /dev/null +++ b/firmware/components/phonerail_hal/esp/hal_pwm_esp.c @@ -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; +} diff --git a/firmware/components/phonerail_hal/esp/hal_time_esp.c b/firmware/components/phonerail_hal/esp/hal_time_esp.c new file mode 100644 index 0000000..e3ec3cd --- /dev/null +++ b/firmware/components/phonerail_hal/esp/hal_time_esp.c @@ -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); +} diff --git a/firmware/components/phonerail_hal/hal_err.c b/firmware/components/phonerail_hal/hal_err.c new file mode 100644 index 0000000..e68865f --- /dev/null +++ b/firmware/components/phonerail_hal/hal_err.c @@ -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 "?"; +} diff --git a/firmware/components/phonerail_hal/include/hal_err.h b/firmware/components/phonerail_hal/include/hal_err.h new file mode 100644 index 0000000..2e02acb --- /dev/null +++ b/firmware/components/phonerail_hal/include/hal_err.h @@ -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 */ diff --git a/firmware/components/phonerail_hal/include/hal_gpio.h b/firmware/components/phonerail_hal/include/hal_gpio.h new file mode 100644 index 0000000..ad09938 --- /dev/null +++ b/firmware/components/phonerail_hal/include/hal_gpio.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 */ diff --git a/firmware/components/phonerail_hal/include/hal_i2c.h b/firmware/components/phonerail_hal/include/hal_i2c.h new file mode 100644 index 0000000..06d105d --- /dev/null +++ b/firmware/components/phonerail_hal/include/hal_i2c.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 +#include + +#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 */ diff --git a/firmware/components/phonerail_hal/include/hal_pwm.h b/firmware/components/phonerail_hal/include/hal_pwm.h new file mode 100644 index 0000000..0d7ab6c --- /dev/null +++ b/firmware/components/phonerail_hal/include/hal_pwm.h @@ -0,0 +1,28 @@ +/* PWM 输出。本项目只有风扇一路(计划 A16 的闭环),留 4 个通道够用。 + + 占空比单位是**千分比**(0~1000),不是硬件码值 —— + 应用层不该知道 LEDC 配了几位分辨率。 */ +#ifndef HAL_PWM_H +#define HAL_PWM_H + +#include + +#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 */ diff --git a/firmware/components/phonerail_hal/include/hal_time.h b/firmware/components/phonerail_hal/include/hal_time.h new file mode 100644 index 0000000..5f41745 --- /dev/null +++ b/firmware/components/phonerail_hal/include/hal_time.h @@ -0,0 +1,28 @@ +/* 单调时钟与延时。sim 下是一个**虚拟时钟**:不真的睡,只把时间往前拨, + 所以状态机的时序测试(A9 的 17 ms 上电时序等)跑起来是零耗时的。 */ +#ifndef HAL_TIME_H +#define HAL_TIME_H + +#include + +#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 */ diff --git a/firmware/components/phonerail_hal/sim/hal_gpio_sim.c b/firmware/components/phonerail_hal/sim/hal_gpio_sim.c new file mode 100644 index 0000000..17404b9 --- /dev/null +++ b/firmware/components/phonerail_hal/sim/hal_gpio_sim.c @@ -0,0 +1,71 @@ +/* GPIO 的假实现。宿主机专用。 */ +#include +#include + +#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; } diff --git a/firmware/components/phonerail_hal/sim/hal_i2c_sim.c b/firmware/components/phonerail_hal/sim/hal_i2c_sim.c new file mode 100644 index 0000000..f0cb649 --- /dev/null +++ b/firmware/components/phonerail_hal/sim/hal_i2c_sim.c @@ -0,0 +1,168 @@ +/* I²C 的假实现:一张寄存器表 + 可注入的 NAK。宿主机专用。 */ +#include +#include + +#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); +} diff --git a/firmware/components/phonerail_hal/sim/hal_pwm_sim.c b/firmware/components/phonerail_hal/sim/hal_pwm_sim.c new file mode 100644 index 0000000..f597220 --- /dev/null +++ b/firmware/components/phonerail_hal/sim/hal_pwm_sim.c @@ -0,0 +1,50 @@ +/* PWM 的假实现:只记住配置与占空比。宿主机专用。 */ +#include + +#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; } diff --git a/firmware/components/phonerail_hal/sim/hal_time_sim.c b/firmware/components/phonerail_hal/sim/hal_time_sim.c new file mode 100644 index 0000000..23fe8bf --- /dev/null +++ b/firmware/components/phonerail_hal/sim/hal_time_sim.c @@ -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; } diff --git a/firmware/components/phonerail_hal/sim/sim_hal.c b/firmware/components/phonerail_hal/sim/sim_hal.c new file mode 100644 index 0000000..3a3347f --- /dev/null +++ b/firmware/components/phonerail_hal/sim/sim_hal.c @@ -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(); +} diff --git a/firmware/components/phonerail_hal/sim/sim_hal.h b/firmware/components/phonerail_hal/sim/sim_hal.h new file mode 100644 index 0000000..8560559 --- /dev/null +++ b/firmware/components/phonerail_hal/sim/sim_hal.h @@ -0,0 +1,86 @@ +/* 假板子的控制面 —— 只有宿主机测试与 sim/ 自己会包含它,应用代码一律不许包含。 + (包含了就编不过:ESP 目标根本不编译 sim/。) + + ⚠️ 模拟器是用来证明**代码逻辑**对的,不是用来证明**设计**对的。 + 电压、余量、时序裕量的结论全部来自设计文档的计算与首板实测。见计划 §4.2。 */ +#ifndef SIM_HAL_H +#define SIM_HAL_H + +#include +#include + +#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 */ diff --git a/firmware/host/CMakeLists.txt b/firmware/host/CMakeLists.txt index fc2a816..fe0f6fa 100644 --- a/firmware/host/CMakeLists.txt +++ b/firmware/host/CMakeLists.txt @@ -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) diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt index d9daf18..f61590c 100644 --- a/firmware/main/CMakeLists.txt +++ b/firmware/main/CMakeLists.txt @@ -4,4 +4,5 @@ idf_component_register( SRCS "app_main.c" "version.c" - INCLUDE_DIRS ".") + INCLUDE_DIRS "." + REQUIRES phonerail_hal) diff --git a/firmware/main/app_main.c b/firmware/main/app_main.c index ec7bed9..498581e 100644 --- a/firmware/main/app_main.c +++ b/firmware/main/app_main.c @@ -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()); } diff --git a/firmware/main/phonerail.h b/firmware/main/phonerail.h index 3e0bf36..a1e101b 100644 --- a/firmware/main/phonerail.h +++ b/firmware/main/phonerail.h @@ -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 diff --git a/firmware/test/test_hal_i2c_sim.c b/firmware/test/test_hal_i2c_sim.c new file mode 100644 index 0000000..e299ebf --- /dev/null +++ b/firmware/test/test_hal_i2c_sim.c @@ -0,0 +1,136 @@ +/* A2 验收主项:能在假总线上挂一个器件并读回预置值。 + 顺带把「器件失联 / 偶发 NAK」这两种后面 A15 要用的故障注入也测掉。 */ +#include + +#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(); +} diff --git a/firmware/test/test_hal_sim.c b/firmware/test/test_hal_sim.c new file mode 100644 index 0000000..d942e53 --- /dev/null +++ b/firmware/test/test_hal_sim.c @@ -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(); +} diff --git a/固件开发计划.md b/固件开发计划.md index 1fbcf73..939cdcd 100644 --- a/固件开发计划.md +++ b/固件开发计划.md @@ -133,7 +133,7 @@ firmware/ │ ├── evlog.c 事件环形缓冲(§10.7) │ └── thermal.c 风扇闭环(§10.3) ├── components/ -│ ├── hal/ i2c / gpio / pwm / time 的**接口** +│ ├── phonerail_hal/ i2c / gpio / pwm / time 的**接口**(`include/`) │ │ ├── esp/ 真实实现(ESP-IDF) │ │ └── sim/ 模拟实现(宿主机) │ └── drv/ ina226 / dac63204 / pca9555 / hc595 / lm75 @@ -144,6 +144,11 @@ firmware/ **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 之前) 没有模拟器,每个任务包的「验收」都只能写成「等板子到了再说」,