完整的字符设备驱动

前言:

字符设备驱动是linux驱动最本质的模样,如输入子系统,其实现也是封装了些许字符设备驱动步骤而实现的。

口诀:

字符设备驱动编写代码的口诀就是--“入号注节硬操

入:驱动模块入口

号:申请设备号

注:注册字符设备到Linux系统

节:生成设备节点

硬:硬件相关的初始化,无论是传统的方式也好还是设备树的方式也好,硬件初始化都是获取gpio,中断,并初始化它们。

操:当然就是经典的fileoperation了,fops结构体,与用户层的open,close,read,write,ioctrl等有神秘联系的函数组。

字符设备驱动与平台驱动的区别,就在于它是相当定制化的,且没有匹配机制的,程序员编写的驱动模块,直接注册设备到系统,直接生成设备节点到/dev/,用户直接使用设备节点操作驱动相对的硬件外设。一切都是那么清晰、直接。

代码:

hw_charDev_describe.h

//#include <linux/device.h>
#include <linux/cdev.h>

struct key_event{
	int code;		//表示按键的内涵:home/esc/Q/W/E/R/T/ENTER
	int value;		///按下1,抬起0
};

struct hw_describe{
	char *dev_name;			//外设名字
	unsigned int dev_major;	//外设设备号
	struct class *hw_class;	//外设设备类型
	struct device *dev; 	//外设设备节点
	struct cdev mycdev;		//注册cdev到系统
	struct device_node *mynode;					//设备节点
	unsigned int phyreg_base;					//外设寄存器物理基地址
	volatile unsigned int *virreg_base;			//外设寄存器虚拟基地址

	unsigned int regmap_size;	//ioremap的长度,一个寄存器是4,即4Byte,32位
	int mygpio;					//gpio号
	int irqno;					//中断号

	//外设类型
	struct key_event mykey;
	
};




说明:

        这个头文件呢,是写了一个结构体,可以用来描述/记录自己要写的字符设备驱动的重要信息。

        比如要写一个led或者key的驱动设备号,设备节点的名字,设备类,cdev机制,这些肯定不会少的。此外还有重要的两个关键硬件信息---gpio的寄存器,中断号。这两个信息,按照传统写法,需要记录寄存器的物理地址、ioremap后的虚拟地址、ioremap的长度;按照设备树的写法,需要记录设备树节点以及由该节点得到的gpio号、中断号。

        所以为了以后开发的方便,搞一个这样的结构体是很有必要的。

4th_charDev.c

#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("LCH");

#include "hw_charDev_describe.h"
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/delay.h>
/*
    阅读这样比较长的驱动代码,都是看完全局变量之后,再从最后的模块入口开始往上追踪着阅读
*/

/*
    插一点c语言知识,指针类型的声明出来之后还需要分配空间(malloc)才能使用,因为它只占4B,只是一个入口罢了,malloc之后才相当于把它这个结构体的所有成员都在内存中占了一个位。
    如果是struct hw_describe led_dev而不是指针类型,就可以直接给其成员赋值。
*/
struct hw_describe *led_dev;//声明用于描述硬件外设


/*
    以下是6.操 步骤的代码,都写好格式预留着,以后要用可以用。
*/
/*打开操作*/
static int chardevnode_open(struct inode *inode, struct file *file){
	printk(KERN_EMERG "chardevnode_open is success!\n");
	
	return 0;
}
/*关闭操作*/
static int chardevnode_release(struct inode *inode, struct file *file){
	printk(KERN_EMERG "chardevnode_release is success!\n");
	
	return 0;
}
/*IO操作*/
static long chardevnode_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
//	printk(KERN_EMERG "chardevnode_ioctl is success! cmd is %ld,arg is %ld \n",cmd,arg);
	
	return 0;
}

ssize_t chardevnode_read(struct file *file, char __user *buf, size_t count, loff_t *f_ops){
	//用户要读,ret = copy_to_user(buf用户的,给用户数据的指针,count);ret!=0是不正常的
	
	return 0;
}

ssize_t chardevnode_write(struct file *file, const char __user *buf, size_t count, loff_t *f_ops){
	//用户写进来,ret = copy_from_user(存数据的指针,buf用户写进来的,count);ret!=0是不正常的

	return 0;
}

/*文件操作的函数组*/
struct file_operations fops ={
	.open = chardevnode_open,
	.release = chardevnode_release,
	.unlocked_ioctl = chardevnode_ioctl,
	.read = chardevnode_read,
	.write = chardevnode_write,
};

/*
 这是驱动模块的入口 里面完成了以下任务
 0.给描述硬件设备信息的结构体malloc空间
 1.申请设备号
 2.注册设备到系统
 3.生成设备节点
 4.硬件初始化
*/
static int __init charDev_init(void)
{
	int ret =0 ;

	
	printk(KERN_EMERG "charDev_init\n");

	
	//0.给外设对象分配空间
	led_dev = kmalloc(sizeof(struct hw_describe),GFP_KERNEL);
	if(led_dev ==NULL){
		printk(KERN_EMERG "kmallock error\n");
		return(-ENOMEM);
	}
	led_dev->dev_name = "myled";

	
	//1.申请主设备号 设备号是MKDEV(led_dev->dev_major,自选)
	led_dev->dev_major=register_chrdev(0,led_dev->dev_name,&fops);
	
	if(led_dev->dev_major<0)
	{
		printk(KERN_EMERG "register_chrdev error\n");
		ret = -ENODEV;
		goto err_0;
	}
	else
	{
		printk(KERN_EMERG "dev_major is %d\n",led_dev->dev_major);
	}

	//2.注册设备到系统 查看/proc/devices可知道有没有注册成功
	
    cdev_init(&(led_dev->mycdev),&fops);
    (led_dev->mycdev).owner = THIS_MODULE;
    (led_dev->mycdev).ops = &fops;
    
    ret = cdev_add(
&(led_dev->mycdev),MKDEV(led_dev->dev_major,0),1);
    if(ret){
        		printk(KERN_EMERG "cdev_add is fail! %d\n",ret);
				goto err_1;
    }else{
        		printk(KERN_EMERG "cdev_add is success!\n");
    }

	//3.生成设备节点
	led_dev->hw_class = class_create(THIS_MODULE,"charDev_class");
	if(IS_ERR(led_dev->hw_class))
	{
		printk(KERN_EMERG "class_create error\n");
		ret = PTR_ERR(led_dev->hw_class);
		goto err_2;
	}
	led_dev->dev=device_create(led_dev->hw_class,NULL,MKDEV(led_dev->dev_major,0),NULL,led_dev->dev_name);//设备节点名字
	if(IS_ERR(led_dev->dev))
	{
		printk(KERN_EMERG "device_create error\n");
		ret = PTR_ERR(led_dev->dev);
		goto err_3;
	}
	else
	{
		printk(KERN_EMERG "dev_name is %s\n",led_dev->dev_name);
	}

	//4.获取硬件资源
	/*
		//	4.1直接从原理图得到硬件信息
		//LED3 GM_INT2--GPX3_1--0x11000C60 
		led_dev->regmap_size = 4;
		led_dev->phyreg_base = 0x11000C60 ;	//寄存器基地址

		//+0是CON	+1是DAT		+2是PUD		+3是DRV
		led_dev->virreg_base = ioremap(led_dev->phyreg_base,4*led_dev->regmap_size);//映射4个寄存器
		if(led_dev->virreg_base==NULL){
			printk(KERN_EMERG "ioremap error\n");
			ret = -ENOMEM;
			goto err_4;

		}

		//配置GPX3-1为输出
		*(led_dev->virreg_base) &= (~(0xF<<4));	//清除4-7位
		*(led_dev->virreg_base) |=((0x1)<<4);	//将4-7位置为0x1
		//初始化GPX3-1为高电平,即亮灯
		*(led_dev->virreg_base+1) &= (~(0x1<<1));	//清除第1位
		*(led_dev->virreg_base+1) |=((0x1)<<1);	//将第1位置为0x1,即高电平
	*/
	
	//	4.2设备树方式获得硬件信息
	//	节点:/myled3 compatible = "gpio-myled3"
    /* 找到设备树中名为“myled3”的节点 */
	led_dev->mynode = of_find_node_by_name(NULL,"myled3");
	if(led_dev->mynode==NULL){
		printk(KERN_EMERG "of_find_node_by_name error\n");
		ret = -ENOMEM;
		goto err_4;
	}
    /* 从这个节点里找到放着gpio信息的属性 返回的是gpio号 */
	led_dev->mygpio = of_get_named_gpio_flags(led_dev->mynode,"gpios",0,NULL);
	if (!gpio_is_valid(led_dev->mygpio))
		printk("gpio isn't valid\n");
	else printk("gpio num=%d",led_dev->mygpio);
    /* 向linux内核申请这个gpio号,即此gpio引脚 */
	ret= gpio_request(led_dev->mygpio, "gpios");
	if(ret!=0){
		printk(KERN_EMERG "gpio_request error\n");
		ret = -ENOMEM;
		goto err_5;
	}
    /* 初始化该gpio为输出,并且初始状态为高电平 */
	gpio_direction_output(led_dev->mygpio, 1);
    
	return 0;
/* 以下是一堆出错操作 */
err_5:
	gpio_free(led_dev->mygpio);

err_4:
	device_destroy(led_dev->hw_class,MKDEV(led_dev->dev_major,0));
err_3:
	class_destroy(led_dev->hw_class);
err_2:
	unregister_chrdev(led_dev->dev_major,led_dev->dev_name);
err_1:
	cdev_del(&(led_dev->mycdev));
err_0:
	kfree(led_dev);
	return(ret);


}
/* 驱动模块的出口,一堆释放资源的操作 */
static void __exit  charDev_exit(void)
{
	gpio_free(led_dev->mygpio);
	//iounmap(led_dev->virreg_base);
	device_destroy(led_dev->hw_class,MKDEV(led_dev->dev_major,0));
	class_destroy(led_dev->hw_class);
	cdev_del(&(led_dev->mycdev));
	unregister_chrdev(led_dev->dev_major,led_dev->dev_name);
	kfree(led_dev);

}

module_init(charDev_init);
module_exit(charDev_exit);

说明:

        这里写了“入号注节硬操”。

下面是解释有关硬件方面的代码是怎么编写的:

硬件信息:

开发板底板PCB

开发板核心板PCB

这样就可以知道LED引脚是GPX3_1了

芯片数据手册

从这就得到了寄存器对应的物理地址

有了这些硬件信息,可以开始写代码了~分为传统写法和设备树写法。

设备树写法:

新增一个自己的节点,主要是加上gpio信息。

设备树的语法方面建议看看讯为的,多高深不至于,但是入门很快。我三倍速看了他的设备树教程,半天就看完了,挺有收获的。

/kernel/arch/arm/boot/dts/exynos4412-itop-elite.dts

	myled3 { 
            //myled3就是节点名称
			compatible = "gpio-myled3";//这个没什么用
            //根据硬件手册找到led是GPX3_1,再模仿其他dts中其他gpio的节点来写
			gpios = <&gpx3 1 GPIO_ACTIVE_HIGH>;

	};

对照代码就是:

定位到myled3这个节点:

	led_dev->mynode = of_find_node_by_name(NULL,"myled3");

获取该节点里的gpio信息:

其中这个"gpios"属性叫什么名字是无所谓的,如果属性里有gpio的信息这个函数会自动解析出来,和属性名叫什么没关系。这样就拿到gpio号了

	led_dev->mygpio = of_get_named_gpio_flags(led_dev->mynode,"gpios",0,NULL);

向系统申请gpio:

我怀疑这里是这个函数帮我们做了ioremap

	ret= gpio_request(led_dev->mygpio, "gpios");

这里是配置gpio的输入/输出模式以及初始化电平:

	gpio_direction_output(led_dev->mygpio, 1);

其他gpio的操作有需要去#include <linux/of_gpio.h>里面找。

传统写法:

通过上面的硬件信息追查,我们记录下了这些信息:

//LED3--GM_INT9--GPX1_1--0x11000C20 

里面比较重要的就是这个GPX3CON的地址0x11000C20 

有了这个地址就可以开始像单片机一样写代码了。

		key_dev->regmap_size = 4;           //每个寄存器是4B
		key_dev->phyreg_base = 0x11000C20 ;	//寄存器基地址

		//+0是CON	+1是DAT		+2是PUD		+3是DRV
		key_dev->virreg_base = ioremap(key_dev->phyreg_base,4*key_dev->regmap_size);//映射4个寄存器到虚拟地址
		if(key_dev->virreg_base==NULL){
			printk(KERN_EMERG "ioremap error\n");
			ret = -ENOMEM;
			goto err_4;
		}

		//配置GPX1-1为输入
		*(key_dev->virreg_base) &= (~(0xF<<4));	//清除4-7位
		*(key_dev->virreg_base) |=((0x0)<<4);	//将4-7位置为0x1
		//初始化GPX1-1为高电平,即和原理图一样
		*(key_dev->virreg_base+1) &= (~(0x1<<1));	//清除第1位
		*(key_dev->virreg_base+1) |=((0x1)<<1);	//将第1位置为0x1,即高电平
		//初始化GPX1-1为上拉,即和原理图一样
		*(key_dev->virreg_base+2) &= (~(0x3<<2));	//清除【3:2】位
		*(key_dev->virreg_base+2) |=((0x3)<<2);	//将其置为0x3,即上拉

也很简单。

运行现象:

查看该设备有没有成功注册到linux中:

cat /pro/devices 可以查看系统中已经注册了的设备

查看设备树节点有没有成功展开:

/sys/bus/platform/devices/这个目录下的每个子目录都是一个设备树展开的节点。可见该节点的编写没有问题

查看设备节点有没有成功产生:

/dev/目录下是已经生成了的节点

编写一个字符设备驱动到这就基本完成了,采用了传统的和设备树两种获取硬件信息的方法。

(完)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值