AliOS Things 驱动开发指南
编制日期 2026-10-05
CSI 硬件抽象层 · AOS 设备驱动框架 · 分级加载 · VFS 设备节点 · 组件化 package.yaml · 内核态与用户态双模驱动
一句话结论: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?这些问题散落在源码、示例和零星文档里,
本指南把它们收拢成一套可照做的流程,配套可直接编译的骨架代码与可逐条勾选的核查清单。
面向 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 给出演进脉络。
请确认三件事:① SDK 版本号(aos --version 或 package.yaml 顶部 version);② 目标 board 目录下是否已有 CSI 实现(components/csi/<chip>/);③ 是否开启用户态驱动(影响你能否在驱动里用某些内核 API)。这三点决定了本指南里哪些写法适用、哪些要打折。
1.3 术语与缩写
下面的术语在全文中反复出现,先对齐一次。注意 AliOS Things 的术语与 Linux 有重叠但含义不同
(例如「设备」在 AOS 里是框架注册对象,不等同于 Linux 的 struct device)。
| 缩写 / 术语 | 全称 | 在本文中指什么 |
|---|---|---|
CSI | Chip System Interface(芯片系统接口) | 芯片厂商实现的底层硬件抽象层,向上提供 csi_spi/csi_gpio/csi_dma/csi_irq 等统一接口;驱动只调它,不碰寄存器 |
AOS API | AliOS Things OS API | 操作系统接口层:aos_task_new / aos_sem_wait / aos_malloc 等,也是驱动可用的内核能力入口 |
VFS | Virtual File System | 虚拟文件系统。驱动可注册为 /dev/xxx 节点,应用用 POSIX 的 open/read/write/ioctl 访问 |
package.yaml | 组件描述文件 | 声明组件名、版本、依赖(depends)、编译配置;驱动就是一个组件,靠它实现按需裁剪 |
aos.mk | 组件 Makefile | 声明源文件、头文件路径、宏定义、依赖组件;与 package.yaml 共同决定一个驱动是否进入编译 |
组件 | Component | 可独立编译、可声明依赖的最小复用单元;驱动、协议栈、文件系统都是组件 |
驱动分级加载 | Driver Init Level | 用 XXX_DRIVER_ENTRY 宏把驱动的初始化函数放进特定启动阶段,解决「总线先于外设」这类依赖 |
HaaS | Hardware as a Service | 阿里云的 IoT 硬件即服务体系:HaaS 开发板 + HaaS Studio/IDE + 云端一体的开发调试体验 |
BSP | Board Support Package | 板级支持包:引脚复用、时钟、外设实例定义、板级初始化入口,AliOS 里对应 board/ 目录 |
OCC | Online Component Center | 在线组件仓库,aos-cube 拉取依赖组件的来源 |
uData / Sensor | 传感器框架 | 把各类传感器抽象成统一的上层框架(V2.x 称 uData,V3.x 收敛为 sensor 组件),传感器驱动对接它而不是各自为政 |
FOTA / OTA | Firmware 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 / ESP8266 | Xtensa | WiFi 插座、灯控 | 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 | 快速验证、云端联调 | 板级配置已就绪,重点在于把驱动连上云端调试通道(在线日志、远程下发) |
大部分 IoT MCU 没有 MMU,因此 V3.x 的「用户态驱动」在纯 MCU 上通常依赖 MPU 分区或软件隔离(受限 API 集)实现,而不是完整虚拟地址空间。在有 MMU 的芯片上才是真正的进程隔离。选择内核态还是用户态之前,先确认你的芯片支持哪一种隔离手段。
1.5 驱动模型总览
图 1 是本指南最重要的一张图,请记住它:自上而下是调用关系,自下而上是支撑关系。 一句话概括——SOC 厂商实现 CSI 底层 → 驱动基于 CSI 实现外设驱动 → 注册到 AOS 设备框架 → 可选挂载 VFS,应用用标准 POSIX 接口访问设备。
与 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/。
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 是完整构建流程。
# ============================================================
# 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 给出可用原语与中断上下文约束。
/* 驱动里最常见的「中断下半部」形态 */
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 是完整时序。
/* 用法:在驱动文件末尾声明「我的 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_ENTRY | VFS 与根设备 | 设备节点注册失败、文件系统挂不上 | VFS 初始化、根挂载 |
LEVEL0_DRIVER_ENTRY | 不依赖其他驱动的基础外设 | 设备不可用 | LED、按键、GPIO 扩展 |
LEVEL1_DRIVER_ENTRY | 挂在总线上的外设 | csi_xxx 返回 BUSY/ERROR,读写全错 | SPI NAND / NOR、I2C 传感器 |
LEVEL2_DRIVER_ENTRY | 依赖其他设备构建的复合设备 | 底层设备还没注册,初始化失败 | 基于 Flash 的分区设备、文件系统挂载 |
它不会编译报错,也不一定每次都失败:总线控制器刚好先起来就正常,一旦启动顺序或时序有抖动就偶发失败。排查时最快的办法是在每个驱动的 init 首尾各加一条日志,看启动日志里的实际顺序,再对照依赖链。
3.3 CSI 硬件抽象层
CSI 是芯片厂商实现的底层接口层,向上暴露统一签名的外设操作。
驱动只调用 csi_xxx,绝不直接读写寄存器,这是跨芯片可移植的唯一保障。
图 7 划清了两层的职责边界。
| CSI 接口族 | 典型函数 | 驱动里的用途 |
|---|---|---|
csi_spi_* | csi_spi_init / mode / baud / send / receive / transfer / attach_callback | SPI 类器件:NOR/NAND Flash、LCD、RF 芯片 |
csi_i2c_* | csi_i2c_init / master_send / master_receive / mem_write / mem_read | I2C 传感器、EEPROM、PMIC、Codec |
csi_gpio_* | csi_gpio_init / mode / write / read / attach_callback | 片选、复位、中断引脚、LED、按键 |
csi_uart_* | csi_uart_init / send / receive / baud / attach_callback | GPS、模组 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 访问内核 |
| 性能 | 最高,无额外开销 | 有 RPC 开销,高频访问明显下降 |
| 中断处理 | ISR 直接进驱动 | 需内核中转,延迟增加 |
| 故障影响 | 野指针导致整系统 panic | 进程崩溃,内核与其他驱动存活 |
| 可用 API | 全部内核 API | 受限 API 集 |
| 升级 | 需整包升级 | 可独立升级 / 重启 |
| 适用 | 高频、低延迟、中断密集 | 逻辑复杂、第三方、易出错 |
3.5 VFS 虚拟文件系统
VFS 让驱动可以注册成 /dev/xxx 节点,应用用标准 POSIX 接口访问,
类似 Linux 字符设备,但没有设备树、没有 udev、节点由驱动显式注册。
图 9 是完整调用链。是否挂 VFS 由驱动自己决定,不是强制的。
/* 驱动侧:定义 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);① 高频访问有开销:每次 read/write 都要经 POSIX → VFS → ops 三层,对采样率很高的场景不合适;② 设备名全局唯一,重名注册失败且容易被忽略返回值;③ VFS 本身占资源,极紧张的场景可以只用 AOS 原生设备接口;④ 存储类驱动通常还要再接一层分区管理与文件系统(LittleFS 等),文件系统是独立组件。
3.6 组件化管理
在 AliOS Things 里,驱动就是一个组件:有独立的目录、package.yaml、aos.mk,
通过 depends 声明依赖,未被依赖就不参与编译。这是 IoT 场景控制 ROM/RAM 的核心机制,
也是与 Linux「编进内核 or .ko 模块」最本质的区别。图 10 是依赖链。
# 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 给出四类必查项。
① CS 被多个器件共用 → 两个器件同时响应,读回数据乱;② WP# / HOLD# 浮空 → 存储器件偶发拒绝写入;③ I2C 地址冲突 → 两个同型号传感器挂在同一总线;④ UART TX/RX 直连未交叉 → 双方都收不到;⑤ 去耦电容离 VCC 引脚太远 → 擦写瞬间掉电,表现为随机写失败或器件锁死。
4.2 板级配置:引脚复用、时钟与 IO 初始化
AliOS Things 的板级配置集中在 board/<board>/ 下,通常包括
引脚复用(pinctrl)、外设时钟使能、外设实例定义、板级初始化入口四件事。
与 Linux 不同,这些全部是编译期确定的静态配置(图 11)。
/* 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 是需要实现的四组接口与推进顺序。
/* 以 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_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 26 | RISC-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、偶发丢字节 |
| 复位时序 | 满足器件要求的上电复位时间与复位脉宽 | 上电后首次访问失败,需软件延时规避 |
| 掉电保护 | 存储器件写入期间的供电稳定性;必要时加储能电容 | 意外掉电导致页/块数据损坏、坏块增加 |
如果某个问题在软件侧只能靠「加延时」「降速率」「多试几次」解决,那它本质上大概率是硬件问题。请回到原理图与电源设计上找原因,而不是把延时写进驱动当成修复——那只是把概率降低,不是消除。