linux设备驱动的实现与理解

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
#include
#include
#include


#include
#include


#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);

你可能感兴趣的:(设备驱动)