A3:INA226 驱动 + 六片地址表
地址表按设计文档 §8.2 钉死(0x40/41/44/45/42 + 输入侧 0x43),只走 ina226_rail_addr(),并有一条测试专门拦「ina_addr = 0x40 + ch」这一手 (CH3/CH4/CH5 三路全错,需求 §10.4)。 驱动只经 hal_i2c_*:认件(MFG/DIE ID)、写 Config 0x4327 + CAL 2048 并 回读校验、读 V/I/分流/功率。电流有符号,反灌为负(SW-8 判据)。 巡检走 read_vi(),每片两次事务,对应 §10.3 的 8% I²C 占用。 两条留给下次的坑:寄存器指针不依赖自增(sim 会自增、真件不会,靠自增 在宿主机上测不出来);CAL 丢了不报错只让 Current/Power 恒读 0,所以 有 ina226_check()。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
这个提交包含在:
+38
-35
@@ -1,53 +1,56 @@
|
||||
# 交接
|
||||
更新:2026-09-06 完成任务包 **A2**(HAL 接口 + sim 实现)
|
||||
更新:2026-09-06 完成任务包 **A3**(INA226 驱动 + 六片地址表)
|
||||
|
||||
## 一句话现状
|
||||
四路 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` 零警告。
|
||||
六片 INA226 的地址表钉死在代码里(`0x40/41/44/45/42` + 输入侧 `0x43`),
|
||||
驱动能认件(MFG/DIE ID)、写 Config+CAL 并回读校验、读出 V / I / 分流 / 功率,
|
||||
电流**有符号**(反灌为负)。4 条 ctest(34 个用例)全绿,`idf.py build` 通过。
|
||||
|
||||
## 下一步
|
||||
**A3**:INA226 驱动 + 六片地址表(`0x40/41/44/45/42/43`,设计文档 §8.2)。
|
||||
驱动只准通过 `hal_i2c_*` 访问总线;测试用 `sim_i2c_add(addr, 2)` 挂假器件、
|
||||
`sim_i2c_set_reg()` 预置值。**16 位寄存器是大端**,sim 已按大端实现。
|
||||
**A4**:DAC63204 驱动 + `rail_set_mv()`(需求 §10.5 全节)。
|
||||
三条断言:①4020 mV 被拒(SR-13)②**写更大的码值输出更低**(反向关系)③3900 mV ↔ Vd 0.548 V。
|
||||
地址 `0x4A`(U6:CH1~3)/ `0x4B`(U7:CH4~5),同样查表不要算。
|
||||
|
||||
## 本次新增/修改的文件
|
||||
- `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 — 记下组件重名那个坑
|
||||
- `components/drv/include/ina226.h` — 寄存器、量程常量、地址表、采样结构(唯一对外契约)
|
||||
- `components/drv/ina226.c` — 驱动本体(≈170 行,只经 `hal_i2c_*`)
|
||||
- `components/drv/CMakeLists.txt` — 新 IDF 组件 `drv`,`REQUIRES phonerail_hal`
|
||||
- `test/test_ina226.c` — 17 个用例
|
||||
- `host/CMakeLists.txt` — 加 `PHONERAIL_DRV_SRCS` 与 `drv/include`
|
||||
- `main/CMakeLists.txt` — `REQUIRES phonerail_hal drv`
|
||||
|
||||
## 验收命令与结果
|
||||
```
|
||||
$ bash host/test.sh
|
||||
100% tests passed, 0 tests failed out of 3 # test_hal_i2c_sim / test_hal_sim / test_version
|
||||
100% tests passed, 0 tests failed out of 4 # +test_ina226(17 个用例)
|
||||
|
||||
$ .\build_idf.ps1 # PowerShell
|
||||
phonerail.bin binary size 0x32550 bytes. ... 0xcdab0 bytes (80%) free.
|
||||
Project build complete.
|
||||
Project build complete. # build/esp-idf/drv/.../ina226.c.obj 确认编到了
|
||||
```
|
||||
|
||||
## 踩到的坑(只写会再犯的)
|
||||
- **组件名不能叫 `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 编到。
|
||||
- **别依赖 I²C 寄存器指针自增**:sim 的假器件**会**自增,INA226 按 datasheet **不会**。
|
||||
「一次 write_read 连读 0x01~0x04」在宿主机上全绿,到板上四个量会读成同一个。
|
||||
驱动每次读之前重写指针,`test_pointer_is_rewritten_before_every_read` 盯着这条。
|
||||
(指针不自增这条**未在实物上验过**,B1 上板时确认。)
|
||||
- **`write_verify()` 之后 `last_write` 是那次回读的指针字节(len 1),不是刚写的三字节。**
|
||||
按「最后一次写就是我写的数据」写断言会红一次 —— 改看 `n_write`/`n_read` 计数。
|
||||
- cygwin 并行链接偶发 `Error running link command: Subprocess aborted`(这次是 test_version)。
|
||||
**重跑一次就好**,不是代码问题,别去查。
|
||||
|
||||
## 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()`。
|
||||
## 这个驱动的几条约定(写 A7 / A10 之前看一眼)
|
||||
- **地址查表**:`ina226_rail_addr(rail)`,rail 是 **1~5**(不是 0~4),越界 abort。
|
||||
`ina226_all_addr[6]` 供扫描用;`ina226_addr_table_ok()` 做启动自检(含与
|
||||
DAC 0x4A/0x4B、PCA9555 0x20、LM75 0x48 的撞车检查)。
|
||||
- **单位**:`bus_mv` / `shunt_uv` / `current_ua` / `power_mw`,全是 `int32_t`。
|
||||
电流 LSB 0.5 mA ⇒ `current_ua` 一定是 500 的整数倍;−60 mA(SW-8 判据)= `-60000`。
|
||||
- **巡检走 `ina226_read_vi()`**(只读母线电压 + 电流,两次事务)——
|
||||
§10.3 那 8 % 的 I²C 占用是按「每片每 20 ms 两次事务」算的,读四个量会翻倍。
|
||||
- **`HAL_ERR_BUS` 在这个驱动里有特定含义**:器件应答了但内容不对
|
||||
(probe 的 ID 不匹配 = 贴错件/地址跳线焊错;init 的回读不一致;check 的 CAL 丢了)。
|
||||
和「没这个器件」的 `HAL_ERR_NAK` 是两回事,降级矩阵(A15)要分开处理。
|
||||
- **CAL 丢了不会报错,只会让 Current/Power 恒读 0**,看上去像「这台手机不耗电」。
|
||||
所以有 `ina226_check()`,巡检里定期查一次、不一致就 `ina226_init()` 重来并记事件。
|
||||
- Config 定为 **0x4327**(AVG=4、VBUSCT=VSHCT=1.1 ms、连续测量分流与母线),
|
||||
转换周期 8.8 ms < t_safety 的 20 ms。改这个值前先算一遍这个不等式。
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# 板上器件驱动(ESP-IDF 侧)。宿主机侧由 host/CMakeLists.txt 的 PHONERAIL_DRV_SRCS 收,
|
||||
# 新增一个驱动两边都要加 —— 漏一处 = 「宿主机绿了但上板编不过」,反之亦然。
|
||||
#
|
||||
# 驱动只准依赖 phonerail_hal,不准直接包含 IDF 的头。
|
||||
idf_component_register(
|
||||
SRCS "ina226.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES phonerail_hal)
|
||||
@@ -0,0 +1,184 @@
|
||||
/* INA226 驱动。只经 hal_i2c_*,不碰任何平台头文件。 */
|
||||
#include "ina226.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "hal_i2c.h"
|
||||
|
||||
/* 设计文档 §8.2:CH1..CH5。⚠️ 不是 0x40 起顺排,CH3/CH4/CH5 三路都不是。 */
|
||||
static const uint8_t s_rail_addr[INA226_RAIL_COUNT] = { 0x40, 0x41, 0x44, 0x45, 0x42 };
|
||||
|
||||
const uint8_t ina226_all_addr[INA226_COUNT] = { 0x40, 0x41, 0x44, 0x45, 0x42, INA226_ADDR_INPUT };
|
||||
|
||||
uint8_t ina226_rail_addr(int rail)
|
||||
{
|
||||
assert(rail >= 1 && rail <= INA226_RAIL_COUNT); /* 越界是代码写错,不许悄悄返回一个地址 */
|
||||
return s_rail_addr[rail - 1];
|
||||
}
|
||||
|
||||
bool ina226_addr_table_ok(void)
|
||||
{
|
||||
/* 同一条 I²C 上的其它器件(§8.2):撞上任何一个都是硬件表被改坏了。 */
|
||||
static const uint8_t others[] = { 0x4A, 0x4B, 0x20, 0x48 };
|
||||
|
||||
for (int i = 0; i < INA226_COUNT; i++) {
|
||||
if (ina226_all_addr[i] > 0x7f) {
|
||||
return false;
|
||||
}
|
||||
for (int j = i + 1; j < INA226_COUNT; j++) {
|
||||
if (ina226_all_addr[i] == ina226_all_addr[j]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (size_t k = 0; k < sizeof(others); k++) {
|
||||
if (ina226_all_addr[i] == others[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 前五个必须和 rail 表逐个一致 —— 两张表分家的那天就是错一路的那天。 */
|
||||
for (int i = 0; i < INA226_RAIL_COUNT; i++) {
|
||||
if (ina226_all_addr[i] != s_rail_addr[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- 寄存器读写 ---------------------------------------------------------
|
||||
⚠️ 每次读之前都重写一遍寄存器指针,**不依赖指针自增**:INA226 的指针按 datasheet
|
||||
是停在最后写入值上的(未在实物上验过,B1 上板时确认)。而 sim 的假器件是**会**自增的,
|
||||
所以「靠自增一次读完 0x01~0x04」在宿主机上能绿,到板上会把四个量读成同一个。 */
|
||||
|
||||
static hal_err_t read_reg(uint8_t addr, uint8_t reg, uint16_t *raw)
|
||||
{
|
||||
uint8_t rd[2];
|
||||
hal_err_t err = hal_i2c_write_read(addr, ®, 1, rd, sizeof(rd));
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
*raw = (uint16_t)(((uint16_t)rd[0] << 8) | rd[1]); /* 大端 */
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
static hal_err_t write_reg(uint8_t addr, uint8_t reg, uint16_t val)
|
||||
{
|
||||
const uint8_t wr[3] = { reg, (uint8_t)(val >> 8), (uint8_t)(val & 0xff) };
|
||||
return hal_i2c_write(addr, wr, sizeof(wr));
|
||||
}
|
||||
|
||||
static hal_err_t write_verify(uint8_t addr, uint8_t reg, uint16_t val)
|
||||
{
|
||||
hal_err_t err = write_reg(addr, reg, val);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
uint16_t back = 0;
|
||||
err = read_reg(addr, reg, &back);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return back == val ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
/* ---- 换算 ---------------------------------------------------------------- */
|
||||
|
||||
static int32_t div_round(int32_t num, int32_t den) /* 四舍五入,负数远离零 */
|
||||
{
|
||||
return num >= 0 ? (num + den / 2) / den : (num - den / 2) / den;
|
||||
}
|
||||
|
||||
static int32_t s16(uint16_t raw) { return (int32_t)(int16_t)raw; }
|
||||
|
||||
static int32_t bus_mv_of(uint16_t raw) { return div_round((int32_t)raw * 5, 4); } /* 1.25 mV */
|
||||
static int32_t shunt_uv_of(uint16_t raw) { return div_round(s16(raw) * 5, 2); } /* 2.5 µV */
|
||||
static int32_t current_ua_of(uint16_t raw) { return s16(raw) * INA226_CURRENT_LSB_UA; }
|
||||
static int32_t power_mw_of(uint16_t raw) { return div_round((int32_t)raw * 25, 2); } /* 12.5 mW */
|
||||
|
||||
/* ---- 对外 ---------------------------------------------------------------- */
|
||||
|
||||
hal_err_t ina226_probe(uint8_t addr)
|
||||
{
|
||||
uint16_t mfg = 0;
|
||||
hal_err_t err = read_reg(addr, INA226_REG_MFG_ID, &mfg);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
uint16_t die = 0;
|
||||
err = read_reg(addr, INA226_REG_DIE_ID, &die);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
/* 应答了但不是 INA226:地址跳线焊错 / 贴错件。不能当「没这个器件」混过去。 */
|
||||
return (mfg == INA226_MFG_ID && die == INA226_DIE_ID) ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
hal_err_t ina226_init(uint8_t addr)
|
||||
{
|
||||
hal_err_t err = write_verify(addr, INA226_REG_CONFIG, INA226_CONFIG);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return write_verify(addr, INA226_REG_CALIB, INA226_CALIB);
|
||||
}
|
||||
|
||||
hal_err_t ina226_check(uint8_t addr)
|
||||
{
|
||||
uint16_t cal = 0;
|
||||
hal_err_t err = read_reg(addr, INA226_REG_CALIB, &cal);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return cal == INA226_CALIB ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
#define READ_ONE(fn, conv, reg) \
|
||||
hal_err_t fn(uint8_t addr, int32_t *out) \
|
||||
{ \
|
||||
if (out == NULL) { \
|
||||
return HAL_ERR_ARG; \
|
||||
} \
|
||||
uint16_t raw = 0; \
|
||||
hal_err_t err = read_reg(addr, (reg), &raw); \
|
||||
if (err != HAL_OK) { \
|
||||
return err; \
|
||||
} \
|
||||
*out = conv(raw); \
|
||||
return HAL_OK; \
|
||||
}
|
||||
|
||||
READ_ONE(ina226_read_bus_mv, bus_mv_of, INA226_REG_BUS_V)
|
||||
READ_ONE(ina226_read_shunt_uv, shunt_uv_of, INA226_REG_SHUNT_V)
|
||||
READ_ONE(ina226_read_current_ua, current_ua_of, INA226_REG_CURRENT)
|
||||
READ_ONE(ina226_read_power_mw, power_mw_of, INA226_REG_POWER)
|
||||
|
||||
hal_err_t ina226_read_vi(uint8_t addr, ina226_vi_t *out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
hal_err_t err = ina226_read_bus_mv(addr, &out->bus_mv);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return ina226_read_current_ua(addr, &out->current_ua);
|
||||
}
|
||||
|
||||
hal_err_t ina226_read_all(uint8_t addr, ina226_sample_t *out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
hal_err_t err = ina226_read_bus_mv(addr, &out->bus_mv);
|
||||
if (err == HAL_OK) {
|
||||
err = ina226_read_shunt_uv(addr, &out->shunt_uv);
|
||||
}
|
||||
if (err == HAL_OK) {
|
||||
err = ina226_read_current_ua(addr, &out->current_ua);
|
||||
}
|
||||
if (err == HAL_OK) {
|
||||
err = ina226_read_power_mw(addr, &out->power_mw);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/* INA226 —— 板上 6 片(CH1~CH5 输出各一片 + 输入侧一片),分流电阻全部 5 mΩ / 1%。
|
||||
地址表见设计文档 §8.2,**不是顺排的**,详见下面 ina226_rail_addr() 的警告。
|
||||
|
||||
本驱动只通过 hal_i2c_* 访问总线,宿主机上用 sim 的假器件即可全部测完。 */
|
||||
#ifndef INA226_H
|
||||
#define INA226_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- 寄存器(全部 16 位、大端) ---------------------------------------- */
|
||||
|
||||
#define INA226_REG_CONFIG 0x00
|
||||
#define INA226_REG_SHUNT_V 0x01
|
||||
#define INA226_REG_BUS_V 0x02
|
||||
#define INA226_REG_POWER 0x03
|
||||
#define INA226_REG_CURRENT 0x04
|
||||
#define INA226_REG_CALIB 0x05
|
||||
#define INA226_REG_MASK_EN 0x06
|
||||
#define INA226_REG_ALERT_L 0x07
|
||||
#define INA226_REG_MFG_ID 0xFE
|
||||
#define INA226_REG_DIE_ID 0xFF
|
||||
|
||||
#define INA226_MFG_ID 0x5449 /* 'TI' */
|
||||
#define INA226_DIE_ID 0x2260
|
||||
|
||||
/* ---- 量程与标定(五路与输入侧同一颗分流电阻,所以是常量不是参数) ------ */
|
||||
|
||||
#define INA226_SHUNT_UOHM 5000 /* 5 mΩ,设计文档 §8.2 */
|
||||
#define INA226_CURRENT_LSB_UA 500 /* 0.5 mA —— 分流 LSB 2.5 µV ÷ 5 mΩ */
|
||||
|
||||
/* CAL = 0.00512 / (Current_LSB × R_shunt) = 0.00512 / (500 µA × 5 mΩ) = 2048 */
|
||||
#define INA226_CALIB 2048
|
||||
|
||||
/* Config:保留位 100 + AVG=4 + VBUSCT=1.1 ms + VSHCT=1.1 ms + 连续测量分流与母线。
|
||||
转换周期 4 × (1.1 + 1.1) = 8.8 ms < t_safety 的 20 ms,所以每次巡检都拿到新的一帧。 */
|
||||
#define INA226_CONFIG 0x4327
|
||||
|
||||
/* ---- 地址表(设计文档 §8.2) ------------------------------------------- */
|
||||
|
||||
#define INA226_RAIL_COUNT 5
|
||||
#define INA226_COUNT 6
|
||||
#define INA226_ADDR_INPUT 0x43 /* U13,输入侧 V+I,整板功率预算用 */
|
||||
|
||||
/* CH1~CH5 → U8~U12 的地址。rail 是 1~5,越界直接 abort。
|
||||
⚠️ **`0x40 + ch` 是错的**,CH3/CH4/CH5 三路全错(0x44 / 0x45 / 0x42)。
|
||||
地址按布线难度重排过(§8.2 那张 A1/A0 表),永远查表,不要算。 */
|
||||
uint8_t ina226_rail_addr(int rail);
|
||||
|
||||
extern const uint8_t ina226_all_addr[INA226_COUNT]; /* CH1..CH5 + 输入侧,扫描用 */
|
||||
|
||||
/* 六个地址互不重复、且不与 DAC63204(0x4A/0x4B) / PCA9555(0x20) / LM75(0x48) 撞车。
|
||||
启动自检调一次;返回 false 说明表被改坏了。 */
|
||||
bool ina226_addr_table_ok(void);
|
||||
|
||||
/* ---- 采样 --------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
int32_t bus_mv; /* 母线电压,1.25 mV/LSB */
|
||||
int32_t current_ua; /* **有符号** —— 反灌时为负(SW-8 靠这个判据) */
|
||||
} ina226_vi_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t bus_mv;
|
||||
int32_t shunt_uv; /* 2.5 µV/LSB,四舍五入到 µV(±1.25 µV ≈ ±0.25 mA) */
|
||||
int32_t current_ua;
|
||||
int32_t power_mw; /* 12.5 mW/LSB,向零截断 */
|
||||
} ina226_sample_t;
|
||||
|
||||
/* 读 Manufacturer ID / Die ID 认件。
|
||||
器件不应答 → HAL_ERR_NAK;**应答了但 ID 不对 → HAL_ERR_BUS**
|
||||
(这个地址上有东西,但不是 INA226:多半地址跳线焊错或贴错件)。 */
|
||||
hal_err_t ina226_probe(uint8_t addr);
|
||||
|
||||
/* 写 Config 与 Calibration 并**逐个回读校验**。回读不一致 → HAL_ERR_BUS。 */
|
||||
hal_err_t ina226_init(uint8_t addr);
|
||||
|
||||
/* 只校验 Calibration 还在。掉了的现象是 **Current/Power 恒读 0**(不是报错),
|
||||
看上去就像「这台手机不耗电」,所以巡检里要定期查一次。 */
|
||||
hal_err_t ina226_check(uint8_t addr);
|
||||
|
||||
/* 巡检主路径:母线电压 + 电流,两次事务。 */
|
||||
hal_err_t ina226_read_vi(uint8_t addr, ina226_vi_t *out);
|
||||
|
||||
/* 诊断/事件记录用:四个量一次读齐,四次事务。 */
|
||||
hal_err_t ina226_read_all(uint8_t addr, ina226_sample_t *out);
|
||||
|
||||
hal_err_t ina226_read_bus_mv(uint8_t addr, int32_t *mv);
|
||||
hal_err_t ina226_read_shunt_uv(uint8_t addr, int32_t *uv);
|
||||
hal_err_t ina226_read_current_ua(uint8_t addr, int32_t *ua);
|
||||
hal_err_t ina226_read_power_mw(uint8_t addr, int32_t *mw);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* INA226_H */
|
||||
+12
-2
@@ -8,6 +8,8 @@
|
||||
# HAL 里新增一路实现则要同时加进:
|
||||
# ../components/phonerail_hal/CMakeLists.txt 的 SRCS(esp 那份)
|
||||
# 和下面的 PHONERAIL_HAL_SIM_SRCS(sim 那份)
|
||||
# 新增一个器件驱动则是:
|
||||
# ../components/drv/CMakeLists.txt 的 SRCS 和 下面的 PHONERAIL_DRV_SRCS
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(phonerail_host C)
|
||||
|
||||
@@ -33,11 +35,19 @@ set(PHONERAIL_HAL_SIM_SRCS
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_time_sim.c
|
||||
)
|
||||
|
||||
add_library(phonerail_core STATIC ${PHONERAIL_CORE_SRCS} ${PHONERAIL_HAL_SIM_SRCS})
|
||||
# 器件驱动:与平台无关(只经 hal_i2c_*),两个目标编的是同一份源码。
|
||||
set(PHONERAIL_DRV_DIR ${FW_ROOT}/components/drv)
|
||||
set(PHONERAIL_DRV_SRCS
|
||||
${PHONERAIL_DRV_DIR}/ina226.c
|
||||
)
|
||||
|
||||
add_library(phonerail_core STATIC
|
||||
${PHONERAIL_CORE_SRCS} ${PHONERAIL_HAL_SIM_SRCS} ${PHONERAIL_DRV_SRCS})
|
||||
target_include_directories(phonerail_core PUBLIC
|
||||
${FW_ROOT}/main
|
||||
${PHONERAIL_HAL_DIR}/include
|
||||
${PHONERAIL_HAL_DIR}/sim)
|
||||
${PHONERAIL_HAL_DIR}/sim
|
||||
${PHONERAIL_DRV_DIR}/include)
|
||||
|
||||
# ---- 测试框架 Unity(从 ESP-IDF v5.4 原样复制到 third_party/,与上板测试同一套)
|
||||
add_library(unity STATIC ${FW_ROOT}/third_party/unity/unity.c)
|
||||
|
||||
@@ -5,4 +5,4 @@ idf_component_register(
|
||||
SRCS "app_main.c"
|
||||
"version.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES phonerail_hal)
|
||||
REQUIRES phonerail_hal drv)
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/* A3:INA226 驱动 + 六片地址表。
|
||||
两个验收主项:①六个地址就是 0x40/41/44/45/42/43;②通道号 ≠ 地址低位。 */
|
||||
#include "ina226.h"
|
||||
#include "sim_hal.h"
|
||||
#include "unity.h"
|
||||
|
||||
void setUp(void) { sim_hal_reset(); }
|
||||
void tearDown(void) {}
|
||||
|
||||
/* 挂一片假 INA226 并预置 ID,返回地址。 */
|
||||
static uint8_t mount(uint8_t addr)
|
||||
{
|
||||
sim_i2c_add(addr, 2);
|
||||
sim_i2c_set_reg(addr, INA226_REG_MFG_ID, INA226_MFG_ID);
|
||||
sim_i2c_set_reg(addr, INA226_REG_DIE_ID, INA226_DIE_ID);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL));
|
||||
return addr;
|
||||
}
|
||||
|
||||
/* ---- 地址表 -------------------------------------------------------------- */
|
||||
|
||||
/* ⭐ 验收主项一:设计文档 §8.2 的六个地址,逐个钉死。 */
|
||||
static void test_addr_table_is_the_one_from_8_2(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_HEX8(0x40, ina226_rail_addr(1));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x41, ina226_rail_addr(2));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x44, ina226_rail_addr(3));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x45, ina226_rail_addr(4));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x42, ina226_rail_addr(5));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x43, INA226_ADDR_INPUT);
|
||||
|
||||
static const uint8_t want[INA226_COUNT] = { 0x40, 0x41, 0x44, 0x45, 0x42, 0x43 };
|
||||
TEST_ASSERT_EQUAL_HEX8_ARRAY(want, ina226_all_addr, INA226_COUNT);
|
||||
}
|
||||
|
||||
/* ⭐ 验收主项二:`0x40 + ch` 这行代码一定会被写出来(需求 §10.4),
|
||||
这条测试就是拦它的 —— 五路里有三路对不上,把表改成算式必红。 */
|
||||
static void test_channel_index_is_not_the_address_low_bits(void)
|
||||
{
|
||||
int mismatches = 0;
|
||||
for (int rail = 1; rail <= INA226_RAIL_COUNT; rail++) {
|
||||
uint8_t naive = (uint8_t)(0x40 + (rail - 1));
|
||||
if (ina226_rail_addr(rail) != naive) {
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(3, mismatches, "CH3/CH4/CH5 三路必须与顺排不同");
|
||||
|
||||
/* 反过来也钉一遍:拿地址低位算回通道号同样会算错。 */
|
||||
TEST_ASSERT_NOT_EQUAL(0x42, ina226_rail_addr(3)); /* CH3 是 0x44,不是 0x42 */
|
||||
TEST_ASSERT_NOT_EQUAL(0x44, ina226_rail_addr(5)); /* CH5 是 0x42,不是 0x44 */
|
||||
}
|
||||
|
||||
static void test_addr_table_has_no_collisions(void)
|
||||
{
|
||||
TEST_ASSERT_TRUE(ina226_addr_table_ok()); /* 含与 DAC/PCA9555/LM75 的撞车检查 */
|
||||
}
|
||||
|
||||
/* ---- 认件 ---------------------------------------------------------------- */
|
||||
|
||||
static void test_probe_accepts_real_ina226(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_probe(mount(0x40)));
|
||||
}
|
||||
|
||||
/* 应答了但 ID 不对 = 贴错件或地址跳线焊错,必须和「没这个器件」区分开。 */
|
||||
static void test_probe_rejects_wrong_die_id(void)
|
||||
{
|
||||
uint8_t a = mount(0x41);
|
||||
sim_i2c_set_reg(a, INA226_REG_DIE_ID, 0x1234);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_BUS, ina226_probe(a));
|
||||
}
|
||||
|
||||
static void test_probe_absent_device_naks(void)
|
||||
{
|
||||
mount(0x40);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, ina226_probe(0x44)); /* 没挂 */
|
||||
sim_i2c_set_present(0x40, false);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, ina226_probe(0x40)); /* 失联 */
|
||||
}
|
||||
|
||||
/* 需求 §12.1 步 2:六片全应答。 */
|
||||
static void test_all_six_answer_scan(void)
|
||||
{
|
||||
for (int i = 0; i < INA226_COUNT; i++) {
|
||||
mount(ina226_all_addr[i]);
|
||||
}
|
||||
for (int i = 0; i < INA226_COUNT; i++) {
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_probe(ina226_all_addr[i]));
|
||||
}
|
||||
}
|
||||
|
||||
static void test_bus_not_initialised_propagates(void)
|
||||
{
|
||||
sim_i2c_add(0x40, 2);
|
||||
int32_t mv = 0;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, ina226_read_bus_mv(0x40, &mv));
|
||||
}
|
||||
|
||||
/* ---- 初始化 -------------------------------------------------------------- */
|
||||
|
||||
static void test_init_writes_config_and_calibration(void)
|
||||
{
|
||||
uint8_t a = mount(0x45);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_init(a));
|
||||
TEST_ASSERT_EQUAL_HEX16(INA226_CONFIG, sim_i2c_get_reg(a, INA226_REG_CONFIG));
|
||||
TEST_ASSERT_EQUAL_HEX16(2048, sim_i2c_get_reg(a, INA226_REG_CALIB));
|
||||
|
||||
/* 两个寄存器都是「写完立刻回读校验」:4 次写(两次三字节的写 + 两次一字节的
|
||||
读指针)、2 次读。最后落下的那次写就是 CAL 的读指针。 */
|
||||
const sim_i2c_dev_t *d = sim_i2c_dev(a);
|
||||
TEST_ASSERT_EQUAL_UINT32(4, d->n_write);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, d->n_read);
|
||||
TEST_ASSERT_EQUAL_UINT8(1, d->last_write_len);
|
||||
TEST_ASSERT_EQUAL_HEX8(INA226_REG_CALIB, d->last_write[0]);
|
||||
}
|
||||
|
||||
/* CAL 掉了的现象是 Current/Power 恒读 0,不是报错 —— 所以要能查出来。 */
|
||||
static void test_check_catches_lost_calibration(void)
|
||||
{
|
||||
uint8_t a = mount(0x40);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_init(a));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_check(a));
|
||||
sim_i2c_set_reg(a, INA226_REG_CALIB, 0); /* 器件掉过一次电 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_BUS, ina226_check(a));
|
||||
}
|
||||
|
||||
/* ---- 换算 ---------------------------------------------------------------- */
|
||||
|
||||
/* 满载一路:4.010 V / 4 A / 16 W。5 mΩ + 0.5 mA LSB 下分流码与电流码同值。 */
|
||||
static void test_readings_at_full_load(void)
|
||||
{
|
||||
uint8_t a = mount(0x44);
|
||||
sim_i2c_set_reg(a, INA226_REG_BUS_V, 3208); /* ×1.25 mV */
|
||||
sim_i2c_set_reg(a, INA226_REG_SHUNT_V, 8000); /* ×2.5 µV = 20 mV */
|
||||
sim_i2c_set_reg(a, INA226_REG_CURRENT, 8000); /* ×0.5 mA */
|
||||
sim_i2c_set_reg(a, INA226_REG_POWER, 1280); /* ×12.5 mW */
|
||||
|
||||
ina226_sample_t s = { 0, 0, 0, 0 };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_all(a, &s));
|
||||
TEST_ASSERT_EQUAL_INT32(4010, s.bus_mv);
|
||||
TEST_ASSERT_EQUAL_INT32(20000, s.shunt_uv);
|
||||
TEST_ASSERT_EQUAL_INT32(4000000, s.current_ua);
|
||||
TEST_ASSERT_EQUAL_INT32(16000, s.power_mw);
|
||||
}
|
||||
|
||||
/* ⭐ 反灌(SW-8):电流寄存器是**有符号**的。当成无符号会得到 +32 A 这种数,
|
||||
而判据是「电流为负」,于是保护永远不触发。 */
|
||||
static void test_reverse_current_is_negative(void)
|
||||
{
|
||||
uint8_t a = mount(0x42);
|
||||
sim_i2c_set_reg(a, INA226_REG_CURRENT, (uint16_t)(int16_t)-120); /* −60 mA */
|
||||
sim_i2c_set_reg(a, INA226_REG_SHUNT_V, (uint16_t)(int16_t)-120); /* −300 µV */
|
||||
|
||||
int32_t ua = 0, uv = 0;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_current_ua(a, &ua));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_shunt_uv(a, &uv));
|
||||
TEST_ASSERT_EQUAL_INT32(-60000, ua);
|
||||
TEST_ASSERT_EQUAL_INT32(-300, uv);
|
||||
}
|
||||
|
||||
/* 3.90 V 出厂默认值那一点,顺带看四舍五入。3120 × 1.25 = 3900 mV 整。 */
|
||||
static void test_bus_voltage_rounding(void)
|
||||
{
|
||||
uint8_t a = mount(0x40);
|
||||
sim_i2c_set_reg(a, INA226_REG_BUS_V, 3120);
|
||||
int32_t mv = 0;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_bus_mv(a, &mv));
|
||||
TEST_ASSERT_EQUAL_INT32(3900, mv);
|
||||
|
||||
sim_i2c_set_reg(a, INA226_REG_BUS_V, 3121); /* 3901.25 → 3901 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_bus_mv(a, &mv));
|
||||
TEST_ASSERT_EQUAL_INT32(3901, mv);
|
||||
}
|
||||
|
||||
/* ⚠️ 驱动不许依赖寄存器指针自增:sim 的假器件会自增,INA226 按 datasheet 不会。
|
||||
靠自增省事的写法在这里会绿,到板上四个量全读成同一个。 */
|
||||
static void test_pointer_is_rewritten_before_every_read(void)
|
||||
{
|
||||
uint8_t a = mount(0x41);
|
||||
ina226_sample_t s = { 0, 0, 0, 0 };
|
||||
sim_i2c_dev(a)->n_write = 0;
|
||||
sim_i2c_dev(a)->n_read = 0;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_all(a, &s));
|
||||
TEST_ASSERT_EQUAL_UINT32(4, sim_i2c_dev(a)->n_write);
|
||||
TEST_ASSERT_EQUAL_UINT32(4, sim_i2c_dev(a)->n_read);
|
||||
}
|
||||
|
||||
/* 巡检主路径:只读两个量(§10.3 那 8 % I²C 占用是按这个算的)。 */
|
||||
static void test_read_vi_uses_two_transactions(void)
|
||||
{
|
||||
uint8_t a = mount(0x40);
|
||||
sim_i2c_set_reg(a, INA226_REG_BUS_V, 3120);
|
||||
sim_i2c_set_reg(a, INA226_REG_CURRENT, 4000);
|
||||
sim_i2c_dev(a)->n_read = 0;
|
||||
|
||||
ina226_vi_t vi = { 0, 0 };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, ina226_read_vi(a, &vi));
|
||||
TEST_ASSERT_EQUAL_INT32(3900, vi.bus_mv);
|
||||
TEST_ASSERT_EQUAL_INT32(2000000, vi.current_ua);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, sim_i2c_dev(a)->n_read);
|
||||
}
|
||||
|
||||
/* 器件中途失联:错误必须一路传上去,不能返回一个看着正常的 0。 */
|
||||
static void test_read_propagates_nak(void)
|
||||
{
|
||||
uint8_t a = mount(0x45);
|
||||
sim_i2c_inject_nak(a, -1);
|
||||
ina226_vi_t vi = { 123, 456 };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, ina226_read_vi(a, &vi));
|
||||
}
|
||||
|
||||
static void test_null_output_rejected(void)
|
||||
{
|
||||
mount(0x40);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, ina226_read_vi(0x40, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, ina226_read_all(0x40, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, ina226_read_bus_mv(0x40, NULL));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_addr_table_is_the_one_from_8_2);
|
||||
RUN_TEST(test_channel_index_is_not_the_address_low_bits);
|
||||
RUN_TEST(test_addr_table_has_no_collisions);
|
||||
RUN_TEST(test_probe_accepts_real_ina226);
|
||||
RUN_TEST(test_probe_rejects_wrong_die_id);
|
||||
RUN_TEST(test_probe_absent_device_naks);
|
||||
RUN_TEST(test_all_six_answer_scan);
|
||||
RUN_TEST(test_bus_not_initialised_propagates);
|
||||
RUN_TEST(test_init_writes_config_and_calibration);
|
||||
RUN_TEST(test_check_catches_lost_calibration);
|
||||
RUN_TEST(test_readings_at_full_load);
|
||||
RUN_TEST(test_reverse_current_is_negative);
|
||||
RUN_TEST(test_bus_voltage_rounding);
|
||||
RUN_TEST(test_pointer_is_rewritten_before_every_read);
|
||||
RUN_TEST(test_read_vi_uses_two_transactions);
|
||||
RUN_TEST(test_read_propagates_nak);
|
||||
RUN_TEST(test_null_output_rejected);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -403,6 +403,13 @@ typedef struct {
|
||||
> ⚠️ **`ina_addr = 0x40 + ch` 这行代码一定会被写出来,而且只有 CH3/CH4/CH5 是错的。**
|
||||
> 实际地址是 `0x40 / 0x41 / 0x44 / 0x45 / 0x42`。查表,不要算。
|
||||
|
||||
> **A3 已落地**:表在 `firmware/components/drv/include/ina226.h`,取值只走
|
||||
> `ina226_rail_addr(rail)`(rail 是 1~5,越界 abort),
|
||||
> `test_channel_index_is_not_the_address_low_bits` 专门拦「改回算式」这一手。
|
||||
> 同处还记了两条 INA226 的实现约定:**寄存器指针不自增**(每次读前重写指针,
|
||||
> sim 会自增、真件不会,靠自增在宿主机上测不出来);**Calibration 丢了不报错、
|
||||
> 只让 Current/Power 恒读 0**,所以巡检要定期 `ina226_check()`。
|
||||
|
||||
同理,**PCA9555 的方向寄存器只能配成全输入**:备用脚 Port0[7] / Port1[7:5] 在硬件上
|
||||
直接接地,配成输出并写 1 就是对地短路(设计文档 §17.2 已在图纸上注明)。
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户