1、新字符设备驱动原理
一、分配和释放设备号
使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会 带来两个问题: ①、需要我们事先确定好哪些主设备号没有使用。 ②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置 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)
- 如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
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 来提取出主设备号和次设备号,当然了,第 10 和 11 行提取主设备号和次设备号的代码可以不要。
- 如果要注销设备号的话,使用如下代码即可:
unregister_chrdev_region(devid, 1); /* 注销设备号 */
2、新的字符设备注册方法
一、字符设备结构
在 cdev 中有两个重要的成员变量:ops 和 dev,这两个就是字符设备文件操作函数集合 file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个 变量就表示一个字符设备,如下所示:
struct cdev test_cdev;
二、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 系统添加这个字符设备。 cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
//参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参
//数 count 是要添加的设备数量。
四、cdev_del 函数
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,cdev_del 函数原型如下:
void cdev_del(struct cdev *p)
//参数 p 就是要删除的字符设备。如果要删除字符设备,
//cdev_del(&testcdev); /* 删除 cdev */
cdev_del 和 unregister_chrdev_region 这两个函数合起来的功能相当于 unregister_chrdev 函 数。
3、自动创建设备节点
mdev 机制
udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除,udev 可以检 测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用 modprobe 命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用 rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。
创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添 加自动创建设备节点相关代码。首先要创建一个 class 类,class 是个结构体,class_create 是类创建函数,class_create 是个宏定义。
- 将宏 class_create 展开以后
struct class *class_create (struct module *owner, const char *name)
//class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
//返回值是个指向结构体 class 的指针,也就是创建的类。
- 卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
//参数 cls 就是要删除的类。
4、创建设备
- 创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设 备。使用 device_create 函数在类下面创建设备,device_create 函数原型如下:
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父 设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用 的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件。返回值就是创建好的设备。
- 同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原 型如下:
void device_destroy(struct class *class, dev_t devt)
//参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。
参考示例
- 在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备
5、设置文件私有数据
每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state) 等等,在编写驱动的时候你可以将这些属性全部写成变量的形式
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
在 open 函数里面设置好私有数据以后,在 write、read、close 等函数中直接读取 private_data 即可得到设备结构体。
6、LED 灯驱动程序编写
#include <linux/module.h> //所有模块都需要的头文件
#include <linux/init.h> // init&exit 相关宏
#include <linux/kernel.h>
#include <linux/ide.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#define NEWCHARLED "newled" //设备名
#define NEWCHARLED_COUNT 1
/*寄存器物理地址*/
#define CCM_CCGR1_BASE (0x020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0x02030068)
#define SW_PAD_GPIO1_IO03_BASE (0x020E02F4)
#define GPIO1_DR_BASE (0x0209C000)
#define GPIO1_GRIR_BASE (0x0209C004)
/*地址映射后的虚拟地址指针*/
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
#define LEDOFF 0 //关闭
#define LEDON 1 //打开
/*LED设备结构体*/
struct newcharled_dev
{
struct cdev cdevled; //字符设备
struct class *class; /*类*/
struct device *device; //设备
dev_t devid; //设备号
int major; //主设备号
int minor; //次设备号
};
struct newcharled_dev newcharled; //led设备
/*LED灯打开/关闭*/
static void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON)
{
val = readl(GPIO1_DR);
val &= ~(1 << 3); //bit3清零,打开led灯
writel(val,GPIO1_DR);
}
else if (sta == LEDOFF)
{
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val,GPIO1_DR);
}
}
static int newled_open(struct inode *inode, struct file *file)
{
return 0;
}
static int newled_close(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t newled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
int retvalue;
unsigned char databuf[1];
retvalue = copy_from_user(databuf,buf,count);
if(retvalue < 0)
{
printk("kernel write failed!\r\n");
return -EFAULT;
}
/*判断是开灯还是关灯*/
led_switch(databuf[0]);
return 0;
}
const struct file_operations newcharled_fops = {
.owner = THIS_MODULE,
.write = newled_write,
.open = newled_open,
.release = newled_close,
};
/*注册驱动模块入口*/
static int __init newled_init(void)
{
int ret = 0;
unsigned int val = 0;
printk("newchardev init\r\n");
/*初始化led灯,地址映射*/
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE,4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE,4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE,4);
GPIO1_DR = ioremap(GPIO1_DR_BASE,4);
GPIO1_GDIR = ioremap(GPIO1_GRIR_BASE,4);
/*初始化*/
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 <<26); //先清除以前的配置bit26,27
val |= 3 << 26;
writel(val,IMX6U_CCM_CCGR1);
writel(0x5,SW_MUX_GPIO1_IO03); //设置复用
writel(0x10b0,SW_PAD_GPIO1_IO03); //设置电气属性
val = readl(GPIO1_GDIR);
val |= 1 << 3; //bit3置1,设置为输出
writel(val,GPIO1_GDIR);
val = readl(GPIO1_DR);
val |= ~(1 << 3); //bit3置1,打开led灯
writel(val,GPIO1_DR);
newcharled.major = 0; /*设置清零,表示由系统申请设备号*/
/*2.注册字符设备*/
if(newcharled.major) /*给定主设备号*/
{
newcharled.devid = MKDEV(newcharled.major,0);
ret = register_chrdev_region(newcharled.devid,NEWCHARLED_COUNT,NEWCHARLED);
}
else /*没有给定主设备*/
{
ret = alloc_chrdev_region(&newcharled.devid,0,NEWCHARLED_COUNT,NEWCHARLED);
newcharled.major = MAJOR(newcharled.devid);
newcharled.minor = MINOR(newcharled.devid);
if(ret < 0)
{
printk("newcharled chardev_region err\r\n");
return -1;
}
newcharled.major = MAJOR(newcharled.devid);
}
printk("newcharled major = %d,minor = %d\r\n",newcharled.major,newcharled.minor);
/*3.注册字符设备*/
newcharled.cdevled.owner = THIS_MODULE;
cdev_init(&newcharled.cdevled,&newcharled_fops);
ret = cdev_add(&newcharled.cdevled,newcharled.devid,NEWCHARLED_COUNT);
/*4.自动创建设备节点*/
newcharled.class = class_create(THIS_MODULE, NEWCHARLED);
if (IS_ERR(newcharled.class))
return PTR_ERR(newcharled.class);
/*创建设备*/
newcharled.device = device_create(newcharled.class, NULL,newcharled.devid,NULL, NEWCHARLED);
if(IS_ERR(newcharled.device))
return PTR_ERR(newcharled.device);
return 0;
}
/*驱动模块出口*/
static void __exit newled_exit(void)
{
printk("newchardev exit\r\n");
/*取消地址映射*/
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
/*1.注销字符设备*/
cdev_del(&newcharled.cdevled);
/*2.注销设备号*/
unregister_chrdev_region(newcharled.devid, NEWCHARLED_COUNT);
/*3.摧毁设备*/
device_destroy(newcharled.class,newcharled.devid);
/*4.卸载设备节点类*/
class_destroy(newcharled.class);
}
/*注册和卸载驱动模块*/
module_init(newled_init);
module_exit(newled_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("hsj");
编写测试 APP
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define LEDOFF 0
#define LEDON 1
int main(int argc,char *argv[])
{
int fd,retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3)
{
printf("error usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename,O_RDWR);
if(fd < 0)
{
printf("open failed\r\n");
return -1;
}
databuf[0] = atoi(argv[2]); //将字符转换为数字
retvalue = write(fd,databuf,1);
if (retvalue < 0)
{
printf("LED Control failed!\r\n");
close(fd);
return -1;
}
close(fd);
return 0;
}
7、运行测试
depmod //第一次加载驱动的时候需要运行此命令
modprobe newchrled.ko //加载驱动