文件
phonerail/firmware/main/cmd_exec.c
T
zhou yang和Claude Opus 5.5 c85eec53e6 固件 A15:启动顺序 + 降级矩阵(bringup.{h,c},已上板)
- 启动序列从 app_main.c 拆进 main/bringup.{h,c},宿主机可测:
  bringup_devices()(§10.9 第 1~4 步)→ 起 t_safety → bringup_power_restore()(第 5~6 步)
- 六片 INA226 写 Config/Calibration(之前没做,电流恒 0);输入侧 0x43 登记对调接
- 按「意图」逐路上电、错开 100 ms、每路等 PG 500 ms,等不到关回去并记 LOG_EV_PG_TIMEOUT(17)
- 意图只由 power 命令改(cmd_env_t.intent → NVS phonerail/rail_want)
- §10.10 逐行降级:INA/DAC/PCA9555/mux/PRSNT#/CH224A;mux 失联时不碰主干
- BOOT 事件 arg = 复位原因;HUB_RST# 仍只在上电复位时脉冲
- rail_volt_init() 一片失败不再放弃另一片;CH224A 主干 / 通道 5 两处都找
- sim:sim_gpio_rise_us / sim_gpio_set_hook,sim_gpio_drive 可在配置前调
- test_bringup.c 24 例 + test_cmd 意图 1 例,共 425 例全绿;版本 0.1.0-a15
- rack:事件表加 17 PG_TIMEOUT(已部署)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 13:24:17 +08:00

256 行
8.0 KiB
C

/* 六条「会动东西的命令」的执行体 + 风扇下限(A12,需求 §10.8)。
每一条都是同一个形状:**路号 → 在位 → 参数限幅 → 忙不忙 → 下发**。
限幅与拒绝原因全在这里,适配器一个字都不碰(cmd.h 规矩 1)。
判重、名字表、回应拼装在 cmd.c;status / events 的正文在 cmd_report.c。 */
#include <string.h>
#include "cmd_priv.h"
#include "evlog.h"
#include "i2cbus.h"
#include "rail.h"
#include "rail_cfg.h"
#include "slot.h"
#include "t_actuator.h"
/* ---- 共用的几步 ---------------------------------------------------------- */
static cmd_err_t reject(ctx_t *c, cmd_err_t e, int64_t bad)
{
c->bad = bad;
return e;
}
/* 路号 → 在位。**空槽一律拒**(计划 §5.8:空槽上电 = 金手指裸露带 12 V)。 */
static cmd_err_t need_rail(ctx_t *c)
{
int64_t r = 0;
if (cj_get_int(c->o, "rail", &r) != 1) {
return CMD_ERR_ARG;
}
if (r < 1 || r > PHONERAIL_RAIL_COUNT) {
return reject(c, CMD_ERR_RANGE, r);
}
c->rail = (int)r;
if (!slot_present(c->rail)) {
return reject(c, CMD_ERR_EMPTY, r);
}
return CMD_OK;
}
static cmd_err_t from_hal(hal_err_t e)
{
switch (e) {
case HAL_OK: return CMD_OK;
case HAL_ERR_ARG: return CMD_ERR_RANGE; /* 前面已经限过幅,走到这里是两边的限值不一致 */
case HAL_ERR_STATE: return CMD_ERR_BUSY; /* 执行器:这一路正在跑别的序列 */
default: return CMD_ERR_IO;
}
}
/* ---- 各条命令 ------------------------------------------------------------ */
typedef struct {
int rail;
int mv;
} set_mv_arg_t;
static hal_err_t set_mv_fn(void *arg)
{
const set_mv_arg_t *a = arg;
return rail_set_mv(a->rail, a->mv);
}
cmd_err_t do_set_voltage(ctx_t *c)
{
cmd_err_t e = need_rail(c);
if (e != CMD_OK) {
return e;
}
int64_t mv = 0;
if (cj_get_int(c->o, "mv", &mv) != 1) {
return CMD_ERR_ARG;
}
/* SR-13:没有任何命令能让输出超过 4.2 V。这里限的是 DAC 能到的 3524~4010,
比 4.2 V 更紧;`rail_set_mv()` 自己还会再判一次(第四道之后的第五道)。 */
if (mv < RAIL_MV_MIN || mv > RAIL_MV_MAX) {
return reject(c, CMD_ERR_RANGE, mv);
}
/* ⚠️ DAC 在主干上,但 `rail_set_mv()` 自己**不拿锁**(它比 i2cbus 早)。
不包这一层就是在巡检的事务中间插一笔 —— 总线上两个主机同时说话。 */
set_mv_arg_t a = { .rail = c->rail, .mv = (int)mv };
e = from_hal(i2cbus_xact(I2CBUS_CH_TRUNK, SLOT_LOCK_TIMEOUT_MS, set_mv_fn, &a));
if (e == CMD_OK) {
cj_w(c->w, ",\"set\":%d", rail_setpoint_mv(c->rail));
}
return e;
}
static int32_t now_mv(int rail)
{
const safety_snap_t *s = cmd_env()->safety;
return (s != NULL && s->rail_ok[rail - 1]) ? s->rail[rail - 1].bus_mv : 0;
}
static int32_t now_ua(int rail)
{
const safety_snap_t *s = cmd_env()->safety;
return (s != NULL && s->rail_ok[rail - 1]) ? s->rail[rail - 1].current_ua : 0;
}
cmd_err_t do_power_cycle(ctx_t *c)
{
cmd_err_t e = need_rail(c);
if (e != CMD_OK) {
return e;
}
/* 断电时长不收参数:§7.4 的 3~5 s 窗口由 `ACT_HR_OFF_MS` 定死。
让上位机传一个 1 s 进来,现象是「有的机型压根没重启」(t_actuator.h)。 */
e = from_hal(t_actuator_hard_reset(c->rail));
if (e == CMD_OK) {
evlog_put((uint8_t)c->rail, LOG_EV_REBOOT, CMD_REBOOT_ARG(true, c->link),
now_mv(c->rail), now_ua(c->rail));
}
return e;
}
/* "pwr+vdn" → 逻辑键位。未知的键名是参数错,不是「忽略掉那一个」。 */
static bool parse_keys(const char *s, uint32_t *keys)
{
static const struct { const char *name; uint32_t bit; } k[] = {
{ "pwr", ACT_KEYS_PWR }, { "vup", ACT_KEYS_VUP }, { "vdn", ACT_KEYS_VDN },
};
uint32_t m = 0;
while (*s != '\0') {
const char *end = strchr(s, '+');
const size_t n = end ? (size_t)(end - s) : strlen(s);
bool hit = false;
for (size_t i = 0; i < sizeof(k) / sizeof(k[0]); i++) {
if (strlen(k[i].name) == n && strncmp(k[i].name, s, n) == 0) {
m |= k[i].bit;
hit = true;
}
}
if (!hit) {
return false;
}
s += n;
if (*s == '+') {
s++;
if (*s == '\0') {
return false; /* "pwr+" */
}
}
}
*keys = m;
return m != 0;
}
cmd_err_t do_key(ctx_t *c)
{
cmd_err_t e = need_rail(c);
if (e != CMD_OK) {
return e;
}
const char *ks = NULL;
int64_t ms = 0;
uint32_t keys = 0;
if (cj_get_str(c->o, "keys", &ks) != 1 || !parse_keys(ks, &keys) ||
cj_get_int(c->o, "ms", &ms) != 1) {
return CMD_ERR_ARG;
}
if (ms < 1 || ms > ACT_KEY_MAX_MS) {
return reject(c, CMD_ERR_RANGE, ms); /* 拦「把电源键按死」的手误 */
}
e = from_hal(t_actuator_key(c->rail, keys, (uint32_t)ms));
if (e == CMD_OK && keys == ACT_KEYS_PWR && ms >= ACT_KEY_MS_FORCE) {
/* 长按电源键 ≥10 s = 两级重启的「一级(软)」(需求 §4.1),记一条重启事件。 */
evlog_put((uint8_t)c->rail, LOG_EV_REBOOT, CMD_REBOOT_ARG(false, c->link),
now_mv(c->rail), now_ua(c->rail));
}
return e;
}
cmd_err_t do_edl(ctx_t *c)
{
const cmd_err_t e = need_rail(c);
return (e != CMD_OK) ? e : from_hal(t_actuator_edl(c->rail));
}
cmd_err_t do_vbus(ctx_t *c)
{
cmd_err_t e = need_rail(c);
if (e != CMD_OK) {
return e;
}
bool on = false;
if (cj_get_bool(c->o, "on", &on) != 1) {
return CMD_ERR_ARG;
}
/* EDL 序列自己要动 VBUS,中途插手就把它的时序打乱了。 */
if (t_actuator_busy(c->rail)) {
return CMD_ERR_BUSY;
}
/* eFuse 关着时开 VBUS = 让手机只靠 USB 供着电 —— `rail_all_off()` 专门
「先断 VBUS 再关 eFuse」就是为了不留这个窗口,命令也不许把它造出来。 */
if (on && !rail_efuse_is_on(c->rail)) {
return reject(c, CMD_ERR_STATE, 1);
}
return from_hal(slot_vbus_enable(c->rail, on));
}
/* A15:意图出口(cmd.h 的 `cmd_intent_fn`)。 */
static void note_intent(int rail, bool on)
{
const cmd_env_t *env = cmd_env();
if (env != NULL && env->intent != NULL) {
env->intent(env->intent_ctx, rail, on);
}
}
cmd_err_t do_power(ctx_t *c)
{
cmd_err_t e = need_rail(c);
if (e != CMD_OK) {
return e;
}
bool on = false;
if (cj_get_bool(c->o, "on", &on) != 1) {
return CMD_ERR_ARG;
}
if (t_actuator_busy(c->rail)) {
return CMD_ERR_BUSY; /* 硬重启走到一半再开关一次是自作主张(t_actuator_abort 同理) */
}
if (on) {
const hal_err_t ee = rail_efuse_enable(c->rail, true);
if (ee == HAL_OK) {
note_intent(c->rail, true);
}
return from_hal(ee);
}
/* 关:与 `rail_all_off()` 同序 —— **先断 VBUS 再关 eFuse**。
VBUS 走 I²C 可能断不掉,eFuse 是 GPIO 永远关得掉:前者失败也照样关后者,
回应报 io,上位机据此知道「直供断了,VBUS 可能还通着」。 */
const hal_err_t ev = slot_vbus_enable(c->rail, false);
const hal_err_t ee = rail_efuse_enable(c->rail, false);
if (ee == HAL_OK) {
note_intent(c->rail, false); /* 直供确实断了:下次开机也别开它 */
}
return from_hal(ee != HAL_OK ? ee : ev);
}
cmd_err_t do_fan(ctx_t *c)
{
int64_t duty = 0;
if (cj_get_int(c->o, "duty", &duty) != 1) {
return CMD_ERR_ARG;
}
if (duty < 0 || duty > 1000) {
return reject(c, CMD_ERR_RANGE, duty);
}
/* 手动值只是**下限**:温度曲线要得更高时照曲线来(t_thermal.h)。0 = 交回自动。 */
t_thermal_set_floor((uint16_t)duty);
cj_w(c->w, ",\"floor\":%u", (unsigned)t_thermal_floor());
return CMD_OK;
}