Linux|驱动程序设计与安装

原理

在Linux系统中,设备驱动通常分为字符设备、块设备和网络设备三种类型,在这三种类型中,字符设备模型是最常见的,如显卡、声卡、摄像头、串口等设备都是采用字符设备驱动模型。字符设备的硬件特征是应用程序和驱动程序在数据传输过程中以字符为单位,这些数据传输的数据比较慢,但是是实时的,按照固定的顺序传输的。在Linux内核中提供了字符设备对应的驱动模型框架,包括文件系统接口和字符设备驱动管理框架。在内核中,为了进一步简化字符设备驱动的设计过程,还对字符设备驱动进行了封装,产生了如 RTC 模型,I2C 模型,SPI 模型等。本次介绍基本的字符设备驱动模型的设计方法。

Linux驱动程序设计流程

1.设计一个module
module是Linux的内核模块,相当于存放驱动的一个盒子,每设计一个驱动程序,都需要先设计一个module,驱动程序放到module中。module是独立的模块,一般相互之间是没有关联的。正常情况下,每个硬件都有一个驱动程序(module)。
module(驱动程序)编译后会生成一个ko文件,这个ko文件就是驱动程序的安装文件。
驱动的安装和卸载:
#insmod led_drv.ko
自动生成设备文件–/dev/led_drv ---->才可以执行应用程序
#rmmod led_drv.ko
自动删除设备文件
module由三部分组成:
1)、头文件
设计驱动程序的头文件来在于Linux内核源码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>

2)、module的入口和出口
//驱动的安装函数
static int __init gec6818_led_init(void)
{
//从Linux内核申请资源,建立Linux驱动的模型,并将驱动程序加入到Linux内核
}
//驱动的卸载函数
static void __exit gec6818_led_exit(void)
{ //释放kernel资源,并注销驱动
iounmap(GPIOE_BASE);
iounmap(GPIOC_BASE);
release_mem_region(0xC001E000, 0x1000);
release_mem_region(0xC001C000, 0x1000);
device_destroy(led_class,led_dev_num);
class_destroy(led_class);
cdev_del(&gec6818_led_dev);
unregister_chrdev_region(led_dev_num, 1);
printk(KERN_ERR “gec6818 led driver exit…\n”);
}
//入口 #insmod led_drv.ko–>module_init()–>gec6818_led_init()
module_init(gec6818_led_init);//main()

//出口 #rmmod led_drv.ko–>module_exit()–>gec6818_led_exit()
module_exit(gec6818_led_exit);

3)、module的描述
//驱动的描述。 #modinfo led_drv.ko
MODULE_AUTHOR(“bobeyfeng@163.com”);
MODULE_DESCRIPTION(“S5P6818 LED Device Driver”);
MODULE_LICENSE(“GPL”);
MODULE_VERSION(“V1.0”);
2.使用struct cdev 定义一个变量
cdev ---- charactor device (字符设备、块设备、网络设备)
LED灯的驱动是字符设备驱动,设计字符设备驱动首先要定义一个cdev,cdev表示一个字符设备。
3.给cdev申请设备号
设备号由主设备号和次设备号组成:
设备号 = (主设备号<<20) + 次设备号
动态分配: 让内核自动分配一个空闲的设备号
静态注册: 自己定义一个设备号,注册到内核
4.定义并初始化文件操作集—file_operations
file_operations ---->给应用程序提供的接口

static const struct file_operations gec6818_led_fops = {
	.owner = THIS_MODULE,
	.open = gec6818_led_open,
	.write = gec6818_led_write,
	.release = gec6818_led_release,
};

文件操作集中,由给应用程序的API—接口函数。这里是提供了三个、
//buf–>存放应用程序write()写下来的数据:buf[1]–灯号(7/8/9/10),buf[0]–灯的状态(1/0)
//size_t len—>应用程序write()数据的字节数。loff_t *offp —>文件偏移的指针

static ssize_t gec6818_led_write(struct file *filp, const char __user *buf, size_t len, loff_t *offp)
{
	int ret;
	char led_buf[2];
	if(len != 2)
		return -EINVAL;
	ret = copy_from_user(led_buf, buf, len);
	if(ret != 0)
		return -EFAULT;
	switch(led_buf[1]){
	case 7: //led7
		if(led_buf[0] == 1)//led7 on
			*GPIOEOUT &= ~(1<<13);
		else if(led_buf[0] == 0)//led7 off
			*GPIOEOUT |= (1<<13);
		else 
			return -EINVAL;
		break;
		
	case 8: //led8
		if(led_buf[0] == 1)//led8 on
			*GPIOCOUT &= ~(1<<17);
		else if(led_buf[0] == 0)//led8 off
			*GPIOCOUT |= (1<<17);
		else 
			return -EINVAL;	
		break;
		
	case 9: //led8
		if(led_buf[0] == 1)//led9 on
			*GPIOCOUT &= ~(1<<8);
		else if(led_buf[0] == 0)//led9 off
			*GPIOCOUT |= (1<<8);
		else 
			return -EINVAL;	
		break;
		
	case 10: //led10
		if(led_buf[0] == 1)//led10 on
			*GPIOCOUT &= ~(1<<7);
		else if(led_buf[0] == 0)//led10 off
			*GPIOCOUT |= (1<<7);
		else 
			return -EINVAL;	
		break;
		
	default:
		return -EINVAL;	
	}
	return len;
}

应用程序的write()写两个字节的数据给驱动程序,在驱动程序中,使用文件操作集的write()来接收这两个字节的数据。接收的时候,使用了copy_from_user()来接收数据。接收到数据后,利用寄存器的虚拟地址访问寄存器,控制LED灯。
5.cdev的初始化
gec6818_led_dev.owner = THIS_MODULE;
cdev_init(&gec6818_led_dev, &gec6818_led_fops);
6.将cdev加入内核
cdev_add()
7.创建class
class_create()
8.创建device
device_create()
有了class和device,安装驱动后,可以自动生成设备文件—/dev/led_drv
9.申请寄存器的物理地址作为一个资源

led_res = request_mem_region(0xC001C000, 0x1000, "GPIOC_MEM");
	if(led_res == NULL){
		printk(KERN_ERR "request gpioc mem error\n");
		ret = -EBUSY;
		goto request_gpiocmem;
	}

10.得到寄存器物理地址对一个的虚拟地址

GPIOC_BASE = ioremap(0xC001C000, 0x1000);
	if(GPIOC_BASE == NULL){
		printk(KERN_ERR "ioremap gpioc error\n");
		ret = -EFAULT;
		goto ioremap_gpioc_err;		
	}
	GPIOCOUT = GPIOC_BASE + 0x00;
	GPIOCOUTENB = GPIOC_BASE + 0x01; //0x04
	GPIOCALTFN0 = GPIOC_BASE + 0x08; //0x20
	GPIOCALTFN1 = GPIOC_BASE + 0x09; //0x24

11.通过虚拟地址访问寄存器
//将GPIOE13配置成输出,输出高电平,D7灭
*GPIOEALTFN0 &= ~(3<<26);
*GPIOEOUTENB |= (1<<13);
*GPIOEOUT |= (1<<13);

//将GPIOC17/8/7配置成输出,输出高电平,D8/9/10灭
*GPIOCALTFN1 &= ~(3<<2);
*GPIOCALTFN1 |= (1<<2);
*GPIOCALTFN0 &= ~(0xf<<14);
*GPIOCALTFN0 |= (0x5<<14);
*GPIOCOUTENB |= (1<<17) +(1<<8) + (1<<7);
*GPIOCOUT |= (1<<17) +(1<<8) + (1<<7);

字符设备驱动设计流程

字符设备驱动在设计的过程中,主要过程是先设计一个内核模块,然后在内核模块中定 义一个 cdev,并为该 cdev 申请设备号和定义文件操作集 file_operations;最后完成 cdev 的初始化,将 cdev 加入内核。在内核中的 cdev 需要应用程序通过设备文件进行访问,我们 安装完 cdev 后,需要创建设备文件,设备文件可以手动创建也可以自动创建。
在这里插入图片描述
在这里插入图片描述

设备号

字符设备和块设备都有设备号,设备号相当于一个硬件设备的 ID,每个有设备驱动程 序的硬件都有一个
这样的 ID,设备号在 Linux 内核中是唯一的。 在 Linux 内核中用 dev_t 类型变量来标识一个设备号,dev_t 在内核中定义为 32bits 无符号整型值。在内核源码/include/linux/types.h 文件中:
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
主设备号和次设备号
设备号是由主设备号和次设备号组成的,设备号的高 12 位用来表示主设备号,低 20 位用来表示次设备号。主设备号对应同一类型的硬件,即对应一个设备驱动程序;次设备号 对应该硬件类型下某一个具体的硬件设备。
内核中提供了几个宏,用来完成主设备号和次设备号之间的关系计算。MKDEV 宏实现有 主设备号和次设备号得到设备号,MAJOR 宏实现由设备号得到主设备号,MINOR 宏实现由设 备号得到次设备号。在内核源码/include/linux/kdev_t.h 中定义如下:

#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1) 
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) 
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))

在嵌入式 Linux 平台中,我们在/dev 目录下可以看到设备类型,主设备号、次设备号 以及设备文件等信息,如: # ls /dev/s3c2410_serial* -l
crw-rw---- 1 root root 204, 64 Jan 1 12:00 /dev/s3c2410_serial0
crw-rw---- 1 root root 204, 65 Jan 1 12:00 /dev/s3c2410_serial1
crw-rw---- 1 root root 204, 66 Jan 1 12:00 /dev/s3c2410_serial2
crw-rw---- 1 root root 204, 67 Jan 1 12:00 /dev/s3c2410_serial3
第一个字母 c 表示这是一个字符设备(charactor device),该设备是 UART 串口设备, 四个串口共用一个主设备号 204,每个串口各有一个次设备号 64~67,一共由四个次设备。 由此可见在这个嵌入式平台上在一个设备驱动程序中完成了四个串口的程序设计,这四个串 口共用主设备号,由个次设备号来区分具体的某个串口。 又如在嵌入式平台上查看一个 nand flash 设备(nand flash 相当于嵌入式平台的硬盘), 可见如下信息:
ls /dev/mtdblock* -l
brw-rw---- 1 root root 31, 0 Jan 1 12:31 /dev/mtdblock0
brw-rw---- 1 root root 31, 1 Jan 1 12:31 /dev/mtdblock1
brw-rw---- 1 root root 31, 2 Jan 1 12:31 /dev/mtdblock2
brw-rw---- 1 root root 31, 3 Jan 1 12:31 /dev/mtdblock3
brw-rw---- 1 root root 31, 4 Jan 1 12:31 /dev/mtdblock4
第一个字母 b 表示这是一个块设备(block device),因为是同一个 nand flash,所 以主设备号是相同的,每个次设备号表示一个 nand flash 的分区。 需要注意的是,网络设备是没有设备文件和设备号的。内核的/document/devices.txt 文件中定义了常见的字符设备和块设备的主设备号与次设备号。
1.分配设备号
如果要 Linux 内核分配空闲的设备号,可以使用给 alloc_chrdev_region 函数,该函数分配的主设备号在 1~254 范围内,而且从 254 起递减,找到一个空闲的主设备号值。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数说明: dev_t *dev —>指向分配设备号开始值的指针
unsigned baseminor —>次设备号的开始值
unsigned count —>次设备号的个数
const char *name —>设备名称
返回值:成功,返回 0;失败,返回负数的错误码。
注册设备号的过程要放在设备驱动程序的安装函数中,在安装驱动过程中为 cdev 申请 设备号。
2.注销设备号
设备号作为一种系统资源,在安装驱动的时候注册了设备号,在卸载驱动时,就一定需 要把设备号注销,将占用的设备号还给系统,以便系统可以将设备号分配个其他设备驱动程 序使用。注销设备号采用如下函数: void unregister_chrdev_region(dev_t from, unsigned count)
参数说明: dev_t from —>注册设备号的开始值,设备号中是包含了主设备号的。
unsigned count —>连续设备编号的个数,即次设备号的数量,代表当前设备驱动程 序所管理的同类设备的数量。

文件操作集

Linux 应用程序通过设备文件访问驱动程序,例如显卡的设备文件是/dev/fb0,应用程序可以通过 Linux 系统 IO 函数,如 open(),read(),write(),ioctl(),mmap(),和close(),等函数访问设备驱动程序,在设备驱动程序中采用的就是文件操作集(file_operations)。文件操作集是设计设备驱动程序时,十分重要的一个结构体,每个字符设备 cdev 都有会自己的文件操作集 file_operations,在字符设备中, 如果没有文件操作集,应用程序就无法文成和设备驱动程序的数据交换。struct file_operations 结构定义了设备驱动程序中的操作函数,是在设备驱动程序中给应用程序提供的 API。
file_operations 结构体
文件操作集在内核源码/linux/fs.h 中定义,它主要包含了一组函数指针。文件操作集 中的函数最终会被应用程序的系统 IO 函数系统调用。file_operations 结构体比较庞大, 下面截取常见的函数指针成员,并做详细说明。

struct file_operations { 
struct module *owner; 
int (*open) (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 *);
 loff_t (*llseek) (struct file *, loff_t, int);
 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
 int (*release) (struct inode *, struct file *); 
........................................ 
int (*mmap) (struct file *, struct vm_area_struct *);
 ........................................  
}
struct module *owner;

这不是一个操作,而是一个指向拥有该 file_operations 的模块的指针。使用这个字段 以避免在模块正在使用的时候而卸载这个模块。owner 成员一般会被初始化为内核定义好的 宏 THIS_MODULE。
int (*open) (struct inode *inode, struct file *filp);
这个 open 函数是给应用程序的 open()做系统调用的一个接口,inode 和 file 两个结构 体在后续章节介绍。如果函数成功,系统会返回一个文件描述符,否则返回-1,同时内核会 设置一个错误码。 open()常常是对设备文件进行的第一个操作,在驱动程序中也可以没有 open()接口,这个项是 NULL,设备文件也可以打开成功,应用程序的 open()可以正常得到 一个文件描述符。应用程序的 open()原型如下:
int open(const char *pathname, int flags);
由此可见,文件操作集中的 open 函数和应用程序的 open 函数的参数是完全不同的,应 用程序的 open()调用驱动程序的 open()是一个系统调用过程,不是函数调用过程,系统调 用过程比较复杂,这里就不详细讲述,可以参照下图分析 open()的系统调用过程。
ssize_t read(struct file *filp, char __user *buff, size_t len, loff_t *offp); ssize_t write(struct file *filp, const char __user *buff, size_t len, loff_t *offp);
read()函数用来从设备中读取数据,成功时返回读取的字节数,出错时返回一个负数的 错误码,是给应用程序 read()提供的系统调用接口。write()函数向应用程序写入数据,成 功时返回写入的字节数,出错时返回一个负数的错误码,是给应用程序 write()提供的系统 调用接口。这两个函数中,filp 是内核中指向驱动文件的指针;len 是请求的传输数据的 字节数;buff 参数指向读取或者写入数据的缓存;offp 表示当前文件的位置指针,它指出 用户正在存取的文件位置,如果要实现想普通文件一样的文件指针移动功能,就要在驱动程 序中使用这个参数,一般情况下可以不用。
如果在 file_operations 中 read()后者 write()是空的(即 NULL),则应用程序系统 调用的时候会得到一个负数的错误码-EINVAL (“Invalid argument”) ,表示参数错误。 __user 符号修饰的 buff 表明指针是一个用户空间的地址,因此在驱动程序中使用这个 地 址 需 要 使 用 专 门 的 拷 贝 函 数 是 实 现 数 据 在 用 户 空 间 和 内 核 空 间 的 交 换 。 在 file_operations 的 read()中通常使用 copy_to_user()将内核空间(驱动程序)的数据拷贝给用户空间(应用程序);在 write()中使用 copy_from_user()将数据从用户空间拷贝到 内核空间。
loff_t (*llseek) (struct file *filp, loff_t offset, int whence);
llseek()函数用来改变文件中的当前读或写的位置, 并且新位置作为返回值。loff_t 参数是一个“long offset”,并且就算在 32 位平台上也至少 64 位宽。 loffset 是指文 件光标移动的数量值,可以是正数,也可以是负数。Whence 是指移动光标的指针位置,有 三种选择,分别是文件的开始(SEEK_SET)、当前位置(SEEK_CUR)和文件结尾(SEEK_END)。出 错时,这个函数会返回一个负数的错误码。
long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg);
unlocked_ioctl 函数是应用程序 ioctl 函数的系统调用接口,ioctl 主要用法是应用程 序向驱动程序发送命令,通过命令对驱动程序进行控制。应用程序在发送命令的同时,也可 以向驱动程序发送数据或者从驱动程序接收数据。早期的 file_operations 中还有 ioctl 函数的接口,如:int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);在 Linux-3.0.8 以后的版本中,取消了 ioctl 函数,只能使用 unlocked_ioctl()函数。
int (*release) (struct inode *inode, struct file *filp);
release()函数是给应用程序的 close()函数做系统调用的接口,当 file 文件结构被释 放时将引用这个操作。与 open()函数类似,,release()函数也可以为 NULL。
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
mmap 用来请求将设备内存映射到进程的地址空间。如果设备驱动程序中没有 mmap 函数, 即为 NULL,则应用程序系统调用返回 –ENODEV。mmap()函数将内核空间中驱动程序的缓存 映射到用户空间,则应用程序直接可以通过映射后的缓存地址来访问内核中的缓存,无需执 行拷贝函数,大大提高了用户空间和内核空间做数据交换的效率,通常在显卡、声卡和摄像 头驱动中使用该函数。

设备文件

当 cdev_add()函数将 cdev 加入 Linux 内核中后,在 Linux 内核中就有了一个字符设备 驱动模型,但是
没有设备文件,应用程序无法访问该设备驱动程序,所以需要创建设备文件, 创建设备文件有两种方法,一种是使用 mknod 命令手动创建,另一种方法是自动创建。
手动创建设备文件
手动创建设备文件使用 mknod 命令,当一个编译号的设备驱动程序映像安装好后,可以 通过查看/proc/devices 文件得到该字符设备驱动模型的主设备号和设备名称,主设备号和 设备名称是通过 register_chrdev_region()和 alloc_chrdev_region()函数得到的。下面是 对 ko 文件的使用步骤:
1.安装驱动

# insmod led_drv.ko
 [ 1286.344285] led driver on gec210

2.查看驱动模块

# lsmod 
led_drv 1420 0 - Live 0xbf038000 

3.查看主设备号和设备名称

# cat /proc/devices 
Character devices: 
....................... 
10 misc 
13 input 
14 sound 
21 sg 
29 fb
81 video4linux
86 ch 
89 i2c 
100 led_device
........................ 

4.手动创建设备文件

# mknod /dev/led_drv c 100 0 

说明:
5.查看设备文件

# ls /dev/led_drv -l 
crw-r--r--   1 root  root   100,   0 Jan 1 14:44 /dev/led_drv

手动创建设备文件可以使用 mknod 命令,mknod 命令的格式为:
mknod Name { b | c } Major Minor
其中 Name 是设备文件的名字,采用的是设备文件的绝对路径,如“/dev/led_drv”;b 和 c 可以选择一个,b 是指为块设备创建一个设备文件,c 是指为字符设备创建一个设备文 件;Major 是主设备号;Minor 是次设备号。当然使用 mknod 也可以创建一个管道(pipe), 如:$ mknod pipe1 p。

自动创建设备文件
使用 mknod 命令手动创建设备文件比较繁琐,在产品级设计中很不方便。Linux 内核提 供了一组函数,可以用来在驱动安装后,在/dev 目录下自动创建对应的设备文件;并且在 卸载驱动时,自动删除设备文件。自动创建设备文件的方法是先创建一个 class,再创建属 于这个 class 的 device,最后由 mdev 工具生成设备文件。
1.class_create()和 class_destroy()
Linux 内核中定义了 struct class 结构体,一个 struct class 结构体类型变量对应一个类,可以使用class_create()来创建一个类,创建好的 class 由 sysfs 管理。class 创建 好后,可以调用 device_create()函数来创建 calss 下面的 device。这样 mdev 会根据创建 好的 class 和 device 创建设备文件。sysfs 文件系统会挂载到/sys 目录下,可以通过/sy 路径查看创建好的 class 和 device。class_create()的原型如下:

#include <linux/device.h>
 struct class *class_create(struct module *owner, const char *name) 

参数说明:
struct module *owner—class 的所有者,一般写成 THIS_MODULE
const char *name —>自定义 class 的名字。
返回值:
struct class * —>指向创建好的 class 的指针。
需 要 注 意 的 是 , 如 果 在 /sys/class/ 目 录 下 已 经 有 了 一 个 同 名 的 class , 则 class_create()函数会调用失败,所以需要判断该函数的返回值。

void class_destroy(struct class *cls);

参数说明:
struct class *cls — 需要销毁的 class。
在驱动的卸载函数中,需要调用 class_destroy()函数销毁这个 class,但是需要注意 的是销毁 class 之前,需要先销毁 device,因为 device 是属于 class 的,如果先销毁 class, 驱动卸载过程会出错。
2. device_create()和 device_destroy()
创建 class 后,要使用 device_create()函数创建属于这个 class 的 device。驱动程序 卸载的时候,需要使用 device_destory()函数销毁这个 device。

#include <linux/device.h>
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata,const char *fmt) 

参数说明:
struct class *cls — 创建的 device 属于哪个 class
struct device *parent — device 的 parent,一般为 NULL
dev_t devt — 设备号
void *drvdata — device 的私有数据,一般设为 NULL
const char *fmt — device 的名字,即设备文件的名字,此处不需要路径名。
返回值:
struct device * —创建好的 device
void device_destroy(struct class *cls, dev_t devt);
参数说明:
struct class *cls — device 属于哪个 class
dev_t devt — 设备号

过程

蜂鸣器驱动:
//头文件来自于Linux内核源码,不能使用C库函数<stdio.h>
1.设计一个module

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>

2.创建一个cdev

static struct cdev gec6818_feng_dev;

static unsigned int feng_major = 0; //选择是动态分配还是静态注册
static unsigned int feng_minor = 0;
static dev_t feng_dev_num;
static struct class *feng_class = NULL;
static struct device *feng_device = NULL;

/* static struct resource *feng_res = NULL;
static struct resource *beep_res = NULL; */

static __iomem volatile unsigned int *GPIOC_BASE = NULL;
static __iomem volatile unsigned int *GPIOCOUT = NULL;
static __iomem volatile unsigned int *GPIOCOUTENB = NULL;
static __iomem volatile unsigned int *GPIOCALTFN0 = NULL;
static __iomem volatile unsigned int *GPIOCALTFN1 = NULL;

static __iomem volatile unsigned int *GPIOE_BASE = NULL;
static __iomem volatile unsigned int *GPIOEOUT = NULL;
static __iomem volatile unsigned int *GPIOEOUTENB = NULL;
static __iomem volatile unsigned int *GPIOEALTFN0 = NULL;
static __iomem volatile unsigned int *GPIOEALTFN1 = NULL;

3.定义并初始化文件操作集

static int gec6818_feng_open(struct inode *inode, struct file *filp)
{
	printk(KERN_ERR "feng driver is openning...\n");
	return 0;		
}

//buf-->存放应用程序write()写下来的数据:buf[1]--灯号(7/8/9/10),buf[0]--灯的状态(1/0)
//size_t len--->应用程序write()数据的字节数。loff_t *offp --->文件偏移的指针
static ssize_t gec6818_feng_write(struct file *filp, const char __user *buf, size_t len, loff_t *offp)
{
	int ret;
	char feng_buf[2];
	if(len != 2)
		return -EINVAL;
	ret = copy_from_user(feng_buf, buf, len);
	if(ret != 0)
		return -EFAULT;
	//蜂鸣器
	if(feng_buf[0] == 1)//on
		*GPIOCOUT |=(1<<14);
	else if(feng_buf[0] == 0)//off
		*GPIOCOUT &= ~ (1<<14);
	else 
		return -EINVAL;
	
	return len;
}
static int gec6818_feng_release(struct inode *inode, struct file *filp)
{
	printk(KERN_ERR "feng driver is closing...\n");
	return 0;		
}

static const struct file_operations gec6818_feng_fops = {
	.owner = THIS_MODULE,
	.open = gec6818_feng_open,
	.write = gec6818_feng_write,
	.release = gec6818_feng_release,
};

static int __init gec6818_feng_init(void) //驱动的安装函数
{
	//从kernel申请资源,并注册驱动
	int ret;
	//3.给cdev申请设备号
	if(feng_major == 0){
		ret = alloc_chrdev_region(&feng_dev_num, feng_minor, 1, 
		              "feng_device");		
	}else{
		feng_dev_num = MKDEV(feng_major,feng_minor);
		ret = register_chrdev_region(feng_dev_num, 1, "feng_device");
	}
	if(ret < 0){
		printk(KERN_ERR "can not get a device number\n");
		return ret;//返回错误
	}

4.cdev的初始化

gec6818_feng_dev.owner = THIS_MODULE;
	cdev_init(&gec6818_feng_dev, &gec6818_feng_fops);

5.将cdev加入内核

ret = cdev_add(&gec6818_feng_dev, feng_dev_num, 1);
	if(ret < 0){
		printk(KERN_ERR "cdev add error\n");
		goto cdev_add_err;
	}
  1. 创建class—//注意不能和/sys/class下的类同名
feng_class = class_create(THIS_MODULE,"gec6818_fengs");
	if (IS_ERR(feng_class)){
		printk(KERN_ERR "class create error\n");
		ret = PTR_ERR(feng_class);
		goto class_create_err;
	}
  1. 创建device—>设备文件
feng_device = device_create(feng_class, NULL,
						feng_dev_num, NULL, "feng_drv");
	if (IS_ERR(feng_device)){
		printk(KERN_ERR "device create error\n");
		ret = PTR_ERR(feng_device);
		goto device_create_err;
	}

8.申请特殊功能寄存器的物理地址内存区作为一个资源(不是必需的)
feng_res = request_mem_region(0xC001C000, 0x1000, “GPIOC_MEM”);
if(feng_res == NULL){
printk(KERN_ERR “request gpioc mem error\n”);
ret = -EBUSY;
goto request_gpiocmem;
}

beep_res = request_mem_region(0xC001E000, 0x1000, “GPIOE_MEM”);
if(beep_res == NULL){
printk(KERN_ERR “request gpioe mem error\n”);
ret = -EBUSY;
goto request_gpioemem;
}

9.IO内存动态映射,得到物理地址对应的虚拟地址

GPIOC_BASE = ioremap(0xC001C000, 0x1000);
	if(GPIOC_BASE == NULL){
		printk(KERN_ERR "ioremap gpioc error\n");
		ret = -EFAULT;
		goto ioremap_gpioc_err;		
	}
	GPIOCOUT = GPIOC_BASE + 0x00;
	GPIOCOUTENB = GPIOC_BASE + 0x01; //0x04
	GPIOCALTFN0 = GPIOC_BASE + 0x08; //0x20
	GPIOCALTFN1 = GPIOC_BASE + 0x09; //0x24

	GPIOE_BASE = ioremap(0xC001E000, 0x1000);
	if(GPIOE_BASE == NULL){
		printk(KERN_ERR "ioremap gpioe error\n");
		ret = -EFAULT;
		goto ioremap_gpioe_err;		
	}
	GPIOEOUT = GPIOE_BASE + 0x00;
	GPIOEOUTENB = GPIOE_BASE + 0x01; //0x04
	GPIOEALTFN0 = GPIOE_BASE + 0x08; //0x20
	GPIOEALTFN1 = GPIOE_BASE + 0x09; //0x24
	
	//将GPIOC14配置成输出,输出低电平,蜂鸣器不响
	*GPIOCALTFN0 &= ~(1<<29);
	*GPIOCALTFN0 |= (1<<28);
	*GPIOCOUTENB |= (1<<14);
	*GPIOCOUT &= ~(1<<14);
	
	printk(KERN_ERR "gec6818 feng driver init.....\n");
	return 0;
	
ioremap_gpioe_err:
	iounmap(GPIOC_BASE);	
ioremap_gpioc_err:
	release_mem_region(0xC001E000, 0x1000);	
/* request_gpioemem:	
	release_mem_region(0xC001C000, 0x1000);
request_gpiocmem:	
	device_destroy(feng_class,feng_dev_num); */
device_create_err:
	class_destroy(feng_class);	
class_create_err:
	cdev_del(&gec6818_feng_dev);
cdev_add_err:
	unregister_chrdev_region(feng_dev_num, 1);
	return ret;	
}
static void __exit gec6818_feng_exit(void)//驱动的卸载函数
{
	//释放kernel资源,并注销驱动
	iounmap(GPIOE_BASE);
	iounmap(GPIOC_BASE);
    release_mem_region(0xC001E000, 0x1000);
	release_mem_region(0xC001C000, 0x1000);
	device_destroy(feng_class,feng_dev_num);
	class_destroy(feng_class);
	cdev_del(&gec6818_feng_dev);
	unregister_chrdev_region(feng_dev_num, 1);
	
	
	printk(KERN_ERR "gec6818 feng driver exit.....\n");
}
//驱动程序的入口和出口
//入口 #insmod feng_drv.ko-->module_init()-->gec6818_feng_init()
module_init(gec6818_feng_init);//main()

//出口 #rmmod feng_drv.ko-->module_exit()-->gec6818_feng_exit()
module_exit(gec6818_feng_exit);

//驱动的描述。 #modinfo feng_drv.ko
MODULE_AUTHOR("bobeyfeng@163.com");
MODULE_DESCRIPTION("S5P6818 feng Device Driver");
MODULE_LICENSE("GPL");
MODULE_VERSION("V1.0");

linux移植、驱动编写最详细教程,Linux 操作系统的安装以及配置............................................................................................3 1 如何安装RedHat9.0 ................................................................................................3 2 在RedHat 中添加新用户.......................................................................................16 3 配置PC 机Linux 的ftp 服务...................................................................................16 4 配置PC 机Linux 的telnet .....................................................................................17 5 建立交叉编译环境...................................................................................................17 6 编译内核..................................................................................................................17 Linux 的移植.......................................................................................................................19 1 Bootloader 的移植.................................................................................................19 1.1 vivi 的配置与编译..........................................................................................19 1.2 配置和编译vivi .............................................................................................20 1.3 vivi 代码分析..................................................................................................21 1.4 vivi 的运行.....................................................................................................21 1.5 启动代码执行流程图.....................................................................................45 1.6 vivi 的配置文件..............................................................................................45 2 Linux 内核
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值