驱动程序的核心工作是读写硬件寄存器,以配置模式、读取状态或写入数据。然而,在启用MMU(内存管理单元)的Linux系统中,无法直接操作物理地址。
CPU访问外设主要有两种架构方式:
- I/O 端口 (Port I/O)
- 代表架构:x86(传统方式)
- 特点:内存和I/O拥有独立的地址空间,CPU使用专门的指令(如
IN, OUT)访问。
- Linux API:
inb(), outb() 等。
- 内存映射 I/O (MMIO - Memory Mapped I/O)
- 代表架构:ARM, RISC-V, MIPS, PowerPC,以及现代x86的大多数外设。
- 特点:统一编址,硬件寄存器被映射到CPU的内存地址空间中,读写寄存器就像读写普通内存。
- 场景:嵌入式Linux开发中99%会遇到的情况。
MMU 与虚拟地址
- 物理地址:硬件手册上定义的地址。例如,某SoC的GPIO控制器基地址是
0x12340000。
- 虚拟地址:Linux内核代码中实际使用的地址。
关键原则:在Linux内核驱动中,绝对不能直接使用物理地址。例如,int *p = (int *)0x12340000; *p = 1; 会导致内核报错“Unable to handle kernel paging request”并崩溃。必须通过映射,将物理地址转换为内核可用的虚拟地址。
步骤一:映射(Mapping)
将物理地址映射为虚拟地址的标准接口如下:
#include <linux/io.h>
// 经典方式
void __iomem *base_addr = ioremap(unsigned long phys_addr, unsigned long size);
// 释放
iounmap(base_addr);
phys_addr:物理起始地址(通常从设备树获取)。
size:需要映射的区域大小(字节)。
base_addr:返回的虚拟地址指针。__iomem修饰符用于提醒编译器这是I/O内存,不要进行优化。
最佳实践:推荐使用devm_ioremap或devm_ioremap_resource。这些托管函数将映射的生命周期与设备绑定,驱动卸载时自动释放,避免资源泄漏。
步骤二:访问(Read/Write)
获得base_addr后,切勿直接使用指针解引用(如*base_addr = 0)。原因包括:
- 架构差异:不同CPU对I/O内存的访问方式不同。
- 缓存问题:寄存器访问不能被缓存,必须直达硬件。
- 指令重排:编译器优化可能打乱访问顺序,需要内存屏障来保证。
必须使用内核提供的专用读写宏:
/* 读寄存器 */
u8 readb(void __iomem *addr) // 8位 (Byte)
u16 readw(void __iomem *addr) // 16位 (Word)
u32 readl(void __iomem *addr) // 32位 (Long - 最常用)
u64 readq(void __iomem *addr) // 64位 (Quad)
/* 写寄存器 */
void writeb(u8 value, void __iomem *addr)
void writew(u16 value, void __iomem *addr)
void writel(u32 value, void __iomem *addr) // 最常用
void writeq(u64 value, void __iomem *addr)
实战:基于设备树(Device Tree)的MMIO操作
1. 设备树描述
在设备树中定义硬件寄存器区域:
my_led_device {
compatible = "learn,my-mmio-led";
/* reg属性:<物理基地址 长度> */
reg = <0x10005000 0x100>;
};
2. 驱动代码实现
以下是一个完整的驱动示例,展示了从映射到读写的完整流程。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/of.h>
/* 假设的寄存器偏移量 */
#define REG_CTRL 0x00 // 控制寄存器
#define REG_DATA 0x04 // 数据寄存器
struct my_chip_data {
void __iomem *base_addr; // 保存映射后的基地址
struct device *dev;
};
static int my_probe(struct platform_device *pdev)
{
struct my_chip_data *data;
struct resource *res;
u32 val;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data) return -ENOMEM;
data->dev = &pdev->dev;
platform_set_drvdata(pdev, data);
/* 1. 获取资源 (Physical Address)
* 从设备树的 ‘reg’ 属性中提取物理地址范围
*/
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "无法获取内存资源\n");
return -ENODEV;
}
/* 2. 映射 (Mapping)
* devm_ioremap_resource 内部完成:
* a. 检查资源合法性
* b. request_mem_region (申请独占该物理区域)
* c. ioremap (建立页表映射)
*/
data->base_addr = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(data->base_addr)) {
dev_err(&pdev->dev, "映射失败\n");
return PTR_ERR(data->base_addr);
}
dev_info(&pdev->dev, "映射成功! 物理地址: %pr, 虚拟地址: %p\n", res, data->base_addr);
/* 3. 读写操作 (Access) */
// --- 写操作示例:开启芯片使能位 (Bit 0 置 1) ---
// 标准 Read-Modify-Write 流程
val = readl(data->base_addr + REG_CTRL); // A. 读出当前值
val |= (1 << 0); // B. 修改位
writel(val, data->base_addr + REG_CTRL); // C. 写回
dev_info(&pdev->dev, "已向 CTRL 寄存器写入: 0x%x\n", val);
// --- 读操作示例 ---
val = readl(data->base_addr + REG_DATA);
dev_info(&pdev->dev, "读取到 DATA 寄存器值: 0x%x\n", val);
return 0;
}
static int my_remove(struct platform_device *pdev)
{
// 使用 devm_ 系列函数,无需手动释放
dev_info(&pdev->dev, "驱动卸载\n");
return 0;
}
static const struct of_device_id my_match[] = {
{ .compatible = "learn,my-mmio-led" },
{ }
};
MODULE_DEVICE_TABLE(of, my_match);
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_mmio_driver",
.of_match_table = my_match,
},
};
module_platform_driver(my_driver);
MODULE_LICENSE("GPL");
该示例清晰地展示了在设备树驱动框架下,如何安全、正确地访问MMIO区域,是嵌入式Linux驱动开发的通用范式。