前言:
字符设备驱动是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/目录下是已经生成了的节点
编写一个字符设备驱动到这就基本完成了,采用了传统的和设备树两种获取硬件信息的方法。
(完)