linux驱动开发-地址映射

地址映射

在Linux驱动开发中,
地址映射是指如何将设备的物理地址映射到虚拟地址空间,
从而使得内核可以通过虚拟地址与设备进行交互。
这一过程在设备的初始化和操作中都是至关重要的,
尤其是在涉及到内存映射I/O时。

1. 地址映射的基本概念

物理地址:设备实际使用的物理内存地址。
虚拟地址:内核空间或用户空间中的地址,应用程序通过此地址来访问内存。
映射:将物理地址转换为虚拟地址的过程。

2. 映射的类型

在Linux中,主要有两种类型的地址映射:

直接映射:通过使用特定的函数将设备的物理地址直接映射到内核虚拟地址空间。
缓冲映射:这种映射涉及使用DMA(直接内存访问)来减少CPU的负担。

3. 主要函数

在Linux驱动程序中,常用的映射相关函数包括:

ioremap:用于将物理地址映射到内核虚拟地址空间。常用于设备内存和寄存器的访问。
void __iomem *ioremap(unsigned long offset, unsigned long size);

ioremap_nocache:与ioremap类似,但不进行缓存,适用于要求实时性较高的设备。
void __iomem *ioremap_nocache(unsigned long offset, unsigned long size);

iounmap:用于解除之前通过ioremap或ioremap_nocache创建的映射。
void iounmap(void __iomem *addr);

4. 映射步骤

获取物理地址:通常在设备树或PCI设备结构中获取设备的基地址。

调用ioremap:将设备的物理地址映射到虚拟地址空间。

访问设备寄存器:通过返回的虚拟地址访问设备的寄存器或缓冲区。

解除映射:在驱动卸载时调用iounmap释放之前的映射。

5. 注意事项

映射的地址范围需要在设备的地址空间内,超出范围可能导致未定义行为。

进行内存映射时,应该了解访问的内存区域是否需要特殊的同步操作,例如使用锁。

在多核处理器环境中,确保适时进行缓存同步,以避免数据不一致的问题。

示例

#include <linux/module.h>                // 包含内核模块相关的头文件
#include <linux/platform_device.h>       // 包含平台设备相关的头文件
#include <linux/io.h>                    // 包含内存映射I/O相关的头文件

// 定义设备结构体
struct my_device {
    void __iomem *reg_base;             // 用于存储映射后的设备寄存器基地址
};

// 驱动程序的probe函数,当设备被识别到时执行
static int my_device_probe(struct platform_device *pdev) {
    struct my_device *dev;               // 定义设备结构体指针
    resource_size_t addr;                // 用于存储设备的物理地址

    // 分配设备结构体的内存
    //kzalloc 是 Linux 内核中用于动态分配内存的一种函数,结合了内存分配和初始化的功能。它会在分配内存的同时将这块内存的所有字节初始化为零
    //devm_kzalloc 是 devres 内存管理系统的一种分配函数,它会在驱动程序卸载时自动释放分配的内存。
    //@param dev 指向设备结构体的指针
    //@param size 要分配的内存大小
    //@param flags 内存分配标志 GFP_KERNEL 表示分配内核内存,在可以睡眠的上下文中分配内存(适用于大多数内核线程和进程)。
    //                        ,GFP_ATOMIC 表示分配原子内存,在不可睡眠的上下文中分配内存,速度更快但可能会失败。
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)                            // 检查内存分配是否成功
        return -ENOMEM;                  // 分配失败返回内存不足错误

    // 这里假设获取物理地址的代码在此(具体实现依赖于设备)
    addr = /* 获取物理地址 */;

    // 将物理地址映射到内核虚拟地址空间
    //与 ioremap 不同,devm_ioremap 会在设备释放时自动解除映射,这样可以简化资源管理,减少内存泄漏的风险。
//@param dev 指向设备结构体的指针
//@param offset 设备的物理地址偏移量
//@param size 要映射的内存大小
    dev->reg_base = devm_ioremap(&pdev->dev, addr, /* 映射大小 */);
    if (!dev->reg_base)                  // 检查映射是否成功
        return -EIO;                     // 失败返回输入输出错误

    // 访问设备寄存器(以写入0x1为例)
    //`writel` 是 Linux 内核中用于写 32 位数据的函数,它会将传入的 32 位数据写入指定内存地址。
    //`dev->reg_base + OFFSET` 是设备寄存器的虚拟地址,可以通过偏移量访问。
    //`0x1` 是要写入的值。
    writel(0x1, dev->reg_base + OFFSET); // 使用偏移量访问寄存器

    // 保存设备私有数据到平台设备结构中以便后续操作
    //platform_set_drvdata 用于保存设备私有数据到平台设备结构中,以便后续操作。
    //`pdev` 是平台设备结构体的指针。
    //`dev` 是设备私有数据指针。
    platform_set_drvdata(pdev, dev);

    return 0;                            // probe函数返回0,表示成功
}

// 驱动程序的remove函数,在驱动卸载时执行
static int my_device_remove(struct platform_device *pdev) {
    struct my_device *dev = platform_get_drvdata(pdev); // 获取设备私有数据
    /* 在这里添加驱动卸载时需要进行的清理操作 */
    return 0;                            // remove函数返回0,表示成功
}

// 设备树支持,定义与设备匹配的ID表
static const struct of_device_id my_device_ids[] = {
    { .compatible = "my_device" },      // 指定支持的设备名称
    {}
};

MODULE_DEVICE_TABLE(of, my_device_ids); // 将设备ID表与内核模块注册

// 定义平台驱动结构体
static struct platform_driver my_device_driver = {
    .probe = my_device_probe,            // 设备探测函数
    .remove = my_device_remove,          // 设备移除函数
    .driver = {
        .name = "my_device",             // 驱动名称
        .of_match_table = my_device_ids, // 设备树匹配表
    },
};

// 注册平台驱动
module_platform_driver(my_device_driver); 
MODULE_LICENSE("GPL");                    // 声明模块为GPL许可证

LED的字符设备驱动

myled.h

#ifndef __MYLED_H__ // 防止头文件被重复包含
#define __MYLED_H__

// 定义寄存器的地址
#define RCC_ADDR     0x50000a28 // RCC控制寄存器地址
#define GPIOE_MODER  0x50006000 // GPIOE模式寄存器地址
#define GPIOE_ODR    0x50006014 // GPIOE输出数据寄存器地址

#endif // __MYLED_H__

myled.c

#include <linux/fs.h> // 包含文件系统相关的头文件
#include <linux/init.h> // 包含模块初始化/退出的头文件
#include <linux/module.h> // 包含模块相关的头文件
#include <linux/io.h> // 包含I/O映射相关的头文件
#include "myled.h" // 包含自定义的头文件

#define CNAME "myled" // 定义字符设备的名字为 "myled"

int major; // 存储主设备号
char kbuf[128] = { 0 }; // 定义一个字符缓冲区
unsigned int *rcc,*moder,*odr; // 保存对应寄存器的虚拟地址指针

// 打开设备时调用的函数
int myled_open(struct inode* inode, struct file* file)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__); // 输出调试信息

    //地址映射
    rcc = ioremap(RCC_ADDR,4); // 将RCC_ADDR地址映射到内存
    if(rcc == NULL){
        pr_err("ioremap rcc error\n"); // 映射失败输出错误信息
        return -ENOMEM; // 返回错误码
    }
    moder = ioremap(GPIOE_MODER,4); // 映射GPIOE_MODER寄存器
    if(moder == NULL){
        pr_err("ioremap moder error\n"); // 映射失败输出错误信息
        return -ENOMEM; // 返回错误码
    }
    odr = ioremap(GPIOE_ODR,4); // 映射GPIOE_ODR寄存器
    if(odr == NULL){
        pr_err("ioremap odr error\n"); // 映射失败输出错误信息
        return -ENOMEM; // 返回错误码
    }
    
    //初始化led1熄灭
    *rcc |= (1<<4); // 使能GPIOE时钟
    *moder &= ~(3<<20); // 清空模式位
    *moder |= (1<<20); // 设置GPIOE为输出模式
    *odr &= ~(1<<10); // 将LED1熄灭

    return 0; // 打开设备成功
}

// 从设备读取数据的函数
ssize_t myled_read(struct file* file,
    char __user* ubuf, size_t size, loff_t* offs)
{
    int ret;
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__); // 输出调试信息
    
    if (size > sizeof(kbuf)) // 如果请求的大小大于kbuf的大小
        size = sizeof(kbuf); // 则将size设置为kbuf的大小
    
    ret = copy_to_user(ubuf, kbuf, size); // 将内核缓冲区内容拷贝到用户缓冲区
    if (ret) {
        pr_err("copy_to_user error\n"); // 拷贝失败输出错误信息
        return -EIO; // 返回输入输出错误
    }

    return size; // 返回拷贝的大小
}

// 向设备写入数据的函数
ssize_t myled_write(struct file* file,
    const char __user* ubuf, size_t size, loff_t* offs)
{
    int ret;
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__); // 输出调试信息
    
    if (size > sizeof(kbuf)) // 如果请求的大小超出kbuf的大小
        size = sizeof(kbuf); // 则调整大小
    
    ret = copy_from_user(kbuf, ubuf, size); // 从用户缓冲区拷贝数据到内核缓冲区
    if (ret) {
        pr_err("copy_from_user error\n"); // 拷贝失败输出错误信息
        return -EIO; // 返回输入输出错误
    }

    // kbuf[0]为1时点亮LED1,为0时熄灭LED1
    kbuf[0] == 1 ? (*odr |= (1<<10)) : (*odr &= ~(1<<10)); // 控制LED状态
    return size; // 返回写入的大小
}

// 关闭设备时调用的函数
int myled_close(struct inode* inode, struct file* file)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__); // 输出调试信息
    iounmap(odr); // 解除对ODR寄存器的映射
    iounmap(moder); // 解除对MODER寄存器的映射
    iounmap(rcc); // 解除对RCC寄存器的映射
    return 0; // 关闭设备成功
}

// 声明文件操作的结构体
const struct file_operations fops = {
    .open = myled_open, // 打开设备时调用的函数
    .read = myled_read, // 读取设备时调用的函数
    .write = myled_write, // 写入设备时调用的函数
    .release = myled_close, // 关闭设备时调用的函数
};

// 模块初始化函数
static int __init myled_init(void)
{
    // 1.注册字符设备
    major = register_chrdev(0, CNAME, &fops); // 注册字符设备并获取主设备号
    if (major < 0) {
        pr_err("register_chrdev error\n"); // 注册失败输出错误信息
        return major; // 返回错误码
    }
    pr_info("register char device driver success. major = %d\n", major); // 注册成功输出信息
    return 0; // 返回成功
}

// 模块退出函数
static void __exit myled_exit(void)
{
    // 2.注销字符设备
    unregister_chrdev(major, CNAME); // 注销字符设备
}

// 指定模块的初始化和退出函数
module_init(myled_init);
module_exit(myled_exit);
MODULE_LICENSE("GPL"); // 模块许可证声明

test.c

#include <head.h> // 包含自定义的头文件

int main(int argc, const char * argv[])
{
    int fd; // 文件描述符
    char buf[128] = {0}; // 定义缓冲区

    if((fd = open("/dev/myled", O_RDWR)) == -1) // 打开字符设备
        PRINT_ERR("open error"); // 打开失败输出错误信息

    while(1) { // 无限循环
        buf[0] = !buf[0]; // 切换buf[0]的值
        write(fd, buf, sizeof(buf)); // 写入数据到设备
        sleep(1); // 每隔1秒循环一次
    }
    close(fd); // 关闭设备
    return 0; // 返回成功
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

可能只会写BUG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值