Linux字符设备驱动

前言

驱动就是获取外设、传感器的数据,或者控制外设,数据最终会提交给应用程序。Linux下驱动和应用是分开的,我们除了写驱动程序外,还要写一个简单的测试的应用程序。驱动程序运行在内核空间,应用程序运行在用户空间。在Linux中用户空间不能直接调用内核空间的函数,所以必须要经过系统调用,应用程序才可以调用驱动程序的函数
应用程序访问内核资源:
1.系统调用
2.异常(中断)
3.陷入

编译内核驱动的时候需要用到Linux内核源码,要提前将内核源码解压缩,并编译,得到zimage和dtb。需要使用zimage和dtb启动系统。
应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所示:

其中,比较重要和常用的函数为:
第 1589 行, owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
第 1590 行, llseek 函数用于修改文件当前的读写位置。
第 1591 行, read 函数用于读取设备文件。
第 1592 行, write 函数用于向设备文件写入(发送)数据。
第 1596 行, poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
第 1597 行, unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
第 1598 行, compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是
unlocked_ioctl。
第 1599 行, mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
第 1601 行, open 函数用于打开设备文件。
第 1603 行, release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
第 1604 行, fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
第 1605 行, aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。

一.编写第一个hello_world程序

1.代码框架


#include <linux/init.h> 初始化头文件
#include <linux/module.h> 初始化加载模块头文件
#include <linux/kernel.h> 初始化内核头文件
MODULE_LICENSE() //添加模块 LICENSE 信息
_init:该关键字标记表示该函数只会在模块初始化期间被调用,而不会在模块运行期间被调用。
_exit:该关键字可以用于标记函数只会在模块退出期间被调用,而不会在模块运行期间被调用。
module_init:该宏用于指定模块初始化函数
module_exit:该宏用于指定模块退出函数
printk属于内核函数,它与printf在实现上唯一的区别就是printk可以通过指定消息等级来区分消息输出,在在这里,printk输出的消息被输出到/var/log/kern.log文件中,我们可以通过另开一个终端来查看内核日志消息:
tail -f /var/log/kern.log

2.Makefile(编译)

linux下编译程序一般使用make工具(简单的程序可以直接命令行来操作),以及一个Makefile文件,在内核开发中,Makefile并不像应用程序那样,而是经过了一些封装,我们只需要往其中添加需要编译的目标文件即可:

其中hello_world.o就是目标文件,make工具会根据目标文件自动推导来编译hello_world.c文件
然后使用make指令进行编译,编译结果会在当前目录下生成hello_world.ko文件,这个文件就是我们需要的内核模块文件了。

3.加载

编译生成了内核文件,接下来就要将其加载到内核中,linux支持动态地添加和删除模块,所以我们可以直接在系统中进行加载:

我们可以通过lsmod命令来检查模块是否被成功加载:

二.字符设备驱动的开发步骤

1.旧字符设备

1.1 驱动模块的加载和卸载

Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。
模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块: insmod和modprobe, insmod 是最简单的模块加载命令,此命令用于加载指定的.ko 模块,比如加载 drv.ko 这个驱动模块,命令如下:
insmod drv.ko
insmod 命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用 insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。但是 modprobe 就不会存在这个问题, modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此 modprobe 命令相比 insmod 要智能一些。
驱动模块的卸载使用命令“rmmod”即可,比如要卸载 drv.ko,使用如下命令即可:
rmmod drv.ko
也可以使用“modprobe -r”命令卸载驱动,比如要卸载 drv.ko,命令如下:
modprobe -r drv.ko
使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod 命令。

1.2 字符设备的注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。
**unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:**major: 要注销的设备对应的主设备号。 name: 要注销的设备对应的设备名。
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。在示例代码中字符设备的注册和注销,内容如下所示:

1.3 实现设备的具体操作函数

file_operations 结构体就是设备的具体操作函数,在示例代码中我们定义了 file_operations结构体类型的变量test_fops,但是还没对其进行初始化,也就是初始化其中的open、release、 read 和 write 等具体的设备操作函数。
需求分析:
1.能够对设备进行打开和关闭操作。设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现 file_operations 中的 open 和 release 这两个函数。
2.能够对设备进行读写操作。应用程序需要通过 read 和 write 这两个函数对设备的缓冲区进行读写操作。所以需要实现 file_operations 中的 read 和 write 这两个函数。
代码如下:

1.4 添加LICENSE和作者信息

最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下两个函数:
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息

2.新字符设备

字符设备驱动开发重点是使用 register_chrdev 函数注册字符设备,当不再使用设备的时候就使用 unregister_chrdev 函数注销字符设备,驱动模块加载成功以后还需要手动使用 mknod 命令创建设备节点。 register_chrdev 和 unregister_chrdev 这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动API函数。

2.1新字符设备驱动原理

2.1.1分配和释放设备号

使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:
1.需要我们事先确定好哪些主设备号没有使用。
2.会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为200,那么 0~1048575(2^20-1) 这个区间的次设备号就全部都被 LED 一个设备分走了。这样太浪费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
解决这两个问题最好的方法就是要使用设备号的时候向 Linux 内核申请,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号。
如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev:只是一个输出参数,保存申请到的设备号。
baseminor:次设备号,它常常是0;
count:要申请的设备号数量。
name:设备名字。
动态分配的缺点是你无法提前创建设备节点,因为分配给你的主设备号会发生变化。我们申请到了设备节点之后,可以用宏定义 MAJOR() 来获取主设备号。
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char name)
/

from:要申请的起始设备号
count:要申请的数量,一般都是一个
name:设备名字
*/
注 销 字 符 设 备 之 后 要 释 放 掉 设 备 号 , 不 管 是 通 过 alloc_chrdev_region 函 数 还 是 register_chrdev_region 函数申请的设备号,统一使用如下释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
新字符设备驱动下,设备号分配示例代码如下:

第 1~3 行,定义了主/次设备号变量 major 和 minor,以及设备号变量 devid。
第 5 行,判断主设备号 major 是否有效,在 Linux 驱动中一般给出主设备号的话就表示这个设备的设备号已经确定了,因为次设备号基本上都选择 0,这算个 Linux 驱动开发中约定俗成的一种规定了。
第 6 行,如果 major 有效的话就使用 MKDEV 来构建设备号,次设备号选择 0。
第 7 行,使用 register_chrdev_region 函数来注册设备号。
第 9~11 行,如果 major 无效,那就表示没有给定设备号。此时就要使用 alloc_chrdev_region函数来申请设备号。设备号申请成功以后使用 MAJOR 和 MINOR 来提取出主设备号和次设备号。

2.1.2新字符设备注册方法

字符设备结构体:
在 Linux 中使用 cdev 结构体表示一个字符设备, cdev 结构体在 include/linux/cdev.h 文件中的定义如下:

在 cdev 中有两个重要的成员变量: ops 和 dev,这两个就是字符设备文件操作函数集合 file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备,如下所示:
struct cdev test_cdev;
获取cdev:struct cdev *cdev_alloc(void)
cdev_init 函数:
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化, cdev_init 函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。使用 cdev_init 函数初始化 cdev 变量的示例代码如下:

cdev_add 函数:
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。加入 cdev_add 函数,内容如下所示:

cdev_del 函数:
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备, cdev_del 函数原型如下:
void cdev_del(struct cdev *p)
参数 p 就是要删除的字符设备。如果要删除字符设备,参考如下代码:

cdev_del 和 unregister_chrdev_region 这两个函数合起来的功能相当于 unregister_chrdev 函数。

2.2自动创建设备节点

当我们使用 modprobe 加载驱动程序以后还需要使用命令 “mknod”手动创建设备节点:

本节记录如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。

2.2.1mdev机制

udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除, udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用 modprobe 命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用 rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。 使用 busybox 构建根文件系统的时候, busybox 会创建一个 udev 的简化版本—mdev,所以在嵌入式 Linux 中我们使用 mdev 来实现设备节点文件的自动创建与删除,

2.2.2创建和删除类

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。首先要创建一个 class 类, class 是个结构体,定义在文件 include/linux/device.h 里面。 class_create 是类创建函数, class_create 是个宏定义,内容如下:

根据上述代码,将宏 class_create 展开以后内容如下:

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

2.2.3创建设备

一小节创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用 device_create 函数在类下面创建设备, device_create 函数原型如下:

device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:

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

2.3新字符设备开发流程图

三.Linux设备号

1.设备号的组成

为了方便管理, Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号, 低 20 位为次设备号。因此 Linux 系统中主设备号范围为 0~4095,所以在选择主设备号的时候一定不要超过这个范围。
在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:

第 6 行,宏 MINORBITS 表示次设备号位数,一共是 20 位。
第 7 行,宏 MINORMASK 表示次设备号掩码。
第 9 行,宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
第 10 行,宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
第 11 行,宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。

2.设备号的分配

2.1静态分配设备号

设备号分配主要是主设备号的分配。注册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态的指定一个设备号,比如选择 200 这个主设备号。使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号,静态分配的设备号不能和已经使用了的设备号重复

2.2动态分配设备号

静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题, Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可。
设备号申请函数:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char name)
/

dev:保存申请到的设备号。
baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这
些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递 增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count: 要申请的设备号数量。
name:设备名字。
/
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
/

from:要释放的设备号。
count: 表示从 from 开始,要释放的设备号数量。
*/

四.inode与file结构体

1.inode结构体

当我们在Linux中创建一个文件时,就会在相应的文件系统中创建一个inode与之对应,文件实体和文件inode是一一对应的,创建好一个inode会存在存储器中。第一次open就会将inode在内存中有一个备份,同一个文件被多次打开并不会产生多个inode,当所有被打开的文件都被close之后,inode在内存中的实例才会被释放。既然如此,当我们使用mknod(或其他方法)创建一个设备文件时,也会在文件系统中创建一个inode,这个inode和其他的inode一样,用来存储关于这个文件的静态信息(不变的信息),包括这个设备文件对应的设备号,文件的路径以及对应的驱动对象等。

我们一般比较关心的只有两个变量:

  • dev_t i_rdev: 代表设备文件的节点,这个成员包含实际的设备编号
  • struct cdev *i_cdev: 这个结构体代表字符设备,这个成员包含一个指针,指向这个结构体。

2.file结构体

file结构体代表一个打开的文件。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后的关闭。在文件的所有实例都关闭后,内核释放这个数据结构。
struct file结构体 用来表示一个动态的设备,每当open打开一个文件时就会产生一个struct file结构体 与之对应。

五.Linux字符设备驱动完整框架及其实验

本节记录了Linux字符设备驱动的框架在PC(UBUNTU 16.04)机上运行的结果。
首先编写驱动代码如下:

#include<linux/init.h>
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#define MINOR_NUM 3
static int hello_major=0;
static int hello_minor=0;
static dev_t hello_no=0;
static struct cdev hello_cdev;
static struct file_operations hello_ops;
static struct class *hello_class; /*定义一个class用于自动创建类*/
/*
        int (*open) (struct inode *, struct file *);
        int (*release) (struct inode *, struct file *);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
*/
static int hello_open(struct inode * node, struct file *file)
{
        printk("%s\n",__FUNCTION__);
        return 0;
}
static int hello_release(struct inode * node, struct file *file)
{
        printk("%s\n",__FUNCTION__);
        return 0;
}
static ssize_t hello_read(struct file *file, char __user *p_user, size_t num, loff_t * ploff)
{
        printk("%s\n",__FUNCTION__);
        return 0;
}
static ssize_t hello_write(struct file *file, const char __user *p_user, size_t num, loff_t * ploff)
{
        printk("%s\n",__FUNCTION__);
        return 0;
}
static void register_hello_ops_cdev(void)
{
        // initial file_opeartions variable of hello_ops;
        hello_ops.open=hello_open;
        hello_ops.release=hello_release;
        hello_ops.read=hello_read;
        hello_ops.write=hello_write;
        // initial cdev variable of hello_cdev
        hello_cdev.ops=&hello_ops;
        hello_cdev.dev=&hello_no;
        hello_cdev.count=MINOR_NUM;
        hello_cdev.owner=THIS_MODULE;
        cdev_init(&hello_cdev,&hello_ops);
        cdev_add(&hello_cdev,hello_no,MINOR_NUM);
}
static void allocate_cdev_num(void)
{
        alloc_chrdev_region(&hello_no,0,MINOR_NUM,"hello");
        hello_major=MAJOR(hello_no);
        hello_minor=MINOR(hello_no);
        printk("hello_major= %d, hello_minor= %d\n",hello_major,hello_minor);
}
static int __init hello_init(void)
{
        printk("hello_init func is called\n");
        allocate_cdev_num();
        register_hello_ops_cdev();
        hello_class = class_create(THIS_MODULE, "hello"); /*创建设备类型*/
        device_create(hello_class, NULL, hello_no, NULL, "hello");
        /*/dev/chrdev 注册这个设备节点*/
        return 1;
}
static void __exit hello_exit(void)
{
        printk("hello_exixt is called\n");
        device_destroy(hello_class, hello_no); /*注销这个设备节点*/
        class_destroy(hello_class); /*删除这个设备类型*/
        unregister_chrdev_region(hello_no,MINOR_NUM);
        cdev_del(&hello_cdev);
}
module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("Dual BSD/GPL");

代码里面使用了自动创建设备节点的方法。
编译这个文件需要一个Makefile文件,Makefile代码如下:

obj-m+= hello.o
all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
~

随后,在命令行输入make即可编译这个驱动代码

接着编写测试程序代码:

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

int main(int argc,char *argv[])
{
        int fdr=0;
        char load[100];
        int size=0;
        fdr=open(argv[1],O_RDWR);
        assert(fdr>2);// stdin 0 stdout 1,stderr 2
        size=sizeof(load)/sizeof(load[0]);
        read(fdr,load,size);
        write(fdr,load,size);
        close(fdr);
        return 1;
}

使用gcc命令编译测试程序代码:

至此,程序的编译全部通过。接下来就是测试了。前面已经说过,printk输出的消息被输出到/var/log/kern.log文件中,我们可以通过另开一个终端来查看内核日志消息:
tail -f /var/log/kern.log
我们在命令行中输入加载模块的指令:

insmod hello.ko

可以在内核日志中看到打印的消息:

由于在驱动代码里使用的是自动创建设备节点的方式,所以这个时候我们直接去/dev下查看,是否创建了“hello”设备:

可见设备创建成功
修改它的权限:

chmod 777 /dev/hello

运行代码:

./show_hello /dev/hello

可以看到内核日志中的消息:

最后,我们卸载模块

rmmod hello.ko


此时,/dev下的设备节点也被销毁

六.编写一个蜂鸣器驱动程序

1.原理图


其中,PWM0对应:

2.开发手册

GPD0相关寄存器如下:

GPD0CON详细内容:

其中,GPD0CON[0]控制端口GPD0_0
物理地址为:基地址+偏移地址

GPD0DAT详细内容

3.地址映射

对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB,如果开发板上有 512MB 的 DDR3,这 512MB 的内存就是物理内存,经过 MMU (内存管理单元)可以将其映射到整个 4GB 的虚拟空间,如图所示:

物理内存只有 512MB,虚拟内存有 4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理,这里我们不要去深究。
例如,我们要往GPD0DAT的物理地址0X1140_00A4写数据,必须得到 它在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数: ioremap 和 iounmap。

3.1 ioremap 函数

ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 ,函数原型如下:

void __iomem *ioremap(resource_size_t addr, size_t size);

ioremap()函数接受两个参数,addr表示要映射的物理地址,size表示映射的大小(size的单位是bit还是字节?)。该函数返回一个指向虚拟内存区域的指针。

3.2 iounmap 函数

卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射, iounmap 函数原型如下:

void iounmap (volatile void __iomem *addr)

iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。

4.读写I/O内存

使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
读操作函数:

unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);

这三个函数分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。
与上述函数对应较早版本的函数为(这些函数在Linux2.6中仍然被支持):

unsigned readb(void *addr);
unsigned readw(void *addr);
unsigned readl(void *addr);

写操作函数:

void iowrite8(u8 value,void *addr);
void iowrite16(u16 value,void *addr);
void iowrite32(u32 value,void *addr);

与上述函数对应较早版本的函数为(这些函数在Linux2.6中仍然被支持):

void writeb(unsinged value,void *addr);
void writew(unsinged value,void *addr);
void writel(unsinged value,void *addr);

5.程序

驱动程序:

#include<linux/init.h>
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#include<linux/uaccess.h>
#include<linux/io.h>
#define MINOR_NUM 3
#define GPD0_0_CON 0X114000A0
#define GPD0_0_DAT 0X114000A4
static int beep_major=0;
static int beep_minor=0;
static dev_t beep_no=0;
static struct cdev beep_cdev;
static int *p_conf=NULL;
static int *p_data=NULL;
static struct file_operations beep_ops;
/*
        int (*open) (struct inode *, struct file *);
        int (*release) (struct inode *, struct file *);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
*/
static int beep_open(struct inode * node, struct file *file)
{
        unsigned int data=0;
        printk("%s\n",__FUNCTION__);
        p_conf=(int*)ioremap(GPD0_0_CON,16);
        p_data=(int*)ioremap(GPD0_0_DAT,16);
        data=ioread16(p_conf);
        data&=~0x1<<1;// set bit1 of data to zero
        data&=~0x1<<2;//set  bit2 of data to zero
        data&=~0x1<<3;
        data|=0x1;
        iowrite16(data,p_conf);
        return 0;
}
static void beep_start(void)
{
        unsigned int data=0;
        data=ioread16(p_data);
        data|=0x1;
        iowrite16(data,p_data);
}
static void beep_stop(void)
{
        unsigned int data=0;
        data=ioread16(p_data);
        data&=~0x1<<0;
        iowrite16(data,p_data);
}

static int beep_release(struct inode * node, struct file *file)
{
        printk("%s\n",__FUNCTION__);
        iounmap(p_data);
        iounmap(p_conf);
        return 0;
}

static long beep_ioctl(struct file *fp,unsigned int cmd,unsigned long num)
{
        printk("%s\n",__FUNCTION__);
        switch(cmd)
        {
                case 0:// beep stop
                        beep_stop();
                break;
                case 1:// beep start
                        beep_start();
                break;
        }
        return 0;
}
static void register_beep_ops_cdev(void)
{
        // initial file_opeartions variable of beep_ops;
        beep_ops.open=beep_open;
        beep_ops.release=beep_release;
        beep_ops.unlocked_ioctl=beep_ioctl;
        // initial cdev variable of beep_cdev
        beep_cdev.ops=&beep_ops;
        beep_cdev.dev=beep_no;
        beep_cdev.count=MINOR_NUM;
        beep_cdev.owner=THIS_MODULE;
        cdev_init(&beep_cdev,&beep_ops);
        cdev_add(&beep_cdev,beep_no,MINOR_NUM);
}
static void allocate_cdev_num(void)
{
        alloc_chrdev_region(&beep_no,0,MINOR_NUM,"beep");
        beep_major=MAJOR(beep_no);
        beep_minor=MINOR(beep_no);
        printk("beep_major= %d, beep_minor= %d\n",beep_major,beep_minor);
}
static int __init beep_init(void)
{
        printk("beep_init func is called\n");
        allocate_cdev_num();
        register_beep_ops_cdev();
        return 1;
}
static void __exit beep_exit(void)
{
        printk("beep_exixt is called\n");
        unregister_chrdev_region(beep_no,MINOR_NUM);
        cdev_del(&beep_cdev);
}
module_init(beep_init);
module_exit(beep_exit);

应用程序:

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

int main(int argc,char *argv[])
{
        int fdr=0,i=0;
        fdr=open(argv[1],O_RDWR);
        assert(fdr>2);// stdin 0 stdout 1,stderr 2
        while(i<5)
        {
                ioctl(fdr,1);
                sleep(1);
                ioctl(fdr,0);
                sleep(1);
                i++;
        }
        close(fdr);
        return 1;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

嵌入式小李

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

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

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

打赏作者

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

抵扣说明:

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

余额充值