博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
linux设备驱动的实现与理解
阅读量:2172 次
发布时间:2019-05-01

本文共 8295 字,大约阅读时间需要 27 分钟。

linux设备驱动的实现与理解

    在linux中对字符设备的驱动编写,驱动插入以及使用驱动文件进行逻辑控制,其中这份代码写在嵌入式板中,通过控制io来实现灯的亮灭,但是设备驱动的实现流程与灯无关,大致的流程都体现在代码中。我感觉这份博客我自己不会看,太难看了,算是自己对设备驱动理解的记录吧。

一、   程序解读

1、      系统调用

open() 打开文件

ioctl()设备驱动程序中对设备的I/O通道进行管理的函数

exit() 使进程停止运行,exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。

2、      寄存器地址

#define FS4412_GPF3CON  0x114001E0

通过数据手册可以找到每个端口的物理地址。

gpf3con = ioremap(FS4412_GPF3CON, 4);

在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到虚地址空间内,然后才能根据映射所得到的虚地址范围,通过访问内存指令访问这些I/O内存资源。通过ioremap来进行静态映射,4表示映射一个寄存器的地址,也就是4个字节,返回虚地址保存在本地全局的指针变量中。

3、      寄存器读写代码

writel((readl(gpf3con) & ~(0xff << 16)) | (0x11 << 16),gpf3con);

   writel(readl(gpx2dat) &~(0x3<<4), gpf3dat);

这是初始化端口时的f端口控制寄存器的初始化以及数据寄存器的初试化,其他的端口初始化同样的方法:

(1)首先根据数据手册初始化控制寄存器,配置固定的位,将需要的引脚的状态改为输出状态。

(2)通过写数据寄存器初始化引脚为低电平,即使led灯初始状态为熄灭。

(3)在收到测试文件的命令后,根据命令相应输出引脚为低电平或高电平

(4)先将端口数据寄存器32个引脚数据全部读出,然后只将需要的位置1或清零,然后再写回端口数据寄存器,可以防止修改没有用到的引脚。

二、   原理

在这里将实验的原理描述为两个部分,第一部分为驱动的加载部分,第二部分为驱动加载完成后,驱动的使用时的调用过程。

驱动加载:

1、将驱动源代码编译后,生成ko文件,这是将要加载的驱动模块。2、调用命令insmod加载模块,首先会找代码里边固定的宏moudle_init()来找到驱动中的初始化函数这里是s5pv210_led_init()和退出函数s5pv210_led_exit()。

3、调用s5pv210_led_init()来进行设备号注册,和设备添加。MKDEV是一个宏,可以通过移位把主设备号和次设备号进行处理,生成一个32位的数据。调用register_chrdev_region()注册设备号,linux驱动根据散列hash表来建立设备描述cdev结构体的索引,当hash表的index冲突时,采用链表的方式避免冲突,这样可以通过设备号快速找到cdev结构体的地址。

4、调用cdev_init()来初始化结构体cdev,最重要的是将file_operations保存在cdev中,file_oerations里边有本地实现的open release ioctl等具体功能的函数指针,这样可以在使用驱动的时候找到相应的实现函数。

5、调用cdev_add()来添加设备结构体cdev到hash表中,根据参数设备号可以找到注册设备号时在hash表中的位置,然后将cdev结构体地址添加进去

6、映射io端口,即映射io端口的物理地址为虚拟地址,因为在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定,可以在数据手册中找到。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内,然后才能根据映射所得到的核心虚地址范围,通过访问内存指令访问这些I/O内存资源。

7、使用命令mknod 添加/dev目录下设备描述文件,其实主要就是描述了我们输入的三个参数,首先c代表字符型设备,500代表主设备号,0代表次设备号。三个参数的用法在下边的流程描述。

8、调用rmmod命令后,卸载驱动,找到驱动中moudle_exit()宏来找到卸载驱动的退出函数,在这个里边调用cdev_del()和unregister_chrdev_region()删除设备并且去掉设备号的注册,相反的过程,不用赘言。

驱动使用:

1、在驱动测试文件中,首先打开了/dev目录下的设备文件,但是这个文件只是设备基本信息的描述,没有实质的动作,具体的作用可以看作为设备的索引,通过打开文件可以找到设备驱动的位置。这里分析我们输入的三个参数,c表示字符型设备open系统调用中拿到c就知道要去找字符型设备的结构体。主设备号和次设备号用来索引hash散列表,找到cdev结构体的地址,而cdev中保存有file_operations的地址,就可以找到驱动的具体实现函数,就是驱动加载的逆过程。而open执行完之后,返回一个文件描述符,这个描述符中就带有找到的cdev地址。

2、Open函数根据得到的cdev找到file_operations 中的.open对应的的函数指针,调用这个函数来初始化驱动,这里可以做io端口控制寄存器的设置,将相关的端口设置为输出。

3、调用ioctl通信,ioctl是io管道管理函数,是linux系统封装用来给驱动用的通信函数,方便用户使用,不用关心通信的实现方式,也不用考虑通信是否跨线程或者跨进程,可以看作是一个通道,在使用时塞入数据,在驱动里边写好拿出数据并做相应的处理及可以,感觉非常像socket套接字。

4、测试文件中调用ioctl传入数据,ioctl根据传入的文件描述符参数中的cdev找到file_operations 中的. unlocked_ioctl对应的的函数指针,在这个函数里边调用copy_from_user(),可以取出传入的参数,根据参数做驱动对应的动作。

5、退出驱动后,跟2-4同样的道理找到.release执行,释放驱动。

三、    代码的理解

实验过程中仔细研究了代码的流程,感觉有几点不足:

1、端口的控制寄存器和数据寄存器初始化放在驱动模块加载的函数中,这从实际的情况来讲并不合适。

2、io端口映射也放在驱动模块加载的过程中,也不太合适,端口映射是在消耗系统资源,而加载之后一直不使用驱动时,端口映射一直存在系统中,只有在驱动卸载之后才释放,从实际情况来讲也不合适。

3、对于cdev结构体的声明和定义放在驱动文件中有过疑惑,因为之前我以为cdev是一个链表,通过系统中的全局变量保存链表头,然后根据设备号去索引链表,如果放在本地,发生意外修改了结构体中指向下一个结构体的指针,会导致链表断裂,这是个很严重的问题。后来研究发现是用hash散列表实现的,故不存在这样的问题。

4、测试文件中只有open函数,没有调用close函数,当然这可以理解为需要在实验过程中自己实现,比如不用ctl c退出而是用读取输入字符判断退出函数,然后退出之前调用close。

四、和裸机平台设备驱动的不同:

第一次接触带有操作系统的驱动编程,之前感觉系统驱动比较神秘的面纱也被揭开了,跟裸机平台的设备驱动相比,区别就是

1、系统给驱动的编程增加了一个框架,需要依照系统对于驱动的统一管理来实现框架的内容,比如增加moudle_init,在init中注册设备号 添加设备等, 这都是在告诉系统,我们写的驱动具体实现的东西在哪里。

2、具体实际的动作,跟裸机驱动是一致的,因为驱动的本质就是直接操作硬件,而对于硬件的操作方法是硬件的数据手册里边统一定义的,手册没有改,对硬件的操作同样不会变化,

3、系统实现控制驱动的通信方法,裸机驱动调用控制驱动的方法直接调用就可以,而系统中需要通过ioctl来实现应用驱动的程序跟驱动程序的通信控制。

源码:

1、Makefile

ifeq ($(KERNELRELEASE),)

KERNELDIR ?= /home/linux/linux-3.14-fs4412/
#KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) 
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions Module* modules*
.PHONY: modules modules_install clean
else
    obj-m := fs4412_led.o

endif

2、头文件

#ifndef S5pV210_LED_HH

#define S5pV210_LED_HH
#define LED_MAGIC 'L'
/*
 * need arg = 1/2 
 */
#define LED_ON _IOW(LED_MAGIC, 0, int)
#define LED_OFF _IOW(LED_MAGIC, 1, int)
 

#endif

3、实现文件

#include <linux/kernel.h>

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include "fs4412_led.h"
MODULE_LICENSE("Dual BSD/GPL");
#define LED_MA 500
#define LED_MI 0
#define LED_NUM 1
#define FS4412_GPF3CON 0x114001E0
#define FS4412_GPF3DAT 0x114001E4
#define FS4412_GPX1CON 0x11000C20
#define FS4412_GPX1DAT 0x11000C24
#define FS4412_GPX2CON 0x11000C40
#define FS4412_GPX2DAT 0x11000C44
static unsigned int *gpf3con;
static unsigned int *gpf3dat;
static unsigned int *gpx1con;
static unsigned int *gpx1dat;
static unsigned int *gpx2con;
static unsigned int *gpx2dat;
struct cdev cdev;
void fs4412_led_on(int nr)
{
switch(nr) {
case 1: 
writel(readl(gpx2dat) | 1 << 7, gpx2dat);
break;
case 2: 
writel(readl(gpx1dat) | 1 << 0, gpx1dat);
break;
case 3: 
writel(readl(gpf3dat) | 1 << 4, gpf3dat);
break;
case 4: 
writel(readl(gpf3dat) | 1 << 5, gpf3dat);
break;
}
}
void fs4412_led_off(int nr)
{
switch(nr) {
case 1: 
writel(readl(gpx2dat) & ~(1 << 7), gpx2dat);
break;
case 2: 
writel(readl(gpx1dat) & ~(1 << 0), gpx1dat);
break;
case 3: 
writel(readl(gpf3dat) & ~(1 << 4), gpf3dat);
break;
case 4: 
writel(readl(gpf3dat) & ~(1 << 5), gpf3dat);
break;
}
}
static int s5pv210_led_open(struct inode *inode, struct file *file)
{
return 0;
}
static int s5pv210_led_release(struct inode *inode, struct file *file)
{
return 0;
}
static long s5pv210_led_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int nr;
if(copy_from_user((void *)&nr, (void *)arg, sizeof(nr)))
return -EFAULT;
if (nr < 1 || nr > 4)
return -EINVAL;
switch (cmd) {
case LED_ON:
fs4412_led_on(nr);
break;
case LED_OFF:
fs4412_led_off(nr);
break;
default:
printk("Invalid argument");
return -EINVAL;
}
return 0;
}
int fs4412_led_ioremap(void)
{
int ret;
gpf3con = ioremap(FS4412_GPF3CON, 4);
if (gpf3con == NULL) {
printk("ioremap gpf3con\n");
ret = -ENOMEM;
return ret;
}
gpf3dat = ioremap(FS4412_GPF3DAT, 4);
if (gpf3dat == NULL) {
printk("ioremap gpx2dat\n");
ret = -ENOMEM;
return ret;
}
gpx1con = ioremap(FS4412_GPX1CON, 4);
if (gpx1con == NULL) {
printk("ioremap gpx2con\n");
ret = -ENOMEM;
return ret;
}
gpx1dat = ioremap(FS4412_GPX1DAT, 4);
if (gpx1dat == NULL) {
printk("ioremap gpx2dat\n");
ret = -ENOMEM;
return ret;
}
gpx2con = ioremap(FS4412_GPX2CON, 4);
if (gpx2con == NULL) {
printk("ioremap gpx2con\n");
ret = -ENOMEM;
return ret;
}
gpx2dat = ioremap(FS4412_GPX2DAT, 4);
if (gpx2dat == NULL) {
printk("ioremap gpx2dat\n");
ret = -ENOMEM;
return ret;
}
return 0;
}
void fs4412_led_iounmap(void)
{
iounmap(gpf3con);
iounmap(gpf3dat);
iounmap(gpx1con);
iounmap(gpx1dat);
iounmap(gpx2con);
iounmap(gpx2dat);
}
void fs4412_led_io_init(void)
{
writel((readl(gpf3con) & ~(0xff << 16)) | (0x11 << 16), gpf3con);
writel(readl(gpx2dat) & ~(0x3<<4), gpf3dat);
writel((readl(gpx1con) & ~(0xf << 0)) | (0x1 << 0), gpx1con);
writel(readl(gpx1dat) & ~(0x1<<0), gpx1dat);
writel((readl(gpx2con) & ~(0xf << 28)) | (0x1 << 28), gpx2con);
writel(readl(gpx2dat) & ~(0x1<<7), gpx2dat);
}
struct file_operations s5pv210_led_fops = {
.owner = THIS_MODULE,
.open = s5pv210_led_open,
.release = s5pv210_led_release,
.unlocked_ioctl = s5pv210_led_unlocked_ioctl,
};
static int s5pv210_led_init(void)
{
dev_t devno = MKDEV(LED_MA, LED_MI);       //构建设备号
int ret;
ret = register_chrdev_region(devno, LED_NUM, "newled");  //分配设备号 ,这里用静态申请
if (ret < 0) {
printk("register_chrdev_region\n");
return ret;
}
cdev_init(&cdev, &s5pv210_led_fops);     //cdev结构体初始化,最关键的是将file_operations结构体的地址关联到cdev中
cdev.owner = THIS_MODULE;
ret = cdev_add(&cdev, devno, LED_NUM);   //增加设备,将主设备号和次设备号关联到cdev中
if (ret < 0) {
printk("cdev_add\n");
goto err1;
}
ret = fs4412_led_ioremap();        //映射io端口的物理地址为虚拟地址,因为般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访问内存指令访问这些I/O内存资源。
if (ret < 0)
goto err2;
fs4412_led_io_init();
printk("Led init\n");
return 0;
err2:
cdev_del(&cdev);
err1:
unregister_chrdev_region(devno, LED_NUM);     //注销设备号
return ret;
}
static void s5pv210_led_exit(void)
{
dev_t devno = MKDEV(LED_MA, LED_MI);
fs4412_led_iounmap();
cdev_del(&cdev);
unregister_chrdev_region(devno, LED_NUM);
printk("Led exit\n");
}
module_init(s5pv210_led_init);
module_exit(s5pv210_led_exit);

转载地址:http://aqhzb.baihongyu.com/

你可能感兴趣的文章
剑指offer 25.二叉树中和为某一值的路径
查看>>
剑指offer 60. 不用加减乘除做加法
查看>>
Leetcode C++《热题 Hot 100-14》283.移动零
查看>>
Leetcode C++《热题 Hot 100-15》437.路径总和III
查看>>
Leetcode C++《热题 Hot 100-17》461.汉明距离
查看>>
Leetcode C++《热题 Hot 100-18》538.把二叉搜索树转换为累加树
查看>>
Leetcode C++《热题 Hot 100-19》543.二叉树的直径
查看>>
Leetcode C++《热题 Hot 100-21》581.最短无序连续子数组
查看>>
Leetcode C++《热题 Hot 100-22》2.两数相加
查看>>
Leetcode C++《热题 Hot 100-23》3.无重复字符的最长子串
查看>>
Leetcode C++《热题 Hot 100-24》5.最长回文子串
查看>>
Leetcode C++《热题 Hot 100-26》15.三数之和
查看>>
Leetcode C++《热题 Hot 100-28》19.删除链表的倒数第N个节点
查看>>
Leetcode C++《热题 Hot 100-29》22.括号生成
查看>>
Leetcode C++《热题 Hot 100-44》102.二叉树的层次遍历
查看>>
Leetcode C++《热题 Hot 100-47》236.二叉树的最近公共祖先
查看>>
Leetcode C++《热题 Hot 100-48》406.根据身高重建队列
查看>>
《kubernetes权威指南·第四版》第二章:kubernetes安装配置指南
查看>>
Leetcode C++《热题 Hot 100-49》399.除法求值
查看>>
Leetcode C++《热题 Hot 100-51》152. 乘积最大子序列
查看>>