Skip to content

第三层:驱动开发与外设编程

驱动开发是嵌入式工程里最靠近硬件的一层。本章的重点是学会从寄存器、时钟、引脚配置、通信时序和中断/DMA 等角度去理解外设,而不是只会调用库函数。

建议学习目标:

  • 掌握寄存器地址、位域和掩码操作的基本方法。
  • 理解 GPIO、UART、SPI、I2C、ADC、DMA、CAN 等常见外设的工作原理。
  • 能区分寄存器级开发、HAL、LL 三种开发方式的差异。
  • 具备排查通信异常、时序错误和配置错误的基本思路。

阅读建议:从 GPIO、UART 这类简单外设切入,再过渡到 DMA、CAN 等更复杂的模块。


寄存器级开发

驱动开发的本质,就是让软件按照芯片手册规定的方式操作寄存器,从而控制硬件行为。

地址映射与寄存器偏移

芯片会把不同外设映射到固定地址范围。例如:

  • GPIOA 可能映射在某个总线地址区间
  • USART1 可能映射在另一个地址区间

访问寄存器时,本质上是在读写“基地址 + 偏移地址”的某个内存单元。

例如:

c
#define GPIOA_BASE   0x40020000U
#define GPIOA_MODER  (*(volatile unsigned int *)(GPIOA_BASE + 0x00U))
#define GPIOA_ODR    (*(volatile unsigned int *)(GPIOA_BASE + 0x14U))

理解驱动代码时,要优先问自己:

  • 这个寄存器控制什么功能
  • 哪几位有效
  • 写入和读取时机是什么

位操作技巧

大多数寄存器并不是整块重写,而是修改其中若干位,所以位操作非常常见。

c
#define SET_BIT(REG, BIT)    ((REG) |= (BIT))
#define CLEAR_BIT(REG, BIT)  ((REG) &= ~(BIT))
#define READ_BIT(REG, BIT)   ((REG) & (BIT))
#define TOGGLE_BIT(REG, BIT) ((REG) ^= (BIT))

常见做法:

  • 用掩码清除旧配置
  • 用移位写入新配置
  • 用按位与检测状态位
c
GPIOA_MODER = (GPIOA_MODER & ~(0x3U << 10)) | (0x1U << 10);

工程建议:

  • 位定义不要硬编码,尽量用宏或枚举统一管理。
  • 写寄存器前先确认“读改写”是否安全,特别是在中断或 DMA 参与的场景。

通用外设驱动

GPIO(通用输入输出)

GPIO 是最基础的外设,也是理解引脚复用和中断配置的起点。

常见模式:

  • 输入
  • 输出
  • 复用功能
  • 模拟输入

典型应用:

  • 读取按键
  • 控制 LED
  • 检测中断源
  • 给外设让出引脚复用功能

外部中断配置的一般流程:

  1. 配置引脚输入模式
  2. 选择中断线来源
  3. 配置触发边沿
  4. 打开 NVIC 中断

UART / USART

UART 是最常用的调试和模块通信接口之一。

要点:

  • 波特率要匹配
  • 起始位、数据位、停止位要一致
  • 常见问题包括丢字节、乱码、中断接收不连续

典型场景:

  • 调试日志输出
  • 与 GPS、蓝牙、4G 模块通信
  • Bootloader 下载协议

SPI

SPI 是高速同步串行接口,适合连接显示屏、Flash、高速 ADC 和各类外设。

重点关注:

  • 主从模式
  • 时钟极性和相位(CPOL / CPHA)
  • 片选控制
  • 收发时序

SPI 的典型问题往往不是代码逻辑,而是模式配置或时序不匹配。

I2C

I2C 适合挂接多个中低速外设,如温湿度传感器、IMU、RTC 等。

要点:

  • 只有两根线:SCL、SDA
  • 使用地址区分设备
  • 支持 ACK / NACK
  • 需要关注上拉电阻和总线速度

常见问题:

  • 地址写错
  • 上拉不合适
  • 总线被设备拉死
  • 主从收发顺序错误

ADC

ADC 用于采集模拟信号,是传感器接入的关键模块。

重点关注:

  • 分辨率
  • 参考电压
  • 采样时间
  • 转换速度
  • 校准与滤波

工程上要意识到:ADC 读数不稳定往往不是代码错,而是供电、参考源、采样时序或模拟前端问题。

RTC 实时时钟

RTC 用于记录时间、唤醒系统或做低功耗定时。

常见用途:

  • 时间戳
  • 定时唤醒
  • 数据记录
  • 断电后保持时间

RTC 常与低功耗设计一起出现,尤其是在电池设备中。


复杂外设支持

DMA 控制器

DMA 的价值是让数据搬运绕开 CPU,降低中断负担。

典型用途:

  • UART 连续接收
  • ADC 扫描采样
  • SPI 大块传输

要理解 DMA,至少要看清:

  • 谁是源
  • 谁是目的
  • 传输方向是什么
  • 是一次传输还是循环模式

看门狗

看门狗用于在系统异常时自动复位。

常见类型:

  • 独立看门狗(IWDG)
  • 窗口看门狗(WWDG)

设计看门狗时要避免两个极端:

  • 根本不喂狗,系统频繁误复位
  • 随便在任意地方喂狗,导致程序异常时也无法复位

CAN

CAN 常见于汽车和工业场景,优势是抗干扰强、总线结构清晰。

使用时应重点关注:

  • 波特率和位时序
  • ID 过滤
  • 帧格式
  • 错误帧和总线恢复

开发库 & 工具链

STM32 HAL

HAL 封装程度高,适合快速上手和中小型项目。

优点:

  • 接口统一
  • 文档与示例丰富
  • 配合 CubeMX 使用方便

缺点:

  • 抽象较厚
  • 某些场景效率和可控性不足

STM32 LL

LL(Low Layer)比 HAL 更贴近寄存器。

优点:

  • 代码更轻
  • 性能更可控
  • 更适合对时序和效率敏感的驱动

缺点:

  • 学习门槛更高
  • 代码可读性依赖开发者水平

STM32CubeMX

CubeMX 的核心价值不是“自动生成代码”,而是帮助你快速完成:

  • 时钟树配置
  • 引脚复用配置
  • 外设参数初始化
  • 中间件启用

使用时应注意:

  • 不要完全依赖自动生成结果
  • 生成后仍要对照手册理解关键配置
  • 用户代码区要和自动生成区分开

实战技巧与常见问题

外设初始化流程

大部分外设初始化都可以套同一个思路:

  1. 打开时钟
  2. 配置 GPIO
  3. 配置外设寄存器或库参数
  4. 配置中断 / DMA
  5. 使能外设
  6. 做最小功能验证

只要这个流程清楚,换 UART、SPI、ADC 或定时器都能迁移。

中断处理优化

中断服务函数应尽量短,只做必要动作:

  • 读取状态
  • 清中断标志
  • 把数据或事件交给主循环 / RTOS 任务处理

不要在 ISR 中做的事情:

  • 大量打印日志
  • 长时间循环
  • 阻塞等待

调试技巧

驱动问题的排查顺序建议固定下来:

  1. 先确认时钟和引脚配置
  2. 再看寄存器值
  3. 再看通信波形
  4. 最后再怀疑上层逻辑

工具组合建议:

  • 寄存器查看:调试器
  • 波形检查:示波器 / 逻辑分析仪
  • 数据链路验证:串口日志 / 协议解析

面试高频问题

  1. HAL 与 LL 库如何选择? 一般快速开发优先 HAL,性能敏感、代码体积敏感或需要细粒度控制时考虑 LL 或寄存器级开发。

  2. I2C 中 ACK / NACK 的作用是什么? ACK 表示正确接收并愿意继续,NACK 表示当前不接受数据或传输结束。

  3. ADC 采样时间为什么会影响精度? 采样时间过短时,采样电容可能尚未充满,导致转换结果偏差更大。

  4. DMA 相比 CPU 直接搬运的优缺点是什么? DMA 能降低 CPU 占用、提高吞吐,但配置更复杂,也会占用总线资源。


本章小结

学驱动开发时,最重要的是形成“外设初始化 -> 参数配置 -> 数据传输 -> 中断/错误处理 -> 调试验证”的固定思路。只要这个框架稳定下来,换不同芯片和不同库都能迁移。

以 GitHub Pages 发布,使用 VitePress 构建。