AliOS Things 系统开发指南
编制日期 2026-10-05
面向 Cortex-M / RISC-V / C-Sky 的 AliOS Things 系统级开发规范 · 覆盖 Rhino 内核、CSI 硬件抽象、组件化配置、分级驱动加载、VFS 驱动、系统组件、调试、低功耗与 OTA · 系统总文档,驱动开发为其子章节
一句话结论:AliOS Things 的系统设计,本质是用「Rhino 内核 + CSI 硬件抽象 + yaml 组件依赖 + 分级驱动加载」把「无序的外设初始化」变成「可声明的顺序」。 它比 FreeRTOS 多了组件依赖解析与 VFS 统一设备接入,比 Linux 少了设备树与虚拟内存——代价是板级资源全部写死在代码里,改一颗料的引脚就要动 BSP。 线上故障最集中的三处:同一加载级别内的初始化顺序随机、中断上下文调用了阻塞 API、栈与堆被低估。管住这三处,系统稳定性解决八成。
这份文档解决什么
本文是 AliOS Things 系统级开发总文档:从环境搭建、内核原理、启动与加载机制、CSI 抽象与 BSP 移植、组件化配置、设备驱动,到系统组件、调试、性能与低功耗、可靠性、测试、OTA 与发布规范,构成一条完整链路。 驱动开发作为第 8 章子章节嵌入;专项器件(SPI NOR / SPI NAND / SD NAND 等)的具体时序与命令细节,仍以对应器件的设计指南为准,本文只讲「在这一层该如何把驱动挂进系统」。
使用约定
- API 与目录以 AliOS Things 3.x(3.1 / 3.3) 为主线,兼顾 2.x;两代差异明显处均单独标注。目标平台以 Cortex-M3/M4/M33 为主,RISC-V 与 C-Sky(平头哥)差异另行提示。
- 代码示例只保留系统层逻辑;具体寄存器操作统一写成
/* chip 层提供 */注释。 - 标 红线 为强制约束,标 警惕 为高频踩坑点,标 建议 为经验推荐值。
- 文中出现的默认数值(栈大小、优先级、阈值、目录名)以 SDK 模板与官方仓库为参考;不同版本目录名差异较大(如 2.x 顶层
board/在 3.3 收进hardware/),落地前以工程内实际目录为准。 - 芯片参数、时钟、外设行为一律以官方 datasheet 与 AliOS Things 官方文档 / 源码为准。
1 · 文档概述与系统架构
这一章解决三件事:这份文档覆盖到哪一层、AliOS Things 长什么样、以及它和 Linux / RT-Thread / FreeRTOS 的边界在哪。读完再决定后面该精读哪几章。
1.1 编写目的、适用范围与读者
本指南面向基于 AliOS Things 做系统级开发的工程团队:把一颗裸的 MCU 变成可量产、可升级、可运维的物联网设备。它要回答的不是「某个寄存器怎么写」,而是「一堆驱动和组件如何按正确顺序长成一个系统」。
| 维度 | 本指南覆盖 | 本指南不覆盖 |
|---|---|---|
| 目标读者 | 系统 / BSP 工程师、驱动工程师、应用与云端对接工程师、测试与发布负责人 | 只写业务逻辑、不接触系统的上层应用开发 |
| 硬件范围 | 无 MMU 的 MCU:Cortex-M0/M3/M4/M33、RISC-V、C-Sky、Xtensa;linuxhost 仿真 | 带 MMU 的 Cortex-A 应用处理器(走 Linux 分支) |
| 软件范围 | Rhino 内核、CSI / HAL、BSP 移植、组件化构建、VFS 驱动、系统组件、调试、OTA、发布 | 单个器件的电气与命令时序(见各器件设计指南) |
| 文档定位 | 系统总文档;驱动开发是其第 8 章子章节 | 云端平台侧配置与业务规则(见阿里云 IoT 平台文档) |
1.2 版本基线、内核与支持平台
AliOS Things 有两条实际在用的代际:2.x(Rhino 内核 + 顶层 board/ 与 platform/mcu/ + 组件 .mk 描述)与 3.x(3.1 / 3.3,统一 VFS 接入、yaml 构建、内核对象统一管理、轻应用框架)。本指南以 3.x 为主、2.x 兼容,两代差异较大处显式标注。
| 基线项 | 说明 | 对开发的影响 |
|---|---|---|
| 内核 | Rhino RTOS:小体积硬实时,支持静态与动态对象分配、多内存区域、work queue | 内核特性靠 k_config.h 裁剪,改配置不改源码 |
| 构建 | 2.x:组件各自 .mk;3.x:组件各自 package.yaml,构建系统解析依赖图 | 增删功能 = 增删 yaml 依赖,不手写 Makefile |
| 设备接入 | 3.3 起统一走 VFS,应用一律按标准文件方式访问 /dev 下节点 | 驱动必须注册成 VFS 节点才能被标准方式访问 |
| 驱动框架 | 对上给 AOS API,对下给原子驱动原语,复杂逻辑封装在驱动子系统内 | 驱动开发者写的是「原语 + 注册」,不是裸读写 |
| 调试 | 栈回溯解析率提升、新增 printk、cpuusage 与诊断命令增强 | 崩溃定位不必每次都接仿真器 |
| 官方硬件 | HaaS100、HaaS200、HaaS EDU K1 等评估板 | 新项目优先复用已适配板级,再改 board 层 |
新项目一律从 3.3 之后的基线起:yaml 构建、统一 VFS、轻应用三件事能显著降低后期维护成本。已有 2.x 老项目不要中途升代际——目录结构(顶层 board/ 收进 hardware/)与构建描述(.mk 换 package.yaml)都会变,属于重写级改造。若必须升,走第 16 章的迁移发布流程。
1.3 术语与缩写
| 术语 | 全称 / 含义 | 出现场景 |
|---|---|---|
| AOS | AliOS Things 的简称,也是 aos_ API 前缀的来源 | 所有 aos_* 接口、aos make 命令 |
| Rhino | AliOS Things 自研的实时内核 | 任务、IPC、内存、定时器 |
| krhino | Rhino 内核原生 API 前缀,返回 kstat_t | 需要极致控制或读内核源码时 |
| CSI | Chip System Interface,芯片系统接口(硬件抽象层接口族) | GPIO / UART / SPI / I2C / ADC / DMA 等 |
| HAL | Hardware Abstraction Layer,更高一层的模块级适配(WiFi、Flash) | 网络与存储子系统适配 |
| VFS | Virtual File System,虚拟文件系统,把设备统一成 /dev 下节点 | 驱动注册、open/read/write/ioctl |
| KV | Key-Value 持久化键值存储 | 保存配网信息、序列号、运行参数 |
| Yloop | AliOS 的事件循环框架,主任务默认实例 | 应用入口、事件驱动编程 |
| SAL | Socket Abstract Layer,套接字抽象层 | 多模网络统一 socket |
| uData | 传感器管理框架,统一传感器数据模型 | 加速度计、温湿度等器件接入 |
| ID2 | IoT Device ID,设备身份认证与密钥体系 | 安全连接、安全存储、安全 OTA |
| LinkSDK | 阿里云物联网平台设备端 SDK | MQTT 上云、物模型、设备影子 |
| OTA | Over-The-Air 固件升级,含差分与 A/B 回滚 | 量产设备远程维护 |
| 多 bin / 轻应用 | 应用与系统分离编译,JS / MicroPython 脚本引擎 | 小镜像增量升级、脚本化业务逻辑 |
| 分级加载 | 驱动按 9 个等级在链接段内自动排序初始化 | 第 4 章核心机制 |
| package.yaml | 组件的声明式描述文件(名称、版本、依赖、源文件、宏) | 第 7 章核心机制 |
1.4 五层架构总览
AliOS Things 采用分层架构 + 组件架构双视角:分层决定「谁能调用谁」,组件化决定「编译时怎么拼装」。两者叠加的效果是——上层只依赖 API 契约,下层只保证原语正确,中间靠构建系统把需要的组件拼进来。
- ⑤ 应用与解决方案层:示例工程、业务应用、轻应用脚本、云端接入。一个 solution 组件可以依赖多个 board 组件,构建时由
board_name指定本次用哪块板。 - ④ 通用组件层:OTA、uData、uDisplay、netmgr、ulog、cli 等增值中间件。它们与业务平级,靠组件依赖被拉进固件,靠加载等级或显式 init 挂进系统。
- ③ AOS API 与系统服务层:应用唯一应当直接依赖的一层。VFS 把设备变成文件,KV 提供持久化,Yloop 提供事件循环。
- ② 内核层 Rhino:任务、调度、IPC、内存、定时器、对象管理。3.3 起对任务 / 互斥量 / 信号量做一致性对象管理,可维护性与可扩展性明显提升。
- ① 硬件抽象与板级层:CSI 接口族屏蔽寄存器差异,arch 层屏蔽 CPU 差异,board 层承载引脚、时钟与分区表。
1.5 AOS API 的三层封装
一个容易踩的坑是:同一件事在 AliOS Things 里有三套 API。混用不会报错,但会让代码在换内核或升版本时碎掉。
业务与组件代码只用 aos_ 前缀或 POSIX 层;只有三类代码允许碰 krhino_:内核自身、BSP 里追求极致开销的路径、以及读内核源码定位问题的调试代码。
原因很实在:aos_ 是版本间承诺兼容的官方接口,krhino_ 是内核实现细节,代际升级时签名与语义都可能调整。
1.6 与 Linux / RT-Thread / FreeRTOS 的差异
把 AliOS Things 放进坐标系里看,它的独特性主要在三处:无设备树、分级加载、yaml 组件依赖。其余部分与主流 RTOS 大同小异。
| 项目 | AliOS Things | Linux 内核 | RT-Thread | FreeRTOS |
|---|---|---|---|---|
| 内核形态 | Rhino 实时内核,硬实时、小体积 | 宏内核,需 MMU(Cortex-A) | 类 UNIX 分层 RTOS | 极简调度内核 |
| 硬件描述 | 无 DTS,板级静态配置 | 设备树 DTS / DTSI | 无 DTS(少数 BSP 用) | 无,全部写在代码里 |
| 驱动模型 | CSI + VFS 统一接入 + 分级加载 | 总线模型 + probe 匹配 | I/O 设备模型 rt_device | 无统一模型,各写各的 |
| 工程管理 | package.yaml 声明依赖 | Kconfig + Makefile | SCons + Kconfig | 手写 Makefile / CMake |
| 构建工具 | aos-cube / aos make、HaaS Studio | make / bitbake | scons / RT-Thread Studio | IDE 或 CMake |
| 文件系统 | LittleFS / FatFS / KV,轻量 | EXT4 / UBIFS / SquashFS | DFS + elm-FatFS / LittleFS | 需外挂组件 |
| 云与生态 | LinkSDK / OTA / 阿里云原生整合 | 需自行移植 SDK | 软件包中心,云接入自选 | 需自行移植 SDK |
| 驱动故障域 | 驱动多在系统态;部分平台支持隔离 | 驱动全在内核态,崩即 panic | 全在内核态 | 全在内核态 |
要做连云 + 量产 OTA 的 MCU 设备,AliOS Things 的组件与云服务整合能省掉大量自研;要带屏、协议栈复杂、依赖丰富驱动生态的 MCU 项目,RT-Thread 的软件包更全;要极致精简或已有成熟代码栈,FreeRTOS 心智负担最小;上了 Cortex-A、要跑复杂业务,直接 Linux。
1.7 阅读路线图与本系列文档的关系
- 先看第 1、2 章:确认基线版本、把环境跑通、知道代码该放哪个目录。哪怕只是照抄 helloworld 示例,也要自己编译烧录一遍。
- 再看第 3、4 章:内核对象与启动 / 加载顺序。这两章决定了后面所有「为什么会卡住」类问题的定位速度。
- 做移植读第 5、6 章:CSI 接口族与移植四要素;做功能读第 7、9 章:组件依赖与系统组件。
- 写驱动读第 8 章:VFS 注册、open/ioctl 链路、并发与低功耗约束。专项器件的时序细节回到对应器件设计指南。
- 联调与上线读第 11、12、13 章:调试手段、资源与功耗、可靠性;交付读第 14、15、16 章:测试、OTA、发布。
本系列中《RT-Thread 系统开发指南》《RT-Thread 驱动开发指南》《FreeRTOS 系统开发指南》与本指南同属 RTOS 系列,章节结构对齐,便于横向对照;涉及具体存储器件(SPI NOR / SPI NAND / SD NAND / eMMC)的电路与命令细节,请查阅对应器件系列文档。
2 · 开发环境与工程结构
这一章的目标很具体:把示例工程编译、烧录、跑起来,并且知道每一行代码该放在哪个目录。环境没跑通之前,后面所有章节都只是纸上谈兵。
2.1 宿主机环境与依赖
推荐 Ubuntu 20.04 / 22.04(64 位)。AliOS Things 的构建体系基于 Python 脚本(aos-cube)与构建框架混合,对宿主机版本比较敏感。
# 1) 基础依赖
sudo apt-get update
sudo apt-get install -y build-essential git python3 python3-pip python3-setuptools \
libssl-dev libffi-dev curl minicom
# 2) 构建工具 aos-cube(AliOS Things 命令行工具集)
pip3 install aos-cube # 若系统 Python 版本冲突,可先建 venv
aos --version # 验证安装,例如 aos-cube 版本号 Rev 1.4.x
# 3) Cortex-M 工具链(以 ARM 官方 GCC 为例)
sudo apt-get install -y gcc-arm-none-eabi binutils-arm-none-eabi
arm-none-eabi-gcc --version
# 4) 其它架构按需安装
# C-Sky(平头哥):csky-abiv2-elf-* (芯片厂商提供)
# RISC-V :riscv64-unknown-elf-* 或 riscv32-elf-*
# Xtensa :xtensa-lx106-elf / xtensa-esp32-elf(厂商提供)
# 5) linuxhost 目标(PC 上跑仿真,验证业务逻辑很方便)
sudo apt-get install -y g++-multilib # 64 位机器上编 32 位目标需要
① Python 版本:aos-cube 对 Python 版本敏感,系统自带 python3 与 pip 混用常出问题,建议用 python3 -m venv 隔离;
② 缺 32 位运行库:编译 linuxhost 时会报无法执行 32 位指令,装 g++-multilib;
③ 路径含中文或空格:部分脚本处理不好,源码目录必须是纯 ASCII 路径。
2.2 SDK 获取与分支管理规范
git clone https://github.com/alibaba/AliOS-Things.git
cd AliOS-Things
git checkout rel_3.3.0 # 或项目指定的基线 tag
git submodule update --init --recursive # 部分版本含子模块
# 团队内建议:基线用 tag,开发用分支
git checkout -b feature/<board>-port rel_3.3.0
- 基线一律用 tag(如
rel_3.3.0),禁止直接把 master 当产品基线——master 上的组件版本号会漂移(常见master、dev_aos),导致构建不可复现。 - 组件版本号必须与仓库 branch / tag 一致:
package.yaml里的version不是随便写的,构建系统依赖它解析依赖;对不上会直接报依赖解析失败。 - 私有组件建议独立仓库 + 版本 tag,通过依赖引入,避免把 SDK 仓库改得无法升级。
2.3 交叉工具链配置
| 架构 | 工具链前缀 | 典型目标芯片 | 配置要点 |
|---|---|---|---|
| ARM Cortex-M | arm-none-eabi | STM32、NXP、GD32、HaaS 系列 | 最常见;-mcpu 与 FPU 选项要与实际内核匹配 |
| C-Sky | csky-abiv2-elf | 平头哥 CK 系列 | 厂商提供,需在 chip 组件 package.yaml 写 toolchain_prefix |
| RISC-V | riscv64-unknown-elf / riscv32-elf | GD32V、BL 系列、K210 | ABI(ilp32 / lp64)与 ISA 扩展要一致 |
| Xtensa | xtensa-lx106-elf | ESP8266 / ESP32 | 厂商专用工具链,烧录需配套 esptool |
| Linux host | gcc(32 位) | linuxhost 仿真目标 | 纯业务逻辑调试,装 g++-multilib |
工具链前缀通常在芯片组件的 package.yaml 里通过 toolchain_prefix 指定,构建系统据此拼出编译器名。若编译时报「找不到编译器」,先确认芯片组件里的前缀与实际安装的工具链名一致。
2.4 工具链全景
| 工具 | 定位 | 典型用法 |
|---|---|---|
| aos-cube | 命令行构建工具集:make / upload / monitor / convert | aos make app@board、aos upload、aos monitor |
| AliOS Studio / HaaS Studio | VSCode 插件:图形化编译、烧录、串口、调试 | 新建工程、一键编译下载、串口终端 |
| 串口终端 | 日志与 CLI 交互 | 波特率以板级 UART 定义为准(常见 115200) |
| J-Link / OpenOCD | 断点、单步、内存查看 | 配合 GDB 做 HardFault 现场分析 |
| SmartTrace / SystemView | 系统行为 Trace:任务切换、中断、事件 | 分析调度抖动与卡顿 |
| coredump 解析脚本 | 崩溃现场离线解析 | 配合 elf 与 addr2line 还原调用栈 |
2.5 SDK 目录结构详解
AliOS Things 的目录按系统架构分层划分:components 放内核之上的所有功能组件,hardware 放板级支持包与 CPU 适配代码,kernel 放内核,solutions 放完整应用工程。2.x 与 3.x 目录名差异较大,落地前以工程实际目录为准。
| 目录(3.x) | 对应 2.x | 内容 | 什么时候动它 |
|---|---|---|---|
| components/ | components/、framework/、utility/ | 每个子目录一个组件 + package.yaml | 增删功能、新增自定义组件 |
| hardware/ | board/、platform/ | board + chip + arch 三类组件 | 移植新板 / 新芯片 |
| kernel/ | kernel/、core/ | Rhino 内核与内核对象管理 | 基本不动,改 k_config.h |
| solutions/ | application/、example/、projects/ | 完整应用工程(solution 类型) | 产品工程入口 |
| build/、tools/ | build/、tools/ | 构建框架、CLI、打包烧录脚本 | 一般不动 |
| documentation/、test/ | test/ | 文档与 UT 用例 | 交付与回归 |
记住一句话:要加功能去 components,要换硬件去 hardware,要写产品去 solutions。内核目录尽量别碰——AliOS Things 的设计目标就是内核零修改,任何需要改内核源码才能实现的需求,八成是设计方式不对。
2.6 编译、清理与单组件编译
# 配置(交互式选择 app / board;首次会自动下载 kconfig 工具)
aos make menuconfig
aos make helloworld_demo@haas100 -c config # 指定 app@board 并重新配置
# 编译:app@board 是最核心的一条命令
aos make helloworld_demo@haas100
aos make helloworld_demo@haas100 JOBS=8 # 并行加速
# 清理
aos make clean # 清理当前目标产物
aos make distclean # 彻底清理(含配置)
# 烧录与串口
aos upload helloworld_demo@haas100 # 交互式选择串口号后烧录
aos monitor helloworld_demo@haas100 # 打开串口终端
# 生成组件模板(把已有源码目录快速变成组件)
aos convert <dir> # 在 dir 下生成 package.yaml 模板
① app@board 名字写错:app 名来自 solutions 下的工程名,board 名来自 board 组件的 name,拼错会直接报找不到目标;
② 首次编译自动下载工具链 / kconfig:离线环境需提前准备,否则卡在下载环节;
③ 配置未生效:改了 package.yaml 或 def_config 后要 -c config 重新生成配置头,否则宏不生效。
关于单组件编译:AliOS 的构建以 app@board 为整体单位,没有独立的「只编一个组件」开关。实践做法是——建一个最小 solution 组件,只依赖要验证的那个组件,用它做编译与链接验证。这比在全量工程里排除依赖快得多,还能顺带验证组件的依赖声明是否自洽。
2.7 编译产物说明
| 产物 | 路径 / 名称 | 用途 |
|---|---|---|
| 可执行文件 | binary 目录下的 .elf | GDB 调试、addr2line 符号解析 |
| 烧录镜像 | binary 目录下的 .bin | 量产烧录与 OTA 源镜像 |
| 链接映射 | binary 目录下的 .map | 查 ROM/RAM 占用、定位大符号 |
| 编译选项快照 | 目标目录下的 config.mk | 复现构建、排查宏定义差异 |
| 自动配置头 | 工程内的 aos_config.h | 所有 def_config 宏的最终落地 |
把每次量产版本的 elf、map、config.mk 一并归档。线上崩溃时,只有与现场固件完全一致的 elf 才能正确解析栈回溯——版本对不上的符号表解析出来的行号全是错的。
2.8 烧录与串口调试环境
- 确认串口号与波特率:Linux 下
/dev/ttyUSB0,Windows 下 COMx;波特率以板级 UART 配置为准。 - 烧录:
aos upload,交互式选择端口;特殊芯片依赖厂商工具(如 esptool),配置写在 upload 描述文件里。 - 打开终端:
aos monitor或串口助手,确认能看到启动日志与 CLI 提示符。 - 验证:输入
help看命令列表,再敲内核与内存类命令确认组件已起来。 - 固化配置:把串口号、波特率、烧录偏移写进项目 README,避免团队内反复问。
不同芯片的 bootloader 偏移与分区表位置各不相同。照抄别的板子的烧录地址是变砖的最快路径——尤其涉及 OTA 分区时,地址错会导致校验失败而反复重启。地址以目标板的链接脚本与分区表为准(见 6.6 与 15.1)。
3 · Rhino 内核基础
Rhino 是 AliOS Things 自研的实时内核:小体积、硬实时、可裁剪。这一章不追求罗列每个 API,而是把任务、IPC、中断、内存、调度、定时器六类机制的设计意图与约束讲清楚——绝大多数系统级 Bug 都能追溯到这六处之一。
3.1 V2 与 V3 的内核形态差异
| 方面 | 2.x | 3.x | 工程影响 |
|---|---|---|---|
| 内核 | Rhino,功能以宏内核方式提供 | Rhino 继续演进,新增内核对象统一管理 | 任务 / 互斥量 / 信号量用一致方式管理,可维护性提升 |
| 设备接入 | 多种访问方式并存 | 统一 VFS 接入 | 应用一律按文件方式访问设备,驱动必须注册节点 |
| 驱动框架 | 驱动各自实现 | 对上 AOS API、对下原子驱动原语 | 驱动写「原语 + 注册」,子系统承担复杂逻辑 |
| 构建 | 组件 .mk | 组件 package.yaml | 依赖关系显式化,增删功能改 yaml |
| 调试 | 基础手段 | 栈回溯、printk、cpuusage 增强 | 崩溃定位效率显著提升 |
| 应用形态 | C 应用为主 | 新增 JS / MicroPython 轻应用 | 业务逻辑可脚本化,支持小镜像增量升级 |
业界常把 3.x 描述为向微内核 / 多 bin 方向演进(组件化、服务化、应用与系统分离编译)。但要清醒:在没有 MPU / TrustZone 的 Cortex-M 上,所谓隔离是软约束——一个野指针照样能写穿整个地址空间。 工程上真正能拿到收益的是故障域收敛(驱动崩溃不至于拖垮调度)与增量升级(应用单独编镜像),而不是内存级隔离。是否需要用户态驱动,取决于芯片有没有 MPU,而不是取决于版本号。
3.2 内核对象与统一对象管理
3.3 起,任务、互斥量、信号量等内核资源采用一致的管理方式:统一的创建 / 销毁路径、统一的错误码体系、可被诊断命令统一遍历。这带来两个直接好处:
- 诊断命令能列出全部对象:任务列表、内存信息、CPU 占用等命令依赖的就是这套对象管理;对象不进容器,命令行里就看不到它。
- 静态与动态创建路径对齐:静态对象(自带控制块与缓冲)与动态对象(堆上分配)语义一致,只是内存来源不同,便于后期把动态改静态以抗碎片。
错误码:内核原语返回 kstat_t(如 RHINO_SUCCESS、RHINO_NULL_PTR、RHINO_NO_MEM),而 aos_ 层通常转成 int(0 成功 / 负值失败)。混用两层时不要拿 kstat_t 当 errno 用。
3.3 任务状态机与优先级
任务(task)是 Rhino 的调度实体。理解状态机的价值在于:看到某个任务不动了,能立刻判断它是被阻塞、被挂起,还是压根没被调度。
/* AOS 层创建任务的典型形态 */
static aos_task_t g_sensor_task;
static uint8_t g_sensor_stack[2048]; /* 静态栈:抗碎片,推荐 */
static void sensor_task_entry(void *arg)
{
while (1) {
sample_once(); /* 采样 */
aos_msleep(100); /* 让出 CPU,不要用忙等 */
}
}
int sensor_task_start(void)
{
int ret = aos_task_new_ext(&g_sensor_task, "sensor", sensor_task_entry,
NULL, sizeof(g_sensor_stack), SENSOR_PRIO);
/* 返回值必须检查:栈不足 / 优先级非法 / 内存不足都会失败 */
return ret;
}
① 数值越小优先级越高(0 为最高),与 FreeRTOS / RT-Thread 一致,但与某些 OS 相反,跨项目移植时务必确认;
② 优先级数量由 k_config.h 决定,不是无限的——优先级级数越多,就绪表开销越大;
③ 空闲任务必须存在且优先级最低,它是低功耗与 CPU 占用统计的锚点,不要用业务任务把它饿死。
优先级分档方法论(建议)
| 档位 | 优先级区间建议 | 放什么 | 约束 |
|---|---|---|---|
| 中断下半部 / 硬实时 | 最高档(数值最小) | 协议时序、采样触发、马达控制 | 执行时间必须可预期,禁止阻塞 |
| 通信与协议栈 | 中高档 | 网络收发、串口协议解析 | 允许短阻塞,禁止长时间占用 |
| 业务处理 | 中档 | 状态机、云端消息、业务计算 | 按业务重要性细分 1~2 档 |
| 存储与日志 | 中低档 | Flash 写入、日志落盘 | 允许较长阻塞 |
| 监控与空闲 | 最低档 | 看门狗喂狗、空闲钩子、统计 | 绝不能抢占业务 |
3.4 IPC 原语与选型
Rhino 提供的内核原语相当丰富:缓冲队列、环形缓冲、定时器、信号量、互斥量、队列、事件等。选择的原则不是「哪个功能多」,而是「哪个语义最贴合」——用信号量保护临界区是经典的优先级反转来源。
| 原语 | AOS 层接口(示意) | 内核原语 | 关键点 |
|---|---|---|---|
| 信号量 | aos_sem_new / aos_sem_signal / aos_sem_wait | krhino_sem_create / give / take | 可指定超时;中断里只 signal |
| 互斥量 | aos_mutex_new / lock / unlock | krhino_mutex_create / lock / unlock | 带优先级继承;必须由持有者释放 |
| 事件 | aos_event_new / get / set | krhino_event_create / get / set | 支持「与 / 或」组合等待 |
| 队列 | aos_queue_new / send / recv | krhino_queue_create / send / recv | 定长消息,多生产者多消费者 |
| 缓冲队列 | 对应带长度的数据队列 | krhino_buf_queue_create | 变长数据块,适合流式数据 |
| 工作队列 | aos_workqueue / aos_work | krhino_work_init / run | 中断下半部与延迟执行 |
信号量没有所有权概念:A 任务拿、B 任务放也能通过,一旦发生优先级反转,系统没有任何机制兜底。 保护共享资源一律用互斥量(带优先级继承);信号量只用于「同步通知」与「资源计数」。
3.5 中断模型与中断上下文约束
中断是实时性的来源,也是稳定性事故的高发区。Rhino 的模型与主流 RTOS 一致:上半部极短、下半部延后。
中断上下文禁用清单(红线)
- 任何可能阻塞的调用:带超时的 sem_wait / mutex_lock / queue_recv、延时接口。
- 拿互斥量:中断没有任务上下文,持有者语义不成立。
- 大批量日志打印:串口打印是毫秒级操作,会把中断拖成灾难。
- 动态内存分配:不可重入风险 + 时长不确定。
- 浮点运算(无 FPU 上下文保存时):会破坏任务侧浮点现场。
ISR 里做三件事:读状态、清中断、把数据投递出去(sem give / queue send 的非阻塞版本 / 提交 work)。其余一律交给下半部任务。 需要量化指标时,建议约束:单条 ISR 执行不超过数十微秒、中断服务总占用 CPU 不超过 10%(具体阈值按产品实时性定)。
3.6 内存管理与碎片
Rhino 为大多数内核对象提供静态与动态两种分配;小内存块分配器既支持定长块也支持变长块,并支持多个内存区域。这套设计的目的是让固件能塞进资源极其有限的设备,代价是碎片与容量必须自己管。
① 长期运行的对象静态化:任务、互斥量、队列、网络缓冲等,能静态就静态,避免长期运行后堆碎成筛子; ② 动态分配集中在初始化期:运行期尽量不再 malloc / free,要用就做成定长块池; ③ 变长数据用缓冲队列而不是反复 malloc,让内存行为可预测。
泄漏与踩踏的检测手段
| 手段 | 能发现什么 | 使用时机 |
|---|---|---|
| 内存信息命令 | 当前总量、已用、峰值、剩余 | 联调期与压力测试中定期观察 |
| 内存泄漏检测 | 未归还的分配点及其调用栈 | 长时间拷机后执行,比对分配点 |
| 栈水位统计 | 任务栈历史最大使用量 | 压力测试后逐个任务核对 |
| 静态分配改造 | 根本性规避碎片 | 稳定性要求高的量产项目 |
3.7 调度器与抢占
Rhino 是基于优先级的抢占式调度器:任何时刻就绪的最高优先级任务在运行;同优先级任务可按时间片轮转(取决于配置)。调度点包括:任务阻塞 / 退出、信号量与队列操作、中断返回、时间片到期、显式让出。
- 抢占代价:上下文切换要压栈弹栈,优先级级数与任务数越多开销越大。频率极高的任务要考虑合并或降频。
- 临界区:关中断 / 关调度的时间直接决定最坏中断延迟。关中断区间内禁止任何函数链不确定的操作。
- 优先级反转:低优先级任务持锁、中优先级任务抢占、高优先级任务等锁——靠互斥量的优先级继承缓解,但不能靠它根治错误的锁设计。
3.8 定时器与工作队列
| 机制 | 上下文 | 适合做什么 | 不适合做什么 |
|---|---|---|---|
| 软件定时器回调 | 定时器服务上下文 | 周期性心跳、超时判定、状态轮询 | 耗时操作、拿锁、大量 IO |
| 任务延时 | 任务自身上下文 | 让出 CPU、周期性任务节拍 | 精确时序(受调度抖动影响) |
| 工作队列 | 工作线程上下文 | 中断下半部、延迟执行 | 长阻塞操作(会堵住队列) |
| 硬件定时器 | 中断上下文 | 精确定时、波形输出、输入捕获 | 任何阻塞操作 |
① 回调里不能阻塞,否则整个定时器服务被拖住;② 回调要短小可预期,长活投递给任务;③ 不要在回调里创建 / 删除内核对象,容易与服务自身的锁形成竞态。
4 · 启动流程与分级驱动加载
这一章是 AliOS Things 与其他 RTOS 拉开差距的地方。Linux 靠设备树描述硬件、靠 probe 顺序解决依赖;AliOS Things 靠链接段 + 九个初始化等级解决同样的问题。理解它,就能解释「为什么我的驱动有时好用有时不好用」。
4.1 上电到应用任务的全链路
七个阶段的分工非常明确:arch 层只管 CPU,chip 层只管芯片,board 层只管这块板子,components 只管软件服务。排查启动故障时,先看卡在哪一段,就基本知道该翻哪个目录的代码。
4.2 系统服务初始化内部顺序
内核初始化之后,系统会依次拉起一组核心服务。以 2.x / 3.x 共有的结构为例,典型顺序是:
/* 概念化顺序,具体以工程源码为准 */
vfs_init(); /* 虚拟文件系统:建立 inode 表与互斥保护 */
vfs_device_init(); /* 注册设备节点与操作方法 */
cli_service_init(); /* 命令行服务(未启用 CLI 时走无 CLI 的日志初始化) */
aos_kv_init(); /* 键值存储初始化:配网信息、序列号从这里读 */
sal_device_init(); /* 套接字抽象层(启用网络时) */
aos_loop_init(); /* Yloop 事件循环:主任务默认实例 */
ota_service_init(); /* OTA 服务(启用时) */
sensor_init(); /* 传感器框架(启用时) */
/* 之后进入分级驱动加载,再进入应用入口 */
① 依赖 VFS 的驱动必须排在 VFS 初始化之后——这正是 VFS_DRIVER_ENTRY 这一级存在的意义;
② KV 初始化早于业务,意味着业务可以在启动早期读到配网信息;反过来,任何在 KV 之前就要读存储的代码都会失败。
4.3 九级驱动加载机制
驱动框架把设备驱动分成 9 个级别,每个级别对应一个声明宏与一个链接段。用同一宏声明的函数指针会在链接阶段被放进同一个段,系统启动时按段顺序依次调用。这是 AliOS Things 解决驱动依赖的核心手段。
/* 典型用法:用宏声明驱动初始化函数 */
static void my_spi_driver_entry(void)
{
/* 1) 初始化控制器(CSI 已提供时直接调用 CSI 接口) */
csi_spi_init(&g_spi, 0);
/* 2) 注册到 VFS / 设备框架 */
register_dev_node("/dev/spi0", &my_spi_ops, &g_spi);
}
BUS_DRIVER_ENTRY(my_spi_driver_entry) /* 总线控制器:第 2 级 */
static void my_sensor_driver_entry(void)
{
/* 依赖 SPI 总线,必须晚于 BUS 级 */
sensor_dev_t *dev = sensor_create(...);
sensor_register(dev);
}
LEVEL1_DRIVER_ENTRY(my_sensor_driver_entry) /* 第 6 级 */
分级选择决策(建议)
| 你的驱动是什么 | 推荐级别 | 理由 |
|---|---|---|
| 中断控制器、时钟、tick 源 | CORE | 内核本身依赖,必须最早 |
| SPI / I2C / UART 等总线控制器本体 | BUS | 挂在总线上的设备必然依赖它 |
| GPIO、Flash 控制器、看门狗 | EARLY | 早期日志与存储需要 |
| 需要注册设备节点、提供 open/ioctl | VFS | 必须在 VFS 初始化之后 |
| 传感器、显示屏等普通外设 | LEVEL0 ~ LEVEL2 | 按相互依赖逐级后移 |
| 收尾、统计、巡检、业务级钩子 | POST 或 LEVEL3 | 所有驱动就绪后再跑 |
| 耗时长但可延后的初始化 | VFS_DRIVER_BG(后台) | 不阻塞主启动链路 |
4.4 同级别内顺序随机(头号陷阱)
驱动框架只保证级与级之间的顺序;同一级内部的调用顺序由链接器决定,不可依赖。 这解释了一种极其经典的现象:同样的代码,加了一个源文件、改了一个编译选项,某个驱动就时好时坏——因为它恰好和另一个同级别驱动产生了隐式依赖。
规避方法
- 有依赖就分到不同级别:被依赖方放更低的级别号,不要试图在同一级里「碰巧先跑」。
- 同级别驱动之间禁止隐式依赖:A 驱动的初始化函数不得假设 B 已完成。
- 需要严格次序的一组初始化,合并成一个 entry:在一个初始化函数内按固定顺序调用,比分散到多个宏可靠得多。
- 改用显式初始化:对强依赖的模块,宁可在业务启动阶段显式调用 init,也不要赌链接顺序。
- 用后台声明延后:耗时且非关键的初始化用后台声明,避免拖慢启动并降低耦合。
4.5 内核态 / 用户态与多 bin
3.x 在架构上支持应用与系统分离编译(多 bin)与轻应用运行时,目标是三件事:
- 增量升级:应用单独编成小镜像,OTA 时只下应用,省流量、降风险。
- 故障域收敛:应用逻辑出错不至于直接拖死内核(在具备 MPU / TrustZone 的平台上效果更实)。
- 脚本化业务:JS / MicroPython 轻应用让业务改动不必重新编译整个固件。
| 问题 | 内核态驱动 | 用户态驱动 / 多 bin |
|---|---|---|
| 性能 | 最佳:直接访问,无切换开销 | 有跨态调用开销 |
| 故障影响 | 驱动崩溃即系统崩溃 | 故障可收敛,系统可继续 |
| 硬件依赖 | 无特殊要求 | 需要 MPU / TrustZone 才有实际隔离效果 |
| 升级粒度 | 整包升级 | 应用 / 模块单独升级 |
| 适用 | 高频、强实时、与中断强耦合的驱动 | 协议类、业务类、非实时外设 |
判断标准只有两条:是否需要硬实时响应与是否必须直接碰中断。只要有一条成立,就留在系统态;否则拆出去能换来更好的可维护性与升级灵活性。 反过来,为了「架构好看」把高频驱动拆到用户态,只会换来抖动与复杂度。
4.6 启动期故障排查
启动不起来时,先按「卡在哪一段」定位,再谈细节。绝大多数问题能在下表中找到对应项。
| 现象 | 可能根因 | 排查动作 |
|---|---|---|
| 完全无串口输出 | 波特率 / 引脚不对;时钟未起;烧录地址错 | 量串口 TX 波形;查板级 UART 引脚与时钟配置;核对烧录偏移 |
| 打印几行后停住 | 卡在某个驱动的初始化里(忙等或失败重试) | 在各级 entry 加标记日志;用后台声明把可疑驱动延后验证 |
| 反复重启 | 看门狗未喂 / 栈溢出 / 断言失败 | 看复位原因寄存器;临时关看门狗验证;检查任务栈 |
| 某个设备节点不存在 | 驱动未注册或注册晚于访问 | 确认加载级别;确认访问发生在初始化之后 |
| 时好时坏 | 同级别顺序随机导致的隐式依赖 | 把有依赖的驱动拆到不同级别 |
| 启动很慢 | 某驱动初始化耗时长;Flash 读取慢;日志过多 | 用时间戳打点;把非关键初始化改后台声明 |
在每一级驱动的入口加一行带时间戳的日志(哪怕只是级别编号),是定位启动问题性价比最高的手段。把这套日志做成默认打开的编译选项,量产前再关掉,比事后接仿真器快得多。