- warm_esp.{h,c}:warm_restart() 记下开着的路(RTC_NOINIT,魔数 + 掩码 + 反码)→ gpio_hold 锁五根 EN → 重启;
目标镜像没有能力标记(第 288 字节 PRCAPS1,.rodata_custom_desc)就不锁 —— 老固件写不动锁住的 EN。
warm_take():软件复位 + 标记对得上才接管,一次性;再读 EN 真电平核对
- bringup:adopt 掩码;rail_en_init_keep() 接管的路写高、五根一律放开 hold(崩溃时旧锁可能还在);
刀片不在 / mux 失联的接管路立刻关(SLOT_EMPTY arg 1);第二段已开着的路不重开、不等 PG
- pca9555_init_keep():方向寄存器是本固件写的才留 VBUS(掉过电的片子锁存器是 0xFF),键 / EDL 清零
- HAL:hal_gpio_hold();ESP 输出脚配 INPUT_OUTPUT(读得到真电平);sim 给 hold 建模
- 事件 LOG_EV_OTA arg 7 = 接管;兜底定时器回滚故意不走接管
- 顶层 CMake:phonerail.h 进 CMAKE_CONFIGURE_DEPENDS(否则镜像头版本停在旧值);-u g_phonerail_caps 防 gc
- test_bringup 加 6 例(接管 / 非计划复位全关 / 空槽 / mux 失联 / PCA 掉过电 / hold 冻结),全工程 448 例全绿
- rack:固件库识别能力标记,推送对话框按「支持 / 不支持重启不断电」给不同提示(已部署)
- 需求文档:SR-6 改为「非计划复位全关」+ 计划内重启例外;§10.9 新增「重启不断电」
- 板 B:台架镜像(空槽强制 CH1 在位,未提交)开 CH1 → OTA 带重启 → 事件 18/7 mv=1,CH1 一直开着;
现跑 0.1.0-c3。未测:示波器看重启期间 EN / 输出毛刺(要插刀片)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
375 行
14 KiB
C
375 行
14 KiB
C
/* OTA 的 ESP 胶水(C2)。**只进 IDF 目标**;三条规矩(明文 HTTP + sha256、不签名、
|
||
120 s 自证健康)与「重启 = 五路全断一次」在 ota.h。
|
||
|
||
---- 谁在哪个任务里跑 ------------------------------------------------------
|
||
| 事 | 任务 |
|
||
|---|---|
|
||
| `ota` 命令判定、把请求交出去 | 执行器任务(命令处理器 → `ota_request()` → `start()`,只入队) |
|
||
| HTTP 下载、算 sha256、写 flash、切启动分区 | **本文件自己的 `t_ota` 任务**,优先级与 `t_link` 同(最低一档) |
|
||
| 健康判据、到点重启 | `t_link`(`ota_esp_poll()`) |
|
||
| 兜底回滚 | esp_timer(app_main 卡死、`t_link` 永远起不来时也能回滚) |
|
||
|
||
下载要几秒到几十秒,放进执行器任务就是让按键序列等 TCP;放进 `t_link` 就是让遥测停摆。
|
||
|
||
---- 写 flash 对巡检的影响 ----------------------------------------------
|
||
擦一个 4 KB 扇区要几十 ms,期间 cache 关掉、两个核上跑 flash 代码的任务都停。
|
||
`OTA_WITH_SEQUENTIAL_WRITES` 让擦除跟着写走(一次一个扇区),而不是在 esp_ota_begin 里
|
||
一口气擦 1 MB(那是秒级的停顿);每写一块再让出一个 tick。代价是 t_safety 偶尔晚一两拍,
|
||
上板看 status 的 `safety.skipped` 与巡检日志里的「最迟」。
|
||
|
||
---- 重启前等一下 --------------------------------------------------------
|
||
回应要先发出去(命令处理器调 reboot 回调时回应还没写),事件要先落盘(t_log 攒批最多 1 s)。
|
||
所以重启一律是「排到 `REBOOT_DELAY_MS` 之后」,由 `t_link` 执行。 */
|
||
#include <string.h>
|
||
|
||
#include "esp_app_desc.h"
|
||
#include "esp_http_client.h"
|
||
#include "esp_log.h"
|
||
#include "esp_ota_ops.h"
|
||
#include "esp_system.h"
|
||
#include "esp_timer.h"
|
||
#include "evlog.h"
|
||
#include "freertos/FreeRTOS.h"
|
||
#include "freertos/queue.h"
|
||
#include "freertos/task.h"
|
||
#include "hal_time.h"
|
||
#include "mbedtls/sha256.h"
|
||
#include "netcfg.h"
|
||
#include "ota.h"
|
||
#include "ota_esp.h"
|
||
#include "tasks.h"
|
||
#include "warm_esp.h"
|
||
|
||
static const char *TAG = "ota";
|
||
|
||
#define REBOOT_DELAY_MS (EVLOG_MAX_AGE_MS + 1000u) /* 回应发出去 + 事件落盘 */
|
||
#define HTTP_TIMEOUT_MS 15000
|
||
#define CHUNK 4096
|
||
#define STATUS_EVERY (64u * 1024u) /* 进度多久更新一次 status(每块都更新也行,只是没必要) */
|
||
#define BACKSTOP_EXTRA_MS 30000u /* 兜底定时器比判据晚这么久:正常路径先走 */
|
||
|
||
static QueueHandle_t s_q;
|
||
static volatile bool s_boot_done;
|
||
static volatile uint32_t s_reboot_at; /* 0 = 没排;否则 = 上电毫秒 */
|
||
static volatile bool s_rollback; /* 排的是回滚(标记无效再重启),不是普通重启 */
|
||
static bool s_pending; /* 只在 t_link 里读写(初始化之后) */
|
||
static esp_timer_handle_t s_backstop;
|
||
|
||
static void status_edit(void (*fn)(ota_status_t *, uint32_t), uint32_t v)
|
||
{
|
||
ota_status_t st;
|
||
ota_status_get(&st);
|
||
fn(&st, v);
|
||
ota_status_set(&st);
|
||
}
|
||
|
||
static void set_got(ota_status_t *st, uint32_t v) { st->got = v; }
|
||
|
||
static void schedule_reboot(bool rollback)
|
||
{
|
||
s_rollback = rollback;
|
||
uint32_t at = hal_time_now_ms() + REBOOT_DELAY_MS;
|
||
s_reboot_at = (at == 0) ? 1 : at;
|
||
}
|
||
|
||
/* ota.h 的 reboot 回调:已就绪的同一镜像再来一条带 reboot 的命令。执行器任务里调。 */
|
||
static void cb_reboot(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
evlog_put(0, LOG_EV_OTA, OTA_EV_REBOOT, 0, 0);
|
||
schedule_reboot(false);
|
||
}
|
||
|
||
/* ota.h 的 start 回调:只入队。执行器任务里调。 */
|
||
static hal_err_t cb_start(void *ctx, const ota_req_t *req)
|
||
{
|
||
(void)ctx;
|
||
return (s_q != NULL && xQueueSend(s_q, req, 0) == pdTRUE) ? HAL_OK : HAL_ERR_STATE;
|
||
}
|
||
|
||
/* 下载 + 校验 + 写 + 切分区。返回 OTA_E_NONE = 已就绪。 */
|
||
static ota_err_t run(const ota_req_t *req)
|
||
{
|
||
static uint8_t buf[CHUNK]; /* 静态:只有本任务用 */
|
||
|
||
const esp_partition_t *running = esp_ota_get_running_partition();
|
||
const esp_partition_t *part = esp_ota_get_next_update_partition(NULL);
|
||
if (part == NULL || part == running) {
|
||
return OTA_E_NOSLOT;
|
||
}
|
||
if (req->size > part->size) {
|
||
return OTA_E_SIZE;
|
||
}
|
||
/* 上一次 READY 的是另一个镜像:先把启动分区指回自己,下到一半断电也不会去启动半个镜像。 */
|
||
const esp_partition_t *boot = esp_ota_get_boot_partition();
|
||
if (boot != NULL && boot != running) {
|
||
(void)esp_ota_set_boot_partition(running);
|
||
}
|
||
|
||
const esp_http_client_config_t hc = {
|
||
.url = req->url,
|
||
.timeout_ms = HTTP_TIMEOUT_MS,
|
||
.keep_alive_enable = true,
|
||
};
|
||
esp_http_client_handle_t cli = esp_http_client_init(&hc);
|
||
if (cli == NULL) {
|
||
return OTA_E_HTTP;
|
||
}
|
||
ota_err_t err = OTA_E_NONE;
|
||
esp_ota_handle_t h = 0;
|
||
bool begun = false;
|
||
mbedtls_sha256_context sha;
|
||
mbedtls_sha256_init(&sha);
|
||
|
||
if (esp_http_client_open(cli, 0) != ESP_OK) {
|
||
err = OTA_E_HTTP;
|
||
goto out;
|
||
}
|
||
const int64_t len = esp_http_client_fetch_headers(cli);
|
||
const int code = esp_http_client_get_status_code(cli);
|
||
if (code != 200) {
|
||
ESP_LOGE(TAG, "HTTP %d", code);
|
||
err = OTA_E_HTTP;
|
||
goto out;
|
||
}
|
||
if (len > 0 && len != (int64_t)req->size) { /* 0 / -1 = 没给 Content-Length(chunked),按字节数核 */
|
||
ESP_LOGE(TAG, "Content-Length %lld ≠ size %lu", (long long)len, (unsigned long)req->size);
|
||
err = OTA_E_SIZE;
|
||
goto out;
|
||
}
|
||
|
||
esp_err_t e = esp_ota_begin(part, OTA_WITH_SEQUENTIAL_WRITES, &h);
|
||
if (e != ESP_OK) {
|
||
ESP_LOGE(TAG, "esp_ota_begin: %s", esp_err_to_name(e));
|
||
/* 待验证的固件去覆盖备用槽,IDF 自己也会拒(ota.c 的 PENDING 已经先挡了一道) */
|
||
err = (e == ESP_ERR_OTA_ROLLBACK_INVALID_STATE) ? OTA_E_NOSLOT : OTA_E_FLASH;
|
||
goto out;
|
||
}
|
||
begun = true;
|
||
mbedtls_sha256_starts(&sha, 0);
|
||
|
||
uint32_t got = 0;
|
||
uint32_t shown = 0;
|
||
while (got < req->size) {
|
||
const uint32_t want = (req->size - got < CHUNK) ? (req->size - got) : CHUNK;
|
||
const int n = esp_http_client_read(cli, (char *)buf, (int)want);
|
||
if (n < 0) {
|
||
err = OTA_E_HTTP;
|
||
goto out;
|
||
}
|
||
if (n == 0) {
|
||
break; /* 连接收了:字节数不够由下面判 */
|
||
}
|
||
mbedtls_sha256_update(&sha, buf, (size_t)n);
|
||
e = esp_ota_write(h, buf, (size_t)n);
|
||
if (e != ESP_OK) {
|
||
ESP_LOGE(TAG, "esp_ota_write: %s", esp_err_to_name(e));
|
||
err = OTA_E_FLASH;
|
||
goto out;
|
||
}
|
||
got += (uint32_t)n;
|
||
if (got - shown >= STATUS_EVERY) {
|
||
shown = got;
|
||
status_edit(set_got, got);
|
||
}
|
||
vTaskDelay(1); /* 把 cache 关掉的空档摊开,巡检不至于连着晚好几拍 */
|
||
}
|
||
status_edit(set_got, got);
|
||
if (got != req->size) {
|
||
ESP_LOGE(TAG, "只收到 %lu / %lu 字节", (unsigned long)got, (unsigned long)req->size);
|
||
err = OTA_E_SIZE;
|
||
goto out;
|
||
}
|
||
|
||
uint8_t digest[32];
|
||
mbedtls_sha256_finish(&sha, digest);
|
||
if (memcmp(digest, req->sha256, sizeof(digest)) != 0) {
|
||
char hex[65];
|
||
ota_sha256_hex(digest, hex);
|
||
ESP_LOGE(TAG, "sha256 对不上:算出来 %s", hex);
|
||
err = OTA_E_HASH;
|
||
goto out;
|
||
}
|
||
|
||
begun = false; /* esp_ota_end 无论成败都释放句柄 */
|
||
e = esp_ota_end(h);
|
||
if (e != ESP_OK) {
|
||
ESP_LOGE(TAG, "esp_ota_end: %s", esp_err_to_name(e));
|
||
err = (e == ESP_ERR_OTA_VALIDATE_FAILED) ? OTA_E_IMAGE : OTA_E_FLASH;
|
||
goto out;
|
||
}
|
||
/* 哈希对得上只说明「是服务器想发的那个文件」;再核一眼是不是本工程的固件 ——
|
||
服务器那头传错一个 .bin(别的项目的),ESP 镜像校验照样过。 */
|
||
esp_app_desc_t d;
|
||
if (esp_ota_get_partition_description(part, &d) != ESP_OK ||
|
||
strncmp(d.project_name, esp_app_get_description()->project_name, sizeof(d.project_name)) != 0) {
|
||
ESP_LOGE(TAG, "不是本工程的固件(project_name「%.32s」)", d.project_name);
|
||
err = OTA_E_IMAGE;
|
||
goto out;
|
||
}
|
||
e = esp_ota_set_boot_partition(part);
|
||
if (e != ESP_OK) {
|
||
ESP_LOGE(TAG, "esp_ota_set_boot_partition: %s", esp_err_to_name(e));
|
||
err = OTA_E_BOOT;
|
||
goto out;
|
||
}
|
||
ESP_LOGI(TAG, "新固件 %.32s 已就绪(%s @0x%lx),%s", d.version, part->label,
|
||
(unsigned long)part->address, req->reboot ? "马上重启" : "下次复位生效");
|
||
|
||
out:
|
||
if (begun) {
|
||
esp_ota_abort(h);
|
||
}
|
||
mbedtls_sha256_free(&sha);
|
||
esp_http_client_close(cli);
|
||
esp_http_client_cleanup(cli);
|
||
return err;
|
||
}
|
||
|
||
static void task_ota(void *arg)
|
||
{
|
||
(void)arg;
|
||
ota_req_t req;
|
||
for (;;) {
|
||
if (xQueueReceive(s_q, &req, portMAX_DELAY) != pdTRUE) {
|
||
continue;
|
||
}
|
||
ESP_LOGI(TAG, "开始下载 %s(%lu 字节)", req.url, (unsigned long)req.size);
|
||
evlog_put(0, LOG_EV_OTA, OTA_EV_START, 0, (int32_t)req.size);
|
||
const ota_err_t err = run(&req);
|
||
|
||
ota_status_t st;
|
||
ota_status_get(&st);
|
||
st.err = err;
|
||
if (err == OTA_E_NONE) {
|
||
st.phase = OTA_READY;
|
||
ota_status_set(&st);
|
||
evlog_put(0, LOG_EV_OTA, OTA_EV_READY, 0, (int32_t)req.size);
|
||
if (req.reboot) {
|
||
evlog_put(0, LOG_EV_OTA, OTA_EV_REBOOT, 0, (int32_t)req.size);
|
||
schedule_reboot(false);
|
||
}
|
||
} else {
|
||
st.phase = OTA_IDLE;
|
||
ota_status_set(&st);
|
||
evlog_put(0, LOG_EV_OTA, OTA_EV_FAIL, 0, (int32_t)err);
|
||
ESP_LOGE(TAG, "OTA 失败:%s", ota_err_str(err));
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 兜底:判据的正常路径在 t_link 里;app_main 卡死 / t_link 起不来时它来回滚。
|
||
esp_timer 任务里调,事件来不及落盘 —— 这条路本来就只在出大事时走。
|
||
⚠️ **故意不走 warm_restart()**:走到这里说明这份固件卡死了,它的「开着哪几路」不可信 ⇒ 按非计划复位全关。 */
|
||
static void backstop_cb(void *arg)
|
||
{
|
||
(void)arg;
|
||
esp_ota_img_states_t s;
|
||
if (esp_ota_get_state_partition(esp_ota_get_running_partition(), &s) == ESP_OK &&
|
||
s == ESP_OTA_IMG_PENDING_VERIFY) {
|
||
ESP_LOGE(TAG, "兜底定时器:新固件 %u s 内没自证健康,回滚",
|
||
(unsigned)((OTA_HEALTH_MS + BACKSTOP_EXTRA_MS) / 1000u));
|
||
esp_ota_mark_app_invalid_rollback_and_reboot();
|
||
}
|
||
}
|
||
|
||
hal_err_t ota_esp_init(void)
|
||
{
|
||
ota_init(NULL, NULL, NULL);
|
||
ota_status_t st;
|
||
ota_status_get(&st);
|
||
|
||
const esp_partition_t *running = esp_ota_get_running_partition();
|
||
esp_ota_img_states_t state;
|
||
if (running != NULL && esp_ota_get_state_partition(running, &state) == ESP_OK) {
|
||
s_pending = (state == ESP_OTA_IMG_PENDING_VERIFY);
|
||
}
|
||
st.pending = s_pending;
|
||
|
||
const esp_partition_t *bad = esp_ota_get_last_invalid_partition();
|
||
esp_app_desc_t d;
|
||
if (bad != NULL && esp_ota_get_partition_description(bad, &d) == ESP_OK) {
|
||
snprintf(st.bad, sizeof(st.bad), "%.*s", (int)sizeof(st.bad) - 1, d.version);
|
||
ESP_LOGW(TAG, "%s 里的固件 %s 被判过无效(回滚过)", bad->label, st.bad);
|
||
}
|
||
ota_status_set(&st);
|
||
|
||
const esp_partition_t *next = esp_ota_get_next_update_partition(NULL);
|
||
ESP_LOGI(TAG, "运行在 %s @0x%lx%s;备用槽 %s", running ? running->label : "?",
|
||
running ? (unsigned long)running->address : 0ul,
|
||
s_pending ? "(⚠️ 待验证:" "120 s 内要连上 broker,否则回滚)" : "",
|
||
next ? next->label : "没有(分区表不是双槽)");
|
||
if (next != NULL && next->size != OTA_SLOT_BYTES) {
|
||
ESP_LOGE(TAG, "备用槽 %lu 字节 ≠ OTA_SLOT_BYTES %lu:ota.h 与 partitions.csv 没对上",
|
||
(unsigned long)next->size, (unsigned long)OTA_SLOT_BYTES);
|
||
}
|
||
|
||
if (s_pending) {
|
||
const esp_timer_create_args_t ta = { .callback = backstop_cb, .name = "ota_backstop" };
|
||
if (esp_timer_create(&ta, &s_backstop) == ESP_OK) {
|
||
esp_timer_start_once(s_backstop, (uint64_t)(OTA_HEALTH_MS + BACKSTOP_EXTRA_MS) * 1000u);
|
||
}
|
||
}
|
||
|
||
if (next == NULL) {
|
||
return HAL_ERR_STATE; /* 命令回 state:ota_init 没登记回调 */
|
||
}
|
||
s_q = xQueueCreate(1, sizeof(ota_req_t));
|
||
if (s_q == NULL ||
|
||
xTaskCreate(task_ota, "t_ota", 6144, NULL, task_desc(TASK_LINK)->prio, NULL) != pdPASS) {
|
||
return HAL_ERR_STATE;
|
||
}
|
||
ota_init(cb_start, cb_reboot, NULL);
|
||
ota_status_set(&st); /* ota_init 清了状态,把 pending / bad 放回去 */
|
||
return HAL_OK;
|
||
}
|
||
|
||
void ota_esp_boot_done(void) { s_boot_done = true; }
|
||
|
||
void ota_esp_poll(void)
|
||
{
|
||
const uint32_t now = hal_time_now_ms();
|
||
|
||
if (s_pending) {
|
||
netcfg_t cfg;
|
||
net_status_t ns;
|
||
netcfg_get(&cfg);
|
||
net_status_get(&ns);
|
||
const ota_health_in_t in = {
|
||
.pending = true,
|
||
.now_ms = now,
|
||
.boot_done = s_boot_done,
|
||
.wifi_set = cfg.ssid[0] != '\0',
|
||
.wifi_up = ns.wifi == NET_UP,
|
||
.mqtt_set = cfg.uri[0] != '\0',
|
||
.mqtt_up = ns.mqtt == NET_UP,
|
||
};
|
||
const ota_health_t h = ota_health(&in);
|
||
if (h == OTA_H_VALID) {
|
||
const esp_err_t e = esp_ota_mark_app_valid_cancel_rollback();
|
||
s_pending = false;
|
||
if (s_backstop != NULL) {
|
||
esp_timer_stop(s_backstop);
|
||
}
|
||
ota_status_t st;
|
||
ota_status_get(&st);
|
||
st.pending = false;
|
||
ota_status_set(&st);
|
||
evlog_put(0, LOG_EV_OTA, OTA_EV_VALID, 0, (int32_t)e);
|
||
ESP_LOGI(TAG, "新固件 %s 自证健康(%lu ms),取消回滚:%s", esp_app_get_description()->version,
|
||
(unsigned long)now, esp_err_to_name(e));
|
||
} else if (h == OTA_H_ROLLBACK && s_reboot_at == 0) {
|
||
s_pending = false; /* 不再判;下面到点回滚 */
|
||
evlog_put(0, LOG_EV_OTA, OTA_EV_ROLLBACK, 0, 0);
|
||
ESP_LOGE(TAG, "新固件 %u s 内没自证健康(WiFi %s / MQTT %s),回滚",
|
||
(unsigned)(OTA_HEALTH_MS / 1000u), net_state_str(ns.wifi), net_state_str(ns.mqtt));
|
||
schedule_reboot(true);
|
||
}
|
||
}
|
||
|
||
if (s_reboot_at != 0 && (int32_t)(now - s_reboot_at) >= 0) {
|
||
/* 计划内重启:目标固件认接管就锁住 EN、手机不断电;不认就五路全断一次(warm_esp.c)。 */
|
||
warm_restart(s_rollback);
|
||
}
|
||
}
|