一、linux物理地址到虚拟地址的映射

linux不能直接对物理地址进行操作,如果需要操作硬件,需要先把物理地址转成虚拟地址,因为linux使能了MMU,所以不能直接操作物理地址。

1、使能MMU有什么好处?

(1)让虚拟地址成了可能

(2)可以让系统更加安全,有了MMU,上层应用看到的内存都是虚拟内存,应用就不能直接访问硬件,这样保证了系统安全。

2、MMU非常复杂,如何完成物理地址到虚拟地址的转换呢?

内核提供了相关的函数。在include/asm-generic 下的io.h

ioremap:把物理地址转换成虚拟地址。(建立映射)

phys_addr_t offset :映射物理地址的起始地址

size_t size:要映射多大的内存空间

返回值:返回虚拟地址的首地址

失败:返回NULL
 static inline void __iomem *ioremap(phys_addr_t offset, size_t size)
 {
    return (void __iomem *)(unsigned long)offset;
 }

iounmap:把虚拟地址转成物理地址。(取消映射)
__iomem *addr :取消映射的虚拟地址的首地址。

static inline void iounmap(void __iomem *addr)
 {
 }

注意

物理地址只能被映射一次,多次映射就会失败。

4、如何查看哪些物理地址被映射过了呢?
cat /proc/iomem

实践课

需求:

1、使用杂项设备完成蜂鸣器的一个驱动

2、完成一个上层测试应用。
应用要求:传入参数1为打开蜂鸣器,传入参数0为关闭蜂鸣器。

驱动代码:

#include<linux/init.h>
#include<linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>

#define GPIO5_DR 0x020AC000//查看数据手册,寄存器DR的地址

unsigned int *vir_gpio_dr;//存放映射完成的虚拟地址的首地址


int misc_open(struct inode *inode,struct file *file)
{
    printk("hello misc_open\n");
    
    return 0;
}

int misc_close(struct inode *inode,struct file *file)
{
    printk("bye bye\n");
    
    return 0;
}


int misc_read(struct file *file,char __user *ubuf,size_t size,loff_t *loff_t)
{
    char kbuf[64] = "hello";

    if(copy_to_user(ubuf,kbuf,sizeof(kbuf))!=0)
    {
        printk("copy to user error\n");
        return -1;
    }  
    return 0;

}

int misc_write(struct file *file,const char __user *ubuf,size_t size,loff_t *loff_t)
{
   char kbuf[64] = {0};

    if(copy_from_user(kbuf,ubuf,size)!=0)  //ubuf  传送进来 保存到 kbuf中
    {
        printk("copy_from_user\n");
        return -1;
    }  

    if (kbuf[0] == 0)
        *vir_gpio_dr |= (1 <<1);//关闭
    else if (kbuf[0] == 1)
        *vir_gpio_dr &= ~(1 <<1);//打开
        
    return 0;

}

struct file_operations  misc_fops ={
    .owner = THIS_MODULE,
    .open = misc_open,
    .release = misc_close,
    .read = misc_read,
    .write = misc_write
};

struct miscdevice misc_dev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "hello_misc",
    .fops = &misc_fops
};

static int misc_init(void)
{
    int ret;
    
    ret = misc_register(&misc_dev);//注册杂项设备
    
    if (ret<0)
    {
        printk("misc_register is error\n");
        return -1;
    }
    printk("misc_register is successful\n");
   
    vir_gpio_dr = ioremap(GPIO5_DR,4);//指定地址映射4个字节大小

    if(vir_gpio_dr == NULL) 
    {
        printk("GPIO5_DR ioremap error\n");
        return -EBUSY;
    }
    printk("GPIO5_DR ioremap ok\n");

    return 0;
}

static void misc_exit(void)
{
    misc_deregister(&misc_dev);//卸载

    printk("bye bye\n");

    iounmap(vir_gpio_dr);//取消映射
}

module_init(misc_init);

module_exit(misc_exit);

MODULE_LICENSE("GPL");

应用代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    int fd;

    char buf[64] = {};

    fd = open("/dev/hello_misc",O_RDWR);
    if (fd < 0) 
    {
        printf("open error\n");
        
        return fd;
    }

    buf[0] = atoi(argv[1]);
    
    write(fd, buf, sizeof(buf));//向驱动层写入,驱动层调用misc_write
    
    close(fd);
    
    return 0;
}

驱动模块传参

1、什么是驱动传参

驱动传参就是传递参数给驱动。
举例:
insmod beep.ko a=1

2、驱动传参有什么作用?

(1)设置驱动相关参数,比如设置缓冲区的大小

(2)设置内核的安全校验,防止写的驱动被别人盗用

3、怎么给驱动传递参数?
(1)传递普通的参数,比如char,int类型

函数:
module_param(name,type,perm);
参数:
name 要传递进去参数的名称
type:类型
perm:参数读写的权限

static int a;

module_param(a,int,S_IRUSR);

static int hello_init(void)
{
    printk("a=%d\n",a);
    
    return 0;
}

(2)传递数组
函数:
module_param_array(name,type,numo,perm);
参数:
name:需要传递的参数
type:传递的参数类型
numo:实际传入进入的个数
perm:参数读写的权限

static int b[5];

static int count;

module_param(a,int,S_IRUSR);

module_param_array(b,int,&count,S_IRUSR);

static int hello_init(void)
{
    int i;
    for (i=0;i<count;i++)
    {
        printk("b[i]=%d\n",i,b[i]);
    }

    printk("count = %d\n",count);

    printk("a=%d\n",a);

    return 0;
}

使用命令传入参数

实验现象

实验结果如下图所示:打印出a的数值
在这里插入图片描述
实验结果如下图所示:打印出b的数组的数值

insmod parameter.ko b=1,2,1,3,3

在这里插入图片描述

(3)如果传递参数超过个数,会发生什么?

会报错,崩溃~~~!!!如图所示!!!

在这里插入图片描述

二、申请字符类设备号

1、字符设备和杂项设备的区别(复习)

杂项设备的主设备号是固定的,固定为10。

字符设备的主设备号不是固定的,那么需要自己或者系统来分配设备号。

杂项设备可以自动生成设备节点。

字符设备需要手动生成设备节点。

2、注册字符类设备号的两个方法。

第一种:静态分配一个设备号

在include/linux/fs.h函数:

int register_chrdev_region(dev_t, unsigned, const char *);

需要明确知道系统里面哪些设备号没有被使用。
参数:
dev_t 设备号的起始值。类型是dev_t。
unsigned:次设备号的个数。
const char* : 设备号的名称。
返回值:成功返回0,失败返回非0。

dev_t 用来保存设备号,是一个32位的数据类型。定义在linux/types.h中。

高12位用来保存主设备号。

低20位用来保存次设备号。

Linux提供的几个宏定义来操作设备号。

//次设备号的位数,一共是20位
#define MINORBITS   20	

//次设备号的掩码
#define MINORMASK   ((1U << MINORBITS) - 1)

//dev_t中获取主设备号
#define MAJOR(dev)  ((unsigned int) ((dev) >> MINORBITS))

//dev_t中获取次设备号
#define MINOR(dev)  ((unsigned int) ((dev) & MINORMASK))

//将主设备号和次设备号组成dev_t类型,第一个参数是主设备号,第二个参数是次设备号。
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

第二种:动态分配

使用函数为:

int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);

参数:
第一个:保存生成的设备号。

第二个:请求的第一个次设备号,通常为0。

第三个:连续申请的设备号的个数。

第四个:设备名称。

返回值:成功返回0,失败返回非0。

使用动态分配会优先使用255-234之间的号码。

3、注销设备号

参数1:分配设备号的起始地址。

参数2:申请设备号的数目。

void unregister_chrdev_region(dev_t,unsigned);

驱动代码编写如下所示:分为两种设备号分配方式(静态分配和动态分配)

#include<linux/init.h>
#include<linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>

static int major_num,minor_num;

#define DEVICE_NUMBER  1        //分配设备号数目
#define DEVICE_SNAME "schrdev"  //主设备号名称
#define DEVICE_ANAME "achrdev"  //次设备号名称

#define DEVICE_MIN_NUMBER 0     //次设备号起始位置

module_param(major_num,int,S_IRUSR);

module_param(minor_num,int,S_IRUSR);


static int hello_init(void)
{
    dev_t dev_num;
    
    int ret;

    printk("major_num = %d\n",major_num);

    printk("minor_num = %d\n",minor_num);    

    if (major_num)//主设备号传递进来,静态方法
    {
        dev_num = MKDEV(major_num,minor_num);

        ret = register_chrdev_region(dev_num,DEVICE_NUMBER,DEVICE_SNAME);
        if (ret<0)
        {
            printk("register_chrdev_region error\n");
        }
        printk("register_chrdev_region ok\n");
    }
    else //主设备号没有传递进来,使用动态方法分配设备号
    {
        ret = alloc_chrdev_region(&dev_num,DEVICE_MIN_NUMBER,DEVICE_NUMBER,DEVICE_ANAME);
        if (ret<0)
        {
            printk("register_chrdev_region error\n");
        }
        printk("register_chrdev_region ok\n");

        major_num = MAJOR(dev_num);

        minor_num = MINOR(dev_num);

        printk("major_num = %d\n",major_num);

        printk("minor_num = %d\n",minor_num);

    }
    
    return 0;
}

static void hello_exit(void)
{
    unregister_chrdev_region(MKDEV(major_num,minor_num),DEVICE_NUMBER);//注销设备号
    printk("bye bye");
}

module_init(hello_init);

module_exit(hello_exit);

MODULE_LICENSE("GPL");

建议使用动态分配的方式申请设备号。静态分配在多人使用容易冲突。

实验现象

申请字符类设备号之后,打印主设备号和次设备号。之后在注册设备号成功之后,打印主设备号和次设备号。
在这里插入图片描述

三、注册字符类设备

注册杂项设备
misc_register(&misc_dev);

注销杂项设备
misc_deregister(&misc_dev);

cdev结构体:描述字符设备的结构体

在include/linux/cdev.h中

 struct cdev {
			struct kobject kobj;
			struct module *owner;
			const struct file_operations *ops;
			struct list_head list;
			dev_t dev;
			unsigned int count;
 };

步骤一:定义一个cdev结构体

步骤二:使用cdev_init函数初始化cdev结构体成员

void cdev_init(struct cdev *, const struct file_operations *);

参数:
第一个:要初始化的cdev

第二个:文件操作集

cdev->ops = fops;//实际就是把文件操作集写给ops。

步骤三:使用cdev_add函数注册到内核中

int cdev_add(struct cdev *, dev_t, unsigned);

参数1:cdev的结构体指针
参数2:设备号
参数3:次设备号的数量
注销字符设备:

 void cdev_del(struct cdev *);

驱动代码编写

#include<linux/init.h>
#include<linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

struct cdev cdev;

static int hello_init(void)
{
	上节代码添加如下内容
   .
   .
   .
   .
   .
   .
    cdev.owner = THIS_MODULE;

    cdev_init(&cdev,&chrdev_ops);//字符设备初始化

    cdev_add(&cdev,dev_num,DEVICE_NUMBER);//字符设备的注册

    return 0;
}

static void hello_exit(void)
{
    unregister_chrdev_region(MKDEV(major_num,minor_num),DEVICE_NUMBER);

    cdev_del(&cdev);

    printk("bye bye");
}

应用代码编写

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    int fd;

    char buf[64] = {};

    fd = open("/dev/test",O_RDWR);
    if (fd < 0) 
    {
        printf("open error\n");
        return fd;
    }
    return 0;
}
注意!!

字符设备注册完成之后,并不会自动生成设备节点,需要使用mknod命令创建一个设备节点。
格式:

mknod 名称 类型 主设备号 次设备号

举例:

mknod /dev/test c 247 0

创建设备节点:
创建完成之后,查看/dev/目录 可以看到test
在这里插入图片描述运行app

在这里插入图片描述
现象:运行app之后,打印驱动open调用结果。

四、自动创建设备节点(推荐)

先前的实验使用insmod命令加载模块之后,通过mknod命令手动创建设备节点,为了方便操作,学习实现如何自动创建设备节点。当加载模块的时候,在/dev/目录下自动创建相应的设备文件。

1、怎么自动创建一个设备节点。
在嵌入式linux 中使用mdev来实现设备节点的自动创建和删除。

2、什么是mdev?
是udev的简化版本,是busybox中所带的程序,适合用在嵌入式系统。

3、什么是udev?
一种工具,能够根据系统中的硬件设备的状态更新设备文件,包括文件的创建和删除。设备文件通常存放在/dev目录下,使用udev后,在/dev目录下就只包含系统中真正存在的设备。udev一般在PC上使用。

4、怎么自动创建设备节点?

自动创建设备节点分为两步骤
步骤一:使用class_create函数创建一个class的类。
步骤二:使用device_create函数在我们创建的类下面创建一个设备。

5、创建和删除类函数

一般使用两个函数来完成设备节点的创建和删除。首先创建一个class类结构体,class类结构体定义在include/linux/device.h里面。class_create是类创建函数,class_create是个宏定义,内容如下:

 #define class_create(owner, name)       \
({                      \
    static struct lock_class_key __key; \
     __class_create(owner, name, &__key);    \
 })

一共有两个参数,参数owner一般为THIS_MODULE,参数name是类名字。

返回值指向结构体class的指针,也就是创建的类。

卸载驱动程序的时候需要删除掉类,类删除函数为class_destory。函数原型如下:

void class_destory(struct class *cls);

参数cls就是要删除的类。

驱动代码:

注册成功之后会在/sys/class下生成一个类

#define DEVICE_CLASS_NAME "chrdev_class"

static int hello_init(void)
{

	.
	.
	.
	.
	.
	.
    class = class_create(THIS_MODULE,DEVICE_CLASS_NAME);
    
    return 0;
}


static void hello_exit(void)
{
    unregister_chrdev_region(MKDEV(major_num,minor_num),DEVICE_NUMBER);

    cdev_del(&cdev);

    class_destory(class);

    printk("bye bye");
}

6、创建设备函数

当使用上节的函数创建完成一个类之后,使用device_create函数在这个类下创建一个设备。device_create函数原型如下:

 struct device *device_create(struct class *cls, 
 struct device *parent,
 dev_t devt, 
 void *drvdata,
 const char *fmt, ...);

device_create是个可变参数函数,

参数1:在哪个设备下创建
参数2:父设备,一般为空
参数3:devt是设备号
参数4:drvdata是设备可能使用到的数据,一般为NULL;
参数5:fmt设备名字,如果设置fmt=xxx的话,就好生成/dev/xxx这个设备文件。
返回值为创建好的设备。

同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为device_destory,
函数原型如下:

 void device_destroy(struct class *cls, dev_t devt);

参数class是要删除的设备所处的类,参数devt是要删除的设备号。

驱动代码:


static void hello_init(void)
{
   .
   .
   .
   .
   .
   .
	cdev.owner = THIS_MODULE;//字符设备的所有者
	
	cdev_init(&cdev,&chrdev_ops);//字符设备初始化
	
	cdev_add(&cdev,dev_num,DEVICE_NUMBER);//字符设备的注册
	
	class = class_create(THIS_MODULE,DEVICE_CLASS_NAME);
	
	device = device_create(class,NULL,dev_num,NULL,DEVICE_NODE_NAME);
	
	return 0;
}

驱动卸载:

static int hello_exit(void)
{
    unregister_chrdev_region(MKDEV(major_num,minor_num),DEVICE_NUMBER);//设备号删除

    cdev_del(&cdev);//字符设备卸载

    device_destroy(class, dev_num);//设备卸载
  
    class_destroy(class);//类卸载

    printk("bye bye");
}

应用代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    int fd;

    char buf[64] = {};

    fd = open("/dev/chrdev_test",O_RDWR);
    if (fd < 0) 
    {
        printf("open error\n");
        return fd;
    }
    return 0;
}
实验结果

在/sys/class目录下生成了类。
在这里插入图片描述
在/dev/目录下生成了设备节点。在这里插入图片描述

卸载设备
在这里插入图片描述

测试设备节点

调用./app测试设备节点可以使用
在这里插入图片描述

五、字符设备和杂项设备回顾

杂项设备驱动框架

在这里插入图片描述

字符设备驱动框架

在这里插入图片描述

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐