报告总结
写 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)。
| 缩写 / 术语 | 全称 | 在本文中指什么 |
|---|---|---|
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 | 快速验证、云端联调 | 板级配置已就绪,重点在于把驱动连上云端调试通道(在线日志、远程下发) |
关于「没有 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 引用时的标识
版本号 master
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_ENTRY |
VFS 与根设备 | 设备节点注册失败、文件系统挂不上 | 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_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 开销,中断延迟最低正:可直接访问全部内核 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 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、偶发丢字节 |
| 复位时序 | 满足器件要求的上电复位时间与复位脉宽 | 上电后首次访问失败,需软件延时规避 |
| 掉电保护 | 存储器件写入期间的供电稳定性;必要时加储能电容 | 意外掉电导致页/块数据损坏、坏块增加 |
给硬件工程师的一句话
如果某个问题在软件侧只能靠「加延时」「降速率」「多试几次」解决,那它本质上大概率是硬件问题。请回到原理图与电源设计上找原因,而不是把延时写进驱动当成修复------那只是把概率降低,不是消除。
5 · 驱动开发完整步骤
本章是全篇的核心。它以SPI NAND 存储驱动 为主线, 同时给出 I2C 传感器与 UART 的差异点,把从「拿到器件手册」到「驱动作为组件交付」的每一步摊开。 每一步都给出产出物 与常见错误,可以照着做。图 14 是总流程。
驱动开发八步:每一步都有明确的产出物,缺一步都会在后面某个环节爆炸① 需求分析定类型:总线外设 / 传感器定形态:是否需要 VFS 节点产出:驱动类型判定表② 依赖配置package.yaml 声明 dependsaos.mk 声明源文件与宏产出:两份配置文件③ 代码编写CSI 封装 → ops 实现→ 设备注册 → 可选 VFS 挂载产出:drv_xxx.c / .h④ 板级初始化board.h 定义资源宏board.c 完成时钟与引脚复用产出:板级改动⑤ 编译配置加入 product 的 depends确认裁剪与宏开关产出:固件含该驱动⑥ 基础测试注册是否成功 → CLI 是否可见读写是否正确 → 中断是否触发产出:功能验证记录⑦ 集成与压力长时间拷机、并发访问反复重启、休眠唤醒产出:稳定性结论⑧ 打包提交changelog、Known Issues测试用例与测试报告产出:可交付组件顺序不可颠倒的理由:③ 之前必须确定 ①(形态决定是否需要 ioctl);③ 之前必须完成 ④(否则引脚时钟没配,代码正确也读不到器件);⑤ 漏掉则驱动根本不进固件。实践建议:先跑通 ① 到 ⑥ 形成闭环,再补 ⑦ ⑧ ------ 早验证能省掉大量返工。
图 14 · 驱动开发八步流程与每步的产出物
5.1 第一步:需求分析与驱动分类
动手前先回答三个问题,它们决定了后面所有写法: ① 这是什么类型的设备?② 它挂在什么总线上?③ 是否需要 VFS 节点? 尤其是第三个------它决定你是否需要实现 file_ops 并处理 POSIX 语义。
| 驱动类型 | 典型器件 | 依赖总线 | 是否建议挂 VFS | 加载等级建议 |
|---|---|---|---|---|
| 存储类 | SPI NAND、SPI NOR Flash | SPI / SDIO | 建议挂(便于挂载文件系统、shell 直接测试) | LEVEL1_DRIVER_ENTRY |
| 传感器类 | 温湿度、加速度、光感、气压 | I2C / SPI | 通常不挂(走 sensor 框架更合适) | LEVEL1_DRIVER_ENTRY |
| 通信类 | WiFi/BLE 模组、NB-IoT、LoRa | UART / SDIO / SPI | 按模组接口定(AT 指令常用 UART + VFS) | LEVEL1 到 LEVEL2 |
| 人机交互 | 按键、LED、显示屏、Touch | GPIO / SPI / I2C | 一般不挂 | LEVEL0_DRIVER_ENTRY |
| 总线控制器 | SPI 控制器、I2C 控制器 | --- | 不挂 | BUS_DRIVER_ENTRY |
| 复合设备 | 基于 Flash 的分区设备、虚拟设备 | 依赖底层设备 | 视情况 | LEVEL2_DRIVER_ENTRY |
一个判断是否需要 VFS 的经验法则
如果上层希望用文件的方式使用它 (读写流、需要挂载文件系统、希望在 shell 里 用 cat / echo 验证),就挂 VFS;如果上层是结构化调用(读一次采样值、设置一个参数、订阅一个事件),走 AOS 原生设备接口或 sensor 框架更合适,省掉一层查找与拷贝开销。
5.2 第二步:依赖组件配置
在 package.yaml 里声明依赖,在 aos.mk 里声明源文件。 依赖要写最小集:不挂 VFS 就别依赖 vfs,不做调试命令就别依赖 cli。
# components/drivers/drv_spinand/package.yaml
name: drv_spinand
version: master
description: SPI NAND 驱动(基于 CSI,可挂 VFS)
depends:
- drivers: master # 必需:设备框架与分级加载宏
- csi: master # 必需:csi_spi_* / csi_gpio_* 接口
- vfs: master # 可选:仅当需要 /dev/nand0 时
- cli: master # 可选:仅当需要调试命令时
def_config:
CONFIG_DRV_SPINAND: 1
CONFIG_DRV_SPINAND_DEBUG: 0 # 出厂默认关闭调试
CONFIG_DRV_SPINAND_TEST_CMD: 0 # 出厂默认关闭测试命令
# components/drivers/drv_spinand/aos.mk
NAME := drv_spinand
$(NAME)_TYPE := kernel
$(NAME)_SOURCES := drv_spinand.c
$(NAME)_INCLUDES := .
GLOBAL_INCLUDES += .
GLOBAL_DEFINES += CONFIG_DRV_SPINAND=1
$(NAME)_COMPONENTS += csi drivers
# 条件编译:调试与测试命令用宏隔离,避免带调试代码出货
ifeq ($(CONFIG_DRV_SPINAND_DEBUG),1)
GLOBAL_DEFINES += CONFIG_DRV_SPINAND_DEBUG=1
endif
5.3 第三步:驱动代码编写(核心)
一个驱动源文件建议按图 15 的五段组织。这样划分的好处是: 换芯片只重写第 2 段(CSI 封装),换器件只重写第 3 段(器件协议),换框架只改第 4 段。
drv_xxx.c 的内部组织:从上到下五段,每段职责单一第 1 段:配置与私有数据结构从 board.h 引用资源宏;定义设备私有结构体(句柄、缓冲、锁、状态)第 2 段:CSI 封装层把 csi_spi_transfer / csi_i2c_mem_read 包成器件级原语(发命令、读状态、等就绪)第 3 段:器件协议实现器件命令字、页/块读写流程、擦除、坏块管理、ECC 状态判断第 4 段:ops 与设备注册file_ops 或 aos 设备 ops 的填充;设备名、私有数据绑定第 5 段:初始化入口与等级声明init 函数 + DRIVER_ENTRY 宏;可选 CLI 测试命令配套文件:drv_xxx.h(对外暴露 ops 结构与 ioctl 命令码)、package.yaml(依赖)、aos.mk(源文件)第 2 段是关键隔离带:器件协议层(第 3 段)只认第 2 段的接口,因此换芯片时只需重写第 2 段
图 15 · 驱动源文件五段结构:配置、CSI 封装、器件协议、ops 与注册、初始化入口
5.3.1 第 1 段:配置与私有数据结构
/* drv_spinand.c ------ 第 1 段:配置与私有数据结构 */
#include <aos/aos.h>
#include <csi/csi.h>
#include <vfs.h>
#include "board.h" /* 板级资源宏:引脚、总线索引、速率 */
#include "drv_spinand.h"
/* ---- 器件命令字(来自器件 datasheet) ---- */
#define CMD_READ_ID 0x9F
#define CMD_PAGE_READ 0x13
#define CMD_GET_FEATURE 0x0F
#define CMD_SET_FEATURE 0x1F
#define CMD_PROGRAM_LOAD 0x02
#define CMD_PROGRAM_EXEC 0x10
#define CMD_BLOCK_ERASE 0xD8
#define CMD_WRITE_ENABLE 0x06
#define CMD_RESET 0xFF
/* ---- 状态寄存器位 ---- */
#define STATUS_OIP (1 << 0) /* 1 表示忙 */
#define STATUS_ECC_MASK 0x30
#define STATUS_ECC_NO_ERR 0x00
/* ---- 器件几何参数(按识别结果填充) ---- */
typedef struct {
uint32_t page_size; /* 页大小,典型 2048 */
uint32_t spare_size; /* 冗余区大小,典型 64 或 128 */
uint32_t pages_per_block; /* 每块页数,典型 64 */
uint32_t block_count; /* 块数 */
uint32_t max_baud; /* 该器件支持的最高速率 */
} spinand_geo_t;
/* ---- 设备私有结构(静态分配,禁止在 init 里 malloc) ---- */
typedef struct {
csi_spi_t spi; /* CSI SPI 句柄 */
csi_gpio_t cs_gpio; /* 片选 GPIO 句柄 */
uint8_t cs_pin;
spinand_geo_t geo;
uint8_t *page_buf; /* 页缓冲:静态数组或 init 时一次性分配 */
aos_mutex_t lock; /* 并发保护 */
uint32_t busy_timeout_ms;
int inited;
} spinand_dev_t;
static spinand_dev_t g_spinand; /* 单例:静态分配 */
static uint8_t g_page_buf[2048 + 128]; /* 页缓冲:静态分配 */
为什么强调静态分配
IoT MCU 的 RAM 通常只有几十 KB,运行期反复 malloc/free 会产生碎片 ,长时间运行后可能出现「总空闲内存够,但申请大块失败」。驱动里的缓冲区、锁、句柄一律静态分配 ,只在必要且一次性的场景使用 aos_malloc。
5.3.2 第 2 段:CSI 封装层(隔离带)
这一段是唯一调用 csi_xxx 的地方,把通用总线操作包成器件级原语。 器件协议层只认这一段,从而实现跨芯片可移植。
/* 第 2 段:CSI 封装 ------ 只有这里出现 csi_ 前缀 */
static void cs_select(int on)
{
/* 片选:低有效。用 GPIO 软件控制,不依赖控制器硬件 CS */
csi_gpio_write(&g_spinand.cs_gpio, g_spinand.cs_pin, on ? 0 : 1);
}
static int spi_xfer(const uint8_t *tx, uint8_t *rx, uint32_t len)
{
int32_t ret = csi_spi_transfer(&g_spinand.spi, tx, rx, len, 1000 /* ms */);
if (ret < 0) {
LOGE("NAND", "spi xfer failed, ret=%d", ret); /* 返回值必须打印 */
return -EIO;
}
return 0;
}
/* 器件级原语:发一个命令 */
static int nand_cmd(uint8_t cmd)
{
cs_select(1);
int ret = spi_xfer(&cmd, NULL, 1);
cs_select(0);
return ret;
}
/* 器件级原语:读状态寄存器 */
static int nand_get_status(uint8_t *status)
{
uint8_t tx[2] = { CMD_GET_FEATURE, 0xC0 };
uint8_t rx[2] = { 0 };
cs_select(1);
int ret = spi_xfer(tx, rx, sizeof(tx));
cs_select(0);
if (ret != 0) return ret;
*status = rx[1];
return 0;
}
/* 器件级原语:等待器件空闲(必须有超时上限) */
static int nand_wait_ready(uint32_t timeout_ms)
{
uint32_t start = aos_now_ms();
uint8_t st = 0xFF;
while ((aos_now_ms() - start) < timeout_ms) {
if (nand_get_status(&st) != 0)
return -EIO;
if ((st & STATUS_OIP) == 0)
return 0; /* 空闲 */
aos_msleep(1); /* 让出 CPU,不要死等 */
}
LOGE("NAND", "wait ready timeout, st=0x%02X", st);
return -ETIMEDOUT; /* 超时必须返回,不能死循环 */
}
5.3.3 第 3 段:器件协议实现
这一段实现器件真正的协议:读 ID、页读、页编程、块擦除等。 它只调用第 2 段的原语,不出现任何 csi_ 调用。
/* 第 3 段:器件协议 ------ 只调第 2 段的原语 */
/* 读 ID:拿不到 ID 说明时序/片选/供电有问题,是最有效的第一步验证 */
static int nand_read_id(uint8_t *id, uint32_t len)
{
uint8_t tx[2] = { CMD_READ_ID, 0x00 };
uint8_t rx[2 + 4] = { 0 };
uint32_t n = (len > 4) ? 4 : len;
cs_select(1);
int ret = spi_xfer(tx, rx, 2 + n);
cs_select(0);
if (ret != 0) return ret;
memcpy(id, &rx[2], len);
LOGI("NAND", "id = %02X %02X", id[0], id[1]);
return 0;
}
/* 页读:24 位行地址 → 等待器件把数据搬到 cache → 读回 cache */
static int nand_page_read(uint32_t page, uint8_t *buf, uint32_t len)
{
uint8_t cmd[4];
/* 1) 发页读命令 + 24 位行地址 */
cmd[0] = CMD_PAGE_READ;
cmd[1] = (page >> 16) & 0xFF;
cmd[2] = (page >> 8) & 0xFF;
cmd[3] = (page >> 0) & 0xFF;
cs_select(1);
int ret = spi_xfer(cmd, NULL, 4);
cs_select(0);
if (ret != 0) return ret;
/* 2) 等待器件内部搬移完成(这一步最容易漏) */
ret = nand_wait_ready(g_spinand.busy_timeout_ms);
if (ret != 0) return ret;
/* 3) 从 cache 读回数据 */
uint8_t hdr[4] = { 0x03, 0x00, 0x00, 0x00 }; /* 读 cache 命令 + dummy + 列地址 */
cs_select(1);
ret = spi_xfer(hdr, NULL, 4);
if (ret == 0)
ret = spi_xfer(NULL, buf, len);
cs_select(0);
if (ret != 0) return ret;
/* 4) ECC 状态判断:不要只看「有没有数据」,要看纠错结果 */
uint8_t st = 0;
nand_get_status(&st);
if ((st & STATUS_ECC_MASK) != STATUS_ECC_NO_ERR) {
LOGW("NAND", "ecc corrected/flagged, st=0x%02X", st);
}
return 0;
}
/* 块擦除:先写使能,再发擦除命令,再等待 */
static int nand_block_erase(uint32_t block)
{
uint32_t page = block * g_spinand.geo.pages_per_block;
uint8_t cmd[4];
nand_cmd(CMD_WRITE_ENABLE);
cmd[0] = CMD_BLOCK_ERASE;
cmd[1] = (page >> 16) & 0xFF;
cmd[2] = (page >> 8) & 0xFF;
cmd[3] = (page >> 0) & 0xFF;
cs_select(1);
int ret = spi_xfer(cmd, NULL, 4);
cs_select(0);
if (ret != 0) return ret;
return nand_wait_ready(g_spinand.busy_timeout_ms);
}
存储驱动最容易漏的三件事
① 发完页读命令后没有等待器件内部搬移 → 读回全 0xFF 或旧数据;② 没有判断 ECC 状态 → 数据已被纠错但错误被静默吞掉,长期运行后丢数据;③ 忙等待没有超时上限 → 器件异常时整个任务永久挂死,表现为「系统卡住」。
5.3.4 第 4 段:ops 与设备注册
这一段把器件能力包装成框架要求的接口。若挂 VFS 就填充 file_ops; 若走 AOS 原生设备就填充对应的设备 ops 并调用注册接口。图 16 是 SPI NAND 的完整分层。
实例:SPI NAND 驱动在整套模型里的位置与数据流向应用层open("/dev/nand0") → read / write / ioctlVFS + file_ops路径查表 → nand_fops.open / read / write / ioctl分区与磨损均衡(可选)分区划分、坏块表、逻辑块到物理块映射(也可由文件系统组件承担)器件协议层读 ID、页读、页编程、块擦除、状态寄存器轮询、ECC 判断CSI 封装层spi_transfer(命令+地址+dummy+数据)、CS 控制、速率配置CSI + 硬件csi_spi_transfer / csi_gpio_write → SPI 控制器 → 器件关键设计点• 页缓冲:静态分配一页,避免 malloc• 忙等待:轮询状态寄存器要有超时上限• 擦写期间:禁止被其他任务打断访问,需加锁与 Linux 实现的差异• 无 DTS:引脚与速率写死在 board.h• 分区与磨损均衡形态按组件而定• 加载顺序靠 LEVEL1_DRIVER_ENTRY 声明
图 16 · SPI NAND 驱动实例分层:从应用到 CSI 的完整数据流向与关键设计点
/* 第 4 段:ops 与注册 */
/* ---- ioctl 命令码(放在 drv_spinand.h 对外暴露) ---- */
#define NAND_IOC_MAGIC 'N'
#define NAND_IOC_ERASE_BLOCK _IOW(NAND_IOC_MAGIC, 1, uint32_t)
#define NAND_IOC_GET_GEO _IOR(NAND_IOC_MAGIC, 2, spinand_geo_t)
#define NAND_IOC_READ_STATUS _IOR(NAND_IOC_MAGIC, 3, uint8_t)
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,
};
ops 实现的四条纪律
① 返回值语义正确 :成功返回实际字节数或 0,失败返回负错误码;② ioctl 未知命令返回 -EINVAL ,返回 0 会让上层误以为执行成功;③ offset 由驱动维护 ,读写后要推进文件偏移;④ 所有硬件访问加锁,多任务并发访问同一器件必然出竞态。
5.3.5 第 5 段:初始化入口与等级声明
最后一步是把 init 函数挂到正确的启动等级上。等级选错是这个阶段最常见也最隐蔽的错误。
/* 第 5 段:初始化入口与分级加载声明 */
static int spinand_drv_init(void)
{
memset(&g_spinand, 0, sizeof(g_spinand));
/* ① CSI 句柄初始化:依赖 SPI 总线控制器已就绪(BUS 级已加载) */
if (csi_spi_init(&g_spinand.spi, SPI_NAND_BUS_IDX) != CSI_OK) {
LOGE("NAND", "spi init failed");
return -1;
}
/* ② 片选 GPIO:片选必须独占,不能与其他器件共用 */
if (csi_gpio_init(&g_spinand.cs_gpio, SPI_NAND_CS_GPIO_IDX) != CSI_OK) {
LOGE("NAND", "gpio init failed");
return -1;
}
csi_gpio_mode(&g_spinand.cs_gpio, SPI_NAND_CS_PIN, GPIO_MODE_PUSH_PULL);
csi_gpio_write(&g_spinand.cs_gpio, SPI_NAND_CS_PIN, 1); /* 初始未选中 */
g_spinand.cs_pin = SPI_NAND_CS_PIN;
/* ③ 速率:先低速跑通,稳定后再提到器件上限 */
csi_spi_mode(&g_spinand.spi, SPI_MODE_0);
csi_spi_baud(&g_spinand.spi, SPI_NAND_MAX_BAUD);
/* ④ 并发保护与参数 */
aos_mutex_new(&g_spinand.lock);
g_spinand.page_buf = g_page_buf;
g_spinand.busy_timeout_ms = 3000;
/* ⑤ 识别器件:读 ID 是验证硬件链路的第一道关 */
uint8_t id[2] = { 0 };
if (nand_read_id(id, 2) != 0 || id[0] == 0x00 || id[0] == 0xFF) {
LOGE("NAND", "read id failed, id=%02X %02X", id[0], id[1]);
return -1; /* 读不到 ID 就查时钟/片选/供电,不要往下走 */
}
spinand_geo_fill(id, &g_spinand.geo); /* 按 ID 填几何参数 */
/* ⑥ 注册到 AOS 设备框架 */
if (aos_dev_register(&g_spinand_dev) != 0) {
LOGE("NAND", "dev register failed");
return -1;
}
/* ⑦ 可选:挂 VFS,生成 /dev/nand0 */
if (vfs_register_driver("/dev/nand0", &nand_fops, &g_spinand) != 0) {
LOGE("NAND", "vfs register failed");
return -1;
}
g_spinand.inited = 1;
LOGI("NAND", "inited: page=%d block=%d baud=%d",
g_spinand.geo.page_size, g_spinand.geo.block_count, SPI_NAND_MAX_BAUD);
return 0;
}
/* 关键:SPI NAND 挂在 SPI 总线上 → 必须晚于总线 → LEVEL1 */
LEVEL1_DRIVER_ENTRY(spinand_drv_init)
init 函数的正确姿势
① 每步失败都要打印并返回 ,不留「半初始化」状态;② 读 ID 作为硬件链路自检 ,读不到就别往下走(省掉后面大量无效排查);③ 先低速再提速 ,一次把速率拉满会让时序问题与逻辑问题混淆;④ 结尾打印关键参数,让启动日志成为第一手的调试证据。
5.3.6 I2C 传感器驱动的差异点
传感器驱动通常不挂 VFS ,而是对接 sensor 框架或只提供 AOS 设备接口。 核心差异在于:用 csi_i2c_* 而非 csi_spi_*,且多为「寄存器读写」模型。
/* I2C 传感器:寄存器读写模型 */
static csi_i2c_t g_i2c;
static int sensor_read_reg(uint8_t reg, uint8_t *val)
{
int32_t ret = csi_i2c_mem_read(&g_i2c, SENSOR_I2C_ADDR, reg,
CSI_I2C_MEM_ADDR_8BIT, val, 1, 100);
if (ret != 0) {
LOGE("SENSOR", "i2c read reg 0x%02X failed, ret=%d", reg, ret);
return -EIO;
}
return 0;
}
static int sensor_drv_init(void)
{
if (csi_i2c_init(&g_i2c, SENSOR_I2C_BUS_IDX) != CSI_OK)
return -1;
csi_i2c_mode(&g_i2c, CSI_I2C_MODE_MASTER);
csi_i2c_speed(&g_i2c, CSI_I2C_BUS_SPEED_STANDARD); /* 100k 起步 */
uint8_t who = 0;
if (sensor_read_reg(REG_WHO_AM_I, &who) != 0 || who != EXPECTED_ID) {
LOGE("SENSOR", "who am i mismatch: 0x%02X", who); /* 地址错或总线错 */
return -1;
}
/* 中断引脚:配置为输入 + 上升沿触发 */
csi_gpio_init(&g_int_gpio, SENSOR_INT_GPIO_IDX);
csi_gpio_mode(&g_int_gpio, SENSOR_INT_PIN, GPIO_MODE_PULL_UP);
csi_gpio_attach_callback(&g_int_gpio, sensor_isr, &g_int_gpio);
csi_irq_enable(SENSOR_INT_IRQ_NUM);
aos_dev_register(&g_sensor_dev); /* 不挂 VFS */
return 0;
}
LEVEL1_DRIVER_ENTRY(sensor_drv_init) /* 同样挂在 I2C 总线之后 */
I2C 的两个高频坑
① 地址是 7 位还是 8 位 :datasheet 常给 8 位(含读写位),CSI 接口通常要 7 位地址,忘记右移一位会一直 NACK;② 重复起始条件 :读寄存器通常是「写寄存器地址 + 重复起始 + 读数据」,用 mem_read 这类封装接口比自己拼两段传输更可靠。
5.4 第四步:板级初始化
驱动代码里只引用 board.h 的宏,真正的引脚复用、时钟使能写在 board.c。 这样换板只改 board 目录,驱动零改动。完整写法见 4.2 节。
/* board/my_board/board.h ------ 新增 SPI NAND 的资源定义 */
#define SPI_NAND_BUS_IDX 0
#define SPI_NAND_CS_GPIO_IDX 0
#define SPI_NAND_CS_PIN 12
#define SPI_NAND_MAX_BAUD 24000000
/* board/my_board/board.c ------ 在 board_init 中完成时钟与引脚 */
void board_init(void)
{
board_clock_init(); /* ① 先开时钟 */
board_pinmux_init(); /* ② 再配引脚复用 */
/* ③ 片选初始拉高由驱动 init 里再做一次,双保险 */
}
5.5 第五步:编译配置与裁剪
最后确认三处一致:驱动组件被 product 依赖、源文件进入 aos.mk、调试宏按需开关。
# product/<solution>/package.yaml ------ 别漏这一行
depends:
- drv_spinand: master
# 编译
aos make my_solution@my_board -j8
# 编译后确认驱动确实进了固件(三选一)
# ① 看 map 里是否有 drv_spinand 的符号
# ② 看启动日志是否有 NAND inited
# ③ CLI 里看 /dev 节点是否存在
「代码写完了但设备不出现」的三种原因
① product 的 depends 没加 → 整份驱动没编译,且无任何报错;② 驱动 init 返回了 -1 但日志没开 → 注册失败被静默吞掉;③ 加载等级早于总线 → CSI 初始化失败,同样返回 -1。排查顺序:先看有没有编进去(map 或符号),再看有没有被调用(日志),最后看返回值。
5.6 第六步:基础功能测试
用 CLI 做最小闭环验证。建议每个驱动都自带一条测试命令(用宏隔离,出厂关闭), 它能在没有应用层的情况下证明驱动可用。
/* 驱动自带的测试命令(CONFIG_DRV_SPINAND_TEST_CMD=1 时编译) */
static void nand_test_cmd(char *pwbuf, int blen, int argc, char **argv)
{
uint8_t id[2] = { 0 };
spinand_geo_t geo;
if (nand_read_id(id, 2) == 0) {
printf("ID: %02X %02X\n", id[0], id[1]);
} else {
printf("read id FAILED\n");
return;
}
nand_ioctl(NULL, NAND_IOC_GET_GEO, (unsigned long)&geo);
printf("page=%d spare=%d pages/blk=%d blocks=%d\n",
geo.page_size, geo.spare_size, geo.pages_per_block, geo.block_count);
/* 擦 → 写 → 读 → 比对 */
uint8_t pattern[256];
memset(pattern, 0xA5, sizeof(pattern));
if (nand_block_erase(0) == 0 &&
nand_page_program(0, pattern, sizeof(pattern)) == 0) {
uint8_t rb[256] = { 0 };
if (nand_page_read(0, rb, sizeof(rb)) == 0) {
printf("verify: %s\n", memcmp(pattern, rb, sizeof(rb)) == 0 ? "PASS" : "FAIL");
}
}
}
/* 注册到 CLI */
ALIOS_CLI_CMD_REGISTER(nand_test, nand_test_cmd, "spi nand basic test");
| 验证项 | 方法 | 通过标准 |
|---|---|---|
| 驱动已加载 | 启动日志 / CLI 命令 | 出现设备初始化成功日志,无 error |
| 设备已注册 | CLI 列出设备或查看 /dev | 能看到 /dev/nand0 或对应设备名 |
| 读 ID 正确 | 测试命令或应用层 ioctl | ID 与 datasheet 一致,不是 0x00 / 0xFF |
| 读写一致 | 写 pattern 后读回比对 | 多次循环全部 PASS |
| 擦除有效 | 擦除后读回 | 全 0xFF(或器件定义的擦除值) |
| 中断正常 | 触发事件后看计数 | ISR 被调用,且没有触发看门狗 |
| 并发安全 | 两个任务同时读写 | 无数据错乱,无死锁 |
5.7 第七步:集成测试与压力测试
功能通过不等于可用。集成与压力测试要覆盖长时间、并发、异常三类场景, 具体矩阵见第 9 章。这里只强调三条最容易暴露问题的:
- 循环读写:连续读写数万次,观察是否出现偶发错误(暴露时序与忙等待问题)。
- 并发访问:两个以上任务同时访问同一器件(暴露锁缺失)。
- 反复重启:反复上电或复位上百次(暴露初始化竞态与未初始化变量)。
5.8 第八步:驱动组件打包与提交
作为组件提交时,目录结构与交付物要规范,便于他人复用与裁剪。
components/drivers/drv_spinand/
├─ drv_spinand.c # 驱动主体:五段结构
├─ drv_spinand.h # 对外头文件:ioctl 命令码、geo 结构、对外 API
├─ package.yaml # 组件名、版本、依赖、默认宏
├─ aos.mk # 源文件、头文件路径、宏
├─ README.md # 用法说明、板级配置要求、已知限制
└─ test/
├─ test_drv_spinand.c # 测试用例(可编译为独立测试固件)
└─ test_report.md # 测试报告:环境、用例、结果、ROM/RAM 占用
提交前的最后一道自检
① 是否还有直操作寄存器 的代码(应全部在 CSI 层)?② 是否还有裸数字 (引脚号、中断号应来自 board.h)?③ 调试代码是否被宏隔离 ?④ 依赖是否最小化 ?⑤ README 是否说明了板级要求(别人照着能不能配通)?
后续章节请下载「芯参谋」查看
......(篇幅原因, 还有14章· 详见芯参谋→方案设计)
打开路径:🔧解决方案&应用市场分析 -> 原理方案设计 -> 嵌入式驱动&系统开发 -> AliOS Things_驱动开发指南
相关报告 · 1
- 经文机(Sutra Player)_市场分析市场应用分析
- 启英泰伦(Chipintel)_开发指南原理方案设计
- LDO_批量输出漂移_量产排查调试&解决方案
- 智能车机(Smart Head Unit)_市场分析市场应用分析
- 样机通过量产失败_差异对照清单调试&解决方案
相关报告 · 2
- 产线偶发读写失败_治具供电不稳_量产排查调试&解决方案
- 海思(HiSilicon)平台_SPI_NAND_量产常见失败_量产排查调试&解决方案
- 旗芯微(Flagchip)_开发指南原理方案设计
- 肺功能仪(Spirometer)_市场分析市场应用分析
- 智能门锁(Smart Lock)_市场分析市场应用分析