概述
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。
make ota 完成一次不按键、不拆机的整机升级与验收。打开终端,进入本组件自带的最小示例工程(也可以直接照它改自己的工程):
cd ~/Ai-Thinker-WB2/applications/system/hello_ota
说明:
cd是"进入目录"的命令,~表示你的用户主目录。hello_ota是一个"启动 OTA 宿主 + 常驻任务"的最小工程,后文所有make命令都要在这个目录里执行。
给自己的工程接入串口 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再打开,不影响使用。
在工程目录执行编译:
make
⚠️ 串口 OTA 工程一律串行编译,不要
-j8这种并行! SDK 编译带-save-temps=obj,并行时会互相覆盖.s中间文件——实测同一个工程-j8能出现上千条错误、-j1(默认串行)一条都没有。看到"莫名其妙的大片报错",先想想是不是多打了-j8。
编译成功后生成固件 build_out/hello_ota.bin(带引导头 + RF 校准的整包镜像,这正是 OTA 要发的东西;裸 main.bin 不含这些,发了会"能启动但无线没了")。
首次必须先整片烧录一次(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方案(接管期间挂起其它任务)。
常见错误码(负值,源码定义在 include/axk_uart_ota.h):
| 错误码 | 值 | 含义 |
|---|---|---|
AXK_OK | 0 | 成功 |
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_ENABLE | 1 | 是否编入 OTA 宿主(0 = 完全不要这个功能) |
CONFIG_UART_OTA_AUTO_RESET | 1 | 校验通过后自动切 bank + 复位 |
CONFIG_UART_OTA_SUSPEND_OTHERS | 1 | 接管期间挂起其它任务(保证 flash 独占;代价约 +1KB flash/RAM) |
CONFIG_UART_OTA_PARTITION | 4M | 选哪张等长双 bank 分区表 |
CONFIG_UART_OTA_FAST_BAUD | 921600 | 组件启动即把控制台切到这个速率——PC 侧只需这一个速率;置 0 = 不改速率(保持 DTS 值) |
CONFIG_UART_OTA_CONSOLE_BAUD | 921600 | 协商失败/会话结束后要回到的速率 |
⚠️ 这些赋值行不能在行尾写注释——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 完整代码
/*
* 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 相关部分)
#
# ── 串口 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

