找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

549

积分

0

好友

75

主题
发表于 昨天 01:50 | 查看: 5| 回复: 0

驱动程序的核心工作是读写硬件寄存器,以配置模式、读取状态或写入数据。然而,在启用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_ioremapdevm_ioremap_resource。这些托管函数将映射的生命周期与设备绑定,驱动卸载时自动释放,避免资源泄漏。

步骤二:访问(Read/Write)

获得base_addr后,切勿直接使用指针解引用(如*base_addr = 0)。原因包括:

  1. 架构差异:不同CPU对I/O内存的访问方式不同。
  2. 缓存问题:寄存器访问不能被缓存,必须直达硬件。
  3. 指令重排:编译器优化可能打乱访问顺序,需要内存屏障来保证。

必须使用内核提供的专用读写宏:

/* 读寄存器 */
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驱动开发的通用范式。




上一篇:NFSv3协议核心RPC接口深度解析:从挂载到文件操作实战
下一篇:嵌入式状态机编程演进:从传统FSM到QP框架的优缺点剖析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-8 20:33 , Processed in 0.076769 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表