1. ARM外设驱动开发概述

各位同学,欢迎来到这门实战课。我是你们的老朋友,一个在嵌入式底层摸爬滚打了十几年的工程师。今天咱们正式开始第一讲,聊聊ARM外设驱动开发的整体面貌。

说实话,我见过太多新手一上来就盯着寄存器手册死磕,结果越看越懵。为什么?因为脑子里没有一张“地图”。咱们这节课,就是帮你画这张地图。

ARM处理器架构简介

ARM处理器,说白了就是一颗“大脑”。但它这颗大脑跟咱们PC上的x86不太一样。ARM的设计哲学是低功耗、高能效,所以它统治了移动端和嵌入式领域。

我个人习惯把ARM架构分成几个关键层次来看:

  • 内核(Core):执行指令,比如Cortex-M3、Cortex-A72。M系列主打微控制器,A系列主打应用处理器。
  • 总线(Bus):数据在内部跑来跑去的“高速公路”。常见的有AMBA(高级微控制器总线架构),里面又分AHB(高速总线)和APB(外设总线)。
  • 外设(Peripheral):挂在总线上的“功能模块”,比如UART、GPIO、I2C、SPI。咱们这门课的主角就是它们。

这里有个关键点:外设是通过寄存器来控制的。你往某个内存地址写一个值,外设就干活了。这个地址,就是寄存器的映射地址。

核心概念: ARM使用内存映射I/O(MMIO)。外设的寄存器被映射到CPU的地址空间中。读/写这些地址,就是在操作外设。

我刚开始学的时候,总搞不清“总线”和“外设”的关系。后来我画了一张图,一下子就通了。咱们也来画一张。

ARM处理器架构与外设连接关系 ARM处理器架构与外设连接关系 ARM Core (CPU) 系统总线 (AMBA/AHB) 外设总线 (APB) UART 控制器 (串口) GPIO 控制器 (通用IO) I2C 控制器 (I2C总线) CPU通过总线访问外设寄存器,每个外设都有自己独立的地址空间

你看,CPU通过系统总线连接到外设总线,外设总线下面挂着各种控制器。每个控制器内部都有一组寄存器,CPU通过读写这些寄存器来让外设工作。

外设驱动开发模型

驱动开发,说白了就是“操作寄存器”。但怎么操作,有两种主流模型:

模型 特点 适用场景
查询模式(Polling) CPU不断轮询状态寄存器,直到条件满足 简单、低实时性要求、代码量小
中断模式(Interrupt) 外设完成操作后主动通知CPU 高实时性、多任务、复杂系统
DMA模式 外设直接访问内存,CPU不参与数据传输 大数据量传输(如音频、视频)

我在项目中遇到过一个问题:一个同事用查询模式做串口接收,结果CPU被死死卡住,整个系统响应都变慢了。后来改成中断模式,问题立刻解决。所以,选对模型很重要。

我的建议: 初学者先从查询模式入手,理解寄存器操作流程。等熟悉了,再切换到中断模式。别一上来就搞DMA,容易把自己绕晕。

咱们来看一个最简单的GPIO输出代码,感受一下“操作寄存器”是什么感觉:

// 伪代码:设置GPIO引脚为输出,并输出高电平
// 假设GPIO控制器的基地址为 0x40020000

#define GPIO_BASE      0x40020000
#define GPIO_MODER     (GPIO_BASE + 0x00)  // 模式寄存器
#define GPIO_ODR       (GPIO_BASE + 0x14)  // 输出数据寄存器

void gpio_init(void) {
    unsigned int *moder = (unsigned int *)GPIO_MODER;
    // 将引脚0设置为输出模式(假设每2位控制一个引脚,01表示输出)
    *moder |= (0x01 << 0);  // 设置位[1:0] = 01
}

void gpio_set_high(void) {
    unsigned int *odr = (unsigned int *)GPIO_ODR;
    *odr |= (0x01 << 0);   // 引脚0输出高电平
}

你看,就是直接往内存地址写值。简单吧?但实际项目中,寄存器配置要复杂得多,还要考虑时序、电气特性、复用功能等等。

裸机驱动与Linux驱动的区别

这个问题,我几乎每次面试都会问候选人。很多人答不上来,或者答得很模糊。咱们今天把它彻底讲清楚。

裸机驱动,就是没有操作系统,直接操作硬件。你写的代码就是“上帝”,想怎么访问寄存器就怎么访问。优点是简单、直接、实时性好。缺点是:

  • 所有任务自己调度,没有多任务支持
  • 代码复用性差,换个芯片基本重写
  • 没有内存管理,一个野指针就能让系统崩溃

Linux驱动,是在操作系统内核里运行。你不能直接访问硬件了,得通过内核提供的接口。Linux驱动开发,说白了就是写一个“内核模块”,告诉内核:“这个硬件归我管,应用程序想用它,得通过我。”

我整理了一个对比表,大家一看就明白:

对比维度 裸机驱动 Linux驱动
运行环境 无操作系统,直接运行在硬件上 运行在Linux内核空间
硬件访问 直接读写物理地址 通过ioremap映射,使用读写函数(如readl/writel)
中断处理 直接写中断服务函数 注册中断处理函数,遵循内核中断上下文规则
并发管理 基本不考虑,或简单关中断 必须使用自旋锁、信号量等内核同步机制
代码复用 差,与芯片强耦合 好,通过设备树和驱动分离,换芯片只需改设备树
调试难度 相对简单,可用仿真器单步 较复杂,常用printk、ftrace、kgdb

注意: 很多初学者在Linux驱动里直接操作物理地址,这是大忌!Linux有MMU(内存管理单元),你看到的地址是虚拟地址,不是物理地址。必须用ioremap()把物理地址映射到内核虚拟地址空间,再用readl()/writel()来访问。

我曾经见过一个案例:某工程师在Linux驱动里直接用指针访问外设地址,结果系统随机崩溃。查了三天,最后发现是MMU搞的鬼。从那以后,我再也不敢在Linux驱动里“裸奔”了。

咱们这门课,会先带大家从裸机驱动入手,把UART和GPIO的寄存器操作彻底搞明白。然后再进入Linux驱动世界,看看同样的硬件,在操作系统下该怎么玩。这样对比着学,理解会深刻得多。