字符设备驱动

Hntea bio photo By Hntea

概念

字符设备文件:

  • 应用程序通过字符设备文件而调用字符设备驱动的文件操作
  • 映射关系是一个文件描述符

字符设备文件创建方法可以使用以下命令形式:

cat /proc/devices 		// 查看主设备号
mknod /dev/xxx c 100 3 	//(指明是字符设备) 主设备号 次设备号

或者直接用使用函数注册,自动生成。

设备号

  • 数据类型:dev_t
    • 高十二位为主设备号
    • 低二十位为次设备号
  • 主设备号: 标识设备对应的驱动程序

每个在内核中活动的字符设备驱动程序,包括编译进内核的和后来动态加载的,都有唯一的主设备号。内核利用主设备号将设备与相应的驱动程序对应起来

  • 次设备号:  只由驱动程序使用,内核的其他部分不使用它,仅将它传递给驱动程序

设备号操作

  • 设备号合成分解
dev_t dev = MKDEV(主设备号,次设备号);/*主次设备号自己给,已知*/

主设备号= MAJOR(dev_t dev)   /*设备号分解*/

次设备号= MINOR(dev_t dev)
  • 设备号处理(驱动加载时都需要申请相关设备号)
register_chrdev_region() // 静态申请

alloc_chrdev_region()//动态申请

unregister_chrdev_region()//设备号注销

设备的抽象封装

  • 字符设备结构体:
struct cdev
{

	struct kobject kobj; /*内核使用,驱动可以不用处理*/
	
	struct module *owner; /*模块拥有者*/
	
	const struct file_operations *ops; /*模块的文件操作*/
	
	struct list_head list; /*内核链表,驱动可以不处理*/
	
	dev_t dev; /*设备号*/
	
	unsigned int count; /*设备数量*/

};

字符设备操作函数

  • 结构分配:
1. 静态分配:struct cdev xxx_dev;

2. 动态分配:struct cdev* pdev = cdev_alloc();
  • 结构初始化:
void cdev_init(struct cdev *, const struct file_operations *);
  • 字符设备注册:
int cdev_add(struct cdev *, dev_t dev_num, unsigned count);
  • 字符设备删除
void cdev_del(struct cdev *p)

字符设备文件操作封装

  • 上层应用程序使用文件操作函数都是通过该结构体映射到驱动中的文件操作中
struct file_operations {

struct module *owner;

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

int (*open) (struct inode *, struct file *);

int (*release) (struct inode *, struct file *);

.....

. ....

};

文件操作相关结构体解析

struct inode  
  1. 每个存放在文件系统里的文件都会关联一个 inode结构体,该结构主要用来记录文件物理上的信息

  2. 一个文件没有被打开时不会关联 file结构,但会关联一个inode结构

 struct file 
  1. 设备驱动所使用的一个最重要的数据结构

  2. file 代表一个打开的文件,内核在open()时创建且在close()前作为参数传递给操作在设备上的函数

  3. 驱动程序可能会用到的结构成员(其他大部分成员由内核操作,驱动不关心):

fmode_t f_mode; 	//驱动程序的ioctl()可能需要查看文件的权限
loff_t  f_pos; 		//当前读写位置
unsigned int f_flags;//文件标志,

//为了支持非阻塞型操作,驱动程序许需要检查该标志
const struct file_operations *f_op; //与文件对应得操作。

i. 内核在完成open()时对该指针赋值,以后需要分派操作时就读这些数据。

ii. filp->f_op 中的值从不保存供给以后应用;即需要修改文件所对应的操作,下次再操作那个文件的相应操作时就会调用新方法

 void  *private_data;

i. 系统调用驱动程序的open()方法前将这个指针设置为NULL

ii. 驱动程序可以用该字段指向已分配的数据,但要在内核释放file结构前的release()方法中清除它

参数传递是内核传递的,具体是怎么传递,这是操作系统的事。有兴趣看内核的实现

文件操作函数解析

打开设备

int open(struct inode *inode,struct file *filp);// 打开设备文件
  • 一般用于对设备进行初始化

  • 最重要的调用时MOD_INC_USE_COUNT宏,该宏主要用来增加驱动程序使用计数器,以避免不正确的卸载驱动程序

  • 参数说明:

    1. inode:打开文件所对应的i节点,对驱动程序来说,这个参数的主要作用就是获取从设备号

    2. file:一个打开的文件,内核分配

  • 代码模板

static int open(struct inode *inode,struct file *filp)
{
	
	从设备号 = MIMOR(inode->i_rdev);
	
	通过设备号判断需要操作的设备;
	
	if(需要操作的设备使用计数器== 0)
	
	{
	
	初始化设备(包括资源申请);
	
	给filp->private_data赋值,以标识物理设备
	
	}
	
	需要操作的设备的使用计数器++;
	
	return 0;

}

释放资源

int release(struct inode *inode,struct file *filp);//d) 释放设备 
  • 当应用程序不再使用时释放

  • 最重要的宏:MOD_DEC_USE_COUNT,主要用来减少驱动程序使用计数器

  • 一般还用于在驱动程序支持的设备使用完毕时,对驱动程序支持设备进行关闭操作

  • 代码模板

static int release(struct inode* inode,struct file *filep)

{
	
	从设备号 = MINOR(inode->i_rdev);
	
	通过从设备号判断需要操作的设备;
	
	MOD_DEC_USE_COUNT;
	
	需要操作的设备的使用计数器--;
	
	if(需要操作的设备的使用计数器== 0)
	
	{
	
	关闭设备处理(包括释放revalidate()方法获取的资源);
	
	}
	
	return 0;

}

读设备

ssize_t read(struct file *filep, char *buf, size_t count, loff_t *fpos); //e) 文件读取
  • 参数说明:

    1. filep:代表一个“打开的文件”,它由内核在open()时创建且在close()前当做参数传递给操作在设备上的函数,在文件关闭后,内核释放这个数据结构

    2. buf:从设备读取的数据将保存到它指向的内存空间

    3. count:指明将要读取的数据个数

    4. fpos:文本指针

  • 代码模板:

static ssize_t read(struct file* filp ,char *buf, size_t count, loft_t *f_pos)
{
	
	if(!access_ok(VERIFY_WRITE,(void *)buf,count))
	
	{
	
	return -EFAULT;
	
	}
	
	通过filp->private_data确定实际操作的物理设备
	
	从设备的*f_ops位置读取count个字节数
	
	使用copy_to_user()或者put_user()拷贝数据到buf指向的空间;把数据从内核空间拷贝到用户空间
	
	更改 *f_ops;
	
	return 读到的数据总数;

}

写设备

ssize_t write(struct file *filep, char *buf, size_t count, loff_t *fpos);//f) 写文件 
  • 代码模板
static ssize_t write(struct file* filp ,char *buf, size_t count, loft_t *f_pos)
{
	if(!access_ok(VERIFY_READ,(void *)buf,count)) /*检查程序是否能以指定的方式 访问指定地址指定长度的内存*/
	
	{
	
	return -EFAULT;
	
	}
	
	通过filp->private_data确定实际操作的物理设备
	
	使用copy_form_user()或者get_user()buf指向的空间获取数据;把数据从用户空间拷贝到内核空间
	
	从设备的*f_ops位置写入获取的数据
	
	更行 *f_ops;
	
	return 写入的数据总数;

}

重定位

loff_t llseek(struct file *filp,loff_t off, int whence);//g) 修改文件读写位置 
  • 参数说明

    1. filp:文件指针

    2. off:需要移动的偏移量

    3. whence:移动方式

      a) 0:文件头移动

      b) 1:当前位置移动

      c) 2:从文件尾开始移动

static loff_t llseek(struct file *filp,loff_t off, int whence)

{

loff_t newpos;

通过filp->private_data确定实际操作的物理设备

获取文件长度;

switch(whence)

{
	
	case 0:
	
	newpos = off;
	
	break;
	
	case 1: 
	
	newpos = filp->f_pos + off;
	
	break;
	
	case 2:
	
	newpos = 文件长度+ off;
	
	break;
	
	default:
	
	return -EINVAL;
	
	}
	
	if(newpos < 0)
	
	{
	
	return -EINVAL;
	
	}
	
	filp->f_pos = newpos;
	
	return newpos;

}

驱动到应用的数据流实现

  • 从用户空间读取数据:copy_from_user(to,from,n)
int _copy_from_user(void *to, const void __user *from, unsigned long n)
  • 将数据上报到用户空间:copy_to_user(to,from,n)
int copy_to_user(void __user *to, const void *from, int n)

内核空间与I/O空间的数据交换

  • LINUX使用的内存地址都是虚拟地址

  • 要对实际的物理地址进行操作,需要使用使用IO映射函数之后才能对相应的物理地址进行操作

  • 物理寄存器操作过程:

    i. 申请映射物理地址

      ioremap(unsigned long phys_addr, size_t size)/*成功返回虚拟地址的指针*/
    

    ii. IO读写,对返回的虚拟地址进行操作:

注意以下要点,防止破坏硬件初始配置

  • 先读回当前硬件寄存器的值
a) ior32(reg)

b) ior16(reg)

c) ior8(reg)
  • 在读回来的基础上运算,重新设定后再写入
a) iow32(reg, val)

b) iow16(reg, val)

c) iow8(reg, val)

字符设备驱动程序设计框架

1. 驱动初始化

  • 分配cdev结构体

    i. 既使用静态方法;定义一个结构体就好了 ii. 动态分配,使用 cdev_alloc();

  • 申请设备号
      alloc_chrdev_region()
    
  • 初始化cdev
    cdev_init();
    
    • 这里可以创建类别 class_create()
    • 加入类创建设备文件
  • 创建设备:device_create();

  • 注册cdev
    • cdev_add();
    • 硬件初始化

2.填充设备操作

  • 初始化 file_operations

  • 编写操作函数

3.驱动注销

  • 注销设备号 unregister_chrdev_region();

  • 注销字符设备
    • device_destroy()
    • release_region()
    • cdev_del
  • kfree()如果有申请内存的话

  • 销毁类(如果有创建类)
    • class_destroy();

TQ210LED驱动程序设计

/**********************************************

作者:hntea

时间:2016/3/8

功能:TQ210 led字符设备驱动,程序的结构还可以再优化

**********************************************/

#include <linux/init.h>

#include <linux/module.h>

#include <linux/fcntl.h>

#include <linux/kernel.h>

#include <linux/types.h>

#include <linux/cdev.h>

#include <linux/sched.h>

#include <linux/slab.h>

#include <linux/fs.h>

#include <linux/mm.h>

#include <asm/io.h>

#include <asm/uaccess.h>

#define DEV_NAME "led"

#define MAJOR_NR 10

#define MINIR_NR 1

#define LED_ON 0x00000018

#define LED_OFF 0X00000000

#define CON_ADDR 0xE0200060

#define DAT_ADDR 0xE0200064

 

struct cdev led_dev; /*静态分配设备号*/

dev_t dev_nm; /*设备号*/

unsigned int *led_config; /*设计成全局变量,方便写函数使用*/

unsigned int *led_data;

/*************************************************

函数名: led_hardinit 实现

函数参数:

函数功能:led硬件初始化

*************************************************/

static void led_hardinit(void)

{

	unsigned tmp = 0;
	
	/*找出物理地址对应的虚拟地址*/
	
	led_config = ioremap(CON_ADDR,4); /*该寄存器是32位数值*/
	
	led_data = ioremap(DAT_ADDR,4);
	
	/*读取当前寄存器状态*/
	
	tmp = ioread32(led_config);
	
	tmp &= (~(0x11 << 3));
	
	tmp |= (0x11 << 3);
	
	/*寄存器设置写回*/
	
	iowrite32(tmp,led_config);
	
	iowrite32(LED_ON,led_data); /*初始化成功灯亮提示*/

}

 

 

/*************************************************

函数名: file_operations 实现

函数参数:

函数功能:

*************************************************/

static int  led_open (struct inode *inode, struct file *fp)
{
	int ret = 0;
	
	/*led 打开时只需要初始化相关寄存器就够*/
	
	led_hardinit();
	
	return ret;

}

 

static int led_release (struct inode *inode, struct file *fp)

{

return 0;

}

 

static long led_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)

{

return 0;

}

/*************************************************

函数名: led_write 实现

函数参数:

函数功能:向led设备文件写如数据

*************************************************/

static ssize_t led_write(struct file *fp, const char __user *buf, size_t count, loff_t *f_pos)

{

	unsigned tmp = count ; /*获取写入字节数*/
	
	unsigned char led_sta[2]; /*用来存放两个led的状态*/
	
	int i = 0;
	
	unsigned int ledDataTmp;
	
	memset(led_sta,0,2); /*数据清零*/
	
	if( count > 2)
	
	{
	
		tmp = 2;
	
	}
	
	/*将数据从用户空间复制到内核空间*/
	
	if(copy_from_user(led_sta,buf,tmp))
	
	{
	
		return -EFAULT;
	
	}else 
	
	{
	
		/*控制两个led的状态*/
		
		for(i=0;i<2;i++)
		
		{
		
			/*读取当前led寄存器状态,严谨就要处理,防止数据破话而使系统奔溃*/
			
			ledDataTmp = ioread32(led_data);
			
			if( led_sta[i] == '1' )
			
			{
			
			/*粗略处理,只要有一个是1就全亮*/
			
			iowrite32(LED_ON,led_data);
			
			}else{
			
			iowrite32(LED_OFF,led_data);
		
			}
		
		}

}

return 0;

}

 

 

struct file_operations ledfp=

{

	.owner = THIS_MODULE,
	
	.open = led_open,
	
	.write = led_write,
	
	.unlocked_ioctl = led_ioctl,
	
	.release = led_release,

};

struct cdev cdev={

.owner = THIS_MODULE,

.ops = &ledfp,

};

 

static int ledInit(void)

{

	int ret = 0;
	
	/*1.分配设备号,0是以该数字作为主设备号分配的起始,2是次设备号(用来标识设备的个数)*/
	
	if((ret = alloc_chrdev_region(&dev_nm, 0, 1,DEV_NAME))<0)
	
	{
		printk("device num alloc err!\n");
	}

	/*2.分配设备结构;静态分配一个全局变量,打印出来方便手工创建设备文件*/
	
	printk("led major number is %d\n",MAJOR(dev_nm));
	
	printk("led minor number is %d\n",MINOR(dev_nm));
	
	/*3.初始化设备结构*/
	
	 cdev_init(&cdev, &ledfp);
	
	/*4.注册设备*/
	
	 if((ret = cdev_add(&cdev,dev_nm, 1)) < 0) /*最后一个参数说明设备数*/
	
	 {
	
	  printk("cdev add err!\n");
	
	 }
	
	 /*5.创建设备文件,现在使用 手工创建mknod*/
	
	//device_create();
	
	 printk("led.ko insmod success!\n");
	
	return 0;

}

 

static void ledexit(void)

{

	/*2.注销设备号,注销设备*/
	
	unregister_chrdev(MAJOR(dev_nm),DEV_NAME);
	
	cdev_del(&cdev);
	
	printk("led device rmmod success!\n");

}

 

 

MODULE_AUTHOR("hntea");

MODULE_LICENSE("GPL");

module_init(ledInit);

module_exit(ledexit);