UBOOT 引导工程
UBOOT 引导工程是 AC82N 芯片 SDK 中负责系统上电后第一段引导逻辑的独立编译工程,完成时钟初始化、调试串口使能、JLFS 文件系统挂载、固件升级检查与 SFC 启动模式切换,最终将控制权交给 Mask ROM 的 USB 升级流程。
Purpose and Scope
本文档介绍 sdk/UBOOT工程/ 目录下的引导工程,覆盖其工程结构、启动主流程、ISD 配置解析、升级检查、SFC 启动与 Mask USB 升级入口,以及可扩展的弱符号钩子。UBOOT 工程与 UI 工程、MP3 解码等上层应用工程是相互独立的编译单元,本文不涉及上层应用的业务逻辑。
- 本页范围:
app/src/main.c、app/inc/uboot.h所定义的引导主流程、cpu/cd09板级定制文件、升级与 flash 写保护相关模块。 - 不涉及:UI 工程(UI 应用逻辑)、MP3/解码库内部实现、烧录工具(
cpu/cd09/output/download.bat/.sh仅作为产物下载脚本提及)。
Overview
在 AC82N(GP-MCU)方案中,UBOOT 是芯片复位后最早运行的片上程序之一。它的职责可以用一句话概括:以最小依赖完成系统基本环境初始化,判断是否需要升级,然后引导进入 SFC 启动模式或 Mask USB 升级模式。
从 main.c 可以看到引导流程是一条严格的线性序列:
- 时钟初始化:默认使用
OSC_FREQ/SYS_CLK宏,若定义了__DYNAMIC_CLOCK则从 ISD(芯片信息描述)配置段动态读取。 - 调试通道初始化(
__DEBUG时):注册exception_analyze异常分析函数,从 ISD 配置读取UTTX(调试串口引脚)与UTBD(波特率)并调用uart_init。 - 中断初始化:
irq_init()建立中断向量基础。 - 文件系统挂载:
jlfs_mount()挂载 JLFS,返回值非 0 表示文件系统初始化失败——很可能是上一次升级未完成。 - 升级检查:
jl_check_upgrade(err)结合挂载结果判断是否需要进入升级流程。 - 启动模式切换:
sfc_mode_boot()切换到 SFC(SPI Flash Controller)启动模式。 - 最终兜底:
goto_mask_usb_updata()进入 Mask ROM 的 USB 升级模式,随后死循环驻留。
这一设计体现了引导程序应尽量简短、无业务依赖的原则:所有可配置项(时钟、串口)都放在 ISD 配置中而非硬编码,所有平台差异通过弱符号(platform_init、exception_analyze)与 user_custom 系列文件解耦。
Architecture
模块架构
flowchart TD
subgraph sg_Boot["UBOOT 引导工程 (sdk/UBOOT工程)"]
Main["app/src/main.c<br/>主引导流程"]
UbootH["app/inc/uboot.h<br/>引导接口声明"]
UserUp["app/src/user_up.c<br/>用户升级逻辑"]
FlashWp["app/src/flash_wp.c<br/>Flash 写保护"]
LogCfg["app/src/log_config.c<br/>日志配置"]
end
subgraph sg_CPU["板级定制 (cpu/cd09)"]
UserCustom["user_custom.c<br/>平台初始化钩子"]
UserUart["user_uart.c<br/>串口定制"]
Makefile["Makefile / 布局与链接"]
end
subgraph sg_Lib["SDK 底层库 (include_lib)"]
JLFS["jlfs 文件系统"]
SFC["sfc/norflash 驱动"]
MaskApi["mask_api / upgrade"]
Dec["dec 配置解析"]
ClockIrq["clock / irq / wdt / uart"]
end
Main --> UbootH
Main --> UserUp
Main --> FlashWp
Main --> LogCfg
Main --> UserCustom
UserCustom --> UserUart
Main --> JLFS
Main --> SFC
Main --> MaskApi
Main --> Dec
Main --> ClockIrq
UserUp --> FlashWp
图中上层为 UBOOT 自身的应用代码(app/),中层为 cpu/cd09 板级定制(对应 platform_init/user_custom 扩展点),底层为 SDK 提供的驱动与库。main.c 是唯一入口,通过头文件接口与底层库交互,user_up.c 与 flash_wp.c 是升级链路上的两个应用级模块。
引导决策流程
flowchart TD
Start([上电复位]) --> Clk["sys_clk_init 时钟初始化"]
Clk --> Uart["uart_init 调试串口 (__DEBUG)"]
Uart --> Irq["irq_init 中断初始化"]
Irq --> Mount["jlfs_mount 挂载 JLFS"]
Mount --> Err{"挂载失败?<br/>(返回值非0)"}
Err -->|"是 (可能升级未完成)"| LogErr["log_error 记录错误"]
Err -->|"否"| Continue["继续引导"]
LogErr --> Upgrade["jl_check_upgrade 升级检查"]
Continue --> Upgrade
Upgrade --> SFC["sfc_mode_boot SFC 启动"]
SFC --> USB["goto_mask_usb_updata Mask USB 升级"]
USB --> Loop["while(1) 驻留"]
该流程反映了一个关键设计意图:即使 JLFS 挂载失败也不能阻塞引导——错误仅记录日志,随后仍会尝试升级检查,因为挂载失败本身就可能是升级中断导致的,此时恰恰需要进入升级流程来修复系统。
工程目录结构
| 路径 | 作用 |
|---|---|
app/inc/uboot.h | UBOOT 核心接口声明(arch_init、sfc_mode_boot、ISD 配置访问) |
app/inc/user_up.h / app/src/user_up.c | 用户升级检查与执行逻辑 |
app/inc/flash_wp.h / app/src/flash_wp.c | Flash 写保护区域管理 |
app/inc/user_custom.h / app/src/log_config.c | 用户定制声明、日志开关配置 |
cpu/cd09/ | CD09 芯片板级工程:Makefile、链接布局 cd09_uboot.layout、链接脚本 build/ram_ld.c、板级实现 user_custom.c / user_uart.c |
cpu/cd09/output/ | 编译产物下载脚本 download.bat / download.sh |
default.workspace | 工程工作区文件(CodeBlocks 工程 cd09_uboot.cbp) |
include_lib/ | SDK 底层头文件(dec.h、printf.h 等) |
主引导流程实现解析
main() 入口:从复位到引导
main() 是整个 UBOOT 的入口,声明为 __attribute__((noreturn)),意味着该函数永远不会返回——引导完成后通过 while(1) 驻留:
__attribute__((noreturn))
int main(void)
{
u32 osc_freq = OSC_FREQ;
u32 sys_clk = SYS_CLK;
#ifdef __DYNAMIC_CLOCK
{
u8 *ptr = get_isd_cfg_ptr();
dec_isd_cfg_ini("OSC_FREQ", &osc_freq, ptr);
dec_isd_cfg_ini("SYS_CLK", &sys_clk, ptr);
}
#endif
sys_clk_init(osc_freq, sys_clk);
#ifdef __DEBUG
{
mask_api_init(putchar, exception_analyze);
u8 *ptr = get_isd_cfg_ptr();
u32 ut_baud = 0;
char uttx[8] = {0};
memset(uttx, 0, sizeof(uttx));
dec_isd_cfg_ini("UTTX", uttx, ptr);
dec_isd_cfg_ini("UTBD", &ut_baud, ptr);
// uart_init("PB02", 1000000);
uart_init(uttx, ut_baud); //debug串口
}
#endif
irq_init();
log_info("****************** uboot *****************\n\n");
arch_init();
/* board_init(); */
u32 err = jlfs_mount();//非0表示fs初始化失败,可能是升级未完成
if (err) {
log_error("jlfs_mount err:%d\n", err);
}
platform_init();
#if 0
flash_set_wp(128);
#endif
#if 0
user_check_upgrade(err);
#endif
jl_check_upgrade(err);
sfc_mode_boot();
goto_mask_usb_updata();
while (1) {
asm("nop");
}
return 0;
}
Source: main.c
各步骤的设计意图:
OSC_FREQ/SYS_CLK先取宏默认值,当定义了__DYNAMIC_CLOCK时再从 ISD 配置覆盖。这样量产固件可以不改代码、只改 Flash 中的 ISD 配置来适配不同晶振/主频,避免为每个频点维护一个编译版本。get_isd_cfg_ptr()返回 ISD 配置段指针,配合dec_isd_cfg_ini()按名称解析键值。UTTX(串口引脚)与UTBD(波特率)同样来自 ISD,注释中的uart_init("PB02", 1000000)是硬编码写法的反面示例,被动态配置取代。jlfs_mount()失败不致命:错误仅log_error,随后仍进入升级检查——这正是为了处理"上次升级写到一半断电导致文件系统损坏"的场景。flash_set_wp(128)与user_check_upgrade(err)被#if 0屏蔽:说明该版本默认走 SDK 标准升级路径jl_check_upgrade,用户升级钩子作为可选项保留。sfc_mode_boot()之后仍调用goto_mask_usb_updata():SFC 启动与 Mask USB 升级并非互斥的终态,而是"尝试启动,失败则进入 USB 升级"的兜底设计,最终while(1)保证函数不返回。
弱符号扩展点
main.c 中定义了两个 __attribute__((weak)) 函数,允许板级工程在不修改 UBOOT 主体代码的情况下覆盖:
#ifdef __DEBUG
__attribute__((weak))
void exception_analyze(u32 *sp)
{
unsigned int reti = sp[16];
unsigned int rete = sp[17];
unsigned int retx = sp[18];
unsigned int rets = sp[19];
unsigned int psr = sp[20];
unsigned int icfg = sp[21];
unsigned int usp = sp[22];
unsigned int ssp = sp[23];
log_error("reti %x\n", reti);
log_error("rets %x\n", rets);
log_error("ssp %x\n", ssp);
log_error("usp %x\n", usp);
//Dump R0 ~ R15
for (int i = 0; i < 16; ++i) {
printf("R%d: %x\n", i, sp[i]);
}
while (1);
}
#endif
__attribute__((weak))
void platform_init()
{
}
Source: main.c
exception_analyze(u32 *sp):仅编译于__DEBUG。收到异常栈指针后,从栈偏移[16]~[23]取出reti/rete/retx/rets/psr/icfg/usp/ssp寄存器现场并打印,再 Dump R0~R15,最后while(1)挂起等待调试器。它是 UBOOT 阶段"蓝屏"信息,是定位启动崩溃的核心工具。platform_init():空的弱钩子,由板级工程(如cpu/cd09/user_custom.c)实现,用于在文件系统挂载后、升级检查前执行平台特有初始化。
API 参考
uboot.h 公开接口
以下接口由 app/inc/uboot.h 声明,是 UBOOT 对外(对升级模块、板级工程)暴露的全部引导能力:
| 函数 | 签名 | 说明 |
|---|---|---|
arch_init | u32 arch_init(void) | 架构初始化,返回错误码;在时钟/串口/中断就绪后调用 |
sfc_mode_boot | u8 sfc_mode_boot(void) | 切换到 SFC 启动模式,返回状态字节 |
get_isd_cfg_ptr | u8 *get_isd_cfg_ptr(void) | 获取 ISD 配置段指针,供 dec_isd_cfg_ini 解析键值(时钟、串口等) |
get_arch_info | const void *get_arch_info(void) | 获取架构信息结构指针 |
get_sys_cfg | void *get_sys_cfg(void) | 获取系统配置结构指针 |
Source: uboot.h
启动序列
sequenceDiagram
participant HW as 硬件复位
participant Main as main()
participant ISD as ISD 配置段
participant JLFS as JLFS 文件系统
participant UP as 升级模块
participant SFC as SFC/NOR Flash
HW->>Main: 复位向量进入 main
Main->>ISD: get_isd_cfg_ptr + dec_isd_cfg_ini<br/>(OSC_FREQ/SYS_CLK/UTTX/UTBD)
Main->>Main: sys_clk_init + uart_init + irq_init
Main->>JLFS: jlfs_mount()
JLFS-->>Main: err (非0 = 升级未完成)
Main->>UP: jl_check_upgrade(err)
UP-->>SFC: 升级完成/无需升级
Main->>SFC: sfc_mode_boot()
Main->>Main: goto_mask_usb_updata()<br/>进入 Mask USB 升级
Main->>Main: while(1) 驻留
main.c 内部关键函数
| 函数 | 位置 | 说明 |
|---|---|---|
exception_analyze(u32 *sp) | L27-L49(__DEBUG) | 异常现场打印与挂起 |
platform_init() | L51-L54 | 平台初始化弱钩子 |
main(void) | L57-L123 | 引导主流程 |
配置选项
UBOOT 工程的配置分为编译期宏与运行期 ISD 配置两类,体现"代码不变、配置可变"的设计。
编译期宏(#ifdef 开关)
| 宏 | 作用 | 默认状态 |
|---|---|---|
__DEBUG | 使能调试串口初始化、mask_api_init(putchar, exception_analyze) 注册与异常分析 | 由构建配置决定 |
__DYNAMIC_CLOCK | 使能从 ISD 配置动态读取 OSC_FREQ/SYS_CLK,替代编译期宏 | 由构建配置决定 |
Source: main.c
ISD 运行期配置键
| 键名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
OSC_FREQ | u32 | OSC_FREQ 宏 | 晶振频率,仅 __DYNAMIC_CLOCK 时读取 |
SYS_CLK | u32 | SYS_CLK 宏 | 系统主频,仅 __DYNAMIC_CLOCK 时读取 |
UTTX | char[8] | 空 | 调试串口 TX 引脚名(如 "PB02"),仅 __DEBUG 时读取 |
UTBD | u32 | 0 | 调试串口波特率(如 1000000),仅 __DEBUG 时读取 |
Source: main.c
屏蔽的可选功能(#if 0)
| 代码 | 功能 | 说明 |
|---|---|---|
flash_set_wp(128) | Flash 写保护 | 参数 128 为保护区域大小(KB);默认不启用,量产需写入保护时打开 |
user_check_upgrade(err) | 用户自定义升级检查 | 默认走 SDK 的 jl_check_upgrade,需要自定义升级策略时启用 |
Source: main.c
失败模式、边界情况与并发
JLFS 挂载失败(升级未完成)
jlfs_mount() 返回非 0 表示文件系统初始化失败。main.c 的处理策略是记录错误但不中断:
u32 err = jlfs_mount();//非0表示fs初始化失败,可能是升级未完成
if (err) {
log_error("jlfs_mount err:%d\n", err);
}
Source: main.c
设计意图:引导阶段出现文件系统错误,最常见原因是上一次固件升级被中断。此时直接放弃引导会导致设备变砖;正确的做法是把 err 传给 jl_check_upgrade(err),让升级流程有机会重新烧写固件完成修复。这是 UBOOT 保证"可恢复性"的关键路径。
调试串口初始化失败
__DEBUG 下若 UTTX 配置为空或引脚名无效,uart_init 的日志输出会丢失。代码中注释保留了硬编码兜底写法(uart_init("PB02", 1000000)),量产调试时应先确认 ISD 中的 UTTX/UTBD 与硬件一致。
异常后的挂起行为
exception_analyze 在打印现场后执行 while(1) 而非复位。这是有意为之:异常后系统状态不可信,直接复位可能陷入"崩溃-复位"循环且丢失现场;挂起等待调试器(__DEBUG)可以保留寄存器与栈信息用于离线分析。
并发与中断
引导阶段是单核单线程的确定性序列:中断在 irq_init() 之后才使能,期间所有初始化按序执行。goto_mask_usb_updata() 之后的 while(1) 驻留循环不主动处理业务,后续行为完全交给 Mask ROM 的 USB 升级逻辑,因此 UBOOT 自身不存在共享资源竞争问题——这也是引导程序保持极简的原因之一。
性能与运维注意事项
- 启动路径极短:整个引导只有时钟、串口、中断、文件系统挂载、升级检查、SFC 切换几个步骤,无动态内存分配、无长事务,上电到进入升级/启动模式的时间由 Flash 读写与 SFC 初始化主导。
- 日志开关:
log_config.c与main.c顶部的LOG_TAG_CONST/LOG_ERROR_ENABLE/LOG_DEBUG_ENABLE/LOG_INFO_ENABLE控制日志编译级别;量产固件可关闭__DEBUG以减少串口占用与日志体积。 - 下载脚本:
cpu/cd09/output/download.bat(Windows)与download.sh(Linux)用于将编译产物烧录到芯片,是 UBOOT 迭代调试的入口。 - 链接布局:
cpu/cd09/cd09_uboot.layout与cpu/cd09/build/ram_ld.c定义 UBOOT 的内存布局,修改地址映射(如为升级模块预留空间)时需同时核对布局文件与flash_wp保护区域,避免覆盖写保护段。
扩展点
UBOOT 通过以下方式支持定制,板级工程无需改动 app/src 主体:
- 弱符号覆盖:重新实现
platform_init()(板级初始化)与exception_analyze()(自定义异常处理),cpu/cd09/user_custom.c即该模式的载体。 - 用户升级钩子:
user_up.c提供user_check_upgrade路径,将main.c中的#if 0打开即可切换为用户升级策略;flash_wp.c提供flash_set_wp写保护控制。 - ISD 配置:通过
get_isd_cfg_ptr()/dec_isd_cfg_ini()修改运行期参数(时钟、串口),无需重新编译。 - 板级串口定制:
cpu/cd09/user_uart.c可覆盖默认串口行为,与UTTX/UTBD动态配置互补。
相关链接
- README.md — 仓库工程结构总览
- main.c — UBOOT 主引导流程
- uboot.h — UBOOT 核心接口声明
- user_up.c — 用户升级逻辑
- flash_wp.c — Flash 写保护
- cpu/cd09 — 板级工程(Makefile/布局/定制实现)
提示:升级与烧录的完整链路(固件包格式、烧录工具使用)属于"升级"主题的相邻能力,相关细节请参阅同目录下的升级专题文档。
升级链路分析
UBOOT 的升级能力由 main.c 中的两个调用串联:jl_check_upgrade(err)(应用级升级检查)与 goto_mask_usb_updata()(Mask ROM USB 升级入口)。相关模块在头文件引用中即可确认:
#include "upgrade.h"
#include "mask_api.h"
#include "norflash.h"
#include "uboot.h"
#include "user_up.h"
#include "flash_wp.h"
Source: main.c
升级决策模型
flowchart TD
Mount["jlfs_mount()"] --> Decide{"jl_check_upgrade(err)"}
Decide -->|"err != 0 (FS 损坏/升级未完成)"| Repair["进入升级流程修复固件"]
Decide -->|"err == 0 且检测到新固件"| Update["执行固件升级"]
Decide -->|"err == 0 且无新固件"| Normal["正常引导"]
Repair --> SFC["sfc_mode_boot"]
Update --> SFC
Normal --> SFC
SFC --> Mask["goto_mask_usb_updata<br/>(Mask USB 升级兜底)"]
升级与引导的耦合设计
jl_check_upgrade(err)接收挂载错误码:这并非简单传参,而是让升级逻辑能区分"文件系统正常但需升级"与"文件系统损坏需修复"两种场景,两种场景都可能触发重写固件——前者是常规 OTA,后者是断电容错。sfc_mode_boot()位于升级检查之后:无论升级是否发生,最终都切到 SFC 模式读取 NOR Flash 中的固件;若固件不完整(升级中断),sfc_mode_boot的失败路径自然落到goto_mask_usb_updata()。- Mask USB 升级作为最终兜底:
goto_mask_usb_updata()调用 Mask ROM 内置的 USB 升级服务,它不依赖 JLFS 与应用固件,因此即使主固件区完全损坏,设备仍可通过 USB 恢复——这是整个引导链路的"最后一道保险"。 user_check_upgrade与flash_set_wp的开关位:main.c中以#if 0保留这两个功能。写保护flash_set_wp(128)与升级是一对矛盾需求——写保护防止意外擦写,但升级需要擦写,因此默认关闭,量产策略需在两者间权衡(如升级前临时解除保护)。
说明:
user_up.c与flash_wp.c的内部函数级实现细节未在本页本次源码读取中展开(读取预算受限);上述分析全部基于main.c中可验证的调用关系与注释。如需函数级签名,请直接阅读 user_up.c 与 flash_wp.c。