Skip to content

概述 ​

OTA(大白话:不用拆机、不用按按键,直接把新固件"隔空"送进芯片换掉旧的)是产品出厂后的刚需。本教程基于 SDK 的 axk_uart_ota 串口 OTA 组件,演示如何给自己的工程接上 OTA 能力,并用电脑端的 USB 串口线(就是平时看日志的那根)完成一次完整的固件升级与验收。

这个组件的思路是:宿主编进应用里(大白话:OTA 功能是你程序的一部分,不是另刷的一个东西)。它平时完全放行控制台收发——你照常看日志、敲命令;只有 PC 发来合法帧头 A5 5A 01 时才接管 UART0,把新固件写进 flash 的另一块区域(非活动 bank),整包 MD5 校验通过后才切换过去重启。

用大白话讲:芯片的 flash 里预留了两块一样大的"房子"(双 bank)。你的程序住在其中一块,OTA 就是趁它还在运行时,把新程序(新家具)先搬进空着的那块,全部搬完并清点无误(MD5 校验)后,才把"门牌号"一改、重启住进新房——旧房原样保留。所以升级途中断电、复位都不会变砖:门牌没改,下次启动还是住旧房、跑旧程序,顶多这次白升了,重来一遍即可。

本教程基于安信可官方 SDK(Ai-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)的串口 OTA 组件 components/sys/axk_uart_ota/(PC 端工具在 tools/axk_uart_ota/)编写,组件自带说明见其 README.md。

🎯本页目标给自己的工程完成"接线三步"接入串口 OTA,并用 make ota 完成一次不按键、不拆机的整机升级与验收。
🧰前置条件① Ai-WB2 开发板一块(4MB flash,Type-C 数据线)② 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建。
🔗相关章节串口输出见 [UART(串口)](../basic/uart);分区表与 flash 分区概念见 [Flash 操作](flash);首次烧录流程可参考 [blog 日志](blog_log) 的烧录步骤。

①
进入示例工程

打开终端,进入本组件自带的最小示例工程(也可以直接照它改自己的工程):

cd ~/Ai-Thinker-WB2/applications/system/hello_ota

说明:cd 是"进入目录"的命令,~ 表示你的用户主目录。hello_ota 是一个"启动 OTA 宿主 + 常驻任务"的最小工程,后文所有 make 命令都要在这个目录里执行。

②
接线三步(把 OTA 加进工程)

给自己的工程接入串口 OTA,只需要三处改动:

第一步:组件清单里要有 mbedtls_lts(MD5 校验用它)。最新版组件的 app.mk 会自动补上 axk_uart_ota mbedtls_lts blmtd 三个组件,老工程自己在 Makefile 的 INCLUDE_COMPONENTS 里显式写上更稳妥:

INCLUDE_COMPONENTS += ... hosal mbedtls_lts lwip ...

第二步:proj_config.mk 末尾加一行 include(OTA 的全部构建期接线都由这一行完成):

include $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../../../components/sys/axk_uart_ota/axk_uart_ota_app.mk)

⚠️ 这一行不能写成 $(BL60X_SDK_PATH)/components/...——执行到它时 BL60X_SDK_PATH 还没被定义,会展开成空路径。 📌 位置有讲究:必须在 CONFIG_UART_OTA_* 那几个参数之后、project.mk 之前。组件启动时会用这些值算一个"配置指纹",值变了自动清产物强制重编;写早了它记下的就是默认值,等于指纹没生效。

第三步:main 里调用 axk_uart_ota_host_start(),见下一步。

💡 链接期的 --wrap 钩子(让 OTA 能"偷看"控制台收发)和 flash 目标改造,由组件自带的 Makefile.projbuild 自动完成,应用侧一行都不用写。

③
编写代码与两条运行铁律

应用侧只要启动宿主 + 保持常驻,核心代码:

代码 作用
axk_uart_ota_host_start() 启动串口 OTA 宿主;调用后不立即接管串口,平时完全放行控制台
xTaskCreate(app_task, "app", 1024, NULL, 8, NULL) 业务代码另起任务跑,让出 CPU 给 OTA 宿主
printf("build %s %s", __DATE__, __TIME__) 开机横幅里的编译时间戳——make ota-verify 靠它判断"新固件真的在跑"

三项拼起来就是一个最小可跑示例(点开看这个示例的完整代码):

📜 点击展开 hello_ota/main.c 示例代码
/*
 * hello_ota —— 串口 OTA 最小示例(axk_uart_ota 组件)
 *
 * 硬件:Ai-WB2 模组(4MB flash),USB 转串口接 UART0(IO16 TX / IO7 RX)
 *
 * 整个应用只做两件事:启动 OTA 宿主 + 起一个常驻任务。
 *
 * 两条硬性运行条件(组件 README 里 ✗/✓ 那张表):
 *   1. 不能忙等:main() 本身就是 SDK 起的一个任务,在里面写
 *      while (1) { delay_ms(500); } 会把 OTA 宿主饿死 ——
 *      宿主也是一个任务,同优先级下谁先让出谁挨饿,谁都拿不到 CPU。
 *   2. 不能自己复位:升级写到一半复位 = 整包作废,得从头再来一遍。
 *      (双 bank 设计保证不会变砖 —— 复位后旧 bank 照常启动 ——
 *        但"升级到一半重启"的体验很差,而且会白白多等一轮。)
 */
#include <stdio.h>
#include <stdint.h>
#include <FreeRTOS.h>
#include <task.h>
#include "axk_uart_ota.h"

static void app_task(void *arg)
{
    (void)arg;
    while (1) {
        /* 用 vTaskDelay 让出 CPU,不用忙等延时 */
        vTaskDelay(pdMS_TO_TICKS(2000));
        printf("hello_ota: app alive, waiting for OTA...\r\n");
    }
}

void main(void)
{
    int32_t ret;

    /* 开机横幅。`make ota-verify` 靠这一行里的 build 时间戳判断
       "新固件确实在跑"(升级前后各抓一次日志,比对时间戳是否变化),
       所以格式保持 build + __DATE__ + __TIME__(如 build Sep 29 2026 15:20:01)。 */
    printf("\r\n========================================\r\n");
    printf("  hello_ota - serial OTA minimal demo\r\n");
    printf("  build %s %s\r\n", __DATE__, __TIME__);
    printf("========================================\r\n");

    /* 启动串口 OTA 宿主。平时完全放行控制台收发,
       只有 PC 发来合法帧头(A5 5A 01)时才接管 UART0。 */
    ret = axk_uart_ota_host_start();
    if (ret != AXK_OK) {
        printf("hello_ota: axk_uart_ota_host_start() failed: %ld\r\n", (long)ret);
    }

    /* 另起任务后 main 返回,SDK 继续跑其它任务 */
    if (xTaskCreate(app_task, "app", 1024, NULL, 8, NULL) != pdPASS) {
        printf("hello_ota: create app task failed\r\n");
    }
}

两条运行铁律(组件 README 里 ✗/✓ 那张表):

✗ 错误写法 ✓ 正确写法
void main(){ host_start(); while(1){ ...; delay_ms(500); } } void main(){ host_start(); xTaskCreate(app_task, ...); },任务里用 aos_msleep/vTaskDelay
升级期间自己复位(如 helloworld 那种 11 秒倒计时 bl_sys_reset_por()) 升级期间保持常驻,不自复位

⚠️ 为什么不能忙等:main() 本身就是 SDK 起的一个任务,OTA 宿主也是一个任务——你在 main 里原地 delay_ms 循环,同优先级下宿主永远抢不到 CPU(被"饿死"),PC 怎么发都握不上手。 ⚠️ 为什么不能自复位:升级写到一半复位 = 本次会话作废,得从头再来一遍。双 bank 设计保证不会变砖(复位后旧 bank 照常启动),但白白浪费一轮时间。

💡 UART0 引脚:IO16 TX / IO7 RX(设备节点 /dev/ttyS0)。就算你把控制台关了(CONFIG_SYS_AOS_CLI_ENABLE=0),宿主也会自己注册 /dev/ttyS0 再打开,不影响使用。

④
编译工程(串行,不要 -jN)

在工程目录执行编译:

make

⚠️ 串口 OTA 工程一律串行编译,不要 -j8 这种并行! SDK 编译带 -save-temps=obj,并行时会互相覆盖 .s 中间文件——实测同一个工程 -j8 能出现上千条错误、-j1(默认串行)一条都没有。看到"莫名其妙的大片报错",先想想是不是多打了 -j8。

编译成功后生成固件 build_out/hello_ota.bin(带引导头 + RF 校准的整包镜像,这正是 OTA 要发的东西;裸 main.bin 不含这些,发了会"能启动但无线没了")。

⑤
首次烧录(ISP,唯一需要按键的一次)

首次必须先整片烧录一次(boot2 + 分区表 + 固件),之后才谈得上串口 OTA:

make flash p=/dev/ttyUSB0 b=921600

说明:p=/dev/ttyUSB0 是串口设备号,要换成你电脑上实际的串口(Windows 下形如 COM3);b=921600 是烧录波特率。

⏳ 烧录时按提示按住 BOOT 键再按一下 RST 进入下载模式,等待出现 [All Success] 即成功。这是整个流程中唯一需要按键的一次。

分区表必须是等长双 bank 的那一张(表在 tools/axk_uart_ota/partition/,由 CONFIG_UART_OTA_PARTITION 选):

档位 表文件 每 bank 大小 说明
2M partition_cfg_2M_uart_ota.toml 见文件内注释 2MB flash 模组专用
4M partition_cfg_4M_uart_ota.toml 约 0x1BE000(约 1784 KB) 4MB 通用档
4M_clock partition_cfg_4M_clock_uart_ota.toml 0x1B0000(1728 KB) AiPi-Clock 类产品(媒体分区大)

⚠️ make flash(ISP)与 make ota 必须用同一张表(工程里通过 AXK_FLASH_PARTITION 统一)。换表 = 搬动全部分区地址,必须重新整片 ISP 烧一次,否则会出现"OTA 升级后配置丢失甚至启动异常"。

⑥
升级(日常只用这一条命令)

接上 USB 串口线,执行:

make ota p=/dev/ttyUSB0

说明:这一条命令自动完成"编译 → 打包 OTA 镜像 → 通过串口发给板子 → 等待校验结果"全流程。默认波特率 921600(组件启动时已经把控制台主动切到这个速率,PC 侧不用额外设置)。

运行中你会看到上传进度,结束后打印 校验通过 和 端到端 x.x s,板子自动重启跑新固件。

⚠️ 串口独占:一个串口一次只能被一个程序使用。跑 make ota 前先关掉串口助手(以及上一次没退干净的进程),否则会报 device reports readiness to read but returned no data (device disconnected or multiple access on port?)——工具会扫 /proc 并提示是哪个进程占着口。

⑦
运行验证

升级完成后打开串口助手(波特率 921600),复位板子看启动横幅,两处都变了才算成功:

========================================
  hello_ota - serial OTA minimal demo
  build Sep 29 2026 17:35:02      ← ① 编译时间戳变成了"本次编译"的时间
========================================
[OTA] host ready: active_bank=1   ← ② 活动 bank 从 0 翻成了 1

也可以让工具自动验收(升级前后各抓一次横幅、自动比对时间戳):

make ota-verify p=/dev/ttyUSB0

✅ 预期结果:make ota 输出 校验通过;重启后启动横幅的 build 时间戳变成新编译时间、[OTA] host ready: active_bank= 发生翻转(0→1,下次升级再翻回 1→0),即验证通过。若握手不上、日志乱码或升级中断,见文末 FAQ。

代码执行流程 ​

从 PC 敲下 make ota 到新固件跑起来的完整流程如下:


本文 API 汇总 ​

axk_uart_ota_host_start(void) ​

启动串口 OTA 宿主:注册/打开 UART0(/dev/ttyS0)、创建嗅探任务。调用后不立即接管串口,平时完全放行控制台收发。

参数:无

返回值:成功返回 AXK_OK(0);失败返回负值错误码(如 AXK_E_PORT 表示串口打开失败)

axk_uart_ota_host_set_auto_reset(on) ​

设置"整包校验通过后是否自动切 bank 并复位"。默认 1(自动)。启动后只调用一次即可,每次升级会话都会重新生效。

参数:

  • on:1 = 校验通过后自动切 bank + 复位(默认);0 = 不自动复位

返回值:成功返回 AXK_OK;失败返回负值错误码

⚠️ 设为 0 时组件有自毁保护:校验通过后旧 bank 还在跑、但分区表已指向新 bank,此时若再次发起 OTA,会把正在运行的镜像擦掉。所以组件会在本次会话结束后拒绝新的升级、直到复位。正确用法:在 on_release 回调里做清理,然后尽快自己复位。

axk_uart_ota_host_set_task_guard(on_claim, on_release) ​

注册"接管/释放串口"回调,用来在升级期间暂停你自己的业务任务。这是最可靠的"让路"机制——根因是 bl_mtd 没有互斥锁,必须保证升级擦写 flash 时没人来碰。

参数:

  • on_claim:宿主接管 UART0 时调用(可传 NULL)
  • on_release:宿主释放 UART0 时调用(可传 NULL)

返回值:无

📌 两个都传 NULL 时,退回编译期的 AXK_UART_OTA_SUSPEND_OTHERS=1 方案(接管期间挂起其它任务)。

axk_uart_ota_port_get(void) ​

取当前注册的串口端口对象,调试/自检用。

参数:无

返回值:const axk_uart_ota_port_t * 端口对象指针

常见错误码(负值,源码定义在 include/axk_uart_ota.h):

错误码值含义
AXK_OK0成功
AXK_E_PARAM-2参数错误
AXK_E_STATE-3状态不允许(如上一次会话没正常收尾)
AXK_E_CRC-4数据帧 CRC 校验失败(线路噪声)
AXK_E_SEQ-5序号错乱(丢包/重传)
AXK_E_SIZE-6长度超限(固件比 bank 大)
AXK_E_MD5-7整包 MD5 不匹配(传输损坏,重发即可)
AXK_E_PORT-8串口打开失败
AXK_E_NOBUF-9缓冲区不足

make 目标与参数速查 ​

命令作用
make ota日常最常用:编译 → 打包 → 串口发送 → 等待校验结果
make ota-verify验收:升级前后各抓一次启动横幅,自动比对 build 时间戳
make ota-doctor环境自检:串口、分区表、波特率、工具链一把梭(不接板子也能跑)
make ota-image只把编译产物打包成 OTA 镜像,不发串口
make ota-check检查镜像与分区表是否匹配(大小、bank 布局)
make ota-v演练:只打印将要发送的内容,不碰串口
make ota-help打印全部目标与参数说明
make ota-test在 PC 上跑协议自测(x86,不接板子)
参数含义默认值
p=串口设备自动查找 /dev/ttyACM*、/dev/ttyUSB*
b=基波特率(控制台速率)CONFIG_UART_OTA_CONSOLE_BAUD(默认 921600)
payload=xz(压缩)/ raw(原始)xz
seconds=ota-verify 的观察窗口(秒)14
fast=覆盖快速波特率CONFIG_UART_OTA_FAST_BAUD

⚠️ 别把两个 b= 搞混:make flash 的 b= 是 ISP 烧录速率;make ota 的 b= 是 OTA 会话的基波特率——两者用途不同,恰好默认值都是 921600 而已。

配置参数表(proj_config.mk) ​

配置项默认作用
CONFIG_UART_OTA_ENABLE1是否编入 OTA 宿主(0 = 完全不要这个功能)
CONFIG_UART_OTA_AUTO_RESET1校验通过后自动切 bank + 复位
CONFIG_UART_OTA_SUSPEND_OTHERS1接管期间挂起其它任务(保证 flash 独占;代价约 +1KB flash/RAM)
CONFIG_UART_OTA_PARTITION4M选哪张等长双 bank 分区表
CONFIG_UART_OTA_FAST_BAUD921600组件启动即把控制台切到这个速率——PC 侧只需这一个速率;置 0 = 不改速率(保持 DTS 值)
CONFIG_UART_OTA_CONSOLE_BAUD921600协商失败/会话结束后要回到的速率

⚠️ 这些赋值行不能在行尾写注释——make 会把 # 前面的空格一起算进变量值,导致参数莫名其妙不生效。 📌 FAST_BAUD 置 0 的特殊情况:速率跟着 DTS 走,此时 PC 侧要按 DTS 值填 CONFIG_UART_OTA_CONSOLE_BAUD(或 make ota b=<该速率>),否则双方速率对不上、握手失败。 💡 改完这些值不用手动清编译产物:组件会算"配置指纹"(build_out/.axk_ota_cfg),值变了自动重建。 🔍 调试打印:[OTA] host ready / claimed / released 几行日志由 AXK_UART_OTA_DEBUG 控制(默认开,建议保留——验收判据之一就是 host ready: active_bank=);-DAXK_UART_OTA_DEBUG=0 可关闭。


完整代码 ​

以下为示例工程 hello_ota/main.c 完整源码:

📜 点击展开 hello_ota/main.c 完整代码
c
/*
 * hello_ota —— 串口 OTA 最小示例(axk_uart_ota 组件)
 *
 * 硬件:Ai-WB2 模组(4MB flash),USB 转串口接 UART0(IO16 TX / IO7 RX)
 *
 * 整个应用只做两件事:启动 OTA 宿主 + 起一个常驻任务。
 *
 * 两条硬性运行条件(组件 README 里 ✗/✓ 那张表):
 *   1. 不能忙等:main() 本身就是 SDK 起的一个任务,在里面写
 *      while (1) { delay_ms(500); } 会把 OTA 宿主饿死 ——
 *      宿主也是一个任务,同优先级下谁先让出谁挨饿,谁都拿不到 CPU。
 *   2. 不能自己复位:升级写到一半复位 = 整包作废,得从头再来一遍。
 *      (双 bank 设计保证不会变砖 —— 复位后旧 bank 照常启动 ——
 *        但"升级到一半重启"的体验很差,而且会白白多等一轮。)
 */
#include <stdio.h>
#include <stdint.h>
#include <FreeRTOS.h>
#include <task.h>
#include "axk_uart_ota.h"

static void app_task(void *arg)
{
    (void)arg;
    while (1) {
        /* 用 vTaskDelay 让出 CPU,不用忙等延时 */
        vTaskDelay(pdMS_TO_TICKS(2000));
        printf("hello_ota: app alive, waiting for OTA...\r\n");
    }
}

void main(void)
{
    int32_t ret;

    /* 开机横幅。`make ota-verify` 靠这一行里的 build 时间戳判断
       "新固件确实在跑"(升级前后各抓一次日志,比对时间戳是否变化),
       所以格式保持 build + __DATE__ + __TIME__(如 build Sep 29 2026 15:20:01)。 */
    printf("\r\n========================================\r\n");
    printf("  hello_ota - serial OTA minimal demo\r\n");
    printf("  build %s %s\r\n", __DATE__, __TIME__);
    printf("========================================\r\n");

    /* 启动串口 OTA 宿主。平时完全放行控制台收发,
       只有 PC 发来合法帧头(A5 5A 01)时才接管 UART0。 */
    ret = axk_uart_ota_host_start();
    if (ret != AXK_OK) {
        printf("hello_ota: axk_uart_ota_host_start() failed: %ld\r\n", (long)ret);
    }

    /* 另起任务后 main 返回,SDK 继续跑其它任务 */
    if (xTaskCreate(app_task, "app", 1024, NULL, 8, NULL) != pdPASS) {
        printf("hello_ota: create app task failed\r\n");
    }
}
📜 点击展开 proj_config.mk(OTA 相关部分)
makefile
#
# ── 串口 OTA 参数 ─────────────────────────────────────────────────────────
#
# ⚠ 这些赋值行不能在行尾写注释:make 会把 `#` 前的空格一起算进变量值。
# 组件都有同样的默认值,这里写明只是为了让读者一眼看到旋钮在哪:
#   ENABLE         1 = 编入 OTA 宿主(0 = 完全不要这个功能)
#   AUTO_RESET     1 = 整包校验通过后自动切 bank 并复位(推荐)
#   SUSPEND_OTHERS 1 = 接管期间挂起其它任务(保证擦写 flash 时没人来抢)
#   PARTITION      选哪张等长双 bank 分区表(tools/axk_uart_ota/partition/)
#   FAST_BAUD      组件启动即把控制台切到这个速率 —— PC 侧只需这一个速率;
#                  置 0 = 不改速率(保持 DTS 值,此时 PC 侧要按 DTS 填 b=)
#   CONSOLE_BAUD   协商失败/会话结束后要回到的速率,与 FAST_BAUD 保持一致
CONFIG_UART_OTA_ENABLE         := 1
CONFIG_UART_OTA_AUTO_RESET     := 1
CONFIG_UART_OTA_SUSPEND_OTHERS := 1
CONFIG_UART_OTA_PARTITION      := 4M
CONFIG_UART_OTA_FAST_BAUD      := 921600
CONFIG_UART_OTA_CONSOLE_BAUD   := 921600

#
# ── 串口 OTA 接线:一行 include ───────────────────────────────────────────
#
# 位置有讲究(必须在 project.mk 之前、在上面那组 CONFIG_UART_OTA_* 之后):
#   · 应用 Makefile 在 include project.mk 之前会读本文件 —— 组件清单与
#     编译期宏这时要就位;
#   · app.mk 要拿上面的值算"配置指纹",值变了就清产物强制重编,
#     写早了它记下的是组件默认值,指纹等于没做。
#   · 链接期的 --wrap 钩子与 flash 目标改造由组件自己的 Makefile.projbuild
#     完成,应用不用写。
include $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../../../components/sys/axk_uart_ota/axk_uart_ota_app.mk)

常见问题与踩坑提示 ​

⚠️ 编译一片红、错误上千条
原因:用了 make -j8 之类的并行编译,SDK 的 -save-temps=obj 让并行任务互相覆盖 .s 中间文件
解决:改用串行 make(不写 -jN)——实测同工程 -j8 报 1447 条错误、-j1 一条都没有

⚠️ 升级成功,但新固件"无线没了 / 起不来"
原因:把裸 main.bin 当 OTA 载荷发了——它缺少 4KB 引导头和 RF 校准段
解决:OTA 必须发整包镜像(build_out/<工程名>.bin,bflb_iot_tool --build 的产物)。用 make ota 会自动打包,不会犯这个错;单发镜像前可用 make ota-check 检查

⚠️ 报 multiple access on port? / 串口打不开
原因:串口被别的程序占用(串口助手没关、上次进程没退干净)
解决:关掉占用程序再跑;工具的报错里会直接给出占用的 pid 和命令行,照着关即可

⚠️ 握手不上 / 日志全是乱码
原因:PC 侧波特率与板子对不上。默认组件启动就会把控制台切到 921600;但若你把 CONFIG_UART_OTA_FAST_BAUD 置了 0,速率就跟着 DTS 走
解决:默认配置直接 make ota 即可(两端都是 921600);改过 FAST_BAUD=0 的,PC 侧要按 DTS 值传 b=<该速率>,否则会话失败后板子会退回 CONFIG_UART_OTA_CONSOLE_BAUD

⚠️ 升级到一半断电/复位,会变砖吗?
原因:担心写坏正在运行的固件
解决:不会。新固件写的是"非活动 bank",整包 MD5 校验通过前不切换分区表;复位后旧 bank 照常启动。重新 make ota 再升一次即可

⚠️ 升级过程中板子周期性重启 / 会话总是中断
原因:① 应用在 main 里忙等,把 OTA 宿主"饿死";② 应用有自己的定时复位(如 helloworld 的 11 秒倒计时 bl_sys_reset_por())
解决:业务代码放进独立任务、用 vTaskDelay/aos_msleep 让出 CPU;升级期间去掉或推迟一切自复位逻辑

⚠️ make ota 报尺寸/分区不匹配
原因:分区表不是等长双 bank,或固件超过了单 bank 容量(4M_clock 档 App 上限约 1723 KB),或 ISP 与 OTA 用了不同的表
解决:确认 CONFIG_UART_OTA_PARTITION 对应的表是等长双 bank;make flash 与 make ota 用同一张表(工程里经 AXK_FLASH_PARTITION 统一)。换表必须重新整片 ISP 烧一次

运行自检

make ota 跑完输出 校验通过 与 端到端 x.x s;板子重启后启动横幅的 build 时间戳变成新编译时间、[OTA] host ready: active_bank= 从 0 翻到 1(下次升级再翻回去),即串口 OTA 全链路验证通过。

遇到问题?

如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

Released under the MIT License. Build Time 2026-09-30 17:31:25