2. 外设驱动基础:寄存器映射、内存映射I/O与端口I/O、轮询与中断基础
各位同学,咱们今天聊点实在的。做Bootloader,说白了就是跟硬件打交道。你写的每一行代码,最终都要落到某个寄存器的某个位上。我见过不少新手,上来就对着数据手册抄代码,结果板子跑起来完全不是那么回事。为什么?因为没搞懂外设是怎么“接入”CPU的。
这一章,咱们就把外设驱动最底层的三件事说清楚:寄存器怎么映射到你的代码里、CPU怎么跟外设“对话”、以及怎么知道外设活干完了。嗯,这三件事搞明白了,后面写驱动就是搭积木。
2.1 外设寄存器映射:从硬件手册到C代码
每个外设都有一组寄存器。控制寄存器、状态寄存器、数据寄存器……它们都有固定的地址。你的任务,就是把这些地址变成C语言里能直接读写的变量。
我个人习惯用结构体来映射。举个例子,一个典型的UART外设,寄存器布局可能是这样的:
/* UART寄存器映射结构体 */
typedef struct {
volatile uint32_t DR; /* 数据寄存器,地址偏移0x00 */
volatile uint32_t RSR; /* 接收状态寄存器,偏移0x04 */
uint32_t reserved[2]; /* 保留空间,对齐用 */
volatile uint32_t FR; /* 标志寄存器,偏移0x10 */
volatile uint32_t ILPR; /* 红外低功耗寄存器,偏移0x14 */
volatile uint32_t IBRD; /* 整数波特率除数,偏移0x18 */
volatile uint32_t FBRD; /* 小数波特率除数,偏移0x1C */
volatile uint32_t LCR_H; /* 线控寄存器,偏移0x20 */
volatile uint32_t CR; /* 控制寄存器,偏移0x24 */
volatile uint32_t IFLS; /* 中断FIFO级别选择,偏移0x28 */
volatile uint32_t IMSC; /* 中断屏蔽设置/清除,偏移0x2C */
volatile uint32_t RIS; /* 原始中断状态,偏移0x30 */
volatile uint32_t MIS; /* 屏蔽后中断状态,偏移0x34 */
volatile uint32_t ICR; /* 中断清除寄存器,偏移0x38 */
} UART_TypeDef;
/* 将外设基地址强制转换为结构体指针 */
#define UART0_BASE 0x4000C000
#define UART0 ((UART_TypeDef *)UART0_BASE)
这里有个关键点——volatile关键字。我刚开始做嵌入式时,就吃过这个亏。编译器优化会把看似“没变化”的寄存器读取给优化掉,结果你读状态寄存器永远读到同一个值。加了volatile,就是告诉编译器:“别动我的寄存器,每次都要老老实实去内存读。”
reserved占位。否则,你写的FR可能实际操作的是ILPR——板子不干活还算好的,烧了外设就麻烦了。
2.2 内存映射I/O vs 端口I/O
CPU访问外设,主要有两种方式。咱们先看个对比:
| 特性 | 内存映射I/O (MMIO) | 端口I/O (PIO) |
|---|---|---|
| 访问方式 | 用ldr/str等内存指令 |
用专用的in/out指令 |
| 地址空间 | 与内存共用地址空间 | 独立的I/O地址空间 |
| 典型架构 | ARM、RISC-V、MIPS | x86 |
| 保护机制 | 依赖MMU设置 | 依赖I/O权限位图 |
| 编程便利性 | 直接用指针,方便 | 需要特殊指令,稍麻烦 |
现在绝大多数嵌入式处理器都用内存映射I/O。说白了,外设寄存器就在内存地址里。你定义一个指针指向0x4000C000,然后读写它,就是在操作UART。
但这里有个坑——内存屏障。我在项目里遇到过,CPU是乱序执行的,你代码里先写控制寄存器再写数据寄存器,硬件可能先看到数据再看到控制。这在Bootloader里尤其要命,因为Bootloader通常不开MMU和缓存,但CPU内部的重排序依然存在。
__DSB()和__ISB()屏障指令。虽然会损失一点性能,但在Bootloader阶段,稳定比速度重要得多。
2.3 轮询方式:简单但占CPU
轮询,就是CPU不停地问外设:“你好了没?你好了没?”
代码写起来很简单:
/* UART轮询发送一个字节 */
void uart_send_char(char c) {
/* 等待发送FIFO非满 */
while (UART0->FR & (1 << 5)) {
/* 空转,啥也不干 */
}
UART0->DR = c;
}
/* UART轮询接收一个字节 */
char uart_recv_char(void) {
/* 等待接收FIFO非空 */
while (UART0->FR & (1 << 4)) {
/* 空转 */
}
return (char)(UART0->DR & 0xFF);
}
你看,就是两个while循环。好处是逻辑清晰,没有竞态问题。坏处呢?CPU被死死占住了。在Bootloader里,如果你只是打印个“Hello”,轮询没问题。但如果你要等某个外设完成一个耗时操作(比如eMMC擦除,可能要几百毫秒),CPU就一直在那空转。
我曾经在一个项目里,用轮询方式等待Flash写完成。结果Bootloader启动时间长了300毫秒,客户直接投诉。嗯,从那以后,我对轮询就多留了个心眼。
2.4 中断方式:高效但复杂
中断的思路是:CPU告诉外设“你干完了叫我”,然后CPU该干嘛干嘛去。外设干完了,拉一根中断线,CPU停下当前工作,跳去执行中断服务程序(ISR)。
中断的流程大致是这样的:
- 配置中断控制器(如ARM GIC、NVIC)——使能对应的中断源。
- 配置外设——使能外设的中断产生条件(比如UART收到数据就产生中断)。
- 编写ISR——在中断处理函数里读取数据或状态。
- 中断返回——清中断标志,恢复现场。
一个简单的UART中断接收示例:
/* 中断服务程序 */
void UART0_IRQHandler(void) {
uint32_t status = UART0->MIS; /* 读屏蔽后中断状态 */
if (status & (1 << 4)) { /* 接收中断 */
char c = (char)(UART0->DR & 0xFF);
/* 把c放到环形缓冲区,或者直接处理 */
rx_buffer[rx_head++] = c;
/* 清中断 */
UART0->ICR = (1 << 4);
}
if (status & (1 << 5)) { /* 发送中断 */
/* 发送完成,可以继续发下一个字节 */
UART0->ICR = (1 << 5);
}
}
/* 主函数里使能中断 */
void uart_init_interrupt(void) {
/* 配置UART... */
UART0->IMSC |= (1 << 4); /* 使能接收中断 */
UART0->IMSC |= (1 << 5); /* 使能发送中断 */
/* 配置NVIC */
NVIC_EnableIRQ(UART0_IRQn);
NVIC_SetPriority(UART0_IRQn, 1);
}
中断的好处很明显——CPU利用率高。但坏处也一堆:
- 竞态条件:中断可能打断正在执行的代码,共享数据要加保护。
- 中断嵌套:高优先级中断打断低优先级中断,栈开销大。
- 延迟不确定:最坏情况下的中断响应时间很难算。
2.5 轮询 vs 中断:怎么选?
在Bootloader里,我的选择原则很简单:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 时钟、PLL、DDR初始化 | 轮询 | 中断环境还没建立,只能用轮询 |
| 调试串口打印 | 轮询 | 简单可靠,中断反而增加复杂度 |
| 慢速外设(eMMC、NAND) | 中断或DMA | 等待时间长,轮询浪费CPU |
| 快速外设(SPI Flash) | 轮询 | 操作快,中断开销占比大 |
| 多任务Bootloader | 中断 | 需要并发处理多个外设事件 |
你想想看,Bootloader的本质是什么?是把内核从存储介质搬到内存,然后跳过去。这个过程里,大部分时间都在等存储介质。如果每个等待都用轮询,CPU利用率极低。但如果你一上来就搞复杂的中断系统,又容易出bug。
我个人建议:前期用轮询保证功能正确,后期再优化成中断。先跑通,再跑快。
2.6 本章知识体系
下面这张图,把咱们这一章的核心逻辑串起来了:
这张图把咱们这一章的内容串成了流水线:从寄存器映射开始,到CPU怎么访问外设,再到怎么处理外设事件。你写驱动的时候,脑子里就要有这根线。
2.7 避坑总结
最后,我把自己这些年踩过的坑,列成清单,你写代码时对照着看:
- 寄存器地址别算错——结构体成员顺序必须和手册一致,保留位要占位。
- volatile不能省——编译器优化会坑你,尤其是状态寄存器。
- 内存屏障要加——乱序执行在Bootloader里一样存在。
- 中断向量表要放对地方——DDR初始化前,中断向量表必须在SRAM里。
- 中断标志要清干净——不清标志,ISR会反复触发,看起来像死循环。
- 轮询超时要处理——万一外设挂了,轮询不能死等,加个超时退出。
嗯,这一章的内容就到这。寄存器映射、MMIO与PIO、轮询与中断,这三块是外设驱动的基石。你把这些搞透了,后面写具体外设的驱动,就是套模板的事。