目前我们基于LED驱动学习了两种编写Linux驱动程序的方法,分别是传统的方法和上下分层的基于面向对象的方法。其中基于上下分层的面向对象的驱动编写方法还可以进一步细化,把下层进行左右分离,针对使用同一芯片的不同开发板,可以抽象出一个针对芯片的GPIO引脚操作的文件,针对不同的开发板编写一个资源文件,前者可以根据后者的信息实现对芯片GPIO的操作,如下图所示。
在上一篇文章中,我只基于上下分层编写了led驱动程序,并没有使用左右分离,但是左右分离的思想也是需要掌握的,具体可以参考韦东山老师的相关教程。
基于面向对象的上下分层、左右分离的思想,可以进一步引出基于总线设备的驱动模型。
传统的驱动编写如下图所示,引脚的寄存器操作和代码紧密绑定,不易扩展,框架不够清晰
基于面向对象的左右分离的思想,引入platform_device和platform_driver,将资源和驱动分离。platform_device负责定义、描述寄存器资源,包括地址、配置信息等,platform_driver负责下层驱动的具体操作,为上层的驱动框架提供寄存器控制的函数,从platform_device获取资源后实现驱动的具体化,从而实现对寄存器的操作。可以理解为platform_driver是类的成员函数,platform_device是类的成员属性,分别实现二者的定义后就可以得到一个对象,这个对象对应的就是我们要操作的设备或者功能。
从完整的逻辑上,platform_device和platform_driver二者结合起来才是一个完整的驱动程序,那么当分别定义了具体的platform_device结构体和platform_driver结构体后,它们是怎么建立关联的呢?
在Linux系统中,通过总线(BUS)模型将设备(Dev)和驱动(Drv)链接起来,流程就是:BUS有两个链表,其中一条存放platform_device结构体变量,另一条存放platform_driver结构体。在向Linux系统注册platform_device或platform_driver的时候,会向对应的链表添加节点,并基于指定的规则进行匹配,比如向Linux注册platform_device结构体时,会向BUS的devices链表添加节点并遍历platform_driver链表匹配满足条件的驱动。同理向Linux注册platform_driver时类似。匹配过程如下图。
platform_device结构体内容如下:
点击查看代码
//结构体中包含很多字段,具体含义可以在以后学习到了再深入了解,开始时主要掌握框架
struct platform_device {
const char *name; //驱动名字
int id;
bool id_auto;
struct device dev;
u32 num_resources;
struct resource *resource; //匹配优先级最高的驱动名字
const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata;
};
platform_driver结构体内容如下
点击查看代码
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
//其中定义了很多涉及platform_device的函数指针,也说明它确实是从platform_device获取资源然后进行操作
匹配规则如下: (1)首先检查platform_device中是否定义了driver_override,如果定义了,那么将driver_override和platform_driver中的driver结构体中的name比较;
(2)如果platform_device中没有定义driver_override,那么比较platform_device中name和platform_driver中id_table里的每一项的name
(3)如果(2)中没有匹配的项,就比较platform_device中name和platform_driver中dirver中的name字段
下面以一个具体的驱动为例讲解总线设备驱动模型的注册以及匹配流程
platform_device_register的调用过程如下:
点击查看代码
int platform_device_register(struct platform_device *pdev)
{
device_initialize(&pdev->dev);
arch_setup_pdev_archdata(pdev);
return platform_device_add(pdev);
}
点击查看代码
int platform_device_add(struct platform_device *pdev)
{
int i, ret;
if (!pdev)
return -EINVAL;
if (!pdev->dev.parent)
pdev->dev.parent = &platform_bus;
pdev->dev.bus = &platform_bus_type;
switch (pdev->id) {
default:
dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id);
break;
case PLATFORM_DEVID_NONE:
dev_set_name(&pdev->dev, "%s", pdev->name);
break;
case PLATFORM_DEVID_AUTO:
/*
* Automatically allocated device ID. We mark it as such so
* that we remember it must be freed, and we append a suffix
* to avoid namespace collision with explicit IDs.
*/
ret = ida_simple_get(&platform_devid_ida, 0, 0, GFP_KERNEL);
if (ret < 0)
goto err_out;
pdev->id = ret;
pdev->id_auto = true;
dev_set_name(&pdev->dev, "%s.%d.auto", pdev->name, pdev->id);
break;
}
for (i = 0; i < pdev->num_resources; i++) {
struct resource *p, *r = &pdev->resource[i];
if (r->name == NULL)
r->name = dev_name(&pdev->dev);
p = r->parent;
if (!p) {
if (resource_type(r) == IORESOURCE_MEM)
p = &iomem_resource;
else if (resource_type(r) == IORESOURCE_IO)
p = &ioport_resource;
}
if (p && insert_resource(p, r)) {
dev_err(&pdev->dev, "failed to claim resource %d\n", i);
ret = -EBUSY;
goto failed;
}
}
pr_debug("Registering platform device '%s'. Parent at %s\n",
dev_name(&pdev->dev), dev_name(pdev->dev.parent));
ret = device_add(&pdev->dev);
if (ret == 0)
return ret;
failed:
if (pdev->id_auto) {
ida_simple_remove(&platform_devid_ida, pdev->id);
pdev->id = PLATFORM_DEVID_AUTO;
}
while (--i >= 0) {
struct resource *r = &pdev->resource[i];
if (r->parent)
release_resource(r);
}
err_out:
return ret;
}
上述代码关键在ret = device_add(&pdev->dev),查看函数定义
其中有两个关键函数是bus_add_device(dev)和bus_probe_device(dev);前者是将设备放入设备链表中,后者是枚举总线中的驱动链表找到匹配的(dev,drv)
进入bus_probe_device,可以找到device_attach(dev);这个函数就是用于为添加的设备匹配驱动。进入device_attach找到bus_for_each_drv(dev->bus, NULL, dev, __device_attach);这个函数就是枚举总线上所有驱动进行匹配,如果匹配成功就可以调用drv的probe
更具体的细节参考视频https://www.bilibili.com/video/BV14f4y1Q7ti?p=20&vd_source=71fc7d920651b891affb9d75b719c8ad
题外话:学到这里的时候还是有点似懂非懂,那么可以先去做实验,亲自动手用一下这个理论、框架,通过实践反馈、加强对理论的理解,如果一味追求先完全弄懂理论,反而可能会带来理论理解上的偏差,因为难以获取反馈。理论和实践本来就是紧密结合的。
下面基于正点原子的imx6ull开发板编写基于总线设备驱动模型的LED驱动程序
总线设备驱动模型就是典型的上下分层、左右分离的面向对象的思想方法。首先实现上层的驱动框架:
(1)实现file_operations结构体。file_operations中open、write等函数会调用下层驱动向上层驱动的操作函数(里面会有对寄存器的操作),从而避免了在框架里直接对寄存器进行操作。
(2)添加上层驱动的入口/出口函数,注册驱动,创建class结构体用于下层驱动创建设备节点。特别注意,在上层驱动中并没有创建设备节点,而是下层的设备文件在注册时调用上层驱动提供的函数接口基于上层的class创建设备文件,可以理解为下层向上层注册设备。因此上层除了完成file_operations结构体、入口/出口函数外,还需要向下层提供设备创建/设备销毁函数。在(1)中会调用下层的函数,这样会存在交叉引用的问题。为了解决这个问题,需要把下层提供的操作函数结构体定义写在头文件中并在上层驱动代码中包含该头文件,这样在编译上层驱动代码时就包含了操作函数结构体的定义,同时下层驱动也包含该头文件负责实现该结构体并向上层注册,因此上层还需要提供一个下层向上层注册led_operations的结构体。
led_opr.h头文件如下,该文件需要包含在上层驱动led_driver.c中
点击查看代码
#ifndef __LED_ORP__H
#define __LED_ORP__H
struct led_operations{
int (*init)(unsigned int which); //选择哪个LED,使用次设备号进行编号
int (*ctl)(unsigned int which, char status); //控制led状态
};
#endif
led_driver.c代码如下
点击查看代码
#include "asm/memory.h"
#include "asm/uaccess.h"
#include "linux/cred.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/kdev_t.h"
#include "linux/printk.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/printk.h>
#include <linux/init.h>
#include <asm/io.h>
#include "led_opr.h"
/*
led_driver.c 上层驱动框架
实现从下层驱动获取led_operation结构体
为下层提供注册/取消注册接口:下层注册设备时调用上层的函数创建设备文件节点,撤销注册时调用上层函数销毁设备文件节点
*/
// (1)确定主设备号,也可以让内核分配
static int major = 0;
static struct class *led_class;
struct led_operations *p_led_opr;
#define MIN(a, b) (a < b ? a : b)
/*
首先为下层驱动提供注册/撤销注册接口
*/
void led_device_register(int minor)
{
/*
根据传入的次设备号创建设备节点,设备命名上使用次设备号作为后缀便于理解
*/
device_create(led_class, NULL, MKDEV(major, minor), NULL, "led_%d", minor);
}
void led_device_unregister(int minor)
{
/*
根据次设备号销毁设备
*/
device_destroy(led_class, MKDEV(major, minor));
}
void register_led_operation(struct led_operations *led_opr)
{
p_led_opr = led_opr;
}
EXPORT_SYMBOL(led_device_register);
EXPORT_SYMBOL(led_device_unregister);
EXPORT_SYMBOL(register_led_operation);
// (3)实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
//为了不用声明,将函数定义放到前面,但是逻辑顺序应该是在后面
static ssize_t led_drv_write (struct file * file, const char __user * buf, size_t size, loff_t * offset)
{
char val;
int ret;
struct inode *node;
unsigned int which;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/*
读取从用户空间传入的数据,给DR寄存器写0/1控制GPIO1_IO03输出高/低电平以控制LED亮灭
*/
ret = copy_from_user(&val, buf, 1);
node = file_inode(file);
which = iminor(node);
p_led_opr->ctl(which, val);
return 1; //返回1,表示写入1个数据
}
static int led_drv_open (struct inode * node, struct file * file)
{
unsigned int which;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
which = iminor(node);
p_led_opr->init(which); //初始化led,包括地址映射、寄存器配置
return 0;
}
static int led_drv_close (struct inode * node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/*
关闭设备,撤销寄存器地址映射
*/
return 0;
}
// (2)定义自己的 file_operations
static struct file_operations led_drv = {
.owner = THIS_MODULE,
.open = led_drv_open,
.write = led_drv_write,
.release = led_drv_close,
};
// (4)把 file_operations 结构体告诉内核:register_chrdev 注册驱动程序,因此先跳到步骤5
// (5)谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
static int __init led_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "led_driver", &led_drv); //注册驱动
led_class = class_create(THIS_MODULE, "led_class"); //这个class对象与驱动绑定了吗?为什么需要用它来创建设备?
err = PTR_ERR(led_class);
if (IS_ERR(led_class))
{
unregister_chrdev(major, "led_driver");
return -1;
}
return 0;
}
// (6)有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
static void __exit led_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
class_destroy(led_class);
unregister_chrdev(major, "led_driver");
}
// (7)其他完善:提供设备信息,自动创建设备节点:class_create,device_create
module_init(led_init); //凭什么说上面的Init函数就是入口函数?这里这个宏的作用就是告诉内核这是入口函数
module_exit(led_exit);
MODULE_LICENSE("GPL");
接下来实现下层驱动,下层驱动分为两部分,一部分是资源文件,另一个是真正的驱动文件
对于资源文件,主要实现platform_device结构体,里面包含寄存器的地址、配置值、引脚号等信息。对于实现led驱动程序,用到的platform_device结构体中的字段如下:
点击查看代码
struct platform_device led_device = {
.name = "alientek_led",
.num_resources = ARRAY_SIZE(resources),
.resource = resources,
.dev = {
.release = led_device_release, //若未定义这个字段会报错
},
};
重点在于实现resources数组,如果有多个led可以定义多个resource结构体放到数组中,这里我只实现一个led,但是是可以扩展的。每一个struct resource中我将start设置为一个自定义结构体struct led_resouce的地址,这样就可以通过start找到相关的寄存器信息
点击查看代码
struct resource resources[] = {
{
.start = (unsigned int)&GPIO1_IO03, //资源结构体地址
.flags = IORESOURCE_IRQ,
.name = "alientek_led_pin",
},
};
点击查看代码
struct led_resource{
int group_pin; //[31:16]存放组,[15:0]存放
//定义变量存放涉及到的寄存器的地址
unsigned int CCM_CCGR1;
unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
unsigned int GPIO1_GDIR;
unsigned int GPIO1_DR;
};
基于正点原子imx6ull开发板创建的结构体如下
点击查看代码
struct led_resource GPIO1_IO03 = {
.group_pin = GROUP_PIN(1, 3),
.CCM_CCGR1 = 0x20C406C,
.IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 0x20E0068,
.GPIO1_GDIR = 0X209C004,
.GPIO1_DR = 0X209C000
};
完整的下层驱动中的资源文件包含led_resoure.h头文件和board_led.c文件,完整代码如下
led_resoure.h
点击查看代码
#ifndef __LED_RESOURCE__H
#define __LED_RESOURCE__H
#include "linux/types.h"
#define GROUP(x) (x >> 16) //获取引脚属于哪一组
#define PIN(x) (x & (0xffff)) //获取引脚编号
#define GROUP_PIN(grp, pin) ((grp << 16) | (pin)) //这里最好给grp << 16和pin加括号,以免宏展开的时候出现问题
struct led_resource{
int group_pin; //[31:16]存放组,[15:0]存放
//定义变量存放涉及到的寄存器的地址
unsigned int CCM_CCGR1;
unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
unsigned int GPIO1_GDIR;
unsigned int GPIO1_DR;
};
#endif
board_led.c
点击查看代码
/*
下层资源文件,为下层驱动提供寄存器、引脚等信息
1、实现platform_device结构体
2、实现platform_device向内核注册的init和exit函数
*/
#include "linux/ioport.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/platform_device.h>
#include "led_resource.h"
#include "linux/printk.h"
//为了结构清晰,可以将下面的宏定义放入一个头文件,同时头文件中定义一个结构体存放GPIO的信息
// #define GROUP(x) (x >> 16) //获取引脚属于哪一组
// #define PIN(x) (x & (0xffff)) //获取引脚编号
// #define GROUP_PIN(grp, pin) ((grp << 16) | (pin)) //这里最好给grp << 16和pin加括号,以免宏展开的时候出现问题
struct led_resource GPIO1_IO03 = {
.group_pin = GROUP_PIN(1, 3),
.CCM_CCGR1 = 0x20C406C,
.IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 0x20E0068,
.GPIO1_GDIR = 0X209C004,
.GPIO1_DR = 0X209C000
};
struct resource resources[] = {
{
.start = (unsigned int)&GPIO1_IO03, //资源结构体地址
.flags = IORESOURCE_IRQ,
.name = "alientek_led_pin",
},
};
static void led_device_release(struct device *dev)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
}
struct platform_device led_device = {
.name = "alientek_led",
.num_resources = ARRAY_SIZE(resources),
.resource = resources,
.dev = {
.release = led_device_release,
},
};
static int __init led_device_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_device_register(&led_device);
return 0;
}
static void __exit led_device_exit(void)
{
platform_device_unregister(&led_device);
}
module_init(led_device_init);
module_exit(led_device_exit);
MODULE_LICENSE("GPL");
接下来实现具体的驱动代码
驱动代码主要实现platform_driver结构体和操作函数结构体led_operations,以及入口/出口函数,同时在入口函数中向上层注册led_operations结构体
platform_driver结构体如下
点击查看代码
struct platform_driver led_driver = {
.probe = imx6ull_chip_gpio_led_demo_probe, //注册设备时如果匹配到该驱动就执行这个函数
.remove = imx6ull_chip_gpio_led_demo_remove, //卸载驱动时执行
.driver = {
.name = "alientek_led",
},
};
在驱动和设备匹配成功时会执行probe函数,因此主要在probe函数中实现从资源文件结构体platform_device获取资源,读取硬件相关信息
完整的下层驱动操作代码如下:
因为涉及使用上层led_driver.c的注册函数,因此需要包含相关函数的声明,写在头文件led_driver.h中,其内容如下:
点击查看代码
#ifndef __LED_DRIVER__H
#define __LED_DRIVER__H
void led_device_register(int minor);
void led_device_unregister(int minor);
void register_led_operation(struct led_operations *led_opr);
#endif
在下层驱动操作函数board_led.c中涉及到寄存器的映射操作,需要保存映射后地址,因此在led_opr.h头文件中添加相一个结构体定义用于存放led_resource结构体中寄存器的映射地址。完整的led_opr.h代码如下:
点击查看代码
#ifndef __LED_ORP__H
#define __LED_ORP__H
struct led_operations{
int (*init)(unsigned int which); //选择哪个LED,使用次设备号进行编号
int (*ctl)(unsigned int which, char status); //控制led状态
};
struct led_operations* get_led_operations(void);
struct registers{ //进行地址映射后寄存器的地址
unsigned int* CCM_CCGR1;
unsigned int* IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
unsigned int* GPIO1_GDIR;
unsigned int* GPIO1_DR;
};
#endif
完整的下层驱动代码imx6ull_chip_gpio.c如下:
点击查看代码
/*
下层驱动程序,从下层的资源文件获取寄存器、引脚等信息,实现寄存器配置、操作
1、实现platform_driver结构体并实现入口、出口函数
2、实现led_operation结构体,并给出上层驱动获取该结构体的接口。注意,这里存在交叉引用的问题,因此这个函数最终变为上层提供接口,下层向上层注册该结构体
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include "linux/ioport.h"
#include "linux/platform_device.h"
#include "asm/io.h"
#include "led_opr.h"
#include "led_resource.h"
#include "led_driver.h" //包含上层驱动的对外的函数接口声明
static struct led_resource* g_ledpins[10]; //led_resource指针数组,用于接收从资源文件获取的led_resource结构体。g_ledpins是指针数组是因为相应的结构体已经在资源文件创建了,这里只是接收引用
static int g_ledcnt = 0;
struct registers led_registers[10]; //接收寄存器组映射后的地址,这里没有定义成指针,是因为我们需要创建出实体
/*
实现led_operation结构体
*/
static int imx6ull_chip_gpio_led_init(unsigned int which) //选择哪个LED,使用次设备号进行编号
{
int val;
if(!led_registers[which].CCM_CCGR1)
{
led_registers[which].CCM_CCGR1 = ioremap(g_ledpins[which]->CCM_CCGR1, 4);
led_registers[which].IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(g_ledpins[which]->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03, 4);
led_registers[which].GPIO1_GDIR = ioremap(g_ledpins[which]->GPIO1_GDIR, 4);
led_registers[which].GPIO1_DR = ioremap(g_ledpins[which]->GPIO1_DR, 4);
}
/*
使能GPIO1_IO03时钟
CCGR1[27:26] = b11
20C_406Ch
*/
val = *led_registers[which].CCM_CCGR1;
val |= (0x3 << 26);
*(led_registers[which].CCM_CCGR1) = val;
/*
配置IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03为GPIO模式
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03[3:0] = b0101
20E_0068h
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03暂时不使用,这是配置引脚的模式,比如上升沿,上下拉电阻等
*/
val = *led_registers[which].IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
val &= ~(0xF); //低四位清零
val |= (0x5); //低四位赋值b0101
*(led_registers[which].IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03) = val;
/*
配置GPIO1_GDIR寄存器使GPIO1_IO03配置为输出
GPIO1_GDIR[3] = 1
209_C004h
*/
*(led_registers[which].GPIO1_DR) |= (0x1 << 3);
return 1;
}
static int imx6ull_chip_gpio_led_ctl(unsigned int which, char status) //控制led状态
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/*
根据status修改GPIO1_IO03的输出
GPIO1_DR
209_C000h
*/
if(status)
{
*led_registers[which].GPIO1_DR &= ~(1 << 3);
printk("led on\n");
}
else {
*led_registers[which].GPIO1_DR |= (1 << 3);
printk("led off\n");
}
return 0;
}
struct led_operations led_opr = {
.init = imx6ull_chip_gpio_led_init,
.ctl = imx6ull_chip_gpio_led_ctl,
};
static int imx6ull_chip_gpio_led_demo_probe(struct platform_device *dev)
{
/* 驱动匹配成功后就会执行,因此寄存器资源的获取应该在这个函数中进行
(1)进行设备创建
(2)
*/
int i = 0;
struct resource *rsc;
while(1)
{
rsc = platform_get_resource(dev, IORESOURCE_IRQ, i++);
if (!rsc) //直到遍历到resources的最后一项
break;
/* 记录引脚 */
g_ledpins[g_ledcnt] = (struct led_resource*)rsc->start; //强制转换类型为resource结构体指针
/* device_create */
led_device_register(g_ledcnt); //向上层驱动注册,创建设备节点,次设备号等于数组索引。为了使用该函数,需要包含函数声明,写在led_driver.h中
g_ledcnt++;
}
return 0;
}
static int imx6ull_chip_gpio_led_demo_remove(struct platform_device *dev)
{
int i;
/* device_destroy */
for (i = 0; i < g_ledcnt; i++)
{
led_device_register(i);
}
g_ledcnt = 0;
return 0;
return 0;
}
struct platform_driver led_driver = {
.probe = imx6ull_chip_gpio_led_demo_probe, //注册设备时如果匹配到该驱动就执行这个函数
.remove = imx6ull_chip_gpio_led_demo_remove, //卸载驱动时执行
.driver = {
.name = "alientek_led",
},
};
static int led_driver_init(void)
{
int err;
err = platform_driver_register(&led_driver);
register_led_operation(&led_opr); //向上层注册led_operation结构体
return 0;
}
static void led_driver_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
写的有点混乱,但是主要抓住一个原则:上下分层,左右分离
标签:__,led,驱动程序,driver,总线,platform,device,LED,include From: https://www.cnblogs.com/starstxg/p/18146213