Linux驱动开发笔记(十三)——platform设备驱动

视频:第16.1讲 platform设备驱动实验-驱动的分离与分层_哔哩哔哩_bilibili

文档:《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81.pdf》五十四章


一、驱动的分离与分层

如果把传感器的驱动都写到一个.c文件里面,里面有传感器初始化、对某些开发板的IIC初始化巴拉巴拉,这时候想要将这个代码移植到另一个板子上 就需要很多修改。

进行了分离后,就会有专门进行IIC初始化的文件,对外提供统一的接口,传感器初始化文件直接调用这个接口。移植时,只需要换成对应开发板的IIC文件即可。即主机控制器驱动和设备驱动分离。

也就是《开发指南》中的三张图:

在实际的驱动开发中,主机控制器驱动和设备驱动一般都已经由各自的厂家写好,我们只需要提供设备信息即可,比如设备连接到了哪个I2C接口上,I2C的速度是多少等等。相当于将设备信息从设备驱动中剥离,驱动使用标准方法去获取到设备信息(比如从设备树中获取),然后根据获取到的设备信息来初始化设备。

而总线则负责将两者联系起来。当向系统注册驱动 时,总线就会在右侧的设备中查找有没有匹配的设备;当向系统中注册设备时,总线就会在左侧的驱动中查找看有没有匹配的设备。

二、总线-驱动-设备

视频:第16.2讲 platform设备驱动实验-驱动、总线和设备

驱动的分离与分层衍生出 "总线-驱动-设备" 框架。

总线部分代码已由内核提供,我们需要编写驱动和设备 部分。驱动 指设备的驱动,设备指的是设备属性,如地址范围、IIC器件地址和速度等。

2.1 总线

总线主要用于匹配总线下的设备和驱动。

开发板的/sys/bus里面存储了许多总线。进入i2c总线文件夹/sys/bus/i2c/,里面有devices和drivers文件夹,对应i2c总线的设备和驱动。

2.1.1 总线的数据类型

cpp 复制代码
// 定义在include\linux\device.h
struct bus_type {
	const char		*name;        // 总线名
	const char		*dev_name;    // 总线上设备的默认名字前缀
	struct device		*dev_root;// 总线下所有设备的根目录设备指针(可选)
	struct device_attribute	*dev_attrs;	/* use dev_groups instead */
	const struct attribute_group **bus_groups;  // 总线的属性集合
	const struct attribute_group **dev_groups;  // 总线上设备的属性集合
	const struct attribute_group **drv_groups;  // 总线上驱动的属性集合

	int (*match)(struct device *dev, struct device_driver *drv);
        // 非常重要。每个总线都必须实现。
        // 此函数用于进行设备和驱动之间的匹配,总线就是使用match函数来根据注册的设备 / 
        // 来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数
        // match函数的两个参数:dev和drv分别为device和device_driver类型,即设备和驱动

	int (*uevent)(struct device *dev, struct kobj_uevent_env *env); //向用户空间发送热插拔事件
	int (*probe)(struct device *dev);    // 找到匹配驱动时会被调用,用于设备初始化
	int (*remove)(struct device *dev);   // 设备移除或驱动卸载时会被调用,用于清理操作
	void (*shutdown)(struct device *dev);// 系统关机或重启时调用

	int (*online)(struct device *dev);   // 标记设备上线
	int (*offline)(struct device *dev);  // 标记设备下线

	int (*suspend)(struct device *dev, pm_message_t state); // 挂起时调用
	int (*resume)(struct device *dev);  // 恢复时调用

	const struct dev_pm_ops *pm;  // 电源管理操作集合,可以取代上面的suspend和resume

	const struct iommu_ops *iommu_ops;

	struct subsys_private *p;
	struct lock_class_key lock_key;
};

2.1.2 注册、卸载总线

cpp 复制代码
int bus_register(struct bus_type *bus)
void bus_unregister(struct bus_type *bus)

2.2 驱动

2.2.1 驱动的数据类型

cpp 复制代码
// 定义在include\linux\device.h
struct device_driver {
	const char		*name;
	struct bus_type		*bus;
	struct module		*owner;
	const char		*mod_name;	/* used for built-in modules */
	bool suppress_bind_attrs;	/* disables bind/unbind via sysfs */
	const struct of_device_id	*of_match_table;
	const struct acpi_device_id	*acpi_match_table;
	int (*probe) (struct device *dev);
	int (*remove) (struct device *dev);
	void (*shutdown) (struct device *dev);
	int (*suspend) (struct device *dev, pm_message_t state);
	int (*resume) (struct device *dev);
	const struct attribute_group **groups;
	const struct dev_pm_ops *pm;
	struct driver_private *p;
};

2.2.2 注册、卸载驱动

cpp 复制代码
// 定义在drivers\base\driver.c
int driver_register(struct device_driver *drv) 
void driver_unregister(struct device_driver *drv)

2.2.3 调用关系/执行顺序

调用太多,这里就不放完整代码了,看看就行。调用关系如下:

cpp 复制代码
// 注册驱动→挂载到总线→遍历总线设备→匹配设备和驱动→绑定关系→调用probe

int driver_register(struct device_driver *drv) 
// 注册驱动入口函数
    └──int bus_add_driver(struct device_driver *drv)
        // 把驱动添加到它所属总线的驱动链表中
        └──int driver_attach(struct decive_driver *drv) 
            // 查找bus上的所有设备
            └──int bus_for_each_dev(............)
                // 遍历总线上所有已注册的设备
                └──static int __driver_attach(struct device *dev, void *data)
                    // 尝试把一个设备和驱动绑定
                    └──static inline int driver_match_device(struct device_driver *drv, struct device *dev)
                        // 检查驱动和设备是否匹配
                        └──static int really_probe(struct device *dev, struct device_driver *drv)
                            // 真正的探测函数,一旦匹配成功就会调用
                            └──若匹配,则执行dev_probe // 最终要执行的probe函数

2.3 设备

2.3.1 设备的数据类型

cpp 复制代码
// 定义在include\linux\device.h
struct device {
	struct device		*parent;
	struct device_private	*p;
	struct kobject kobj;
	const char		*init_name; /* initial name of the device */
	const struct device_type *type;
	struct mutex		mutex;	/* mutex to synchronize calls to its driver. */
	struct bus_type	*bus;		/* type of bus device is on */
	struct device_driver *driver;	/* which driver has allocated this device */
    ............
};

2.3.2 注册、卸载设备

cpp 复制代码
// 定义在drivers\base\core.c
int device_register(struct device *dev)
void device_unregister(struct device *dev)

三、platform平台驱动模型

视频:第16.3讲 platform设备驱动实验-platform总线简介1

IIC、SPI、USB是有具体的总线的,但是定时器、LCD等没有具体的总线,为此内核提供了一个虚拟的总线:platform总线,以及对应的platform驱动、platform设备。

3.1 platform总线

3.1.1 注册

cpp 复制代码
int __init platform_bus_init(void)  //其核心就是调用了bus_register,注册的内容就是platform_bus_type

3.1.2 数据类型------platform_bus_type

cpp 复制代码
// 定义在drivers/base/platform.c
struct bus_type platform_bus_type = {
	.name		= "platform",         // 总线名
	.dev_groups	= platform_dev_groups,// 设备属性集合
	.match		= platform_match,     // 匹配函数,匹配设备和驱动。详见3.4
	.uevent		= platform_uevent,
	.pm		= &platform_dev_pm_ops,
    // 总线级的.probe是可选的。如果总线没有提供.probe,
    // 设备模型会回退调用驱动自己的.probe(也就是3.2.1中platform_driver中的probe)
};

3.2 platform驱动

3.2.1 数据类型------platform_driver

(本次实验只需要定义probe和remove就够了)

cpp 复制代码
// 定义在include/linux/platform_device.h
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; // 第三种匹配方式用到的id_table。具体结构体定义在include\linux\mod_devicetable.h
	bool prevent_deferred_probe;
};

其中:

3.2.1.1 成员device_driver的定义

(本次实验只需要用到name和of_match_table)

cpp 复制代码
// 定义在include/linux/device.h
struct device_driver {
	const char		    *name; // 驱动名,在没有设备树的情况下用于和设备匹配。
	struct bus_type		*bus;

	struct module		*owner;
	const char		*mod_name;	/* used for built-in modules */

	bool suppress_bind_attrs;	/* disables bind/unbind via sysfs */

	const struct of_device_id	*of_match_table;  // 用于有设备树时的匹配
	const struct acpi_device_id	*acpi_match_table;

	int (*probe) (struct device *dev);
	int (*remove) (struct device *dev);
	void (*shutdown) (struct device *dev);
	int (*suspend) (struct device *dev, pm_message_t state);
	int (*resume) (struct device *dev);
	const struct attribute_group **groups;

	const struct dev_pm_ops *pm;

	struct driver_private *p;
};
3.2.1.2 成员of_match_table的定义
cpp 复制代码
// 定义在include/linux/mod_devicetable.h
struct of_device_id {
	char	name[32];
	char	type[32];
	char	compatible[128]; // 用于和设备树中的节点的compatible属性进行匹配
	const void *data;
};

3.2.2 注册和卸载

cpp 复制代码
// 定义在include\linux\platform_device.h
// 在向内核注册驱动时,如果匹配成功,会直接调用probe函数
#define platform_driver_register(drv) \
	__platform_driver_register(drv, THIS_MODULE)

// 定义在drivers\base\platform.c
void platform_driver_unregister(struct platform_driver *drv)
// drv:要卸载的platform驱动

3.3 platform设备

3.3.1 数据类型------platform_device

cpp 复制代码
// 定义在include/linux/platform_device.h
struct platform_device {
	const char	*name;  // 设备名,用于和驱动匹配
	int		id;         //
	bool	id_auto;    
	struct device	dev;//
	u32		num_resources;    // 资源数量,表示下一行resource资源的大小
	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;
};
// 实际编写代码时,并不是所有成员都需要配置,可以根据具体需要配置

其中:

3.3.1.1 成员struct device的定义
cpp 复制代码
// 定义在include/linux/device.h
struct device {
	struct device		*parent;

	struct device_private	*p;

	struct kobject kobj;
	const char		*init_name; /* initial name of the device */
	const struct device_type *type;

	struct mutex		mutex;	/* mutex to synchronize calls to
					 * its driver.
					 */

	struct bus_type	*bus;		/* type of bus device is on */
	struct device_driver *driver;	/* which driver has allocated this
					   device */
	void		*platform_data;	/* Platform specific data, device
					   core doesn't touch it */
	void		*driver_data;	/* Driver data, set and get with
					   dev_set/get_drvdata */
	struct dev_pm_info	power;
	struct dev_pm_domain	*pm_domain;

#ifdef CONFIG_PINCTRL
	struct dev_pin_info	*pins;
#endif

#ifdef CONFIG_NUMA
	int		numa_node;	/* NUMA node this device is close to */
#endif
	u64		*dma_mask;	/* dma mask (if dma'able device) */
	u64		coherent_dma_mask;/* Like dma_mask, but for
					     alloc_coherent mappings as
					     not all hardware supports
					     64 bit addresses for consistent
					     allocations such descriptors. */
	unsigned long	dma_pfn_offset;

	struct device_dma_parameters *dma_parms;

	struct list_head	dma_pools;	/* dma pools (if dma'ble) */

	struct dma_coherent_mem	*dma_mem; /* internal for coherent mem
					     override */
#ifdef CONFIG_DMA_CMA
	struct cma *cma_area;		/* contiguous memory area for dma
					   allocations */
#endif
	/* arch specific additions */
	struct dev_archdata	archdata;

	struct device_node	*of_node; /* associated device tree node */
	struct fwnode_handle	*fwnode; /* firmware device node */

	dev_t			devt;	/* dev_t, creates the sysfs "dev" */
	u32			id;	/* device instance */

	spinlock_t		devres_lock;
	struct list_head	devres_head;

	struct klist_node	knode_class;
	struct class		*class;
	const struct attribute_group **groups;	/* optional groups */

	void	(*release)(struct device *dev);
	struct iommu_group	*iommu_group;

	bool			offline_disabled:1;
	bool			offline:1;
};
3.3.1.2 成员resource的定义
cpp 复制代码
struct resource {
	resource_size_t start; // 起始地址
	resource_size_t end;   // 终止地址
	const char *name;      
	unsigned long flags;   // 标志该resource是什么类型的资源。资源类型定义在include/linux/ioport.h
	struct resource *parent, *sibling, *child;
};

3.3.2 注册和卸载

cpp 复制代码
无设备树时,需要编写设备注册文件,并使用platform_device_register注册设备
有设备树时,直接修改设备树的设备节点即可。系统启动时会解析设备树并自动将其还原为platform_device结构体
// 定义在drivers\base\platform.c
int platform_device_register(struct platform_device *pdev)
// 当设备与platform驱动匹配时,就会执行platform_driver->probe函数

// 定义在drivers\base\platform.c
void platform_device_unregister(struct platform_device *pdev)
// pdev:要注销的platform设备

3.4 match匹配函数和几种匹配方式

视频:第16.4讲 platform设备驱动实验-platform总线简介2

上面3.1.2 platform_bus_type例子中的platform_match就是匹配函数。具体的定义如下,提供了四种匹配方式:

cpp 复制代码
// 定义在drivers/base/platform.c
static int platform_match(struct device *dev, struct device_driver *drv)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct platform_driver *pdrv = to_platform_driver(drv);

	/* When driver_override is set, only bind to the matching driver */
	if (pdev->driver_override)
		return !strcmp(pdev->driver_override, drv->name);

	/* Attempt an OF style match first */
    // OF类型匹配,即设备树采用的匹配方式
	if (of_driver_match_device(dev, drv)) // 这个函数定义在include/linux/of_device.h
		return 1;
    // 设备驱动结构体device_driver中有个of_match_table成员变量,保存着驱动的compatible匹配表
    // 设备树中的每个设备节点的compatible属性会和of_match_table表中的所有成员比较
    // 如果有相同的条目就表示设备和此驱动匹配
    // 设备和驱动匹配成功以后,就会调用bus_type中的probe函数
    // device_driver结构体定义在include/linux/device.h
    // of_match_table则是of_device_id类型的(定义在include\linux\mod_devicetable.h)
    // 匹配表compatible就是of_device_id的成员,是一个char[128]数组


	/* Then try ACPI style match */
    // ACPI匹配
	if (acpi_driver_match_device(dev, drv))
		return 1;
    // 每个ACPI设备在ACPI表中都会有硬件ID等属性,驱动程序会列出驱动支持的ID,以此判断是否匹配


	/* Then try to match against the id table */
    // id列表匹配
	if (pdrv->id_table)
		return platform_match_id(pdrv->id_table, pdev) != NULL;
    // 设备驱动结构体platform_driver有个成员变量id_table,保存了很多id信息
    // 存放着这个驱动所支持的设备类型

	/* fall-back to driver name match */
    // name字段匹配
	return (strcmp(pdev->name, drv->name) == 0);
    // 如果第三种匹配方式的id_table不存在的话,直接比较驱动和设备的name字段
    // 如果相等的话就匹配成功
}

3.5 platform_get_resource

在没有设备树的情况下,设备信息都保存在platform_device。platform_driver需要获取设备信息,就要用到platform_get_resource函数

cpp 复制代码
// 定义在drivers/base/platform.c
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num)
// 类型resource:这个resource和3.3.1中的resource一样
// dev:  获取哪一个设备的信息
// type: 资源类型,与resource结构体中的flag一样,表示是什么类型的资源。资源类型定义在include/linux/ioport.h
// index:resource下标索引。目标resource可能是数组组成的多个设备,由index指定具体哪个设备。
//        如果只有一个设备就填0

四、实验代码

4.1 无设备树

主要写platform_driver和platform_device

4.1.1 文件结构

cpp 复制代码
18_PLATFORM(工作区)
└── 18_platform
    ├── .vscode
    │   ├── c_cpp_properties.json
    │   └── settings.json
    ├── leddevice.c
    ├── leddriver.c
    ├── 18_platform.code-workspace
    ├── newchrledAPP.c
    └── Makefile

4.1.2 Makefile

bash 复制代码
CFLAGS_MODULE += -w

KERNELDIR := /home/for/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek  # 内核路径
# KERNELDIR改成自己的 linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek文件路径(这个文件从正点原子"01、例程源码"中直接搜,cp到虚拟机里面)

CURRENT_PATH := $(shell pwd)	# 当前路径

obj-m := leddevice.o 		# 编译文件
obj-m += leddriver.o

build: kernel_modules			# 编译模块

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean	

4.1.3 设备部分 leddevice.c

这个文件的功能相当于替代设备树。

cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>


/* 寄存器物理地址 */
#define CCM_CCGR1_BASE         (0x020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0x020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0x020E02F4)
#define GPIO1_DR_BASE          (0x0209C000)
#define GPIO1_GDIR_BASE        (0x0209C004)
#define REGISTER_LENGTH        4   // 地址长度


// 释放
void leddevice_release(struct device * dev){
    printk("leddevice release\r\n");
}

//
// 5个寄存器,需要设置5个resource
static struct resource led_resources[] = {
    [0] = {
        .start = CCM_CCGR1_BASE,  // 起始地址
        .end   = CCM_CCGR1_BASE + REGISTER_LENGTH - 1, // 终止地址
        .flags = IORESOURCE_MEM,  // 寄存器类型
    },
    [1] = {
        .start = SW_MUX_GPIO1_IO03_BASE,
        .end   = SW_MUX_GPIO1_IO03_BASE + REGISTER_LENGTH - 1,
        .flags = IORESOURCE_MEM,
    },
    [2] = {
        .start = SW_PAD_GPIO1_IO03_BASE,
        .end   = SW_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1,
        .flags = IORESOURCE_MEM,
    },
    [3] = {
        .start = GPIO1_DR_BASE,
        .end   = GPIO1_DR_BASE + REGISTER_LENGTH - 1,
        .flags = IORESOURCE_MEM,
    },
    [4] = {
        .start = GPIO1_GDIR_BASE,
        .end   = GPIO1_GDIR_BASE + REGISTER_LENGTH - 1,
        .flags = IORESOURCE_MEM,
    },
};


static struct platform_device leddevice={
    .name = "imx6ull_led", // 设备名,用于和驱动进行匹配
    .id   = -1, // 表示设备没有id号
    .dev  = {
        .release = &leddevice_release, //释放该platform device时执行
    },
    .num_resources =  ARRAY_SIZE(led_resources),
    .resource = led_resources,
};


// 加载设备
static int __init leddevice_init(void){
    platform_device_register(&leddevice);
    return 0;
}

// 卸载设备
static void __exit leddevice_exit(void){
    platform_device_unregister(&leddevice);
}

module_init(leddevice_init);
module_exit(leddevice_exit);
MODULE_LICENSE("GPL");

4.1.4 驱动部分 leddriver.c

驱动部分基本Linux驱动开发笔记(四)的实验代码修改

更新了:

修改了部分设备变量的命名

增加了platform_driver结构体及其remove、probe函数

将原来驱动入口/驱动注册函数的功能移到probe函数中。驱动入口函数只负责调用platform驱动注册函数platform_driver_register

将原来驱动出口/驱动卸载函数的功能移到remove函数中。驱动出口函数只负责调用platform驱动卸载函数platform_driver_unregister

cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>



/* 地址映射后的虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* LED状态 */
#define LEDOFF 0
#define LEDON  1

#define PLATFORM_NAME "platled" // 设备名
#define PLATFORM_COUNT 1          // led设备数量

/////////////////////////////////////////////////////

/* 设备结构体 */
struct newchrled_dev{
    struct cdev cdev;
    dev_t devid;            /* 设备号 */
    int major;              /* 主设备号 */
    int minor;              /* 此设备号 */
    struct class *class;    /* 类 */
    struct device *device;  /* 设备 */
};

struct newchrled_dev newchrled; /* led设备 */

/////////////////////////////////////////////////////

// LED状态翻转
static void led_swtich(u8 sta){
    u32 val = 0;
    if(sta == LEDOFF){
        /* 关灯 */
        printk("turn off\r\n");
        val = readl(GPIO1_DR);  // 32bit读取函数
        val |= (1 << 3);       // 将bit3置为高电平,关闭LED
        writel(val, GPIO1_DR);
    }
    else if(sta == LEDON){
        /* 开灯 */
        val = readl(GPIO1_DR);  // 32bit读取函数
        val &= ~(1 << 3);       // 将bit3置为低电平,打开LED
        writel(val, GPIO1_DR);
        printk("turn on\r\n");
    }
}

static int newchrled_open(struct inode *inode, struct file *filp){
    return 0;
}
static int newchrled_release(struct inode *inode, struct file *filp){
    return 0;
}
static ssize_t newchrled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
    int retvalue;
    unsigned char databuf[1];
    retvalue = copy_from_user(databuf, buf, count);
    if(retvalue < 0){
        printk("kernel write fauled!\r\n");
        return -EFAULT;
    }

    led_swtich(databuf[0]);
    return 0;
}

/* 字符设备操作集合 */
static const struct file_operations newchrled_fops = {
    .owner = THIS_MODULE,
    .write = newchrled_write,
    .open = newchrled_open,
    .release = newchrled_release,
};

///////////////////////////////////////////////////////


// 匹配成功就会执行probe
static int led_probe(struct platform_device *dev){
    unsigned int val = 0;
    int i = 0, ret = 0;

    printk("led driver prob\r\n");
    struct resource *ledsource[5];
    for(i=0; i<5; i++){
        ledsource[i] = platform_get_resource(dev, IORESOURCE_MEM, i);
        if(ledsource[i] == NULL)return -EINVAL;
        // resource_size
    }

    // 地址映射 
    IMX6U_CCM_CCGR1 = ioremap(ledsource[0]->start, resource_size(ledsource[0])); // 这里resource_size(ledsource[0]=4
    SW_MUX_GPIO1_IO03 = ioremap(ledsource[1]->start, resource_size(ledsource[1]));
    SW_PAD_GPIO1_IO03 = ioremap(ledsource[2]->start, resource_size(ledsource[2]));
    GPIO1_DR = ioremap(ledsource[3]->start, resource_size(ledsource[3]));
    GPIO1_GDIR = ioremap(ledsource[4]->start, resource_size(ledsource[4]));

    // 初始化
    val = readl(IMX6U_CCM_CCGR1);  // 32bit读取函数
    val &= ~(3 << 26); // 清除26、27位的数据为0,其余位保持不变。  其中3的二进制为11,~为取反操作。
    val |= 3 << 26;    // 将26、27位置为1
    writel(val, IMX6U_CCM_CCGR1);   // 将 val 写入 IMX6U_CCM_CCGR1,使能CCM_CCGR1的gpio1时钟。

    writel(0x5, SW_MUX_GPIO1_IO03); // 复用GPIO1------IO03
    writel(0x10B0, SW_PAD_GPIO1_IO03); //设置GPIO1_IO03电气属性

    val = readl(GPIO1_GDIR);  // 32bit读取函数
    val |= 1 << 3;  // 设置PIO1_IO03为output。 将GPIO1_GDIR第3位 置为1
    writel(val, GPIO1_GDIR);

    // 关灯
    val = readl(GPIO1_DR);  // 32bit读取函数
    val |= (1 << 3);       // 将bit3置为高电平,关闭LED
    writel(val, GPIO1_DR);

    // 分配设备号
    if (newchrled.major){ // 如果已经给定主设备号
        newchrled.devid = MKDEV(newchrled.major, 0);
        ret = register_chrdev_region(newchrled.devid, PLATFORM_COUNT, PLATFORM_NAME);
    }
    else{ // 没有给定主设备号
        ret = alloc_chrdev_region(&newchrled.devid, 0, PLATFORM_COUNT, PLATFORM_NAME);
        newchrled.major = MAJOR(newchrled.devid);
        newchrled.minor = MINOR(newchrled.devid);
    }
    if (ret < 0)
    {
        printk("newchrled chrdev_region error!\r\n");
        return -1;
    }
    printk("devid = %d:%d\r\n", newchrled.major, newchrled.minor);

    // 注册字符设备
    newchrled.cdev.owner = THIS_MODULE;
    cdev_init(&newchrled.cdev, &newchrled_fops);
    ret = cdev_add(&newchrled.cdev, newchrled.devid, PLATFORM_COUNT);

    // 自动创建设备节点
    newchrled.class = class_create(THIS_MODULE, PLATFORM_NAME); // 创建类
    if(IS_ERR(newchrled.class)){
        return PTR_ERR(newchrled.class);
    }

    newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, PLATFORM_NAME); // 创建设备
    if(IS_ERR(newchrled.device)){
        return PTR_ERR(newchrled.device);
    }

    return 0;
}


static int led_remove(struct platform_device * dev){
    printk("led driver remove\r\n");

    // 删除字符设备
    cdev_del(&newchrled.cdev);

    // 注销设备号
    unregister_chrdev_region(newchrled.devid, PLATFORM_COUNT);

    // 删除类
    device_destroy(newchrled.class,newchrled.devid);
    class_destroy(newchrled.class);

    return 0;
}

// platform驱动结构体
static struct platform_driver led_driver = {
    .driver = {
        .name   = "imx6ull_led", // 驱动名,用于和设别匹配
    },
    .remove = led_remove,
    .probe  = led_probe,
};
///////////////////////////////////////////////////////////////////


// 加载驱动
static int __init leddriver_init(void){
    return platform_driver_register(&led_driver);
}

// 卸载驱动
static void __exit leddriver_exit(void){
    platform_driver_unregister(&led_driver);
}

module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");

4.1.5 应用程序

应用程序直接把Linux驱动开发笔记(四)实验的应用程序搬过来。只有调用方式发生改变。

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>

/* 
 * @description    : main主程序 
 * @param - argc   : argv数组元素个数 
 * @param - argv   : 具体参数 
 * @return         : 0 成功; else失败
 * 调用   ./newchrledAPP <filename> <0:1>  0关灯,1开灯
 * ./newchrledAPP /dev/platled 0  关灯
 * ./newchrledAPP /dev/platled 1  开灯
 */ 

#define LEDOFF 0
#define LEDON  1

int main(int argc, char *argv[]){

    if(argc != 3){  // 判断用法是否错误
        printf("Error Usage!\r\n");
        return -1;
    }

    char *filename;
    int fd = 0;
    unsigned char databuf[1];
    int retvalue = 0;

    filename = argv[1];
    fd = open(filename, O_RDWR);  // 读写模式打开驱动文件filename

    if(fd <0){
        printf("file %s open failed!\r\n");
        return -1;
    }
    
    databuf[0] = atoi(argv[2]);  // char 2 int

    retvalue = write(fd, databuf, sizeof(databuf));   // 缓冲区数据写入fd
    if(retvalue <0){
        printf("LED Control Failed!\r\n");
        close(fd);
        return -1;
    }

    close(fd);
    return 0;
}

4.1.6 测试

bash 复制代码
# VSCODE终端
make
arm-linux-gnueabihf-gcc newchrledAPP.c -o newchrledAPP
sudo cp leddevice.ko leddriver.ko newchrledAPP /.../linux/nfs/rootfs/lib/modules/4.1.15/


# 串口
cd /lib/modules/4.1.15/
depmod
modprobe leddevice.ko
modprobe leddriver.ko  # 此时应当能看到输出led driver prob
                       # 说明执行了platform_driver的prob函数,匹配成功

ls /sys/bus/platform/devices/  # 此时应当能看到设备imx6ull_led
ls /sys/bus/platform/drivers/  # 此时应当能看到驱动imx6ull_led

./newchrledAPP /dev/platled 1  # 此时开发板红灯亮起
./newchrledAPP /dev/platled 0  # 此时开发板红灯熄灭

rmmod leddriver.ko    # 此时应能看到输出"led driver remove",说明执行了platform_driver的remove函数
rmmod leddevice.ko    # 此时应能看到输出"leddevice release",说明执行了platform_device的release函数

可以看到

4.2 有设备树

有设备树以后就不需要向总线注册设备了,直接修改设备树即可。

设备和驱动匹配成功后,就会把设备树对应节点的信息直接转为platform_device结构体的信息,不需要手动传。

4.2.1 文件结构

有了设备树,可以直接删掉leddevice.c了。

bash 复制代码
19_DTSPLATFORM(工作区)
└── 19_dtsplatform
    ├── .vscode
    │   ├── c_cpp_properties.json
    │   └── settings.json
    ├── leddriver.c
    ├── 19_dtsplatform.code-workspace
    ├── newchrledAPP.c
    └── Makefile

4.2.2 Makefile

bash 复制代码
CFLAGS_MODULE += -w

KERNELDIR := /.../linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek  # 内核路径
# KERNELDIR改成自己的 linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek文件路径(这个文件从正点原子"01、例程源码"中直接搜,cp到虚拟机里面)

CURRENT_PATH := $(shell pwd)	# 当前路径

obj-m := leddriver.o 			# 编译文件

build: kernel_modules			# 编译模块

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean	

4.2.3 修改设备树

修改内容与Linux驱动开发笔记(六)3.1部分一致:

①在arch/arm/boot/dts/imx6ull-alientek-emmc.dts文件的设备树根节点最后添加LED设备节点:

bash 复制代码
	/* 2025/7/28 LED设备节点 */
	dtsled{
		#address-cells = <1>;
		#size-cells = <1>;
		status = "okay";
		reg = < 0x020C406C 0x04 /*CCM_CCGR1_BASE*/
				0x020E0068 0x04 /*SW_MUX_GPIO1_IO03_BASE*/
				0x020E02F4 0x04 /*SW_PAD_GPIO1_IO03_BASE*/
				0x0209C000 0x04 /*GPIO1_DR_BASE*/
				0x0209C004 0x04 /*GPIO1_GDIR_BASE*/
			  >;
	};
	gpioled{
		compatible = "alientek,gpioled";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_gpioled>;
		led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;  // GPIO1_pin3 低电平有效
		status = "okay";
	};

② 在imx6ull-alientek-emmc.dts文件中找到&iomuxc节点,在里面的imx6ul-evk里加入新的pinctrl:

bash 复制代码
		pinctrl_gpioled: ledgrp{
			fsl,pins = <
				MX6UL_PAD_GPIO1_IO03__GPIO1_IO03	0x10b0
			>;
		};

③ 编译、复制到tftproot,然后重启开发板:

bash 复制代码
make dtbs
sudo cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /.../tftpboot/ -f

4.2.4 驱动 leddriver.c

代码内容基本与Linux驱动开发笔记(六)3.2.2一致,只是添加了platform_driver相关代码。并将驱动入口函数的内容移到probe函数中、将驱动出口函数的内容移到remove函数中。

cpp 复制代码
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/mod_devicetable.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>

/* LED状态 */
#define LEDOFF 0
#define LEDON  1

#define PLATFORM_NAME "platled" // 设备名
#define PLATFORM_COUNT 1          // led设备数量

/////////////////////////////////////////////////////

/* 设备结构体 */
struct newchrled_dev{
    struct cdev cdev;
    dev_t devid;            /* 设备号 */
    int major;              /* 主设备号 */
    int minor;              /* 此设备号 */
    struct class *class;    /* 类 */
    struct device *device;  /* 设备 */
    struct device_node *nd;
    int led_gpio;
};
struct newchrled_dev newchrled; /* led设备 */

/////////////////////////////////////////////////////

static ssize_t newchrled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
    int ret;
    unsigned char databuf[1];
    struct newchrled_dev *dev = filp->private_data;
 
    ret = copy_from_user(databuf, buf, count);
    if(ret < 0){
        return -EINVAL;
    }
    if(databuf[0] == LEDON){ //开灯
        gpio_set_value(dev->led_gpio, 0); // 低电平开灯
    } else if(databuf[0] == LEDOFF){
        gpio_set_value(dev->led_gpio, 1); // 高电平关灯
    }
 
    return 0;
}
static int newchrled_open(struct inode *inode, struct file *filp){
    filp->private_data = &newchrled;
    return 0;
}
static int newchrled_release(struct inode *inode, struct file *filp){
    return 0;
}

/* 字符设备操作集合 */
static const struct file_operations newchrled_fops = {
    .owner = THIS_MODULE,
    .write = newchrled_write,
    .open = newchrled_open,
    .release = newchrled_release,
};

///////////////////////////////////////////////////////


// 匹配成功就会执行probe
static int led_probe(struct platform_device *dev){
    printk("led probe\r\n");

    int ret = 0;
 
    /* 1.注册字符设备驱动 */
    newchrled.devid = 0;
    if(newchrled.devid){
        newchrled.devid = MKDEV(newchrled.devid, 0);
        register_chrdev_region(newchrled.devid, PLATFORM_COUNT, PLATFORM_NAME);
    } else {
        alloc_chrdev_region(&newchrled.devid, 0, PLATFORM_COUNT, PLATFORM_NAME);
        newchrled.major = MAJOR(newchrled.devid);
        newchrled.minor = MINOR(newchrled.devid);
    }
 
    /* 2.初始化cdev */
    newchrled.cdev.owner = THIS_MODULE;  
    cdev_init(&newchrled.cdev, &newchrled_fops);
 
    /* 3.添加cdev */
    cdev_add(&newchrled.cdev, newchrled.devid, PLATFORM_COUNT); // 错误处理先略过了
 
    /* 4.创建类 */
    newchrled.class = class_create(THIS_MODULE, PLATFORM_NAME);
    if(IS_ERR(newchrled.class)){
        return PTR_ERR(newchrled.class);
    }
 
    /* 5.创建设备 */
    newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, PLATFORM_NAME);
    if(IS_ERR(newchrled.device)){
        return PTR_ERR(newchrled.device);
    }
 
    /* 1.获取设备节点 */
    // newchrled.nd = of_find_node_by_path("/gpioled");  // 找到刚才在imx6ull-alientek-emmc.dts根节点下加入的gpioled节点
    // if(newchrled.nd == NULL){
    //     ret = -EINVAL;
    //     goto fail_findnode;
    // }
    newchrled.nd = dev->dev.of_node; 
    // platform_device类型中的device结构体成员中已经包括了of_node(device_node类型)
    // 可以直接读取,不需要再手动寻找of_find_node_by_path




    /* 2.获取LED对应的GPIO */  // 也就是节点中led-gpios那一行内容
    newchrled.led_gpio = of_get_named_gpio(newchrled.nd, "led-gpios", 0);
    if(newchrled.led_gpio < 0){
        printk("can't find led_gpio\r\n");
        ret = -EINVAL;
        goto fail_findnode;
    }
    printk("led_gpio num = %d\r\n",newchrled.led_gpio);
 
    /*  3.申请IO */
    ret = gpio_request(newchrled.led_gpio, "led-gpios");
    if(ret){
        printk("Failed to request the led gpio\r\n");
        ret = -EINVAL;
        goto fail_findnode;        
    }
 
    /* 4.使用IO,设置为输出 */
    ret = gpio_direction_output(newchrled.led_gpio, 1);
    if(ret){
        goto fail_setoutput; // 如果代码走到这一步,一定已经成功进行了IO申请,因此这里错误处理时需要释放IO
    }
 
    /* 5.输出高电平,关灯 */
    gpio_set_value(newchrled.led_gpio, 1);
 
    return 0;
 
fail_setoutput:
    gpio_free(newchrled.led_gpio);
fail_findnode:
    return ret;
}


static int led_remove(struct platform_device * dev){
    printk("led remove\r\n");

    gpio_set_value(newchrled.led_gpio, 1); // 高电平 关灯
 
    /* 注销字符设备驱动 */
    cdev_del(&newchrled.cdev);
    unregister_chrdev_region(newchrled.devid, PLATFORM_COUNT);
 
    device_destroy(newchrled.class, newchrled.devid);
    class_destroy(newchrled.class);
 
    gpio_free(newchrled.led_gpio);

    return 0;
}

static const struct of_device_id led_of_match[] = {
    {.compatible = "alientek,gpioled"}, // 要和新增的设备数节点gpio中的compatible匹配
    { /* sentinel */ }, // 哨兵元素
    // 后面可以接更多的{.compatible = "............"}
};

// platform驱动结构体
static struct platform_driver led_driver = {
    .driver = {
        .name = "imx6ull_led", // 无设备树时使用name与设备进行匹配
        .of_match_table = led_of_match, // 有设备树时进行匹配
    },
    .probe = led_probe,
    .remove = led_remove,
};

///////////////////////////////////////////////////////////////////


// 加载驱动
static int __init leddriver_init(void){
    return platform_driver_register(&led_driver);
}

// 卸载驱动
static void __exit leddriver_exit(void){
    platform_driver_unregister(&led_driver);
}

module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");

4.2.5 应用程序newchrledAPP.c

newchrledAPP.c与4.1.5完全一致。

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>

/* 
 * @description    : main主程序 
 * @param - argc   : argv数组元素个数 
 * @param - argv   : 具体参数 
 * @return         : 0 成功; else失败
 * 调用   ./newchrledAPP <filename> <0:1>  0关灯,1开灯
 * ./newchrledAPP /dev/platled 0  关灯
 * ./newchrledAPP /dev/platled 1  开灯
 */ 

#define LEDOFF 0
#define LEDON  1

int main(int argc, char *argv[]){

    if(argc != 3){  // 判断用法是否错误
        printf("Error Usage!\r\n");
        return -1;
    }

    char *filename;
    int fd = 0;
    unsigned char databuf[1];
    int retvalue = 0;

    filename = argv[1];
    fd = open(filename, O_RDWR);  // 读写模式打开驱动文件filename

    if(fd <0){
        printf("file %s open failed!\r\n");
        return -1;
    }
    
    databuf[0] = atoi(argv[2]);  // char 2 int

    retvalue = write(fd, databuf, sizeof(databuf));   // 缓冲区数据写入fd
    if(retvalue <0){
        printf("LED Control Failed!\r\n");
        close(fd);
        return -1;
    }

    close(fd);
    return 0;
}

4.6.测试

首先检查设备树中是否有我们新增的goioled节点,并查看其compatible信息:

bash 复制代码
ls /sys/firmware/devicetree/base/   # 应当能找到gpioled节点
cat /sys/firmware/devicetree/base/gpioled/compatible   # 应当显示alientek,gpioled
bash 复制代码
# VSCODE终端
make
arm-linux-gnueabihf-gcc newchrledAPP.c -o newchrledAPP
sudo cp leddriver.ko newchrledAPP /.../linux/nfs/rootfs/lib/modules/4.1.15/

# 串口
cd /lib/modules/4.1.15/
depmod
modprobe leddriver.ko  # 此时应输出led probe,说明匹配成功
./newchrledAPP /dev/platled 1  # 开灯
./newchrledAPP /dev/platled 0  # 关灯
rmmod leddriver.ko     # 此时应输出led remove
相关推荐
nnerddboy3 小时前
QT(c++)开发自学笔记:4.Qt 3D简易实现
笔记
❥ღ Komo·3 小时前
Redis:高性能NoSQL数据库实战指南
linux
运维帮手大橙子3 小时前
CentOS 7 上部署Jenkins
linux·centos·jenkins
煤球王子3 小时前
浅学线程
linux
egoist20233 小时前
[linux仓库]线程与进程的较量:资源划分与内核实现的全景解析[线程·贰]
linux·开发语言·线程·进程·资源划分
半梦半醒*4 小时前
ELK2——logstash
linux·运维·elk·elasticsearch·centos·1024程序员节
java_logo4 小时前
Docker 部署 CentOS 全流程指南
linux·运维·人工智能·docker·容器·centos
半梦半醒*4 小时前
ELK3——kibana
linux·运维·elasticsearch·centos·gitlab
wan5555cn4 小时前
中国启用WPS格式进行国际交流:政策分析与影响评估
数据库·人工智能·笔记·深度学习·算法·wps