只读分享解决方案文档原理方案设计📅 2026-10-05🔖 Rev 1.4⏳ 26天有效(至 2026-11-05)
🔧解决方案&应用市场分析 -> 原理方案设计 -> 嵌入式驱动&系统开发 -> AliOS Things_驱动开发指南

AliOS Things_驱动开发指南

生成时间 2026-10-04 11:48:46 有效期至 2026-11-05 15:32:30(26天有效(至 2026-11-05))
同系列 · 原理方案设计

AliOS Things 驱动开发指南

编制日期 2026-10-05

CSI 硬件抽象层 · AOS 设备驱动框架 · 分级加载 · VFS 设备节点 · 组件化 package.yaml · 内核态与用户态双模驱动

9 级
驱动启动分级加载
CORE → BUS → EARLY → VFS → LEVEL0 → LEVEL1 → LEVEL2 …
0 个
设备树(DTS)
硬件资源全部靠板级静态配置描述,与 Linux 最大差异点
2 态
内核态 / 用户态驱动
用户态驱动崩溃不会整系统死机,靠 RPC 与内核交互
1 套
CSI 接口隔离芯片差异
驱动禁止直操作寄存器,跨 MCU 移植只换 CSI 实现

一句话结论:AliOS Things 驱动开发的本质不是「把 Linux 驱动改写一遍」, 而是把一个器件翻译成一个符合 AOS 设备框架约定的对象:底层只调 CSI、ops 覆盖 open/read/write/ioctl、 用 XXX_DRIVER_ENTRY 声明正确的加载等级、按需挂到 VFS 变成 /dev/xxx、最后用 package.yaml 把它变成可裁剪的组件。 这五件事做对,驱动就能被裁剪、被复用、被别人接手;漏掉任何一件,都会在特定环节表现为「莫名其妙的失败」。

这份文档解决什么

写 AliOS Things 驱动时,真正卡住人的往往不是「SPI 怎么发一个字节」,而是这些结构性问题: 为什么我的驱动 init 没被调用?为什么总线驱动还没起来外设就开始初始化? 我该用 LEVEL1 还是 LEVEL2?这个器件该不该挂 VFS? 内核态驱动和用户态驱动到底差在哪、怎么选?没有设备树,引脚和中断号写在哪? 为什么 open("/dev/xxx") 返回 -1?这些问题散落在源码、示例和零星文档里, 本指南把它们收拢成一套可照做的流程,配套可直接编译的骨架代码与可逐条勾选的核查清单。

路径 1 · 新接一颗外设
只想尽快让设备跑起来
先读第 3 章搞清分级加载与 CSI,再按第 5 章的八步流程走一遍; SPI NAND / SPI NOR 直接抄第 5.3 节的存储驱动骨架,I2C 传感器抄传感器骨架。
关键输出:设备注册成功 + CLI 可见 + open/read/write 通
路径 2 · 驱动不稳定
能跑但偶发错数据、死机、唤醒失败
直奔第 6 章与第 8 章:加载等级依赖、中断上下文约束、并发加锁、 suspend/resume 时钟门控、VFS 设备名冲突。九成偶发问题在这一层的边界条件上。
关键输出:中断 / 线程 / 低功耗三者的边界被明确划线
路径 3 · 交付评审
把驱动交给别人或合入 SDK
按第 9 章做测试矩阵、第 12 章做目录命名与交付清单, 最后用附录 A/B 的配置模板与代码模板对齐,并用第 13 章 FAQ 覆盖常见质疑。
关键输出:可复现、可维护、可裁剪、可被他人接手的驱动组件

面向 AliOS Things V3.x 弹性微内核主线(同时标注 V2.x 差异)。凡涉及具体宏名、结构体字段与 API 签名, 均以你所用版本的 components/csi、components/drivers、core/osal 头文件为准。

1 · 文档概述

本章先回答「这份文档写给谁、基于哪个版本、涉及哪些硬件」,然后用一张图把 AliOS Things 的驱动模型与 Linux 的分层差异讲清楚。读完这一章,你应该能判断自己的驱动 将来会落在哪一层、要跟哪些层打交道。

1.1 文档目的、适用范围与读者

本指南用于规范 AliOS Things 平台下外设驱动的开发、调试、测试与交付, 覆盖从「拿到一颗新器件」到「驱动作为组件合入 SDK」的完整流程。它不讲述具体芯片的寄存器细节 (那是芯片手册与 CSI 实现的事),而是聚焦驱动与内核之间的契约: 设备怎么注册、初始化顺序怎么保证、中断与并发怎么约束、怎么变成可裁剪组件、怎么验证。

角色主要关心什么建议优先阅读
BSP / 板级工程师引脚复用、时钟、CSI 底层是否完备、板级资源表怎么填、无 DTS 时代怎么描述硬件第 3.3、3.6 节,第 4 章全章,附录 A
驱动工程师驱动类型判断、ops 实现、加载等级、VFS 挂载、中断与 DMA、内存策略第 3.2、3.4、3.5 节,第 5 章全章,第 6 章全章,附录 B
应用 / 系统工程师设备怎么被找到、POSIX 与 AOS 接口怎么选、并发与功耗行为、CLI 怎么验证第 3.5 节,第 5.6 节,第 7 章,第 10 章
测试 / 交付负责人测试矩阵、ROM/RAM 占用、回归范围、交付清单与 changelog第 9 章,第 12 章,附录 C

1.2 版本基线:V2.x 与 V3.x 不是一回事

AliOS Things 的驱动模型在 V3.0 发生了一次结构性变化:内核从「宏内核 RTOS」演进为「弹性微内核」, 驱动获得内核态 / 用户态两种运行形态,外设接口统一收敛到 CSI,文件系统统一到 VFS 2.0, 组件管理统一到 package.yaml + OCC(Online Component Center)。 这意味着 V2.x 时代「BSP 里随便写一写」的驱动,在 V3.x 上要么跑不起来,要么跑起来但失去可移植性与裁剪能力。图 2 给出演进脉络。

写驱动前先确认基线:V2.x 与 V3.x 的驱动模型不是一回事V1.x / V2.xRhino 实时内核V3.0弹性微内核V3.1+用户态驱动 / 多进程HaaS硬件即服务 + 云端内核形态宏内核 RTOS,驱动与应用同址弹性内核,内核态 / 用户态可切换多进程地址隔离,驱动可跑用户态HaaS 硬件 + 云端一体开发驱动接口HAL 风格,各家 BSP 自行定义CSI 统一外设接口标准CSI + VFS 2.0 双通道HaaS 组件化驱动硬件描述板级 .c 静态配置板级静态配置(无 DTS)板级静态配置 + 组件声明HaaS 板级配置模板文件系统VFS 1.0(早期)VFS 2.0,/dev 节点统一VFS 2.0 + 多文件系统挂载云端 OTA + 文件系统组件管理Makefile 手工裁剪package.yaml + OCC 组件仓库package.yaml + 依赖自动解析HaaS 组件市场选型建议:新项目一律从 V3.x 起步;老项目迁移先把驱动从「直操作寄存器」改成「调 CSI」,再谈内核态 / 用户态切换
图 1 · 版本基线演进:V2.x 与 V3.x 的驱动模型差异(新项目建议直接 V3.x)
动手前先做一次基线确认

请确认三件事:① SDK 版本号(aos --version 或 package.yaml 顶部 version);② 目标 board 目录下是否已有 CSI 实现(components/csi/<chip>/);③ 是否开启用户态驱动(影响你能否在驱动里用某些内核 API)。这三点决定了本指南里哪些写法适用、哪些要打折。

1.3 术语与缩写

下面的术语在全文中反复出现,先对齐一次。注意 AliOS Things 的术语与 Linux 有重叠但含义不同 (例如「设备」在 AOS 里是框架注册对象,不等同于 Linux 的 struct device)。

缩写 / 术语全称在本文中指什么
CSIChip System Interface(芯片系统接口)芯片厂商实现的底层硬件抽象层,向上提供 csi_spi/csi_gpio/csi_dma/csi_irq 等统一接口;驱动只调它,不碰寄存器
AOS APIAliOS Things OS API操作系统接口层:aos_task_new / aos_sem_wait / aos_malloc 等,也是驱动可用的内核能力入口
VFSVirtual File System虚拟文件系统。驱动可注册为 /dev/xxx 节点,应用用 POSIX 的 open/read/write/ioctl 访问
package.yaml组件描述文件声明组件名、版本、依赖(depends)、编译配置;驱动就是一个组件,靠它实现按需裁剪
aos.mk组件 Makefile声明源文件、头文件路径、宏定义、依赖组件;与 package.yaml 共同决定一个驱动是否进入编译
组件Component可独立编译、可声明依赖的最小复用单元;驱动、协议栈、文件系统都是组件
驱动分级加载Driver Init Level用 XXX_DRIVER_ENTRY 宏把驱动的初始化函数放进特定启动阶段,解决「总线先于外设」这类依赖
HaaSHardware as a Service阿里云的 IoT 硬件即服务体系:HaaS 开发板 + HaaS Studio/IDE + 云端一体的开发调试体验
BSPBoard Support Package板级支持包:引脚复用、时钟、外设实例定义、板级初始化入口,AliOS 里对应 board/ 目录
OCCOnline Component Center在线组件仓库,aos-cube 拉取依赖组件的来源
uData / Sensor传感器框架把各类传感器抽象成统一的上层框架(V2.x 称 uData,V3.x 收敛为 sensor 组件),传感器驱动对接它而不是各自为政
FOTA / OTAFirmware Over-The-Air空中升级。驱动需保证升级过程中器件状态可恢复,存储驱动要支持 A/B 分区与回滚

1.4 硬件平台范围

AliOS Things 面向 IoT MCU,典型主频 80 MHz ~ 400 MHz、RAM 几十 KB 到几 MB、 Flash 几百 KB 到几 MB。这与「跑 Linux 的 MPU」在驱动设计上是两种世界观:前者关心 ROM/RAM 占用与启动顺序, 后者关心内存管理与进程隔离。下表列出常见平台与本指南的适用方式。

平台 / 芯片架构典型场景驱动开发要点
STM32(F1/F4/L4 等)Cortex-M工业传感、网关子节点CSI 实现多为厂商 HAL 封装;注意 L 系列的低功耗时钟门控与 Stop 模式唤醒
ESP32 / ESP8266XtensaWiFi 插座、灯控WiFi/BLE 协议栈与驱动耦合紧,注意 RF 占用 CPU 时的时序抖动与中断延迟
BK7231 / BK7252 等Cortex-M / RISC-V智能家电、低成本 WiFi 模组Flash 与 OTA 分区表要提前规划;注意 flash 擦写期间关中断带来的时序影响
RISC-V(C906 / E907 等)RISC-V国产化替代、AIoT中断控制器(PLIC/CLIC)与 Cortex-M NVIC 差异大,CSI 中断适配要格外小心优先级语义
HaaS 系列开发板Cortex-M / RISC-V快速验证、云端联调板级配置已就绪,重点在于把驱动连上云端调试通道(在线日志、远程下发)
关于「没有 MMU」这件事

大部分 IoT MCU 没有 MMU,因此 V3.x 的「用户态驱动」在纯 MCU 上通常依赖 MPU 分区或软件隔离(受限 API 集)实现,而不是完整虚拟地址空间。在有 MMU 的芯片上才是真正的进程隔离。选择内核态还是用户态之前,先确认你的芯片支持哪一种隔离手段。

1.5 驱动模型总览

图 1 是本指南最重要的一张图,请记住它:自上而下是调用关系,自下而上是支撑关系。 一句话概括——SOC 厂商实现 CSI 底层 → 驱动基于 CSI 实现外设驱动 → 注册到 AOS 设备框架 → 可选挂载 VFS,应用用标准 POSIX 接口访问设备。

AliOS Things 驱动模型总览:SOC 厂商实现 CSI,驱动基于 CSI 实现,注册进 AOS 设备框架,可选挂 VFS① 应用层 Application / 阿里云 SDK业务任务、MQTT/HTTP、HaaS 小程序;只认设备名,不认芯片型号业务逻辑User Task连接与协议MQTT / HTTP文件系统LittleFS传感器应用uData / Sensor两条通路:AOS 原生接口 或 POSIX 接口aos_dev_open / aos_dev_readopen / read / write② VFS 虚拟文件系统(可选层)驱动注册成 /dev/xxx 节点,应用用 POSIX 访问;不挂 VFS 也能直接用 AOS 设备接口设备节点/dev/nand0文件节点/littlefs/a.txtposix 适配open/read/ioctlfd 表进程级管理③ AOS 设备驱动框架(内核)设备注册 / 查找 / 分级加载 / 内核态与用户态驱动统一托管,故障可隔离设备注册表aos_dev_xxx分级加载LEVELx_ENTRYops 分发open/read/ioctl内核态 / 用户态RPC 通道④ 外设驱动层 Driver(本指南的主战场)spi / i2c / uart / gpio / flash / sensor 驱动:只调 CSI,不碰寄存器总线驱动spi / i2c存储驱动spi nand / nor传感器驱动temp / acc通信驱动wifi / ble⑤ 硬件底层:CSI(csi_spi / csi_gpio / csi_dma / csi_irq)+ SOC 寄存器 —— 由芯片厂商实现,驱动不得绕过驱动能看到的只有 CSI 提供的统一接口;换芯片时这一层整体替换,上层零改动
图 2 · AliOS Things 驱动五层模型:应用、VFS、AOS 设备框架、外设驱动、CSI 与硬件

与 Linux 相比,这里有三处结构性差异,几乎决定了后面所有写法:

  • 没有设备树。硬件资源(引脚号、外设基址、中断号、时钟源)不通过 DTS 描述后由内核解析, 而是在 board/ 下以静态 C 配置的方式写死,编译时确定。好处是零解析开销、启动快; 代价是换板子要改代码重新编译。
  • 驱动即组件。驱动不是内核的一部分,也不是可加载的 .ko, 而是一个用 package.yaml 声明依赖的组件。没被依赖就不会被编译进固件, 这是 IoT 场景下控制 ROM/RAM 的核心手段。
  • 初始化顺序靠分级宏显式声明。Linux 有 module_init 的等级与设备模型的 probe 匹配机制;AliOS 用 9 级 XXX_DRIVER_ENTRY,且没有自动的依赖关系推断—— 等级选错,你的驱动会在总线还没准备好时被初始化,然后失败。
一个判断驱动写得对不对的朴素标准

把你的驱动源文件里的所有 #include 扫一遍:如果出现芯片寄存器头(如 xxx_reg.h)或直接出现寄存器地址宏,说明它越界到了 CSI 层,换芯片一定改;如果只出现 csi_xxx.h、aos/xxx.h 与器件自己的 drv_xxx.h,那它就是合格的、可移植的外设驱动。能被多颗芯片复用的部分越往上放,未来的迁移成本越低。

2 · 开发环境搭建

这一章把「从一台空机器到能编译烧录并看到串口日志」的过程走一遍。 重点不是命令本身,而是理解工程结构与两份配置文件——后面所有驱动工作都在它们划定的框架内进行。

2.1 宿主机环境与依赖

AliOS Things 的构建链依赖 Python 与常规 GNU 工具,推荐 Ubuntu LTS(也可在 macOS / WSL 上工作, Windows 原生支持较弱)。下面是最小可用集:

# 1) 系统依赖(Ubuntu / Debian)
sudo apt-get update
sudo apt-get install -y build-essential git python3 python3-pip \
     cmake ninja-build gcc-multilib libssl-dev libncurses5-dev \
     minicom picocom screen

# 2) Python 构建工具 aos-cube(AliOS Things 的命令行构建与烧录工具)
pip3 install aos-cube
aos --version                 # 验证安装

# 3) 交叉工具链(按目标架构选装)
sudo apt-get install -y gcc-arm-none-eabi        # Cortex-M
# RISC-V:      使用芯片厂商提供的 riscv64-unknown-elf / riscv32 工具链
# Xtensa(ESP): 使用乐鑫官方 xtensa-esp32-elf 工具链

# 4) 串口权限
sudo usermod -aG dialout $USER && newgrp dialout # 重新登录后生效
版本踩坑提示

① Python 3 与 pip 的对应关系:确认 pip3 装的包能被构建脚本找到,多 Python 环境下常见「命令装上了但构建时提示找不到模块」;② 工具链路径不要带空格与中文,Makefile 对这类路径容忍度很低;③ WSL 下烧录需额外处理 USB 设备透传,建议烧录环节回到 Windows 原生工具。

2.2 SDK 源码获取与分支管理

SDK 即整个操作系统仓库。建议的分支策略是:以官方 release 分支为基线, 产品侧只做「驱动组件 + board 目录 + product 目录」的增量,避免直接改内核与公共组件。

git clone https://github.com/alibaba/AliOS-Things.git
cd AliOS-Things
git checkout <release_tag>        # 明确切到一个发布版本,不要长期停在 master

# 分支策略建议
#   master / release   —— 只读基线,只用 git fetch 跟进
#   dev/<product>      —— 产品开发分支,只放 board / product / 自定义组件
#   feature/drv_xxx    —— 单个驱动的开发分支,完成后合入 dev/<product>

# 提交前自检
git status                        # 确认没有误改公共组件
git diff --stat                   # 改动范围应集中在 components/drivers/<你的驱动> 与 board/
为什么强调「不要改公共组件」

IoT 项目周期长,SDK 迟早要升级。一旦你在 components/ 或 core/ 里动了别人的代码,升级时的合并冲突会呈指数增长。把改动收敛在「新增目录 + 声明依赖」内,升级 SDK 的成本就是重新编一次。

2.3 工具链与 aos-cube

aos-cube 是构建入口。它负责解析 package.yaml 的依赖树、 从 OCC 拉取缺失组件、生成编译配置并调用交叉工具链。最常用的几条命令如下。

aos make <app>@<board>              # 编译某个产品方案(app 为 product 下的方案名)
aos make <app>@<board> -j8          # 并行编译
aos make <app>@<board> clean        # 清理构建产物
aos upload <app>@<board>            # 烧录(依赖板级支持,部分板子用厂商工具)

# 常见报错速记
#   "no such board"        → board 目录名拼写错误或未纳入 SDK
#   "component not found"  → package.yaml 的依赖没装,检查 OCC 网络或手动 aos install
#   "toolchain not found"  → 工具链未安装或 PATH 未配置

2.4 工程结构说明(重点)

图 3 给出 SDK 的四块地。驱动工程师 90% 的时间花在 components/ 与 board/ 上, 而决定固件最终形态的是 product/。

SDK 工程结构:驱动落在哪、板级配置落在哪、组件声明落在哪board/板级支持包:引脚复用、时钟、外设实例、板级初始化入口board/<board_name>/ # 板级目录 ├─ aos.mk # 板级编译配置 ├─ package.yaml # 板级组件依赖 ├─ board.c # 板级初始化、堆初始化 └─ drivers/ # 板级专属驱动(可选)components/组件区:驱动、CSI、VFS、CLI 都在这里,每个目录一个组件components/ ├─ drivers/ # ④ 外设驱动:spi / i2c / uart / gpio / sensor / flash ├─ csi/ # ⑤ CSI 硬件抽象层(按芯片分目录) ├─ vfs/ # ② VFS 虚拟文件系统与 /dev 节点 └─ cli/ # 串口命令行(调试第一工具)core / osal / kernel/内核与 OS 抽象层:任务、信号量、队列、中断、内存core/ ├─ osal/ # AOS API:aos_task / aos_sem / aos_malloc ├─ rhino/ # Rhino 实时内核(V3 为弹性内核) └─ device/ # ③ 设备框架与分级加载宏定义product / app / solution/产品工程:一个具体产品的入口与配置,决定最终编进哪些组件product/ └─ <solution>/ # 产品方案工程 ├─ package.yaml # 产品级依赖(决定固件里有什么) ├─ aos.mk # 产品级编译配置 └─ main.c # 应用入口口诀:加驱动动 components/drivers 与 package.yaml;换板子动 board;改裁剪动 product
图 3 · SDK 工程结构四块地:board(板级)、components(组件含 drivers/csi/vfs/cli)、core(内核)、product(产品)
AliOS-Things/
├─ board/                          # ① 板级支持包
│  └─ <board_name>/
│     ├─ aos.mk                    #     板级编译配置(源文件、宏)
│     ├─ package.yaml              #     板级组件依赖
│     ├─ board.c                   #     板级初始化入口、堆初始化
│     └─ drivers/                  #     板级专属驱动(可选)
├─ components/                     # ② 组件区(每个子目录一个组件)
│  ├─ drivers/                     #     外设驱动:spi / i2c / uart / gpio / sensor / flash
│  ├─ csi/                         #     CSI 硬件抽象层(按芯片分目录)
│  ├─ vfs/                         #     VFS 虚拟文件系统与 /dev 节点
│  ├─ cli/                         #     串口命令行
│  └─ <其它组件>/                  #     网络、文件系统、安全、OTA 等
├─ core/                           # ③ 内核与 OS 抽象层
│  ├─ osal/                        #     AOS API:aos_task / aos_sem / aos_malloc
│  ├─ rhino/                       #     Rhino 实时内核 / 弹性内核
│  └─ device/                      #     设备框架与分级加载宏
├─ product/                        # ④ 产品方案工程
│  └─ <solution>/
│     ├─ package.yaml              #     产品级依赖(决定固件里有什么)
│     ├─ aos.mk
│     └─ main.c                    #     应用入口
└─ build/                          # 构建输出(编译产物、elf、bin)
目录谁维护放什么驱动相关的动作
board/<board>/BSP 工程师引脚复用、时钟、外设实例、板级初始化新板子在这里建目录;已有板子在这里加外设实例与引脚配置
components/drivers/驱动工程师芯片无关的外设驱动(本指南的主战场)新建 drv_xxx.c/.h + package.yaml + aos.mk
components/csi/芯片厂商 / BSP芯片相关的底层硬件接口实现只有在 SOC 无原生 CSI 支持时才介入;驱动层不应修改这里
components/vfs/系统组VFS 与 /dev 节点管理驱动只需调用注册接口,不实现 VFS 本身
product/<solution>/产品负责人产品的依赖与入口在 depends 里加上你的驱动组件,它才会被编进固件

2.5 编译配置:package.yaml 与 aos.mk

这两份文件决定「驱动会不会被编译」与「编译哪些源文件」。二者缺一不可: package.yaml 决定组件是否被纳入构建,aos.mk 决定组件里的文件怎么编。图 4 是完整构建流程。

构建流程:一条 aos make 命令背后发生了什么aos make <app>@<board>指定产品与板级解析 package.yaml收集组件与 dependsOCC 拉取缺失组件本地无则下载生成编译配置宏开关、头文件路径编译链接交叉工具链出 elf生成镜像bin / hex / ota 包三个决定成败的配置文件package.yaml 决定「有没有这个组件」;aos.mk 决定「这个组件编哪些源文件」;board/aos.mk 与板级 .c 决定「外设实例的引脚、时钟、中断号是什么」常见构建失败与原因• undefined reference —— 源文件没进 aos.mk 的 SOURCES• 头文件找不到 —— GLOBAL_INCLUDES 没加路径• 组件没编进去 —— package.yaml 的 depends 没声明三条实用命令• aos make <app>@<board> -j8 编译• aos make clean 清构建产物• aos upload / aos burn 烧录(视板级支持)
图 4 · 构建流程与三类配置文件:aos make 从 package.yaml 解析依赖到生成可烧录镜像
# ============================================================
# components/drivers/drv_spinand/package.yaml
# ============================================================
name: drv_spinand                 # 组件名:被 depends 引用时的标识
版本号 Rev 1.4
description: SPI NAND 驱动(基于 CSI,可挂 VFS)

depends:
  - drivers: master               # 设备驱动框架(提供注册与分级加载宏)
  - csi: master                   # CSI 硬件抽象层(提供 csi_spi_* 接口)
  - vfs: master                   # 可选:需要 /dev 节点时才依赖
  - cli: master                   # 可选:需要调试命令时才依赖

def_config:                       # 组件级默认宏(可被产品配置覆盖)
  CONFIG_DRV_SPINAND: 1
  CONFIG_DRV_SPINAND_DEBUG: 0

# ============================================================
# components/drivers/drv_spinand/aos.mk
# ============================================================
NAME := drv_spinand

$(NAME)_TYPE := kernel            # kernel:内核态组件
$(NAME)_SOURCES := drv_spinand.c  # 源文件(相对本目录)
$(NAME)_INCLUDES := .             # 对外头文件路径
GLOBAL_INCLUDES += .              # 暴露给其它组件的头文件路径
GLOBAL_DEFINES += CONFIG_DRV_SPINAND=1
$(NAME)_COMPONENTS += csi drivers # 编译期组件依赖(与 package.yaml 保持一致)
最容易踩的三个不一致

① package.yaml 写了依赖、aos.mk 没写 _COMPONENTS → 头文件能找到但链接时报 undefined reference;② aos.mk 的 SOURCES 漏文件 → 函数没进固件,运行时注册不到设备;③ product 的 package.yaml 没有 depends 你的驱动 → 驱动整份不编译,现象是「代码写完了但设备永远不出现」,且没有任何报错。

2.6 烧录、串口调试与 HaaS IDE

烧录方式取决于板级:常见有串口 ISP、J-Link/OpenOCD、厂商专用下载工具三类。 串口是 IoT 开发最可靠的调试通道,建议一开始就把它配好并确认日志能出。

手段用途配置要点
串口 UART日志输出 + CLI 交互 + 基础烧录常见波特率 115200 8N1;确认板级把 console 设备挂到正确 UART 与引脚;日志口不要与业务口冲突
J-Link / ST-Link + OpenOCD断点调试、coredump 抓取、烧录接好 SWD/JTAG;配好 OpenOCD 的 target 配置文件;gdb 连 localhost:3333
GDB断点、单步、查看调用栈与变量用 elf 文件(不是 bin)调试;优化等级高时变量可能被优化掉
HaaS Studio / IDE工程创建、编译烧录一体化、云端联调基于 VSCode 的插件体系;与阿里云 IoT 平台打通,可在线看日志、远程下发指令
阿里云 IoT 平台云端调试设备在线日志、远程指令下发设备需先完成三元组烧录与连云;适合真机在远端或不便接线时的验证
环境自检清单

① aos --version 有输出;② 能编译一个官方 example 并出 elf/bin;③ 串口能看到启动日志与 help 命令可用;④ 能烧录并复位后重复启动;⑤ tasklist / meminfo 等 CLI 命令有响应。这五条全绿再开始写驱动,否则后面所有问题的定位都会被环境噪声污染。

3 · AliOS Things 驱动基础原理

本章是与 Linux 驱动差异最集中的地方。请重点理解三件事: 分级加载(没有 probe 自动匹配)、CSI 隔离(禁止碰寄存器)、组件化(不被依赖就不编译)。 理解这三点,后面所有写法都顺理成章。

3.1 内核基础:任务、同步与中断模型

AliOS Things 的内核提供任务、信号量、互斥量、队列、事件与软件定时器。 驱动本身通常不创建任务,但在两种情况下需要:一是中断线程化(把耗时处理挪出 ISR), 二是器件本身的异步流程(如擦写完成通知)。图 5 给出可用原语与中断上下文约束。

内核基础:驱动能用什么、在哪里用任务与调度• 优先级抢占 + 时间片轮转• aos_task_new / aos_task_exit / aos_task_yield• 每个任务独立栈,栈大小静态指定• 驱动通常不建任务;需要线程化时建一个• aos_msleep 只能在任务上下文调用• 优先级反转:用带优先级继承的互斥量规避同步与通信• 信号量 aos_sem_new / wait / signal(可计数)• 互斥量 aos_mutex(带优先级继承,可嵌套)• 队列 aos_queue(定长消息,拷贝传递)• 事件 aos_event(按位等待,多条件组合)• 定时器 aos_timer(软件定时器,回调在任务上下文)• ISR 中只能用非阻塞版本或 ISR 安全接口中断模型① 注册:csi_irq_attach(irq_num, handler, arg) 或在 CSI 适配层挂接② 上下文:ISR 运行在独立中断栈上,栈极小(常见 256 B ~ 1 KB 级)③ 禁止:不能调用任何可能阻塞的 API(malloc、sem_wait、msleep、长临界区)④ 推荐:ISR 只做清中断 + 收/发一帧 + 发信号,业务逻辑交给线程(中断线程化)⑤ 优先级:Cortex-M 用 NVIC 优先级分组;RISC-V 用 PLIC/CLIC,语义不同需重新映射记住这条线:ISR 只做「必要且极短」的事,其余全部推给任务;这条线守不住,系统会以随机死机的方式报复你判断方法:把中断处理函数里每个调用问一遍「它可能阻塞吗」,只要有一个答案是「可能」,就该搬走
图 5 · 内核基础:任务调度、同步通信原语与中断模型的上下文约束
/* 驱动里最常见的「中断下半部」形态 */
static aos_sem_t  g_spi_done;      /* 静态分配,避免启动时 malloc */
static csi_spi_t  g_spi;

static void spi_event_cb(csi_spi_t *spi, csi_spi_event_t event, void *arg)
{
    /* ① 这里仍是中断/回调上下文:只发信号,不做业务 */
    aos_sem_signal(&g_spi_done);   /* 必须是 ISR 安全版本;不可用 aos_sem_wait */
}

int drv_spinand_wait_ready(uint32_t timeout_ms)
{
    /* ② 任务上下文:可以阻塞等待 */
    return aos_sem_wait(&g_spi_done, timeout_ms);
}
中断上下文的三条硬约束

① 不能调用任何可能阻塞的 API(aos_malloc、aos_sem_wait、aos_msleep、普通互斥锁);② 中断栈极小,禁止在 ISR 里开大数组或深递归;③ 不要在 ISR 里做耗时操作(打印日志、SPI 轮询整页、解析协议),这些都会拉长关中断时间,直接恶化系统的实时性。

3.2 驱动分级加载机制(核心特色)

Linux 靠「总线 + 设备树 + probe 匹配」解决初始化顺序,AliOS Things 没有这套机制, 改为由驱动自己声明启动等级:用 XXX_DRIVER_ENTRY 宏把初始化函数放进特定阶段, 启动时由框架按序调用。这就是分级加载。图 6 是完整时序。

驱动分级加载:谁先起来、谁后起来,全靠 XXX_DRIVER_ENTRY 声明1CORE_DRIVER_ENTRY内核核心设备内核运行必需的最底层设备(如中断控制器、时钟源)2BUS_DRIVER_ENTRY总线控制器SPI / I2C / UART 等总线控制器本体,外设驱动依赖它3EARLY_DRIVER_ENTRY早期设备需要在文件系统之前就绪的设备(如部分 GPIO、看门狗)4VFS_DRIVER_ENTRYVFS 与根设备VFS 初始化、根文件系统挂载、console 设备5LEVEL0_DRIVER_ENTRY基础外设不依赖其他驱动的外设(GPIO 扩展、LED、按键)6LEVEL1_DRIVER_ENTRY总线上的外设挂在 SPI / I2C 总线上的器件:Flash、NAND、传感器7LEVEL2_DRIVER_ENTRY依赖外设的上层设备基于其他设备构建的复合设备(如基于 Flash 的分区设备)8应用初始化业务任务与协议栈应用组件初始化、网络协议栈启动、云端连接9shell / cli调试与运维CLI 组件、调试命令注册,此时可用命令行验证驱动铁律:外设驱动的等级必须晚于它所依赖的总线驱动;依赖链每加一层,等级至少要往后挪一级典型组合:SPI 总线控制器 = BUS;挂在 SPI 上的 NAND/NOR = LEVEL1;基于 Flash 的分区设备 = LEVEL2排查技巧:在每个驱动 init 首尾加一条日志,看启动日志里两个驱动的先后顺序是否符合预期
图 6 · 驱动分级加载 9 级时序:从 CORE 到 shell,外设驱动必须晚于其依赖的总线驱动
/* 用法:在驱动文件末尾声明「我的 init 属于哪一级」 */
static int spinand_drv_init(void)
{
    /* 1) 初始化 CSI 句柄(依赖 SPI 总线已就绪) */
    if (csi_spi_init(&g_spi, SPI_NAND_BUS_IDX) != CSI_OK)
        return -1;

    /* 2) 配置 SPI 模式与速率 */
    csi_spi_mode(&g_spi, SPI_MODE_0);
    csi_spi_baud(&g_spi, SPI_NAND_MAX_BAUD);

    /* 3) 读取器件 ID 并识别容量 */
    if (spinand_read_id(&g_id) != 0)
        return -1;

    /* 4) 注册到 AOS 设备框架 */
    aos_dev_register(&g_spinand_dev);

    /* 5) 可选:挂 VFS,生成 /dev/nand0 */
    vfs_register_driver("/dev/nand0", &spinand_fops, &g_spinand_dev);
    return 0;
}

/* 关键:SPI NAND 挂在 SPI 总线上,必须晚于总线 → 用 LEVEL1 */
LEVEL1_DRIVER_ENTRY(spinand_drv_init)
宏适用对象选错的后果典型例子
CORE_DRIVER_ENTRY内核运行必需的最底层设备系统根本起不来中断控制器、系统时钟源
BUS_DRIVER_ENTRY总线控制器本体所有该总线上的外设全部初始化失败SPI 控制器、I2C 控制器
EARLY_DRIVER_ENTRY需在文件系统之前就绪的设备早期日志丢失、看门狗误复位GPIO、看门狗、console 相关
VFS_DRIVER_ENTRYVFS 与根设备设备节点注册失败、文件系统挂不上VFS 初始化、根挂载
LEVEL0_DRIVER_ENTRY不依赖其他驱动的基础外设设备不可用LED、按键、GPIO 扩展
LEVEL1_DRIVER_ENTRY挂在总线上的外设csi_xxx 返回 BUSY/ERROR,读写全错SPI NAND / NOR、I2C 传感器
LEVEL2_DRIVER_ENTRY依赖其他设备构建的复合设备底层设备还没注册,初始化失败基于 Flash 的分区设备、文件系统挂载
等级选错是最隐蔽的一类 bug

它不会编译报错,也不一定每次都失败:总线控制器刚好先起来就正常,一旦启动顺序或时序有抖动就偶发失败。排查时最快的办法是在每个驱动的 init 首尾各加一条日志,看启动日志里的实际顺序,再对照依赖链。

3.3 CSI 硬件抽象层

CSI 是芯片厂商实现的底层接口层,向上暴露统一签名的外设操作。 驱动只调用 csi_xxx,绝不直接读写寄存器,这是跨芯片可移植的唯一保障。 图 7 划清了两层的职责边界。

CSI 层与驱动层的职责边界:一条不能越的线上层:外设驱动(你写的)• 器件协议:命令字、时序、状态轮询• 器件状态机:忙等待、擦写流程、坏块管理• AOS 设备框架对接:ops、注册、VFS• 允许调用:csi_* 系列接口 + AOS API• 禁止:直接读写寄存器、直接操作 DMA 控制器• 禁止:假设具体芯片型号与时钟频率下层:CSI(芯片厂商实现)• 寄存器级操作:外设控制器配置与启动• 中断控制器挂接与优先级映射• DMA 通道申请、配置与释放• 引脚复用(pinctrl)与时钟门控• 对上暴露统一签名:csi_spi_xxx / csi_gpio_xxx• 不做:任何器件级业务语义csi_xxx 调用回调 / 事件通知CSI 统一接口形态(示意,具体签名以头文件为准)句柄式:csi_xxx_init(handle, idx) → csi_xxx_config(...) → csi_xxx_transfer(...) → csi_xxx_uninit(handle)返回值:csi_error_t(CSI_OK / CSI_ERROR / CSI_BUSY / CSI_TIMEOUT / CSI_UNSUPPORTED),必须逐个判断异步:csi_xxx_attach_callback(handle, cb, arg) 注册回调;不要靠忙等轮询吃掉整个 CPU索引号 idx:同一类外设的第几个实例(如 SPI0/SPI1),与板级配置中的实例编号一一对应
图 7 · CSI 与驱动层的职责边界:驱动只做器件语义,芯片细节全交给 CSI
CSI 接口族典型函数驱动里的用途
csi_spi_*csi_spi_init / mode / baud / send / receive / transfer / attach_callbackSPI 类器件:NOR/NAND Flash、LCD、RF 芯片
csi_i2c_*csi_i2c_init / master_send / master_receive / mem_write / mem_readI2C 传感器、EEPROM、PMIC、Codec
csi_gpio_*csi_gpio_init / mode / write / read / attach_callback片选、复位、中断引脚、LED、按键
csi_uart_*csi_uart_init / send / receive / baud / attach_callbackGPS、模组 AT 指令、调试口
csi_dma_*csi_dma_init / ch_alloc / ch_start / ch_stop / ch_free大批量传输,降低 CPU 占用
csi_irq_*csi_irq_attach / detach / enable / disable外部中断、器件中断引脚
csi_timer_*csi_timer_init / start / stop超时判定、周期采样触发
返回值必须逐个判断

CSI 接口统一返回 csi_error_t,常见取值 CSI_OK / CSI_ERROR / CSI_BUSY / CSI_TIMEOUT / CSI_UNSUPPORTED。CSI_BUSY 往往意味着上一次传输没结束或总线被别的驱动占用;CSI_TIMEOUT 常见于时钟未开、片选没拉对、器件没上电。把返回值打进日志,排查效率能提升一个数量级。

3.4 AOS 设备模型

AOS 设备框架负责设备的注册、查找与生命周期管理,屏蔽了驱动的实现细节。 V3.x 的特色是驱动可以运行在内核态或用户态:内核态性能最好但崩溃会带走整个系统, 用户态通过 RPC 与内核交互、崩溃可隔离,代价是吞吐下降。图 8 给出取舍。

驱动运行在哪:内核态与用户态的两条路内核态驱动驱动代码与内核同址,直接调用内核与 CSI正:性能最好,无 RPC 开销,中断延迟最低正:可直接访问全部内核 API 与硬件资源正:适合高频、低延迟、中断密集的驱动负:一个野指针整系统死机(HardFault / panic)负:无法单独重启,故障影响面是整个设备负:驱动 bug 难以现场定位,只能靠 coredump典型选择:SPI 控制器、DMA、高频采样、网络协议栈底层用户态驱动驱动跑在独立用户进程,通过 RPC 与内核通信正:崩溃只死一个进程,内核与其他驱动存活正:可独立重启、独立升级与灰度正:适合复杂、第三方、易出错的设备逻辑负:每次硬件访问有 RPC 开销,吞吐下降负:中断不能直接进用户态,需要内核中转负:可用 API 受限,某些内核能力调不到典型选择:传感器融合、协议解析、Codec、复杂外设选型口诀:高频中断与时序苛刻的走内核态;逻辑复杂、易崩、可独立的走用户态;两者之间用 RPC 边界切分
图 8 · 内核态与用户态驱动的取舍:性能 vs 故障隔离
维度内核态驱动用户态驱动
运行位置与内核同址,直接调用内核与 CSI独立用户进程,通过 RPC 访问内核
性能最高,无额外开销有 RPC 开销,高频访问明显下降
中断处理ISR 直接进驱动需内核中转,延迟增加
故障影响野指针导致整系统 panic进程崩溃,内核与其他驱动存活
可用 API全部内核 API受限 API 集
升级需整包升级可独立升级 / 重启
适用高频、低延迟、中断密集逻辑复杂、第三方、易出错

3.5 VFS 虚拟文件系统

VFS 让驱动可以注册成 /dev/xxx 节点,应用用标准 POSIX 接口访问, 类似 Linux 字符设备,但没有设备树、没有 udev、节点由驱动显式注册。 图 9 是完整调用链。是否挂 VFS 由驱动自己决定,不是强制的。

VFS 通路:应用 open(/dev/xxx) 之后,调用是怎么走到驱动的应用fd = open("/dev/nand0")POSIX 层open / read / write / ioctlVFS路径查表 → inode → fd 分配file_ops驱动注册的 ops 结构体驱动实现csi_spi_transfer → 器件注册侧(驱动初始化时做一次)static file_ops_t nand_fops = { .open = ..., .read = ..., .write = ..., .ioctl = ..., .close = ... };vfs_register_driver("/dev/nand0", &nand_fops, priv);不挂 VFS 的另一条路驱动注册为 AOS 设备后,应用直接用 aos_dev_open /aos_dev_read 等 AOS 原生接口访问,省掉一层 VFS,代价是失去 POSIX 兼容与 shell 里 cat/echo 的便利什么时候该挂 VFS挂:字符流式设备、需要 shell 里直接测试、希望应用代码与 Linux 保持一致、需要被文件系统组件挂载不挂:超高频访问(层开销不可接受)、纯内部使用、资源极度紧张(VFS 本身占 ROM/RAM)注意:设备名必须全局唯一,重名注册会失败;建议在驱动里用常量统一管理命名
图 9 · VFS 通路:从 open(/dev/xxx) 经 POSIX → VFS inode → file_ops → 驱动实现的调用链
/* 驱动侧:定义 file_ops 并注册(形态示意,签名以头文件为准) */
static int nand_open(inode_t *node, file_t *file)
{
    if (!g_spinand.inited) {
        LOGE("NAND", "device not inited");
        return -ENODEV;
    }
    return 0;                       /* 硬件初始化已在 init 里做过,这里只做状态检查 */
}

static ssize_t nand_read(file_t *file, void *buf, size_t nbytes)
{
    uint32_t page = file->offset / g_spinand.geo.page_size;

    aos_mutex_lock(&g_spinand.lock, AOS_WAIT_FOREVER);   /* 并发保护 */
    int ret = nand_page_read(page, buf, nbytes);
    aos_mutex_unlock(&g_spinand.lock);

    if (ret != 0) return ret;
    file->offset += nbytes;
    return nbytes;
}

static ssize_t nand_write(file_t *file, const void *buf, size_t nbytes)
{
    uint32_t page = file->offset / g_spinand.geo.page_size;

    aos_mutex_lock(&g_spinand.lock, AOS_WAIT_FOREVER);
    int ret = nand_page_program(page, buf, nbytes);   /* 内部分装:使能→load→exec→wait */
    aos_mutex_unlock(&g_spinand.lock);

    if (ret != 0) return ret;
    file->offset += nbytes;
    return nbytes;
}

static int nand_ioctl(file_t *file, int cmd, unsigned long arg)
{
    switch (cmd) {
    case NAND_IOC_ERASE_BLOCK:
        return nand_block_erase((uint32_t)arg);
    case NAND_IOC_GET_GEO:
        memcpy((void *)arg, &g_spinand.geo, sizeof(spinand_geo_t));
        return 0;
    case NAND_IOC_READ_STATUS:
        return nand_get_status((uint8_t *)arg);
    default:
        LOGW("NAND", "unknown ioctl cmd 0x%08X", cmd);
        return -EINVAL;              /* 未知命令必须返回错误,不能返回 0 */
    }
}

static file_ops_t nand_fops = {
    .open  = nand_open,
    .close = nand_close,
    .read  = nand_read,
    .write = nand_write,
    .ioctl = nand_ioctl,
};

/* 注册:路径全局唯一,重名会失败 */
vfs_register_driver("/dev/nand0", &nand_fops, &g_spinand);
/* 应用侧:POSIX 访问(与 Linux 应用写法几乎一致) */
int fd = open("/dev/nand0", O_RDWR);
if (fd < 0) {
    printf("open nand failed: %d\n", errno);
    return -1;
}

uint8_t buf[2048];
ssize_t n = read(fd, buf, sizeof(buf));

/* 擦除、读状态等控制类操作走 ioctl */
ioctl(fd, NAND_IOC_ERASE_BLOCK, block_no);
ioctl(fd, NAND_IOC_GET_INFO, &info);

close(fd);
VFS 不是万能的

① 高频访问有开销:每次 read/write 都要经 POSIX → VFS → ops 三层,对采样率很高的场景不合适;② 设备名全局唯一,重名注册失败且容易被忽略返回值;③ VFS 本身占资源,极紧张的场景可以只用 AOS 原生设备接口;④ 存储类驱动通常还要再接一层分区管理与文件系统(LittleFS 等),文件系统是独立组件。

3.6 组件化管理

在 AliOS Things 里,驱动就是一个组件:有独立的目录、package.yaml、aos.mk, 通过 depends 声明依赖,未被依赖就不参与编译。这是 IoT 场景控制 ROM/RAM 的核心机制, 也是与 Linux「编进内核 or .ko 模块」最本质的区别。图 10 是依赖链。

组件化:驱动是一个组件,靠 package.yaml 声明依赖,不被依赖就不编译product/<solution>package.yamldrv_spinandpackage.yamldrivers驱动框架csi芯片抽象层vfs可选:挂 /devcli可选:调试命令package.yaml 的三段关键信息name 组件名(被 depends 引用) | version 版本号(升级与冲突判定依据)depends 依赖列表(决定这条链上的组件是否进入编译,也决定能否链接到符号)裁剪收益未被引用的驱动整份源码不参与编译,ROM/RAM 零占用这是 IoT MCU 上最有效的资源控制手段常见错误依赖写了但 aos.mk 没加源文件 → 链接期 undefined reference版本写死过窄 → 组件升级时依赖解析失败
图 10 · 组件化依赖:product → 驱动组件 → drivers/csi/vfs/cli,未被依赖即不编译
# product/<solution>/package.yaml —— 决定固件里最终有什么
name: my_solution
version: master
depends:
  - drivers: master
  - csi: master
  - drv_spinand: master     # ← 不加这一行,你的驱动整份不会被编译
  - cli: master
  - vfs: master
组件化的三个好习惯

① 一个驱动一个目录,目录名与组件名一致;② 调试命令单独用宏隔离(CONFIG_DRV_XXX_DEBUG),出厂默认关闭,避免带调试代码出货;③ 依赖写最小集:不需要 VFS 就别依赖 vfs,不要因为「方便」把整条链都拉进来。

4 · 板级硬件与 CSI 适配

这一章讲「驱动下面的那一层」。绝大多数「驱动跑不起来」的问题其实出在这里: 时钟没开、引脚复用没配、片选没拉对、CSI 底层根本没实现。 请把它当作驱动开发的前置检查项,而不是别人的事。

4.1 硬件原理图评审

驱动工程师必须参与原理图评审。原因很直接:硬件错误在软件层表现为「时序不对」「偶发失败」, 排查成本远高于改板前发现。图 13 给出四类必查项。

原理图评审:驱动工程师必须参与的四类检查SPI 链路• SCK / MOSI / MISO / CS 四线完整且无交叉• CS 必须为独占 GPIO(不用硬件 CS 共用)• 上拉/下拉按器件要求,避免浮空• WP# / HOLD# 需固定电平或接 GPIO 控制• 多器件共用总线时每个器件独立 CSI2C 链路• SDA / SCL 上拉阻值与总线电容匹配• 器件地址无冲突(7 位地址全表核对)• 总线挂 4 个以上器件时评估负载能力• 中断引脚 / 复位引脚是否引出• 电平转换:不同电压域需加电平转换器UART 链路• TX / RX 交叉连接(不是直连)• 波特率与双方时钟精度匹配• 流控引脚(RTS/CTS)按需引出• 调试口与业务口不要占用同一个 UART• 长距离或板间连接考虑 ESD 与共地电源与复位• 每颗芯片 VCC 就近 0.1 uF 去耦• 大电流路径的电源完整性与压降评估• 复位引脚的时序要求与上拉/复位电路• 上电时序:多电源域器件的先后约束• 掉电保护:存储器件写入期间供电稳定信号完整性要求• SPI 走线尽量等长、远离高频干扰源;速率越高越要控制反射与串扰(必要时串接 22~33 Ω 阻尼)• I2C 上拉阻值需按总线电容计算:速率越高、节点越多,上拉越小;过小的上拉会超出器件灌电流能力• 存储类器件擦写期间电流波动大,电源设计要留余量,否则会出现偶发写失败• 长线或板间连接的 SPI 建议在驱动侧降速验证:先低速跑通再提速,能快速区分时序问题与链路问题
图 11 · 原理图评审要点:SPI / I2C / UART / 电源复位四类检查与信号完整性要求
最容易翻车的五个硬件细节

① CS 被多个器件共用 → 两个器件同时响应,读回数据乱;② WP# / HOLD# 浮空 → 存储器件偶发拒绝写入;③ I2C 地址冲突 → 两个同型号传感器挂在同一总线;④ UART TX/RX 直连未交叉 → 双方都收不到;⑤ 去耦电容离 VCC 引脚太远 → 擦写瞬间掉电,表现为随机写失败或器件锁死。

4.2 板级配置:引脚复用、时钟与 IO 初始化

AliOS Things 的板级配置集中在 board/<board>/ 下,通常包括 引脚复用(pinctrl)、外设时钟使能、外设实例定义、板级初始化入口四件事。 与 Linux 不同,这些全部是编译期确定的静态配置(图 11)。

同样是描述「SPI0 用哪几个引脚、哪个中断号」,Linux 用 DTS,AliOS 用静态 C 配置Linux:设备树 DTS编译期:dts 转 dtb,与内核镜像分离启动期:内核解析 dtb,生成 platform_device匹配期:of_match_table 与驱动 compatible 匹配初始化:命中后调用 probe,资源由内核传入优点:换板不改代码,只换 dtb;内核与板级解耦优点:同一内核镜像可支持多种板子代价:需要解析器与运行时内存,启动变慢代价:dts 写错常表现为 probe 不调用,难定位典型文件:arch/arm/boot/dts/xxx.dtsdrivers/xxx.c 里的 of_device_id 表驱动拿资源:platform_get_resource / of_property_read依赖顺序:设备树父子关系 + 链接顺序共同决定调试手段:/proc/device-tree、dmesg 中的资源解析日志AliOS Things:板级静态 C 配置编译期:board 目录下用 C 宏或配置表写死资源启动期:无解析,配置表直接放在只读数据段匹配期:没有 compatible 匹配,靠实例索引号初始化:按 DRIVER_ENTRY 宏声明的等级调用优点:零解析开销,启动快、代码体积小优点:资源在编译期确定,部分错误编译期即可发现代价:换板要改代码重新编译,无二进制复用代价:资源写错靠运行现象发现,无统一查看入口典型位置:board/<board>/board.c、board.h、aos.mk驱动内引用板级配置头文件中的宏驱动拿资源:直接引用配置宏或板级导出的配置表依赖顺序:完全由分级加载宏决定,无自动推断调试手段:启动日志 + CLI 自定义命令打印配置结论:AliOS 用「编译期确定」换取「启动快、体积小」,代价是硬件变更必须重新编译,且依赖关系要人工保证
图 12 · 无 DTS:AliOS 板级静态 C 配置与 Linux 设备树描述硬件资源的对照
/* board/my_board/board.h —— 板级资源集中定义(无 DTS,全部静态声明) */
#ifndef BOARD_H
#define BOARD_H

/* ---- SPI 实例:SPI NAND 挂在 SPI0 上 ---- */
#define SPI_NAND_BUS_IDX        0          /* 对应 csi_spi_init 的 idx 参数 */
#define SPI_NAND_CS_GPIO_IDX    0          /* GPIO 端口索引 */
#define SPI_NAND_CS_PIN         12         /* 片选引脚号 */
#define SPI_NAND_MAX_BAUD       24000000   /* 24 MHz,上电先用低速,稳定后再提 */

/* ---- I2C 实例:温湿度传感器挂在 I2C1 ---- */
#define SENSOR_I2C_BUS_IDX      1
#define SENSOR_I2C_ADDR         0x44
#define SENSOR_INT_GPIO_IDX     1
#define SENSOR_INT_PIN          5
#define SENSOR_INT_IRQ_NUM      26         /* 中断号:查芯片手册中断向量表 */

/* ---- 调试串口 ---- */
#define CONSOLE_UART_IDX        0
#define CONSOLE_BAUD            115200

#endif
/* board/my_board/board.c —— 板级初始化入口 */
#include <aos/aos.h>
#include "board.h"

static csi_gpio_t g_cs_gpio;

static void board_pinmux_init(void)
{
    /* 1) 引脚复用:SPI0 的 SCK/MOSI/MISO 切到外设功能 */
    csi_pinmux_config(SPI0_SCK_PIN,  SPI0_SCK_FUNC);
    csi_pinmux_config(SPI0_MOSI_PIN, SPI0_MOSI_FUNC);
    csi_pinmux_config(SPI0_MISO_PIN, SPI0_MISO_FUNC);

    /* 2) 片选配置为 GPIO 输出,初始拉高(未选中) */
    csi_gpio_init(&g_cs_gpio, SPI_NAND_CS_GPIO_IDX);
    csi_gpio_mode(&g_cs_gpio, SPI_NAND_CS_PIN, GPIO_MODE_PUSH_PULL);
    csi_gpio_write(&g_cs_gpio, SPI_NAND_CS_PIN, 1);
}

static void board_clock_init(void)
{
    /* 3) 外设时钟:漏掉这一句,读写全返回 0x00 / 0xFF */
    csi_clk_enable(CLK_SPI0);
    csi_clk_enable(CLK_I2C1);
    csi_clk_enable(CLK_GPIO);
}

void board_init(void)
{
    board_clock_init();      /* 先开时钟,再配引脚 */
    board_pinmux_init();
    /* 其它板级初始化:堆初始化、console 初始化等 */
}
时钟没开是最常见的「低级错误」

现象极具迷惑性:SPI 读回全 0x00 或全 0xFF、I2C 一直 NACK、csi_xxx 返回 CSI_TIMEOUT,而代码逻辑完全正确。排查第一件事:确认外设时钟已使能、引脚复用已切到外设功能(而不是 GPIO 模式)、器件已供电且未处于复位态。

4.3 CSI 底层接口适配

如果你的 SOC 已经有官方 CSI 实现,这一节可以跳过;如果没有(常见于新芯片或小众 RISC-V), 就需要自己补齐底层 HAL。图 12 是需要实现的四组接口与推进顺序。

SOC 没有原生 CSI 支持时,需要按这个清单把底层 HAL 补齐① 控制器初始化• 外设时钟使能与分频• 控制器软复位与寄存器默认值• 工作模式配置(主/从、速率)• 引脚复用与电气属性② 传输接口• 阻塞发送 / 接收• 收发一体 transfer• 异步 + 回调• 超时与错误处理③ 中断与 DMA• 中断号映射与优先级配置• ISR 清中断与事件上报• DMA 通道申请 / 配置 / 释放• 缓存一致性处理(如适用)④ 电源与低功耗• 时钟门控接口• suspend / resume 状态保存恢复• 唤醒源配置• 休眠期间的引脚状态适配顺序建议(按依赖推进,不要跳步)1) 先做 GPIO + 时钟:能点灯、能量到引脚电平,说明时钟与引脚复用正确2) 再做 UART 轮询收发:有串口日志,后续所有调试才有抓手3) 再做 SPI / I2C 阻塞收发:能读回器件 ID,说明时序与片选正确4) 最后补中断与 DMA:性能与异步能力在此阶段引入,此时已有手段验证
图 13 · CSI 适配工作分解:初始化、传输、中断 DMA、低功耗四组接口与推进顺序
/* 以 csi_spi 为例:CSI 层需要提供的接口族(签名示意,以头文件为准) */

/* ① 初始化 / 去初始化 */
csi_error_t csi_spi_init(csi_spi_t *spi, uint32_t idx);
void        csi_spi_uninit(csi_spi_t *spi);

/* ② 参数配置 */
csi_error_t csi_spi_mode(csi_spi_t *spi, csi_spi_mode_t mode);   /* CPOL/CPHA */
csi_error_t csi_spi_baud(csi_spi_t *spi, uint32_t baud);
csi_error_t csi_spi_format(csi_spi_t *spi, uint32_t bits);       /* 数据位宽 */

/* ③ 数据传输(阻塞 / 异步) */
int32_t     csi_spi_send(csi_spi_t *spi, const void *data, uint32_t size, uint32_t timeout);
int32_t     csi_spi_receive(csi_spi_t *spi, void *data, uint32_t size, uint32_t timeout);
int32_t     csi_spi_transfer(csi_spi_t *spi, const void *tx, void *rx, uint32_t size, uint32_t timeout);
csi_error_t csi_spi_send_async(csi_spi_t *spi, const void *data, uint32_t size);

/* ④ 回调与中断 */
csi_error_t csi_spi_attach_callback(csi_spi_t *spi, void *callback, void *arg);
csi_error_t csi_spi_detach_callback(csi_spi_t *spi);

/* ⑤ 状态查询 */
csi_spi_state_t csi_spi_get_state(csi_spi_t *spi);
/* CSI 层实现示例(寄存器操作只允许出现在这里) */
csi_error_t csi_spi_init(csi_spi_t *spi, uint32_t idx)
{
    if (idx >= SPI_MAX_INSTANCE)  return CSI_ERROR;

    spi->idx  = idx;
    spi->base = (SPI_REG_T *)spi_base_addr[idx];   /* 寄存器基址表 */

    csi_clk_enable(clk_id[idx]);                   /* 时钟使能 */
    spi->base->CR1 &= ~SPI_CR1_SPE;                /* 先关外设再配置 */
    spi->base->CR1  = SPI_CR1_MSTR | SPI_CR1_SSI | SPI_CR1_SSM;
    spi->base->CR1 |= SPI_CR1_SPE;                 /* 使能 */

    csi_irq_attach(irq_num[idx], spi_irq_handler, spi);
    csi_irq_enable(irq_num[idx]);
    spi->state = SPI_STATE_READY;
    return CSI_OK;
}

/* ISR:清中断 + 上报事件,绝不做业务 */
static void spi_irq_handler(void *arg)
{
    csi_spi_t *spi = (csi_spi_t *)arg;
    uint32_t sr = spi->base->SR;

    if (sr & SPI_SR_RXNE) {                 /* 收到数据 */
        spi->rx_buf[spi->rx_cnt++] = spi->base->DR;
        if (spi->rx_cnt >= spi->rx_len) {
            spi->state = SPI_STATE_READY;
            if (spi->cb) spi->cb(spi, SPI_EVENT_RX_DONE, spi->cb_arg);
        }
    }
    if (sr & SPI_SR_OVR) {                  /* 溢出错误必须处理 */
        (void)spi->base->DR; (void)spi->base->SR;
        spi->state = SPI_STATE_ERROR;
    }
}
CSI 实现的三条质量线

① 返回值语义准确:CSI_BUSY 表示资源被占用,CSI_TIMEOUT 表示超时,不要一律返回 CSI_ERROR,否则上层无法区分重试与放弃;② 状态机完整:READY / BUSY / ERROR 三态要能自恢复,错误态不能被永久卡住;③ 可重入保护:同一外设被多个驱动访问时,CSI 层要有锁或在文档中声明非线程安全。

4.4 板级资源定义:没有 DTS,靠静态配置

这是与 Linux 最大的差异点:Linux 用 DTS 描述硬件、内核解析后传给 probe; AliOS Things 没有设备树,所有硬件资源以 C 宏或配置表的形式在 board 目录写死,编译期确定。 好处是零解析开销、启动快;代价是换板必须改代码重新编译。

资源类型Linux(DTS)AliOS Things(静态配置)备注
引脚号与复用pinctrl-0 = <&spi0_pins>csi_pinmux_config(pin, func) 或板级表复用功能编号需查芯片手册,不同芯片语义不同
外设基址reg = <0x40013000 0x400>CSI 层内部维护基址表,驱动不感知驱动不应出现任何寄存器地址
中断号interrupts = <0 26 4>#define SENSOR_INT_IRQ_NUM 26RISC-V 与 Cortex-M 的中断号体系不同,移植时必查
时钟clocks = <&rcc SPI1_CLK>csi_clk_enable(CLK_SPI0)AliOS 里时钟常由 CSI 内部处理,驱动不直接操作
DMA 通道dmas = <&dma1 2>CSI 层分配,或板级指定通道号通道冲突需人工协调,没有自动仲裁
器件私有参数spi-max-frequency = <24000000>驱动内宏定义或板级配置头建议集中放在 board.h,避免散落在驱动里
把板级资源集中管理

强烈建议把所有引脚号、中断号、总线索引、速率上限集中到 board.h,驱动里只引用宏,不出现裸数字。这样换板只改一个文件,也能在评审时一眼看出资源是否冲突。散落在驱动里的魔法数字,是后期移植的主要成本来源。

4.5 硬件设计规范、电源与信号完整性

驱动无法弥补硬件缺陷。下面这些要求在原理图与 PCB 阶段就要落实, 否则软件侧只能降速、重试、加延时来「绕」,代价是性能与稳定性双输。

项目要求不满足时的软件侧表现
去耦电容每颗芯片每个 VCC 引脚就近放置 0.1 uF,尽量靠近引脚、回路最短擦写瞬间掉电 → 随机写失败、器件锁死、ID 读错
电源压降按器件峰值电流(Flash 擦写可达数十 mA)评估电源与走线宽度大电流操作时电压跌落 → 数据错误或复位
上拉/下拉CS、WP#、HOLD#、RESET# 等关键控制线不允许浮空器件偶发拒绝写入、上电状态不确定
SPI 走线SCK/MOSI/MISO 尽量等长,远离 RF 与高速信号;高速时串接阻尼电阻高速下读回数据错位、CRC 校验失败
I2C 上拉按总线电容与速率计算上拉阻值,兼顾灌电流能力高速模式波形不达标 → NACK、偶发丢字节
复位时序满足器件要求的上电复位时间与复位脉宽上电后首次访问失败,需软件延时规避
掉电保护存储器件写入期间的供电稳定性;必要时加储能电容意外掉电导致页/块数据损坏、坏块增加
给硬件工程师的一句话

如果某个问题在软件侧只能靠「加延时」「降速率」「多试几次」解决,那它本质上大概率是硬件问题。请回到原理图与电源设计上找原因,而不是把延时写进驱动当成修复——那只是把概率降低,不是消除。

扫码打开本页
微信扫码 · 展会可扫

再分享给同事

同事可直接打开这份资料《AliOS Things_驱动开发指南》,在线阅读;完整章节请下载芯参谋。