Linux 新字符设备驱动实验-基于正点原子IMX6ULL开发板

register_chrdev unregister_chrdev 这两个函数是老版本驱动使用的函数,现在新的
字符设备驱动已经不再使用这两个函数,而是使用 Linux 内核推荐的新字符设备驱动API函数。

1 新字符设备驱动原理

 
1.1 分配和释放设备号
 
使用设备号的时候向 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)
注 销 字 符 设 备 之 后 要 释 放 掉 设 备 号 , 不 管 是 通 过 alloc_chrdev_region 函 数 还 是
register_chrdev_region 函数申请的设备号,统一使用如下释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
新字符设备驱动下,设备号分配示例代码如下:
int major;/* 主设备号 */
int minor;/* 次设备号 */
dev_t devid;/* 设备号 */

if (major) {/* 定义了主设备号 */

devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择 0 */
register_chrdev_region(devid, 1, "test");
} 
else {/* 没有定义设备号 */

alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
major = MAJOR(devid);/* 获取分配号的主设备号 */
minor = MINOR(devid);/* 获取分配号的次设备号 */
}
如果要注销设备号的话,使用如下代码即可:
unregister_chrdev_region(devid, 1); /* 注销设备号 */

1.2 新的字符设备注册方法

1、字符设备结构
Linux 中使用 cdev 结构体表示一个字符设备, cdev 结构体在 include/linux/cdev.h 文件中

的定义如下:

struct cdev {

struct kobject kobj;

struct module *owner;

const struct file_operations *ops;

struct list_head list;

dev_t  dev;

unsigned int count;
};
cdev 中有两个重要的成员变量: ops dev ,这两个就是字符设备文件操作函数集合
file_operations 以及设备号 dev_t 。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个
变量就表示一个字符设备,如下所示:
struct cdev test_cdev;
2 、cdev_init 函数
使用 cdev_init 函数对cdev 变量初始化。cdev_init函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。
使用 cdev_init 函数初始化 cdev 变量的示例代码如下:
 
struct cdev testcdev;

/* 设备操作函数 */
static struct file_operations test_fops = {

.owner = THIS_MODULE,

* 其他具体的初始项 */
};

testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
3 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_add 函数,内容如下所示:
struct cdev testcdev; 

/* 设备操作函数 */
static struct file_operations test_fops = { 
 .owner = THIS_MODULE, 
 /* 其他具体的初始项 */
};
 
testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
cdev_add(&testcdev, devid, 1); /* 添加字符设备 */
3 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




2
自动创建设备节点
 
2.1 mdev 机制
在嵌入式 Linux 中我们使用mdev 来实现设备节点文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev 管理,在/etc/init.d/rcS 文件中如下语句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
2.2 创建和删除类
 
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添
加自动创建设备节点相关代码。首先要创建一个 class 类, class 是个结构体,定义在文件
include/linux/device.h 里面。 class_create 是类创建函数, class_create 是个宏定义,内容如下:
 
#define class_create(owner, name) \
 ({ \
 static struct lock_class_key __key; \
 __class_create(owner, name, &__key); \
 })

struct class *__class_create(struct module *owner, const char *name,
struct lock_class_key *key)

根据上述代码,将宏 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 就是要删除的类。
 
2.3  创建设备
 
上一小节创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设
备。使用 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 是要删除的设备号。

2.3 参考示例

在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备,参考示例如下:

struct class *class; /* 类 */
struct device *device; /* 设备 */
 dev_t devid;/* 设备号 */

 /* 驱动入口函数 */
static int __init led_init(void)
{
/* 创建类 */
class = class_create(THIS_MODULE, "xxx");

/* 创建设备 */
device = device_create(class, NULL, devid, NULL, "xxx");
return 0;
 }

/* 驱动出口函数 */
static void __exit led_exit(void)
{
/* 删除设备 */
device_destroy(newchrled.class, newchrled.devid);

/* 删除类 */
class_destroy(newchrled.class);
}

module_init(led_init);
module_exit(led_exit);

3 设置文件私有数据

每个硬件设备都有一些属性,比如主设备号 (dev_t) ,类 (class) 、设备 (device) 、开关状态 (state)
等等,对于一个设备的所有属性信息我们最好将其做成一个结构体。编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中,如下所示
/* 设备结构体 */
struct test_dev{
dev_t devid; /* 设备号
struct cdev cdev; /* cdev */
struct class *class; /* 类*/
struct device *device; /* 设备*/
int major;/* 主设备号 */
int minor;/* 次设备号 */
};

struct test_dev testdev;

 /* open 函数 */
 static int test_open(struct inode *inode, struct file *filp)
{
filp->private_data = &testdev; /* 设置私有数据 */
return 0;
}

open 函数里面设置好私有数据以后,在 write read close 等函数中直接读取 private_data
即可得到设备结构体。

4 硬件原理图分析

Linux 新字符设备驱动实验-基于正点原子IMX6ULL开发板_第1张图片

 

 5 实验程序编写
5.1 LED 灯驱动程序编写

新建名为“ 3_newchrled ”文件夹,然后在 3_newchrled 文件夹里面创建 vscode 工程,工作
区命名为“ newchrled ”。工程创建好以后新建 newchrled.c 文件,在 newchrled.c 里面输入如下内
容:
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define NEWCHRLED_NAME "newchrled"
#define NEWCHRLED_COUNT 1
#define LEDOFF 0 /*关闭*/
#define LEDON 1	 /*打开*/


/*寄存器物理地址*/
#define CCM_CCGR1_BASE (0x020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0x020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0x020E02F4)
#define GPIO1_DR_BASE (0x0209C000)
#define GPIO1_GDIR_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;




/*LED 设备结构体*/
struct newchrled_dev
{
	struct cdev cdev; 		/*字符设备*/
	dev_t devid; 			/*设备号*/
	struct class *class; 	/*类*/
	struct device *device;  /*设备*/
	int major;	 			/*主设备号*/
	int minor;	 			/*此设备号*/
};

struct newchrled_dev newchrled; /*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; /*bit3置1,关闭LED*/
		writel(val, GPIO1_DR);
	}
}

static int newchrled_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled;
	return 0;
}

static int newchrled_release(struct inode *inode, struct file *filp)
{
	struct newchrled_dev *dev = (struct newchrled_dev*)filp->private_data;
	return 0;
}

static ssize_t newchrled_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;
}

static const struct file_operations newchrled_fops={
	.owner 	= THIS_MODULE,
	.write 	= newchrled_write,
	.open 	= newchrled_open,
	.release= newchrled_release,
};

/*入口*/
static int __init newchrled_init(void)
{
	int ret = 0;
	unsigned int val = 0;
	printk("newchrled_init\r\n");

	/* 初始化 LED */
	/* 1、寄存器地址映射 */
	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_GDIR_BASE, 4);

	/* 2、使能 GPIO1 时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26); /*先清除以前的配置bit26,27*/
	val |= 3 << 26;	   /*bit26,27置1*/
	writel(val, IMX6U_CCM_CCGR1);

   /* 3、设置 GPIO1_IO03 的复用功能,将其复用为
    * GPIO1_IO03,最后设置 IO 属性。
    */
	writel(0x5, SW_MUX_GPIO1_IO03);	   /*设置复用*/

	/* 寄存器 SW_PAD_GPIO1_IO03 设置 IO 属性 */
	writel(0x10B0, SW_PAD_GPIO1_IO03); /*设置电气属性*/

	/* 4、设置 GPIO1_IO03 为输出功能 */
	val = readl(GPIO1_GDIR);
	val |= 1 << 3; /*bit3置1,设置为输出*/
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭 LED */
	val = readl(GPIO1_DR);
	val |= 1 << 3; /*bit3置1,关闭LED*/
	writel(val, GPIO1_DR);

    /*注册字符设备*/
    /* 1、创建设备号 */
	newchrled.major=0; /*设置为0,表示由系统申请设备号*/
	if (newchrled.major)/*给定主设备号*/
	{
		newchrled.devid = MKDEV(newchrled.major,0);
		ret = register_chrdev_region(newchrled.devid, NEWCHRLED_COUNT, NEWCHRLED_NAME);
	}
	else				/*没有给定主设备号*/
	{
		ret = alloc_chrdev_region(&newchrled.devid,0, NEWCHRLED_COUNT,NEWCHRLED_NAME);
		newchrled.major =MAJOR(newchrled.devid);
		newchrled.minor =MINOR(newchrled.devid);
	}
	if(ret<0)
	{
		printk("newchrled chrdev_region err!\r\n");
		goto fail_devid;
	}
	printk("newchrled major = %d,minor=%d\r\n",newchrled.major,newchrled.minor);
	

	/* 2、初始化 cdev */
	newchrled.cdev.owner = THIS_MODULE;
    cdev_init(&newchrled.cdev,&newchrled_fops);

	/* 3、添加一个 cdev */
	ret= cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_COUNT);
	if(ret<0)
	{
		goto fail_cdev;
	}

	/* 4、创建类 */
	newchrled.class = class_create(THIS_MODULE,NEWCHRLED_NAME);
	if (IS_ERR(newchrled.class))
	{
		ret = PTR_ERR(newchrled.class);
		goto fail_class;
	}
		

	/*5.创建设备*/
	newchrled.device = device_create(newchrled.class, NULL,
			     newchrled.devid, NULL, NEWCHRLED_NAME);

	if (IS_ERR(newchrled.device))
	{
		ret=PTR_ERR(newchrled.device);
		goto fail_device;
	}

	return 0;

fail_device:
	class_destroy(newchrled.class);
fail_class:
	cdev_del(&newchrled.cdev);
fail_cdev:
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_COUNT);
fail_devid:
	return ret;
}

/*出口函数*/
static void __exit newchrled_exit(void)
{
	unsigned int val = 0;
	printk("newchrled_exit\r\n");

	val = readl(GPIO1_DR);
	val |= 1 << 3; /*bit3置1,打开LED*/
	writel(val, GPIO1_DR);

	/*1.取消地址映射*/
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

	/*1、删除字符设备*/
	cdev_del(&newchrled.cdev);

	/*2、注销设备号*/
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_COUNT);

	/*3、摧毁设备*/
	device_destroy(newchrled.class, newchrled.devid);
	
	/*4、摧毁类*/
	class_destroy(newchrled.class);
}

/*注册和卸载驱动*/
module_init(newchrled_init);
module_exit(newchrled_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("supersmart");
5.2 编写测试 APP
 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

/*
 *argc:应用程序参数个数
 * argv[]:具体的参数内容,字符串形式
 * ./ledAPP  <0:1> 0 关灯,1 开灯
 * ./ledAPP /dev/led 0 关灯
 * ./ledAPP /dev/led 1 开灯
 */

#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("file %s open failed!\r\n", filename);
        return -1;
    }

    databuf[0] = atoi(argv[2]); /*将字符转化为数字*/
    retvalue = write(fd, databuf, sizeof(databuf));
    if (retvalue < 0)
    {
        printf("LED Control Failed ! \r\n");
        close(fd);
        return -1;
    }
    
    close(fd);

    return 0;
}

6 运行测试
6.1 编译驱动程序和测试 APP

1、编译驱动程序


编写
Makefile 文件
KERNELDIR := /home/znn/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PAHT := $(shell pwd)
obj-m := newchrled.o

build :kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PAHT) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PAHT) clean
输入如下命令,生成“ newchrled.ko ”的驱动模块文件
Linux 新字符设备驱动实验-基于正点原子IMX6ULL开发板_第2张图片
 

 2、编译测试 APP
输入如下命令编译测试
ledAPP.c 这个测试程序:

将上一小节编译出来的 newchrled.ko ledAPP  这两个文件拷贝到 rootfs/lib/modules/4.1.15
目录中

重启开发板,加载驱动模块

 
从上图 可以看出,申请到的主设备号为 248 ,次设备号为 0 。驱动加载成功以后会自动在/dev 目录下创建设备节点文件 /dev/newchrdev ,输入如下命令查看 /dev/newchrdev 这个设备节点文件是否存在:



使用ledAPP测试驱动是否正常工作

打开LED灯测试
./ledAPP /dev/newchrled 1 //打开 LED 灯


Linux 新字符设备驱动实验-基于正点原子IMX6ULL开发板_第3张图片

关闭LED灯测试

./ledAPP /dev/newchrled 0 //关闭 LED 灯

 

 Linux 新字符设备驱动实验-基于正点原子IMX6ULL开发板_第4张图片

 总结:通过以上内容,重点是学习了新的字符设备驱动、设置了文件私有数据、添加了自动创建设备节点相关内容。

你可能感兴趣的:(arm,linux,vscode,功能测试)