1. 课程导论与基础环境:ARM Linux驱动开发全景图

各位同学,欢迎来到这门实战课。我先说说这门课的定位——不是教你怎么写一个hello world驱动,而是教你驱动写完之后,怎么让它跑得快、跑得稳、出了问题能快速找到根因。

我做了十几年嵌入式Linux,踩过的坑不少。有些驱动看起来功能正常,一上高负载就崩;有些吞吐量上不去,查了半天发现是中断处理写得不对。这些经验,我会在这门课里一一拆解。

1.1 ARM Linux驱动开发全景图

先看整体框架。ARM Linux驱动开发,说白了就是让内核能正确、高效地控制外设。整个体系分三层:

  • 应用层:用户态程序通过文件接口(open/read/write/ioctl)与驱动交互
  • 内核层:驱动模块注册到内核,处理硬件操作、中断、DMA、数据同步
  • 硬件层:外设寄存器、中断控制器、DMA引擎、总线(I2C/SPI/UART/USB/PCIe)

我个人习惯把驱动开发分成四个阶段:

  1. 功能实现:先让设备能动起来,数据能收发
  2. 正确性验证:边界条件、并发访问、内存泄漏检查
  3. 性能调优:延迟、吞吐量、CPU占用率优化
  4. 稳定性加固:长时间跑、异常恢复、热插拔处理

很多工程师卡在第一步和第二步之间,觉得功能对了就完事了。其实真正的挑战在后面两步。

核心观点:驱动开发不是写完代码就结束,性能调优和故障定位才是体现功力的地方。

ARM Linux 驱动开发全景图 应用层 (Userspace) open / read / write / ioctl / mmap / poll 内核层 (Kernel Space) 驱动模块注册 / file_operations / 中断处理 / tasklet / workqueue DMA引擎 / 内存映射 / 同步机制 (spinlock/mutex/waitqueue) 设备模型 (platform/device tree / ACPI) 硬件层 (Hardware) 外设寄存器 / 中断控制器 (GIC) / DMA控制器 / 总线 (I2C/SPI/UART/USB/PCIe) 时钟 / 电源管理 / 复位逻辑 性能调优关注点 延迟 / 吞吐量 CPU占用率 故障定位方法 printk / ftrace perf / tracepoint kgdb / kdump 逻辑分析仪 / 示波器

1.2 性能调优的核心指标

调优之前,先搞清楚要调什么。我一般盯着三个指标:

指标 定义 典型场景 我踩过的坑
延迟 从请求发出到响应返回的时间 中断响应、I2C/SPI读写、GPIO电平变化 中断里加了printk,延迟从5us飙到200us
吞吐量 单位时间内处理的数据量 UART/SPI/USB批量传输、网络包处理 DMA没配好,CPU轮询导致吞吐量腰斩
CPU占用率 驱动代码消耗的CPU时间占比 中断频繁、轮询模式、数据拷贝 某驱动用轮询收数据,CPU占用90%+

我的经验:这三个指标往往互相制约。降低延迟可能增加CPU占用,提高吞吐量可能牺牲延迟。调优就是找平衡点。

1.3 故障定位方法论

驱动出问题了,别急着改代码。先问自己三个问题:

  1. 问题能稳定复现吗?——不能复现的问题最难查,先想办法稳定触发
  2. 是硬件问题还是软件问题?——用示波器或逻辑分析仪看波形,排除硬件
  3. 是驱动本身的问题还是上层应用的问题?——写个最小测试程序,绕开应用层

我常用的定位手段,按优先级排:

  • printk + dmesg:最原始但最有效,注意别在中断里加太多
  • ftrace:跟踪函数调用、中断延迟、调度延迟,我几乎天天用
  • perf:分析CPU热点、缓存命中率、分支预测
  • kgdb:内核调试器,适合死锁或crash分析
  • 逻辑分析仪/示波器:看硬件时序,确认外设是否按预期工作

避坑指南:我曾经花了两天查一个SPI驱动偶尔丢数据的问题,最后发现是片选信号时序不满足规格书要求。硬件问题用软件手段查,事倍功半。先确认硬件没问题,再查软件。

1.4 开发板与交叉编译环境搭建

工欲善其事,必先利其器。环境搭不好,后面全是坑。

1.4.1 硬件准备

我建议用以下开发板之一:

  • Raspberry Pi 4/5:资料多,社区活跃,适合入门
  • STM32MP157:Cortex-A7 + M4双核,适合学中断和实时性
  • i.MX8M Mini:工业级,适合学PCIe和高速外设

你手头有什么板子就用什么,关键是能跑Linux主线内核。

1.4.2 交叉编译工具链

在x86主机上编译ARM代码,需要交叉工具链。我推荐两种方式:

方式 命令/工具 适用场景
Linaro GCC arm-linux-gnueabihf-gcc 通用ARM 32位
ARM GCC (aarch64) aarch64-linux-gnu-gcc ARM 64位 (Cortex-A53/A72等)
Buildroot make 一键编译工具链+根文件系统 定制化嵌入式系统
Yocto bitbake 大型项目,生产级

我个人习惯用Linaro的预编译工具链,省时间。安装很简单:

# 下载并解压
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
tar -xf gcc-linaro-*.tar.xz -C /opt/

# 加入PATH
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH

# 验证
arm-linux-gnueabihf-gcc --version

1.4.3 内核编译与模块开发

驱动开发需要内核源码和编译好的内核。步骤大致如下:

  1. 下载对应板子的内核源码(或主线内核)
  2. 配置内核:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
  3. 编译内核:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4
  4. 编译设备树:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
  5. 编译内核模块:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

小技巧:第一次编译内核时加上 CONFIG_DEBUG_INFO=y,后面调试会方便很多。虽然编译时间会长一点,但值得。

1.4.4 第一个驱动模块

写一个最简单的hello模块,验证环境是否正常:

// hello.c
#include <linux/module.h>
#include <linux/kernel.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, ARM Linux Driver!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, ARM Linux Driver!\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");

编译:

make -C /path/to/kernel M=$(pwd) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

把生成的 hello.ko 拷贝到开发板,加载:

insmod hello.ko
dmesg | tail

看到打印信息,环境就通了。

记住:环境搭建是基础中的基础。后面30章的所有实战,都依赖这个环境。花点时间把这一步做扎实,后面会省很多事。