利用Linux的Platform总线设备驱动实现对多个LED的驱动【只是假想对LED进行驱动,并没有实际的硬件操作】【Makefile如何书写能生成多个模块文件】【总线设备驱动的好处】

需要阅读的前面写的博文

阅读本文前,请先阅读下面这篇博文:

概要性了解Linux的总线设备驱动

请先通过上面这篇博文对Linux的总线设备驱动的思想有个基本了解。

本文利用总线设备驱动中的Platform总线设备驱动实现对多个LED的驱动。

既然是用Platform总线设备驱动,那就先对Platform总线设备驱动有个详细了解,然后再开始写代码吧。

Platform总线设备驱动的详细介绍

Platform 总线设备驱动是一种基于 Linux 驱动模型的驱动框架,专门用于管理片上系统 (SoC) 内部的硬件资源。这种框架非常适合开发嵌入式设备中与具体硬件总线无关的驱动程序,例如板载 LED、GPIO、按键等。


1. Platform 总线设备驱动的基本概念

  • Platform 总线 (Platform Bus)
    是一种虚拟总线,与硬件无关,用于匹配设备(platform_device)和驱动(platform_driver)。

  • Platform 设备 (Platform Device)
    表示具体的硬件资源,例如板载的 LED、UART 控制器等。

  • Platform 驱动 (Platform Driver)
    实现对特定硬件资源的操作逻辑。

设备和驱动匹配
  • 匹配依据:设备和驱动通过名称(platform_device.nameplatform_driver.driver.name)进行匹配。
  • 匹配机制:内核在设备注册时,会检查已注册的驱动;反之,在驱动注册时,会检查已注册的设备。如果匹配成功,就调用驱动的 probe 方法初始化设备。

2. Platform 总线设备驱动的主要组成部分

1. Platform 设备 (struct platform_device)

platform_device 是内核中用于描述硬件资源的结构体,定义了设备的基本属性。

关键字段

struct platform_device {
    const char *name;              // 设备名称,用于匹配驱动
    int id;                        // 设备 ID,用于区分多个同类设备
    struct device dev;             // 嵌入式的通用设备结构体
    const struct resource *resource; // 硬件资源(如内存、I/O 等)
    unsigned int num_resources;    // 硬件资源的数量
};

注册接口

  • 动态注册:platform_device_register(),将设备注册到系统。
  • 静态注册:通过设备树或内核代码直接描述设备。

2. Platform 驱动 (struct platform_driver)

platform_driver 是内核中用于描述驱动程序的结构体,定义了驱动的操作方法。

关键字段

struct platform_driver {
    int (*probe)(struct platform_device *);   // 匹配设备时调用,完成设备初始化
    int (*remove)(struct platform_device *);  // 设备卸载时调用
    void (*shutdown)(struct platform_device *); // 系统关机时调用
    struct device_driver driver;              // 通用驱动结构体
};

注册接口

  • platform_driver_register():将驱动注册到系统。
  • platform_driver_unregister():从系统中注销驱动。

3. Platform 总线设备驱动的工作流程

  1. 注册 Platform 设备

    • 定义设备的基本信息,如名称、ID 和硬件资源。
    • 调用 platform_device_register() 注册设备。
    • 或者,通过设备树描述设备并由内核解析。
  2. 注册 Platform 驱动

    • 定义驱动的基本信息,如名称和操作函数(proberemove)。
    • 调用 platform_driver_register() 注册驱动。
  3. 设备和驱动的匹配

    • 内核根据设备名称自动匹配设备和驱动。
    • 匹配成功后,调用驱动的 probe 方法初始化设备。
  4. 操作设备

    • probe 方法中完成设备的初始化和资源申请。
    • 用户通过设备文件或内核接口访问设备。
  5. 卸载设备或驱动

    • 当设备被移除或驱动被卸载时,调用驱动的 remove 方法释放资源。

4. Platform 总线设备驱动的代码示例

1. Platform 设备

定义和注册一个设备:

#include <linux/platform_device.h>

// 硬件资源描述
static struct resource led_resources[] = {
    {
        .start = 0x12340000, // 假设的硬件地址
        .end   = 0x123400FF,
        .flags = IORESOURCE_MEM,
    },
};

// 定义设备
static struct platform_device led_device = {
    .name = "led",      // 设备名称
    .id   = -1,         // 通常为 -1,表示无具体 ID
    .resource = led_resources,
    .num_resources = ARRAY_SIZE(led_resources),
};

// 注册设备
static int __init led_device_init(void) {
    return platform_device_register(&led_device);
}
module_init(led_device_init);

// 注销设备
static void __exit led_device_exit(void) {
    platform_device_unregister(&led_device);
}
module_exit(led_device_exit);

MODULE_LICENSE("GPL");

2. Platform 驱动

定义和注册一个驱动:

#include <linux/platform_device.h>
#include <linux/module.h>

// probe 方法:设备匹配成功时调用
static int led_probe(struct platform_device *pdev) {
    struct resource *res;
    printk("LED device probed!\n");

    // 获取硬件资源
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        printk("No memory resource found!\n");
        return -ENODEV;
    }

    printk("Resource start: %lx, end: %lx\n", res->start, res->end);
    return 0;
}

// remove 方法:设备移除时调用
static int led_remove(struct platform_device *pdev) {
    printk("LED device removed!\n");
    return 0;
}

// 定义驱动
static struct platform_driver led_driver = {
    .driver = {
        .name = "led",  // 匹配设备的名称
    },
    .probe = led_probe,
    .remove = led_remove,
};

// 注册驱动
static int __init led_driver_init(void) {
    return platform_driver_register(&led_driver);
}
module_init(led_driver_init);

// 注销驱动
static void __exit led_driver_exit(void) {
    platform_driver_unregister(&led_driver);
}
module_exit(led_driver_exit);

MODULE_LICENSE("GPL");

5. Platform 总线设备驱动的优点

  1. 灵活性:适合片上资源管理,可通过名称直接匹配。
  2. 简单性:框架简单易用,不需要实现复杂的总线协议。
  3. 适应性:支持静态注册和设备树描述,适应多种开发需求。
  4. 统一性:与其他标准总线驱动一致,便于管理和扩展。

6. Platform 总线设备驱动的典型应用

  1. GPIO 驱动:板载按键或 LED。
  2. PWM 驱动:用于控制风扇或亮度调节。
  3. UART 驱动:串口控制器。
  4. ADC/DAC 驱动:模数/数模转换器。

总结
Platform 总线设备驱动通过虚拟总线实现了硬件资源和驱动程序的解耦,特别适合嵌入式开发中的片上资源管理,是 Linux 驱动模型的重要组成部分。

完整源代码

实现platform_driver的源码(chip_demo_gpio.c)

#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_opr.h"
#include "leddrv.h"
#include "led_resource.h"

static int g_ledpins[100];
static int g_ledcnt = 0;

static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */       
{   
    //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
    
    printk("init gpio: group %d, pin %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
    switch(GROUP(g_ledpins[which]))
    {
        case 0:
        {
            printk("init pin of group 0 ...\n");
            break;
        }
        case 1:
        {
            printk("init pin of group 1 ...\n");
            break;
        }
        case 2:
        {
            printk("init pin of group 2 ...\n");
            break;
        }
        case 3:
        {
            printk("init pin of group 3 ...\n");
            break;
        }
    }
    
    return 0;
}

static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{
    //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
    printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));

    switch(GROUP(g_ledpins[which]))
    {
        case 0:
        {
            printk("set pin of group 0 ...\n");
            break;
        }
        case 1:
        {
            printk("set pin of group 1 ...\n");
            break;
        }
        case 2:
        {
            printk("set pin of group 2 ...\n");
            break;
        }
        case 3:
        {
            printk("set pin of group 3 ...\n");
            break;
        }
    }

    return 0;
}

static struct led_operations board_demo_led_opr = {
    .init = board_demo_led_init,
    .ctl  = board_demo_led_ctl,
};


static int chip_demo_gpio_probe(struct platform_device *pdev)
{
    struct resource *res;
    int i = 0;

    while (1)
    {
        res = platform_get_resource(pdev, IORESOURCE_IRQ, i);
        if (!res)
            break;
        
        g_ledpins[g_ledcnt] = res->start;
        device_file_create(g_ledcnt);
        g_ledcnt++;
        i++;
    }
    return 0;
    
}

static int chip_demo_gpio_remove(struct platform_device *pdev)
{
    struct resource *res;
    int i = 0;

    while (1)
    {
        res = platform_get_resource(pdev, IORESOURCE_IRQ, i);
        if (!res)
            break;
        
        device_file_destroy(i);
        i++;
    }
    return 0;
}


static struct platform_driver chip_demo_gpio_driver = {
    .probe      = chip_demo_gpio_probe,
    .remove     = chip_demo_gpio_remove,
    .driver     = {
        .name   = "gpio_led_suwenhao",
    },
};

static int __init chip_demo_gpio_drv_init(void)
{
    int err;
    
    err = platform_driver_register(&chip_demo_gpio_driver); 
    register_led_operations(&board_demo_led_opr);
    
    return 0;
}

static void __exit lchip_demo_gpio_drv_exit(void)
{
    platform_driver_unregister(&chip_demo_gpio_driver);
}

module_init(chip_demo_gpio_drv_init);
module_exit(lchip_demo_gpio_drv_exit);

MODULE_LICENSE("GPL");

实现platform_device的源码(board_A_led.c)


#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"

//led_dev_release函数即使为空也必须要有,因为platform_device_unregister函数执行时会调用它
static void led_dev_release(struct device *dev)
{
}

static struct resource resources[] = {
        {
                .start = GROUP_PIN(1,3),
                .flags = IORESOURCE_IRQ,
                .name = "GPIO1_IO03_LED1",
        },
        {
                .start = GROUP_PIN(3,5),
                .flags = IORESOURCE_IRQ,
                .name = "GPIO3_IO05_LED2",
        },
};


static struct platform_device board_A_led_dev = {
        .name = "gpio_led_suwenhao",
        .num_resources = ARRAY_SIZE(resources),
        .resource = resources,
        .dev = {
                .release = led_dev_release,
         },
};

static int __init led_dev_init(void)
{
    int err;
    
    err = platform_device_register(&board_A_led_dev);   
    
    return 0;
}

static void __exit led_dev_exit(void)
{
    platform_device_unregister(&board_A_led_dev);
}

module_init(led_dev_init);
module_exit(led_dev_exit);

MODULE_LICENSE("GPL");

头文件led_resource.h

#ifndef _LED_RESOURCE_H
#define _LED_RESOURCE_H

/* GPIO3_0 */
/* bit[31:16] = group */
/* bit[15:0]  = which pin */

#define GROUP_PIN(g,p) ((g<<16) | (p))
#define GROUP(x) (x>>16)
#define PIN(x)   (x&0xFFFF)

#endif

头文件led_opr.h

#ifndef _LED_OPR_H
#define _LED_OPR_H

struct led_operations {
	int (*init) (int which); /* 初始化LED, which-哪个LED */       
	int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
};

#endif

实现驱动程序的源码(leddrv.c)

#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 "led_opr.h"


/* 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 device_file_create(int minor)
{
	device_create(led_class, NULL, MKDEV(major, minor), NULL, "swh_led%d", minor); /* /dev/swh_led0,1,... */
}
void device_file_destroy(int minor)
{
	device_destroy(led_class, MKDEV(major, minor));
}
void register_led_operations(struct led_operations *opr)
{
	p_led_opr = opr;
}

EXPORT_SYMBOL(device_file_create);
EXPORT_SYMBOL(device_file_destroy);
EXPORT_SYMBOL(register_led_operations);



/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* write(fd, &val, 1); */
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	char status;
	struct inode *inode = file_inode(file);
	int minor = iminor(inode);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	//让位于内核空间的变量status接收来自用户空间传来的值,status的值为0或1
	err = copy_from_user(&status, buf, 1); 

	/* 根据次设备号和status控制LED */
	p_led_opr->ctl(minor, status);
	
	return 1;
}

static int led_drv_open (struct inode *node, struct file *file)
{
	int minor = iminor(node);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 根据次设备号初始化LED */
	p_led_opr->init(minor);
	
	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,
	.read    = led_drv_read,
	.write   = led_drv_write,
	.release = led_drv_close,
};

/* 4. 把file_operations结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init led_init(void)
{
	int err;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "swh_led_driver", &led_drv); 

	led_class = class_create(THIS_MODULE, "swh_led_class");
	err = PTR_ERR(led_class);

	if (IS_ERR(led_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "swh_led_driver");
		return -1;
	}
	
	return 0;
}

/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit led_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	class_destroy(led_class);
	unregister_chrdev(major, "swh_led_driver");
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");

应用程序的源码(ledtest.c)


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>


int main(int argc, char **argv)
{
	int fd;
	char status;
	
	/* 1. 判断参数 */
	if (argc != 3) 
	{
		printf("Usage: %s <dev> <on | off>\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open(argv[1], O_RDWR);
	if (fd == -1)
	{
		printf("can not open file %s\n", argv[1]);
		return -1;
	}

	/* 3. 写文件 */
	if (0 == strcmp(argv[2], "on"))
	{
		status = 1;
		write(fd, &status, 1);
	}
	else
	{
		status = 0;
		write(fd, &status, 1);
	}
	
	close(fd);
	
	return 0;
}

Makefile文件

# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88

all:
	make -C $(KERN_DIR) M=`pwd` modules 
	$(CROSS_COMPILE)gcc -o ledtest ledtest.c 

clean:
	make -C $(KERN_DIR) M=`pwd` clean
	rm -rf modules.order
	rm -f ledtest

obj-m += leddrv.o chip_demo_gpio.o board_A_led.o

关键结构体platform_device的详细介绍

关键结构体platform_device的详细介绍见下面这篇博文:
https://blog.youkuaiyun.com/wenhao_ir/article/details/145018442

关键结构体platform_device的详细介绍见下面这篇博文:
https://blog.youkuaiyun.com/wenhao_ir/article/details/145018442

阅读下面的内容前请先看一看下面这篇博文

阅读下面的内容前请先看一看下面这篇博文,有助于你进一步了解Platform总线设备驱动的机制:
https://blog.youkuaiyun.com/wenhao_ir/article/details/145023181

阅读下面的内容前请先看一看下面这篇博文,有助于你进一步了解Platform总线设备驱动的机制:
https://blog.youkuaiyun.com/wenhao_ir/article/details/145023181

利用Platform总线设备驱动书写的多个LED驱动实例的设备部分代码(文件board_A_led.c)

Platform总线设备驱动的设备部分代码主要是描述硬件资源。

3个宏定义方便我们描述硬件资源

文件:led_resource.h
内容:

#ifndef _LED_RESOURCE_H
#define _LED_RESOURCE_H

/* GPIO3_0 */
/* bit[31:16] = group */
/* bit[15:0]  = which pin */

#define GROUP_PIN(g,p) ((g<<16) | (p))
#define GROUP(x) (x>>16)
#define PIN(x)   (x&0xFFFF)

#endif

因为在IMX6ULL和很多CPU中,GPIO都是分组的,每一组里面再去分具体的哪个GPIO口,比如GPIO5_IO03代表第5组GPIO口的第3个GPIO口。

在这里带参数的宏定义GROUP_PIN(g,p)把GPIO口的组号g和具体是组下的哪个GPIO_PIN的编号p合并到一个变量中,这个变量的高16位存储组号,低15位存储具体的GPIO_PIN的编号。

在这里带参数的宏定义GROUP(x)通过对x运算取出组号。

在这里带参数的宏定义PIN(x)通过对x运算取出GPIO_PIN的编号。

结构体platform_device实例化的代码

//led_dev_release函数即使为空也必须要有,因为platform_device_unregister函数执行时会调用它
static void led_dev_release(struct device *dev)
{
}

static struct resource resources[] = {
        {
                .start = GROUP_PIN(1,3),
                .flags = IORESOURCE_IRQ,
                .name = "GPIO1_IO03_LED1",
        },
        {
                .start = GROUP_PIN(3,5),
                .flags = IORESOURCE_IRQ,
                .name = "GPIO3_IO05_LED2",
        },
};


static struct platform_device board_A_led_dev = {
        .name = "gpio_led_suwenhao",
        .num_resources = ARRAY_SIZE(resources),
        .resource = resources,
        .dev = {
                .release = led_dev_release,
         },
};

要了解这段代码,首先要了解结构体platform_device,结构体platform_device的定义如下:
来源:include\linux\platform_device.h

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;
};

这个结构体比较重要,我专门写了篇博文来介绍它,详情见 https://blog.youkuaiyun.com/wenhao_ir/article/details/145018442

另外,这里要特别注意resource的结构体的flags的值按道理应该选IORESOURCE_MEM,而不是IORESOURCE_IRQ,因为GPIO资源属于是内存资源【注意:不是IO资源】,但这里为什么选的是IORESOURCE_IRQ呢,原因见博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145018442 【搜索“特别注意”】

当读懂了上面的博文后,下面这段代码就很好理解了:

//led_dev_release函数即使为空也必须要有,因为platform_device_unregister函数执行时会调用它
static void led_dev_release(struct device *dev)
{
}

static struct resource resources[] = {
        {
                .start = GROUP_PIN(1,3),
                .flags = IORESOURCE_IRQ,
                .name = "GPIO1_IO03_LED1",
        },
        {
                .start = GROUP_PIN(3,5),
                .flags = IORESOURCE_IRQ,
                .name = "GPIO3_IO05_LED2",
        },
};


static struct platform_device board_A_led_dev = {
        .name = "gpio_led_suwenhao",
        .num_resources = ARRAY_SIZE(resources),
        .resource = resources,
        .dev = {
                .release = led_dev_release,
         },
};

对这段代码就不多作介绍了。只是要还要特别注意,成员struct device dev的release成员必须要赋值函数,因为模块卸载时调用的函数platform_device_unregister()会调用struct device dev的release成员函数,即使release函数中什么也不做,也得有这个函数,在这里具体的实现为led_dev_release()函数。

模块初始化函数led_dev_init

static int __init led_dev_init(void)
{
    int err;
    
    err = platform_device_register(&board_A_led_dev);   
    
    return 0;
}

在本篇博文上面给出的示例代码和下面这篇博文中的关键代码和示例中已经掌握了这段代码的含义,这里就不再赘述了。
https://blog.youkuaiyun.com/wenhao_ir/article/details/145018442

模块退出函数led_dev_exit

在本篇博文上面给出的示例代码和下面这篇博文中的关键代码和示例中已经掌握了这段代码的含义,这里就不再赘述了。
https://blog.youkuaiyun.com/wenhao_ir/article/details/145018442

利用Platform总线设备驱动书写的多个LED驱动实例的驱动部分代码(文件chip_demo_gpio.cleddrv.c)

★★文件chip_demo_gpio.cleddrv.c各自的主要内容和作用★★

文件chip_demo_gpio.c中实现Platform总线设备驱动的驱动部分【Platform总线设备驱动分为三部分:分别为“Platform 总线 (Platform Bus)、Platform 设备 (Platform Device)、Platform 驱动 (Platform Driver)”】,

文件leddrv.c实现具体的驱动在内核中注册的部分,即实现博文https://blog.youkuaiyun.com/wenhao_ir/article/details/144888989中描述的驱动程序的注册过程。

另外真正的硬件底层操作也是在文件chip_demo_gpio.c中实现的,文件leddrv.c的作用只是实现注册驱动程序的那套东西,比如分配设备号、设置文件操作结构体、创建设备类、创建设备文件等都在文件chip_demo_gpio.c中实现的,也是前几天我们集中精力学习的东西。

chip_demo_gpio.c中的模块初始化函数chip_demo_gpio_drv_init

static int __init chip_demo_gpio_drv_init(void)
{
    int err;
    
    err = platform_driver_register(&chip_demo_gpio_driver); 
    register_led_operations(&board_demo_led_opr);
    
    return 0;
}

代码:

err = platform_driver_register(&chip_demo_gpio_driver); 

把驱动结构体chip_demo_gpio_driver注册进Platform总线中,结构体实例chip_demo_gpio_driver的定义如下:

static struct platform_driver chip_demo_gpio_driver = {
    .probe      = chip_demo_gpio_probe,
    .remove     = chip_demo_gpio_remove,
    .driver     = {
        .name   = "gpio_led_suwenhao",
    },
};

关于结构体platform_driver的完整定义和详细介绍,请参考 https://blog.youkuaiyun.com/wenhao_ir/article/details/145030037

从对结构体platform_driver的介绍可以知道,当一个platform_device结构体通过Platform总线匹配到相应的驱动结构体(platform_driver)后,系统会去调用函数chip_demo_gpio_probe()用于初始化设备相关资源、配置设备、注册设备相关的文件操作等,所以接下为我们去看函数chip_demo_gpio_probe()
看完函数chip_demo_gpio_probe()再看模块初始化函数chip_demo_gpio_drv_init中用到的函数register_led_operations

chip_demo_gpio.c中的函数chip_demo_gpio_probe()

static int chip_demo_gpio_probe(struct platform_device *pdev)
{
    struct resource *res;
    int i = 0;

    while (1)
    {
        res = platform_get_resource(pdev, IORESOURCE_IO, i);
        if (!res)
            break;
        
        g_ledpins[g_ledcnt] = res->start;
        device_file_create(g_ledcnt);
        g_ledcnt++;
        i++;
    }
    return 0;
    

这个代码不难理解,通过函数platform_get_resource,利用resource中的字段flags去依次取次各设备的硬件描述,然后把它们的寄存器地址res->start分别存放到数组g_ledpins中去。数组g_ledpins是一个全局数组,定义如下:

static int g_ledpins[100];

然后再调用文件leddrv.c中的函数led_class_create_device去依次创建对应的设备文件。

void device_file_create(int minor)
{
	device_create(led_class, NULL, MKDEV(major, minor), NULL, "swh_led%d", minor); /* /dev/swh_led0,1,... */
}

EXPORT_SYMBOL(device_file_create);

注意:这里涉及到一个模块调用另一个模块中的函数,详情见 https://blog.youkuaiyun.com/wenhao_ir/article/details/145012177 【搜索“一个模块要调用另一个模块中的函数怎么办”】

这里要提一个问:设备类led_class是在哪里创建的??
答:是在leddrv.c编译成的leddrv.ko初始化时创建的,下图红线中的代码:
在这里插入图片描述
代码:

led_class = class_create(THIS_MODULE, "swh_led_class");

运行后,会在系统目录/sys/class/下会新加一个名叫swh_led_class的目录,即存在了下面这个目录路径:

/sys/class/swh_led_class/

另外,主设备号也是leddrv.c编译成的leddrv.ko初始化时分配的,如下图所示:
在这里插入图片描述

leddrv.c中的函数register_led_operations

我们发现在chip_demo_gpio.c中的模块初始化函数chip_demo_gpio_drv_init的主体代码中,用到了 leddrv.c中的函数register_led_operations,相关代码如下:
在这里插入图片描述
函数register_led_operations的代码如下:

//位置:leddrv.c
void register_led_operations(struct led_operations *opr)
{
	p_led_opr = opr;
}

可见其实就是把在chip_demo_gpio.c中定义的led_operations结构体board_demo_led_opr的指针传递给leddrv.c中的led_operations结构体指针p_led_opr

chip_demo_gpio.c中定义的led_operations结构体board_demo_led_opr的内容如下:

static struct led_operations board_demo_led_opr = {
    .init = board_demo_led_init,
    .ctl  = board_demo_led_ctl,
};

其中的函数board_demo_led_init的定义如下:

//位置:chip_demo_gpio.c
static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */       
{   
    //printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
    
    printk("init gpio: group %d, pin %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
    switch(GROUP(g_ledpins[which]))
    {
        case 0:
        {
            printk("init pin of group 0 ...\n");
            break;
        }
        case 1:
        {
            printk("init pin of group 1 ...\n");
            break;
        }
        case 2:
        {
            printk("init pin of group 2 ...\n");
            break;
        }
        case 3:
        {
            printk("init pin of group 3 ...\n");
            break;
        }
    }
    
    return 0;
}

其中的函数board_demo_led_ctl的定义如下:

static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{
    //printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
    printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));

    switch(GROUP(g_ledpins[which]))
    {
        case 0:
        {
            printk("set pin of group 0 ...\n");
            break;
        }
        case 1:
        {
            printk("set pin of group 1 ...\n");
            break;
        }
        case 2:
        {
            printk("set pin of group 2 ...\n");
            break;
        }
        case 3:
        {
            printk("set pin of group 3 ...\n");
            break;
        }
    }

    return 0;
}

可见,函数board_demo_led_init和函数board_demo_led_ctl才是真正的操作寄存器从而控制硬件的函数【只是咱们留空,并没有进行实际的操作】,可参考博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/144973219 中的函数board_demo_led_initboard_demo_led_ctl进行理解。
由于通过函数register_led_operations进行了结构体的指针传递,使得位于leddrv.c中的结构体p_led_opr能够调用函数board_demo_led_init和函数board_demo_led_ctl,这两个函数显然会被驱动的文件操作结构体(file_operations)中的相关函数所调用。也的确是这样的,看下面的位于leddrv.c中的代码:

static int led_drv_open (struct inode *node, struct file *file)
{
	int minor = iminor(node);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 根据次设备号初始化LED */
	p_led_opr->init(minor);
	
	return 0;
}

static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	char status;
	struct inode *inode = file_inode(file);
	int minor = iminor(inode);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	//让位于内核空间的变量status接收来自用户空间传来的值,status的值为0或1
	err = copy_from_user(&status, buf, 1); 

	/* 根据次设备号和status控制LED */
	p_led_opr->ctl(minor, status);
	
	return 1;
}

这两个函数中的关键代码如下:

p_led_opr->init(minor);

p_led_opr->ctl(minor, status);

这两个实际上就是调用了函数函数board_demo_led_init和函数board_demo_led_ctl进行真正的硬件级底层操作。

chip_demo_gpio.c中的模块退出函数

static void __exit lchip_demo_gpio_drv_exit(void)
{
    platform_driver_unregister(&chip_demo_gpio_driver);
}

这个没啥好说的,就是把注册进Platform总线的结构体struct platform_driver chip_demo_gpio_driver卸载掉。
不过要注意在卸载掉的时会去调用struct platform_driver chip_demo_gpio_driver的remove成员,即函数chip_demo_gpio_remove,从而实现相关资源的释放。

所以现在我们需要去看下函数chip_demo_gpio_remove的实现:

static int chip_demo_gpio_remove(struct platform_device *pdev)
{
    struct resource *res;
    int i = 0;

    while (1)
    {
        res = platform_get_resource(pdev, IORESOURCE_IO, i);
        if (!res)
            break;
        
        device_file_destroy(i);
        i++;
    }
    return 0;
}

这个函数主要就是调用leddrv.c中的函数device_file_destroy去销毁各设备的设备文件:

void device_file_destroy(int minor)
{
	device_destroy(led_class, MKDEV(major, minor));
}

EXPORT_SYMBOL(device_file_destroy);

注意要在chip_demo_gpio.c引用的头文件#include "leddrv.h"中去声明这个函数,否则chip_demo_gpio.c编译成模块文件时通不过。

至此,文件chip_demo_gpio.c中的代码全部都看了一遍,思路也理清了,而文件leddrv.c中的代码在前面几天对Linux驱动程序的编写中已经是非常清楚了,这里就不再赘述了。

代码思路总结【Platform总线设备驱动实现总结】

在上面分析的代码中,我们实现了利用Platform总线设备为单板上的多个LED创建了设备文件,即为它们都写了驱动程序。

Platform总线设备驱动分为三个部分,分别为:

  • Platform 总线 (Platform Bus)
  • Platform 设备 (Platform Device)
  • Platform 驱动 (Platform Driver)

“Platform 总线 (Platform Bus)”相关的代码Linux的内核源码已经给我们写好了,我们要写的就是Platform 设备 (Platform Device)和Platform 驱动 (Platform Driver)部分。

在文件board_A_led.c中我们对我们的设备资源进行了描述,即实现了Platform 设备 (Platform Device)部分。

而在chip_demo_gpio.c中我们则实现了Platform 驱动 (Platform Driver),当然这个Platform 驱动 (Platform Driver)部分对于驱动注册需要的那套东西(比如分配设备号、设置文件操作结构体、创建设备类、创建设备文件等),则是在文件leddrv.c中实现的,所以文件在chip_demo_gpio.c的实现是依赖于文件leddrv.c的。

把Platform总线设备驱动代码的层次关系理解透了之后,就能知道加载的顺序应该为:leddrv.ko→chip_demo_gpio.ko→board_A_led.ko

测试程序

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>


int main(int argc, char **argv)
{
	int fd;
	char status;
	
	/* 1. 判断参数 */
	if (argc != 3) 
	{
		printf("Usage: %s <dev> <on | off>\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open(argv[1], O_RDWR);
	if (fd == -1)
	{
		printf("can not open file %s\n", argv[1]);
		return -1;
	}

	/* 3. 写文件 */
	if (0 == strcmp(argv[2], "on"))
	{
		status = 1;
		write(fd, &status, 1);
	}
	else
	{
		status = 0;
		write(fd, &status, 1);
	}
	
	close(fd);
	
	return 0;
}

这个程序就没啥好说的了~

Makefile文件的编写

# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88

all:
	make -C $(KERN_DIR) M=`pwd` modules 
	$(CROSS_COMPILE)gcc -o ledtest ledtest.c 

clean:
	make -C $(KERN_DIR) M=`pwd` clean
	rm -rf modules.order
	rm -f ledtest

obj-m += leddrv.o chip_demo_gpio.o board_A_led.o

在这个Makfefile中,我们通过最后一行语句:

obj-m += leddrv.o chip_demo_gpio.o board_A_led.o

会生成三个ko文件,分别为:

leddrv.ko
chip_demo_gpio.ko
board_A_led.ko

把Platform总线设备驱动代码的层次关系理解透了之后,就能知道加载的顺序应该为:leddrv.ko→chip_demo_gpio.ko→board_A_led.ko

交叉编译

源码复制到Ubuntu中
在这里插入图片描述

make

在这里插入图片描述
生成了下面这些我们需要的文件:
在这里插入图片描述
复制到NFS文件中备用…
在这里插入图片描述

上板测试

打开串口终身,然后打开开发板,挂载网络文件系统…

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
cd /mnt/Platform_bus_led_gpio_demo

加载各模块

按下面顺序加载各模块:

leddrv.ko→chip_demo_gpio.ko→board_A_led.ko
insmod leddrv.ko
insmod chip_demo_gpio.ko
insmod board_A_led.ko

在这里插入图片描述
这里加载没完问题~

查看设备类文件和设备文件生成没有

我们检查下相关的设备类文件和设备文件生成没有。

设备类文件:

ls /sys/class/

在这里插入图片描述
可见文件/sys/class/swh_led_class是存在的。

设备文件:

ls /dev/

在这里插入图片描述
可见设备文件/dev/swh_led0和设备文件/dev/swh_led1都已经创建了。

查看驱动程序注册进内核没有

cat /proc/devices

在这里插入图片描述
可见,主设备号为245的驱动程序“swh_led_driver”已经有了。

运行测试程序

然后我们运行下测试程序:

运行前先把设置下printfk的日志级别配置,以便能打印出相关信息:
详情见 https://blog.youkuaiyun.com/wenhao_ir/article/details/145006286

 echo "7 4 1 7" > /proc/sys/kernel/printk

然后运行测试命令:

./ledtest /dev/swh_led0 on

在这里插入图片描述
可见是按我们的设想打印了相关的信息。

再运行测试命令:

./ledtest /dev/swh_led0 off

在这里插入图片描述
也是没问题的,我们再试下打开另一个设备文件/dev/swh_led1

./ledtest /dev/swh_led1 on

在这里插入图片描述

因为测试程序里只有写设备的分支,没有读设备的分支,所以这里就不测试读部分了。

卸载模块

接下来,我们卸载掉这几个模块,看下还有没有相关的驱动…

因为模块间有依赖关系,所以我们得先确定卸载顺序,关于多模块卸载时的卸载顺序问题,可参考我的另一篇博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145012177 【搜索“怎么确定卸载顺序”】

现在我要卸载模块board_A_led、chip_demo_gpio、leddrv,我知道它们之间存在依赖关系,所以需要查看命令lsmod查看下依赖关系,结果如下:
在这里插入图片描述
从中可以看到board_A_led是没有什么依赖情况存在的,它是独立的,所以它的卸载顺序任意。
但是leddrvchip_demo_gpio使用着,即chip_demo_gpio是依赖于leddrv的。
所以对于chip_demo_gpio和leddrv,我们应该先卸载chip_demo_gpio,再卸载leddrv

所以三条卸载命令如下:

rmmod board_A_led
rmmod chip_demo_gpio
rmmod leddrv

从咱们的代码来看,卸载chip_demo_gpio时会导致函数chip_demo_gpio_remove被调用,在这个函数里会调用leddrv里的函数device_file_destroy(),从而使用相应的设备文件被删除。

而卸载leddrv时,会导致下面这个出口函数被调用:

static void __exit led_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	class_destroy(led_class);
	unregister_chrdev(major, "swh_led_driver");
}

从而导致设备类和设备号被销毁。

我们来运行上面三条卸载命令看下情况:
先把printk的日志级别设为默认值,否则会有一大堆无关紧要的日志输出到终端上:

echo "4 4 1 4" > /proc/sys/kernel/printk
rmmod board_A_led

在这里插入图片描述
没问题~

rmmod chip_demo_gpio

在这里插入图片描述
相关的设备文件应该没了,查看一下:

ls /dev/

在这里插入图片描述
设备文件/dev/swh_led0和设备文件/dev/swh_led1的设置文件确实是没了…

接下来卸载leddrv模块:

rmmod leddrv

在这里插入图片描述
也能正常卸载,leddrv这个模块卸载完后设备类文件应该没有了,查看一下:

ls /sys/class/

在这里插入图片描述
可见设备类文件/sys/class/swh_led_class已经不存在了。

当然还可以看下内核中注册的驱动程序:

在这里插入图片描述
可见,主设备号为245的驱动程序“swh_led_driver”已经有了。

至此,测试完美收关…

总线设备驱动好处的感受

Platform总线设备驱动分为三个部分,分别为:

  • Platform 总线 (Platform Bus)
  • Platform 设备 (Platform Device)
  • Platform 驱动 (Platform Driver)

根据上面的过程,我们可以看出,如果一个厂家提供了片上资源所有的总线设备驱动的驱动部分,那我要哪用哪个资源,只需要在总线设备驱动的设备描述好就行…这样嵌入式开发的重心就不在驱动这一块了,而是在应用这一块了。

附完整工程文件

https://pan.baidu.com/s/109W2D-k-H196GPrN_AlE3w?pwd=87f2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昊虹AI笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值