【Linux驱动学习】字符型驱动GPIO

本文旨在基于韦东山的IMX6ULL PRO开发板 总结 学习字符型驱动GPIO的经验。
【一】无硬件操作的情况下,一个简单的字符型驱动应该包括哪几部分?
下面默认设备名为:device
两个基本变量:
major →主设备号
struct class *device_class →设备分类
这两个变量在设备的注册、生成时需要使用。
(1)围绕file_operations 设备文件结构体攥写操作函数
其结构体又包括五个属性:其中open、read、wrie、relase实际上都是函数指针。
owner【THIS_MODULE】→从属
open→文件打开时调用的函数
read→文件读取时调用的函数
write→文件写入时调用的函数
release→文件关闭时调用的函数
(2)声明file_operations设备结构体
实际上就是让 file_operations里的函数指针指向用户攥写的操作函数。

static struct file_operations device_drv = {
    
    
   .owner = THIS_MODULE,
   .open  = device_drv_open,
   .read  = device_drv_read,
   .write = device_drv_write,
   .release = device_drv_close,
}

(3)攥写 __init device_init(void)函数
这个函数是 字符型设备 在被加载时率先进入的函数,但为什么一定是它,先按下不表。
例如:
insmod device.ko
在加载device.ko模块时,会先进入device_init函数进行设备的注册、生成。
第一步注册:
所谓注册,就是向内核申请设备号的过程。

major = register_chrdev(0,"device",&device_drv);
第二步生成类:
device_class = class_create(THIS_MODULE,"device_class");

至此,二个基本的变量就都使用到了。
第三步生成 设备文件:

device_create(device_class,NULL,MKDEV(major,0),NULL,"device");

这一步至关重要,因为是它在 /dev/目录下生成了可操作的设备文件device。
如果你需要生成一系列同一类的设备,就需要更改次设备号以及对应的设备文件名。例如:

for(i = 0;i<num;i++)
device_create(device_class,NULL,MKDEV(major,i),NULL,"device%d",i);

这样就会根据num的个数,生成一系列的设备文件,文件名为:device0、device1····deviceN。值得注意的是,它们的主设备号都是在注册时确定的,而次设备号是根据循环的顺序填入的,在MKDEV(主,次)中确定。
(4)攥写 __exit device_exit(void)函数
这个函数是 字符型设备 在被卸载时进入的函数,但为什么一定是它,先按下不表。
例如:

rmmod device.ko

在卸载device.ko模块时,会先进入device_exit函数进行设备的注销、类清除、设备文件销毁。
第一步:设备文件的销毁

 device_destroy(device_class,MKDEV(major,0));

第二步:类清除

 class_destroy(device_class);

第三步: 设备注销

 unregister_chrdev(major,"device");

(5)完善设备信息
这一步主要是指定模组加载、卸载需要加载的函数指针指向,并且补充模组的通行证信息。

module_init(device_init);//指定加载时使用device_init
module_exit(device_exit);//指定卸载时使用device_exit
MODULE_LICENSE("GPL");

总结:一个基本的字符型驱动主要包括这五步,围绕三个基本元素:major、struct class *device_class、file_operations而展开。
设备模组(.ko)被加载时,先进入指定的init函数进行设备的注册、类、设备文件的生成。然后根据file_operations中的属性,在操作该设备文件时就会调用对应的操作函数。最后在设备模组(.ko)被卸载时,就会进入指定的exit函数进行设备的设备文件销毁、类清除、设备号注销。
major、struct class *device_class这两个变量贯穿了设备模组的加载与卸载。在加载时,major获得内核提供给设备的主设备号,device_class获得设备类。然后根据主设备号、次设备号以及设备类再在/dev/目录下生成对应的可操作设备文件。
file_operations则是贯穿了可操作设备文件的打开、读/写、关闭,它属性中的成员分别指向了不同操作下的操作函数。
【二】如何驱动GPIO
在单片机中,驱动GPIO点亮LED灯主要分三步:
(1)使能GPIOx时钟
(2)声明GPIO设置结构体,并在结构体属性中对GPIO进行设置,最后将该结构体填入GPIO_Init函数进行初始化。
(3)Setbit/Resetbit 该GPIO。
但实际上,无论是使能时钟还是对GPIO进行设置,这一切的一切都是在寄存器上实现的。在IMX6ULL上也是类似,它的GPIO主要分三部分来进行管理:
(1)CCM_CCGRx→时钟寄存器
(2)IOMUXC→输入输出多路控制器(管引脚复用、拉高拉低、环回等设置的)
(3)GPIO→GPIO寄存器(DR\GDIR\PSR\ICRx\IMR\ISR\EDGE_SEL)
第一部分类似于 单片机的时钟控制。
第二部分类似于 单片机对于GPIO结构体设置,只不过这次寄存器不再和GPIO放在一块,而是专门由IOMUXC来管理。
第三部分类似于 对单片机GPIO操作,读/写 高低电平亦或是读取终端等。
GPIO内部模块图
那么如何去操作这些寄存器?利用ioremap对IO地址空间映射到内核的虚拟地址空间上去,便于访问。有映射就有取消映射:iounmap。那么我们应该以怎样的顺序去在驱动程序中安排这三步的进行?
在单片机中,这三步往往是前两步整合在系统初始化阶段,写入高低电平放在用户程序阶段。
驱动程序分为:加载module→init→利用APP对生成的硬件文件进行操作→卸载module→exit
一般而言,最简单的写法就是将内存映射放置于init,在文件open阶段进行CCM、IOMUXC的配置。在读写阶段进行GPIO寄存器的读写。
但这有一个弊端,那就是它不具有相当的可移植性,因为在init阶段就已经涉及到内存映射,这个映射地址与硬件是脱不开关系的。
所以,最为稳妥的办法是将设备再一次抽象成一个结构体,再根据不同的板子/IO情况来攥写对应板子的驱动程序,而字符型驱动更类似于是调用这个驱动程序的接口。内存映射与CCM、IOMUXC的初始化都放置于子驱动程序open函数。
例如:抽象一个这样的设备:

struct Device_operations{
    
    
   int(*init)(int which);//init函数指针
   int(*write)(int which,int value);//写函数指针
   int(*read)(int which);//读函数指针
   int(*exit)(int which);//exit函数指针
};

在主程序中可以这样使用:

struct Device_operations *p_dev_opr;
p_dev_opr->init(iminor);
p_dev_opr->write(iminor,value);
value = p_dev_opr->read(iminor);
p_dev_opr->exit(iminor);

如何解决函数指针的指向问题?可以在主程序的init函数中调用子程序的结构体初始化函数。

p_dev_opr = get_board_device_opr();
static struct Device_operations board_demo_device_opr={
    
    
   .init = board_demo_device_init;
   .exit = board_demo_device_exit;
   .write = board_demo_device_write;
   .read = board_demo_device_read;
};
struct Device_operations *get_board_device_opr(void)
{
    
    
  return &board_demo_device_opr;
}

这样,我们就可以在子程序board_demo_device_init中来进行映射内存、配置等工作。如果换板子且功能相同的情况下,我们只需要更换编译时的子程序即可,主程序可以不动。

猜你喜欢

转载自blog.csdn.net/qq_32006213/article/details/128964268
今日推荐