Skip to content

LV010-设备与驱动的注册

平台设备和平台驱动的驱动程序怎么写?怎么注册?

一、注册平台设备

1. 相关数据结构与 API

1.1 struct platform_device

先来看一下平台设备的数据类型 struct platform_device

c
struct platform_device {
	const char	*name;  // 设备的名称, 用于唯一标识设备
	int		id;         // 设备的 ID, 可以用于区分同一种设备的不同实例
	bool		id_auto;// 表示设备的 ID 是否自动生成
	struct device	dev;// 表示平台设备对应的 struct device 结构体, 用于设备的基本管理和操作
	u32		num_resources;    // 设备资源的数量
	struct resource	*resource;// 指向设备资源的指针
	// 指向设备的 ID 表项的指针, 用于匹配设备和驱动
	const struct platform_device_id	*id_entry;
    // 强制设备与指定驱动匹配的驱动名称
	char *driver_override; /* Driver name to force a match */

	/* MFD cell pointer */
	struct mfd_cell *mfd_cell;// 指向多功能设备( MFD) 单元的指针, 用于多功能设备的描述

	/* arch specific additions */
	struct pdev_archdata	archdata;// 用于存储特定于架构的设备数据
};

struct platform_device 结构体是用于描述平台设备的数据结构。 它包含了平台设备的各种属性和信息, 用于在内核中表示和管理平台设备。

  • name: 设备名称,用于唯一标识设备。 必须提供一个唯一的名称, 以便内核能够正确识别和管理该设备。 总线进行匹配时,会比较设备和驱动的名称是否一致。
  • id: 指定设备的编号,Linux 支持同名的设备,而同名设备之间则是通过该编号进行区分;这个参数是可选的, 如果不需要使用 ID 进行区分, 可以将其设置为-1 。
  • dev: struct device 类型,Linux 设备模型中的 device 结构体,linux 内核大量使用了面向对象思想,platform_device 通过继承该结构体可复用它的相关代码,方便内核管理平台设备;必须为该参数提供一个有效的 struct device 对象, 该结构体的 release 方法必须要实现, 否则在编译的时候会报错
  • num_resources: 记录资源的个数,当结构体成员 resource 存放的是数组时,需要记录 resource 数组的个数,内核提供了宏定义 ARRAY_SIZE() 用于计算数组的个数。
  • resource: struct resource 类型,平台设备提供给驱动的资源,如 irq,dma,内存等等。该结构体会在接下来的内容进行讲解;
  • id_entry: 平台总线提供的另一种匹配方式,原理依然是通过比较字符串,学习平台设备和驱动匹配的时候会去了解,这里的 id_entry 用于 保存匹配的结果

1.2 struct platform_device_id

struct platform_device_id 结构体定义如下:

c
struct platform_device_id {
	char name[PLATFORM_NAME_SIZE];
	kernel_ulong_t driver_data;
};

struct platform_device_id 这个结构体中,有两个成员,第一个是数组用于指定驱动的名称,总线进行匹配时,会依据该结构体的 name 成员与 struct platform_device 中的成员 name 进行比较匹配, 另一个成员变量 driver_data,则是用于来保存设备的配置。我们知道在同系列的设备中,往往只是某些寄存器的配置不一样,为了减少代码的冗余, 尽量做到一个驱动可以匹配多个设备的目的。接下来以 imx 的串口为例,具体看下这个结构体的作用:

c
static struct imx_uart_data imx_uart_devdata[] = {
    [IMX1_UART] = {
        .uts_reg = IMX1_UTS,
        .devtype = IMX1_UART,
    },
    [IMX21_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX21_UART,
    },
    [IMX6Q_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX6Q_UART,
    },
};

static struct platform_device_id imx_uart_devtype[] = {
    {
        .name = "imx1-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX1_UART],
    },
    {
        .name = "imx21-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX21_UART],
    },
    {
        .name = "imx6q-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX6Q_UART],

    },
    {
        /* sentinel */
    }
};

在上面的代码中,支持三种设备的串口,支持 imx1、imx21、imx6q 三种不同系列芯片,他们之间区别在于串口的 test 寄存器地址不同。 当总线成功配对平台驱动以及平台设备时,会将对应的 id_table 条目赋值给平台设备的 id_entry 成员,而平台驱动的 probe 函数是以平台设备为参数, 这样的话,就可以拿到当前设备串口的 test 寄存器地址了。

1.3 platform_device_register()

1.3.1 函数说明

platform_device_register() 函数定义如下:

c
// #include <linux/platform_device.h>
int platform_device_register(struct platform_device *pdev)
{
	device_initialize(&pdev->dev);
	arch_setup_pdev_archdata(pdev);
	return platform_device_add(pdev);
}
EXPORT_SYMBOL_GPL(platform_device_register);

函数用于将 platform_device 结构体描述的平台设备注册到内核中,使其能够参与设备的资源分配和驱动的匹配。

参数说明

  • pdev: struct platform_device 类型结构体指针,描述要注册的平台设备的信息。其中包含了描述平台设备的各种属性和信息。 struct platform_device 结构体包含了设备名称、 设备资源、 设备 ID 等信息, 用于描述和标识平台设备。

返回值】返回 0, 表示设备注册成功。返回负数, 表示设备注册失败, 返回的负数值表示错误代码。

1.3.2 函数分析
image-20250118104700917
c
	device_initialize(&pdev->dev);

调用了 device_initialize() 函数, 用于对 pdev-> dev 进行初始化。 pdev-> dev 是 struct platform_device 结构体中的一个成员, 它表示平台设备对应的 struct device 结构体。 通过调用 device_initialize() 函数, 对 pdev-> dev 进行一些基本的初始化工作, 例如设置设备的引用计数、设备的类型等。

c
	arch_setup_pdev_archdata(pdev);

调用了 arch_setup_pdev_archdata() 函数, 用于根据平台设备的架构数据来设置 pdev 的架构相关数据。 这个函数的具体实现可能与具体的架构相关, 它主要用于在不同的架构下对平台设备进行特定的设置。

c
return platform_device_add(pdev);

调用了 platform_device_add() 函数,将平台设备 pdev 添加到内核中。 platform_device_add() 函数会完成平台设备的添加操作, 包括将设备添加到设备层级结构中、 添加设备的资源等。 它会返回一个 int 类型的结果, 表示设备添加的结果。

1.3.3 总结

platform_device_register() 函数的主要作用是将 struct platform_device 结构体描述的平台设备注册到内核中, 包括设备的初始化、 添加到 platform 总线和设备层级结构、 添加设备资源等操作。通过该函数, 平台设备被注册后, 就能够参与设备的资源分配和驱动的匹配过程。 函数的返回值可以用于判断设备注册是否成功。

1.4 platform_device_unregister()

1.4.1 函数说明

platform_device_unregister() 函数定义如下:

c
// #include <linux/platform_device.h>
void platform_device_unregister(struct platform_device *pdev)
{
	platform_device_del(pdev);
	platform_device_put(pdev);
}
EXPORT_SYMBOL_GPL(platform_device_unregister);

函数用于取消注册已经注册的平台设备, 从内核中移除设备。

参数说明

返回值

1.4.2 函数分析
image-20250118105343219
c
	platform_device_del(pdev);

调用了 platform_device_del() 函数, 用于将设备从 platform 总线的设备列表中移除。它会将设备从设备层级结构中移除, 停止设备的资源分配和驱动的匹配。

c
	platform_device_put(pdev);

这一步调用了 platform_device_put() 函数, 用于减少对设备的引用计数。 这个函数会检查设备的引用计数, 如果引用计数减为零, 则会释放设备结构体和相关资源。 通过减少引用计数, 可以确保设备在不再被使用时能够被释放。

1.4.3 总结

platform_device_unregister() 函数的作用是取消注册已经注册的平台设备, 从内核中移除设备 。 它先调用 platform_device_del() 函数将设备从设备层级结构中移除,然后调用 platform_device_put() 函数减少设备的引用计数, 确保设备在不再被使用时能够被释放。

1.5 platform_add_devices()

platform_add_devices() 函数可以用于注册多个设备,它的定义如下:

c
/**
 * platform_add_devices - add a numbers of platform devices
 * @devs: array of platform devices to add
 * @num: number of platform devices in array
 */
int platform_add_devices(struct platform_device **devs, int num)
{
	int i, ret = 0;

	for (i = 0; i < num; i++) {
		ret = platform_device_register(devs[i]);
		if (ret) {
			while (--i >= 0)
				platform_device_unregister(devs[i]);
			break;
		}
	}

	return ret;
}
EXPORT_SYMBOL_GPL(platform_add_devices);

可以看到就是传入一个二级指针,然后调用 platform_device_register() 函数逐个注册。

2. 设备信息

平台设备的工作是为驱动程序提供设备信息,设备信息包括硬件信息和软件信息两部分。

  • 硬件信息:驱动程序需要使用到什么寄存器,占用哪些中断号、内存资源、IO 口等等
  • 软件信息:以太网卡设备中的 MAC 地址、I2C 设备中的设备地址、SPI 设备的片选信号线等等

2.1 硬件信息

2.1.1 struct resource

对于硬件信息,使用结构体 struct resource 来保存设备所提供的资源,比如设备使用的中断编号,寄存器物理地址等,结构体原型如下:

c
struct resource {
	resource_size_t start; // 资源的起始地址
	resource_size_t end;   // 资源的结束地址
	const char *name;      // 资源的名称
	unsigned long flags;   // 资源的标志位
	unsigned long desc;    // 资源的描述信息
    // parent 指向父资源的指针; sibling 指向同级兄弟资源的指针; child 指向子资源的指针
	struct resource *parent, *sibling, *child;
};
  • start: 指定资源的起始地址。它表示资源的起始位置或者起始寄存器的地址。
  • end: 指定资源的结束地址,它表示资源的结束位置或者结束寄存器的地址。
  • name: 指定资源的名字,可以设置为 NULL。它是一个字符串, 用于标识和描述资源。
  • flags: 用于指定该资源的类型。它包含了一些特定的标志, 用于表示资源的属性或者特征。 例如, 可以用标志位来指示资源的可用性、 共享性、 缓存属性等。 flags 参数的具体取值和含义可以根据系统和驱动的需求进行定义和解释, 但通常情况下, 它用于表示资源的属性、 特征或配置选项。 会有一些宏定义来表示,这些宏定义在 linux/ioport.h
2.1.2 常见资源类型(flags)

在 Linux 中,资源包括 I/O、Memory、Register、IRQ、DMA、Bus 等多种类型,最常见的有以下几种:

资源宏定义描述
IORESOURCE_IO用于 IO 地址空间,对应于 IO 端口映射方式
IORESOURCE_MEM用于外设的可直接寻址的地址空间
IORESOURCE_IRQ用于指定该设备使用某个中断
IORESOURCE_DMA用于指定使用的 DMA 通道

(1)资源类型相关标志位:

c
IORESOURCE_IO : 表示资源是 I/O 端口资源。
IORESOURCE_MEM: 表示资源是内存资源。
IORESOURCE_REG: 表示资源是寄存器偏移量。
IORESOURCE_IRQ: 表示资源是中断资源。
IORESOURCE_DMA: 表示资源是 DMA( 直接内存访问) 资源。

(2)资源属性和特征相关标志位:

c
IORESOURCE_PREFETCH : 表示资源是无副作用的预取资源。
IORESOURCE_READONLY : 表示资源是只读的。
IORESOURCE_CACHEABLE: 表示资源支持缓存。
IORESOURCE_RANGELENGTH: 表示资源的范围长度。
IORESOURCE_SHADOWABLE : 表示资源可以被影子资源替代。
IORESOURCE_SIZEALIGN  : 表示资源的大小表示对齐。
IORESOURCE_STARTALIGN : 表示起始字段是对齐的。
IORESOURCE_MEM_64: 表示资源是 64 位内存资源。
IORESOURCE_WINDOW: 表示资源由桥接器转发。
IORESOURCE_MUXED : 表示资源是软件复用的。
IORESOURCE_SYSRAM: 表示资源是系统 RAM( 修饰符) 。

(3)其他状态和控制标志位:

c
IORESOURCE_EXCLUSIVE: 表示用户空间无法映射此资源。
IORESOURCE_DISABLED : 表示资源当前被禁用。
IORESOURCE_UNSET    : 表示尚未分配地址给资源。
IORESOURCE_AUTO     : 表示地址由系统自动分配。
IORESOURCE_BUSY     : 表示驱动程序将此资源标记为繁忙。

设备驱动程序的主要目的是操作设备的寄存器。不同架构的计算机提供不同的操作接口,主要有 IO 端口映射和 IO 內存映射两种方式。 对应于 IO 端口映射方式,只能通过专门的接口函数(如 inb、outb)才能访问; 采用 IO 内存映射的方式,可以像访问内存一样,去读写寄存器。在嵌入式中,基本上没有 IO 地址空间,所以通常使用 IORESOURCE_MEM

在资源的起始地址和结束地址中,对于 IORESOURCE_IO 或者是 IORESOURCE_MEM,他们表示要使用的内存的起始位置以及结束位置; 若是只用一个中断引脚或者是一个通道,则它们的 start 和 end 成员值必须是相等的。

2.1.3 定义资源的几个宏

一般来说我们定义资源会写成这样:

c
#define MEM_START_ADDR    (0xFDD60000)
#define MEM_LENGTH        (4)
#define IRQ_NUMBER        (101)

/**
 * 定义一个资源数组
 */
static struct resource g_sdev_resources[] = {
    [0] = {
        .start = MEM_START_ADDR,                    // 内存资源起始地址
        .end = MEM_START_ADDR + MEM_LENGTH - 1,     // 内存资源结束地址
        .flags = IORESOURCE_MEM,                    // 标记为内存资源
    },
    [1] = {
        .start = IRQ_NUMBER,     // 中断资源号
        .end = IRQ_NUMBER,       // 中断资源号
        .flags = IORESOURCE_IRQ, // 标记为中断资源
    },
};

这看着好像不够简洁,内核还提供了几个宏来定义资源,这几个宏定义在 ioport.h - include/linux/ioport.h 中:

c
/* helpers to define resources */
#define DEFINE_RES_NAMED(_start, _size, _name, _flags)			\
	{								\
		.start = (_start),					\
		.end = (_start) + (_size) - 1,				\
		.name = (_name),					\
		.flags = (_flags),					\
		.desc = IORES_DESC_NONE,				\
	}
// IORESOURCE_IO 资源
#define DEFINE_RES_IO_NAMED(_start, _size, _name)			\
	DEFINE_RES_NAMED((_start), (_size), (_name), IORESOURCE_IO)
#define DEFINE_RES_IO(_start, _size)					\
	DEFINE_RES_IO_NAMED((_start), (_size), NULL)
// IORESOURCE_MEM 资源
#define DEFINE_RES_MEM_NAMED(_start, _size, _name)			\
	DEFINE_RES_NAMED((_start), (_size), (_name), IORESOURCE_MEM)
#define DEFINE_RES_MEM(_start, _size)					\
	DEFINE_RES_MEM_NAMED((_start), (_size), NULL)
// IORESOURCE_IRQ 资源
#define DEFINE_RES_IRQ_NAMED(_irq, _name)				\
	DEFINE_RES_NAMED((_irq), 1, (_name), IORESOURCE_IRQ)
#define DEFINE_RES_IRQ(_irq)						\
	DEFINE_RES_IRQ_NAMED((_irq), NULL)
// IORESOURCE_DMA 资源
#define DEFINE_RES_DMA_NAMED(_dma, _name)				\
	DEFINE_RES_NAMED((_dma), 1, (_name), IORESOURCE_DMA)
#define DEFINE_RES_DMA(_dma)						\
	DEFINE_RES_DMA_NAMED((_dma), NULL)

这样,通过这些宏,上面的资源数组就可以定义为:

c
#define MEM_START_ADDR    (0xFDD60000)
#define MEM_LENGTH        (4)
#define IRQ_NUMBER        (101)

/**
 * 定义一个资源数组
 */
static struct resource g_sdev_resources[] = {
    [0] = DEFINE_RES_MEM(MEM_START_ADDR, MEM_LENGTH),
    [1] = DEFINE_RES_IRQ(IRQ_NUMBER),
};

2.2 软件信息

对于软件信息,这种特殊信息需要我们以私有数据的形式进行封装保存,我们注意到 struct platform_device 结构体中,有个 struct device 结构体类型的成员 dev

在前面,我们提到过 Linux 设备模型使用 device 结构体来抽象物理设备, 该结构体的成员 platform_data 可用于保存设备的私有数据 dev-> platform_data 是 void *类型的万能指针,无论想要提供的是什么内容,需要把数据的地址赋值给 dev-> platform_data 即可,还是以 GPIO 引脚号为例,示例代码如下:

c
unsigned int pin = 10;

struct platform_device pdev = {
    .dev = {
        .platform_data = &pin;
    }
}

将保存了 GPIO 引脚号的变量 pin 地址赋值给 platform_data 指针,在驱动程序中通过调用平台设备总线中的核心函数,可以获取到我们需要的引脚号。

2.3 使用实例

c
/* 寄存器地址定义*/
#define PERIPH1_REGISTER_BASE (0X20000000) /* 外设 1 寄存器首地址 */
#define PERIPH2_REGISTER_BASE (0X020E0068) /* 外设 2 寄存器首地址 */
#define REGISTER_LENGTH 4

/* 资源 */
static struct resource xxx_resources[] = {
    [0] = {
        .start = PERIPH1_REGISTER_BASE,
        .end = (PERIPH1_REGISTER_BASE + REGISTER_LENGTH - 1),
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = PERIPH2_REGISTER_BASE,
        .end = (PERIPH2_REGISTER_BASE + REGISTER_LENGTH - 1),
        .flags = IORESOURCE_MEM,
    },
};

/* platform 设备结构体 */
static struct platform_device xxxdevice = {
    .name = "xxx-gpio",
    .id = -1,
    .num_resources = ARRAY_SIZE(xxx_resources),
    .resource = xxx_resources,
};

2.4 怎么获取?

获取资源一般是在驱动中获取。

2.4.1 platform_get_resource()

定义了资源后,怎么获取?获取其实就是解析上面那个数组数据,linux 内核为我们提供了一个函数 platform_get_resource()

c
struct resource *platform_get_resource(struct platform_device *dev,
				       unsigned int type, unsigned int num)
{
	int i;

	for (i = 0; i < dev->num_resources; i++) {
		struct resource *r = &dev->resource[i];

		if (type == resource_type(r) && num-- == 0)
			return r;
	}
	return NULL;
}
EXPORT_SYMBOL_GPL(platform_get_resource);

该函数返回该 dev 中某类型(type)资源中的第几个(num,其中 num 的值从 0 开始) 。我们可以看一下这个 resource_type() 函数:

c
static inline unsigned long resource_type(const struct resource *res)
{
	return res->flags & IORESOURCE_TYPE_BITS;
}

里面就是通过 resource.flags 来与上 IORESOURCE_TYPE_BITS,即 0x01000000,所以总的来说 platform_get_resource() 函数会遍历整个 dev-> resource 数组,然后找到第 num 个类型为 resource.flags 的资源,并返回。例如如下资源,包含 3 个 IORESOURCE_MEM 类型资源和 3 个 IORESOURCE_IRQ 资源:

c
#define MEM1_REG_BASE         (0x80000000)
#define MEM2_REG_BASE         (0x80000010)
#define MEM3_REG_BASE         (0x80000100)
#define REGISTER_LENGTH	      (4)

#define IRQ1_NUMBER           (101)
#define IRQ2_NUMBER           (102)
#define IRQ3_NUMBER           (103)
static struct resource g_sdev_resources[SDEV_RESOURCE_CNT] = {
    [0] = {
        .start = MEM1_REG_BASE,                       // 内存资源起始地址
        .end = (MEM1_REG_BASE + REGISTER_LENGTH - 1), // 内存资源结束地址
        .flags = IORESOURCE_MEM,                      // 标记为内存资源
    },
    [1] = {
        .start = IRQ1_NUMBER,    // 中断资源号
        .end = IRQ1_NUMBER,      // 中断资源号
        .flags = IORESOURCE_IRQ, // 标记为中断资源
    },
    [2] = {
        .start = MEM2_REG_BASE,                       // 内存资源起始地址
        .end = (MEM2_REG_BASE + REGISTER_LENGTH - 1), // 内存资源结束地址
        .flags = IORESOURCE_MEM,                      // 标记为内存资源
    },
    [3] = {
        .start = IRQ2_NUMBER,    // 中断资源号
        .end = IRQ2_NUMBER,      // 中断资源号
        .flags = IORESOURCE_IRQ, // 标记为中断资源
    },
    [4] = {
        .start = IRQ3_NUMBER,    // 中断资源号
        .end = IRQ3_NUMBER,      // 中断资源号
        .flags = IORESOURCE_IRQ, // 标记为中断资源
    },
    [5] = {
        .start = MEM3_REG_BASE,                       // 内存资源起始地址
        .end = (MEM3_REG_BASE + REGISTER_LENGTH - 1), // 内存资源结束地址
        .flags = IORESOURCE_MEM,                      // 标记为内存资源
    },
};

我们获取资源的时候如下:

c
platform_get_resource(pdev, IORESOURCE_MEM, 1);// 获取 IORESOURCE_MEM 资源中的第 2 个资源
platform_get_resource(pdev, IORESOURCE_IRQ, 2);// 获取 IORESOURCE_IRQ 资源中的第 3 个资源
2.4.2 platform_get_irq()

platform_get_irq() 函数定义如下:

c
/**
 * platform_get_irq - get an IRQ for a device
 * @dev: platform device
 * @num: IRQ number index
 */
int platform_get_irq(struct platform_device *dev, unsigned int num)
{
	//......
}

返回该 dev 所用的第几个(num)中断。

2.4.3 platform_get_resource_byname()

platform_get_resource_byname() 定义如下:

c
/**
 * platform_get_resource_byname - get a resource for a device by name
 * @dev: platform device
 * @type: resource type
 * @name: resource name
 */
struct resource *platform_get_resource_byname(struct platform_device *dev,
					      unsigned int type,
					      const char *name)
{
	int i;

	for (i = 0; i < dev->num_resources; i++) {
		struct resource *r = &dev->resource[i];

		if (unlikely(!r->name))
			continue;

		if (type == resource_type(r) && !strcmp(r->name, name))
			return r;
	}
	return NULL;
}
EXPORT_SYMBOL_GPL(platform_get_resource_byname);

通过名字(name)返回该 dev 的某类型(type)资源。

2.4.4 platform_get_irq_byname()
c
/**
 * platform_get_irq_byname - get an IRQ for a device by name
 * @dev: platform device
 * @name: IRQ name
 */
int platform_get_irq_byname(struct platform_device *dev, const char *name)
{
	struct resource *r;

	if (IS_ENABLED(CONFIG_OF_IRQ) && dev->dev.of_node) {
		int ret;

		ret = of_irq_get_byname(dev->dev.of_node, name);
		if (ret > 0 || ret == -EPROBE_DEFER)
			return ret;
	}

	r = platform_get_resource_byname(dev, IORESOURCE_IRQ, name);
	return r ? r->start : -ENXIO;
}
EXPORT_SYMBOL_GPL(platform_get_irq_byname);

通过名字(name)返回该 dev 的中断号。

2.4.5 dev_get_platdata()

对于存放在 device 结构体中成员 platform_data 的软件信息,我们可以使用 dev_get_platdata() 函数来获取:

c
static inline void *dev_get_platdata(const struct device *dev)
{
	return dev->platform_data;
}

3. 注册平台设备 demo

3.1 demo 源码

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

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

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

#define PLATFORM_DEV_NAME "sdev" // 设备名称,
                                 // 会在 /sys/bus/platform/devices 中创建对应目录,即/sys/bus/platform/devices/device-name

#define MEM_START_ADDR    (0xFDD60000)
#define MEM_END_ADDR      (0xFDD60004)
#define IRQ_NUMBER        (101)

/**
 * 定义一个资源数组
 */
static struct resource g_sdev_resources[] = {
    {
        .start = MEM_START_ADDR, // 内存资源起始地址
        .end = MEM_END_ADDR,     // 内存资源结束地址
        .flags = IORESOURCE_MEM, // 标记为内存资源
    },
    {
        .start = IRQ_NUMBER,     // 中断资源号
        .end = IRQ_NUMBER,       // 中断资源号
        .flags = IORESOURCE_IRQ, // 标记为中断资源
    },
};

/**
 * @brief  sdev_release()
 * @note   释放资源的回调函数
 * @param [in]
 * @param [out]
 * @retval 
 */
static void sdev_release(struct device *dev)
{
    // 释放资源的回调函数
    const char *device_name = dev_name(dev);
    PRT("device->name=%s\n", device_name);
}

/**
 * 定义一个平台设备全局变量
 */
static struct platform_device g_sdev_platform = {
    .name = PLATFORM_DEV_NAME,                     // 设备名称, 会在 /sys/bus/platform/devices 中创建对应目录,
                                                   // 即/sys/bus/platform/devices/device-name
    .id = -1,                                      // 设备 ID
    .num_resources = ARRAY_SIZE(g_sdev_resources), // 资源数量
    .resource = g_sdev_resources,                  // 资源数组
    .dev.release = sdev_release,                   // 释放资源的回调函数
};

/**
 * @brief  sdev_demo_init()
 * @note   设备结构体以及属性文件结构体注册
 * @param [in]
 * @param [out]
 * @retval 
 */
static __init int sdev_demo_init(void)
{
    int ret = 0;
	printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
    

	ret = platform_device_register(&g_sdev_platform);   // 注册平台设备
    if (ret) 
    {
        PRTE("Failed to register platform device!ret=%d\n", ret);
        goto err_platform_device_register;
    }
    PRT("sdev_demo module init success!\n");
	return 0;

err_platform_device_register:
    return ret;
}

/**
 * @brief  sdev_demo_exit
 * @note   设备结构体以及属性文件结构体注销。
 * @param [in]
 * @param [out]
 * @retval 
 */
static __exit void sdev_demo_exit(void)
{
    platform_device_unregister(&g_sdev_platform);   // 注销平台设备

    PRT("sdev_demo module exit!\n");
}

module_init(sdev_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sdev_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo xxx.ko 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

3.2 开发板测试

将编译好的 sdevice_demo.ko 拷贝到开发板,然后加载驱动:

shell
insmod sdevice_demo.ko

然后就会看到在 /sys/bus/platform/devices/ 中生成了对应的设备目录:

image-20250120100437310

二、注册平台驱动

1. 相关数据结构与 API

1.1 struct platform_driver

先来看一下平台驱动的数据类型 struct platform_driver

c
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;               // 是否阻止延迟探测
};

struct platform_driver 提供了与平台设备驱动相关的函数和数据成员, 以便与平台设备进行交互和管理。

  • probe: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当总线为设备和驱动匹配上之后,会回调执行该函数。我们一般通过该函数,对设备进行一系列的初始化。
  • remove: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当我们移除某个平台设备时,会回调执行该函数指针,该函数实现的操作,通常是 probe 函数实现操作的逆过程。
  • driver: Linux 设备模型中用于抽象驱动的 struct device_driver 结构体,其中包括驱动程序的名称、 总线类型、 模块拥有者、 属性组数组指针等信息。struct platform_driver 继承该结构体,也就获取了设备模型驱动对象的特性。
  • id_table: 表示该驱动能够兼容的设备类型。指向 struct platform_device_id 结构体数组的指针, 用于匹配平台设备和驱动程序之间的关联关系。 通过该关联关系, 可以确定哪个平台设备与该驱动程序匹配, 和.driver.name 起到相同的作用, 但是优先级高于.driver.name。

使用 struct platform_driver 结构体, 开发人员可以定义平台设备驱动程序, 并将其注册到内核中。 当系统检测到与该驱动程序匹配的平台设备时, 内核将调用相应的函数来执行设备的初始化、 配置、 操作和管理。 驱动程序可以利用提供的函数指针和通用数据与平台设备进行交互, 并提供必要的功能和服务。

需要注意的是,struct platform_driver 结构体继承了 struct device_driver 结构体, 因此可以直接访问 struct device_driver 中定义的成员。 这使得平台驱动程序可以利用通用的驱动程序机制, 并与其他类型的设备驱动程序共享代码和功能。

1.2 struct platform_device_id

和前面是一样的,struct platform_device_id 结构体定义如下:

c
struct platform_device_id {
	char name[PLATFORM_NAME_SIZE];
	kernel_ulong_t driver_data;
};

struct platform_device_id 这个结构体中,有两个成员,第一个是数组用于指定驱动的名称,总线进行匹配时,会依据该结构体的 name 成员与 struct platform_device 中的成员 name 进行比较匹配, 另一个成员变量 driver_data,则是用于来保存设备的配置。我们知道在同系列的设备中,往往只是某些寄存器的配置不一样,为了减少代码的冗余, 尽量做到一个驱动可以匹配多个设备的目的。接下来以 imx 的串口为例,具体看下这个结构体的作用:

c
static struct imx_uart_data imx_uart_devdata[] = {
    [IMX1_UART] = {
        .uts_reg = IMX1_UTS,
        .devtype = IMX1_UART,
    },
    [IMX21_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX21_UART,
    },
    [IMX6Q_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX6Q_UART,
    },
};

static struct platform_device_id imx_uart_devtype[] = {
    {
        .name = "imx1-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX1_UART],
    },
    {
        .name = "imx21-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX21_UART],
    },
    {
        .name = "imx6q-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX6Q_UART],

    },
    {
        /* sentinel */
    }
};

在上面的代码中,支持三种设备的串口,支持 imx1、imx21、imx6q 三种不同系列芯片,他们之间区别在于串口的 test 寄存器地址不同。 当总线成功配对平台驱动以及平台设备时,会将对应的 id_table 条目赋值给平台设备的 id_entry 成员,而平台驱动的 probe 函数是以平台设备为参数, 这样的话,就可以拿到当前设备串口的 test 寄存器地址了。

1.3 platform_driver_register()

1.3.1 函数说明

platform_driver_register() 函数定义如下:

c
// #include <linux/platform_device.h>
#define platform_driver_register(drv) \
	__platform_driver_register(drv, THIS_MODULE)

int __platform_driver_register(struct platform_driver *drv,
				struct module *owner)
{
	drv->driver.owner = owner;// 将平台驱动程序的所有权设置为当前模块
	drv->driver.bus = &platform_bus_type;    // 将平台驱动程序的总线类型设置为平台总线
	drv->driver.probe = platform_drv_probe;  // 设置平台驱动程序的探测函数
	drv->driver.remove = platform_drv_remove;// 设置平台驱动程序的移除函数
	drv->driver.shutdown = platform_drv_shutdown;// 设置平台驱动程序的关机函数

	return driver_register(&drv->driver);// 将平台驱动程序注册到内核
}
EXPORT_SYMBOL_GPL(__platform_driver_register);

这个函数是一个宏,调用了 __platform_driver_register() 函数,它将一个平台驱动程序注册到内核中。 通过注册平台驱动程序, 内核可以识别并与特定的平台设备进行匹配, 并在需要时调用相应的回调函数。

参数说明

  • driver : struct platform_driver 类型结构体指针,描述了要注册的平台驱动程序的属性和回调函数。

返回值】返回一个整数值, 表示函数的执行状态。 如果注册成功, 返回 0; 如果注册失败, 返回一个负数错误码。

由于 platform_driver 继承了 driver 结构体,结合 Linux 设备模型的知识, 当成功注册了一个平台驱动后,就会在/sys/bus/platform/drivers 目录下生成一个新的目录项。

1.3.2 函数分析
image-20250118131225389
c
	drv->driver.owner = owner;// 将平台驱动程序的所有权设置为当前模块

将指向当前模块的指针 owner 赋值给平台驱动程序的 owner 成员。 这样做是为了将当前模块与平台驱动程序关联起来, 以确保模块的生命周期和驱动程序的注册和注销相关联

c
	drv->driver.bus = &platform_bus_type;// 将平台驱动程序的总线类型设置为平台总线

将指向平台总线类型的指针 &platform_bus_type 赋值给平台驱动程序的 bus 成员。这样做是为了指定该驱动程序所属的总线类型为平台总线, 以便内核能够将平台设备与正确的驱动程序进行匹配。

c
	drv->driver.probe = platform_drv_probe;// 设置平台驱动程序的探测函数

将指向平台驱动程序探测函数 platform_drv_probe() 的指针赋值给平台驱动程序的 probe 成员。 这样做是为了指定当内核发现与驱动程序匹配的平台设备时, 要调用的驱动程序探测函数。

c
	drv->driver.remove = platform_drv_remove;// 设置平台驱动程序的移除函数

将指向平台驱动程序移除函数 platform_drv_remove() 的指针赋值给平台驱动程序的 remove 成员。 这样做是为了指定当内核需要从系统中移除与驱动程序匹配的平台设备时,要调用的驱动程序移除函数。

c
	drv->driver.shutdown = platform_drv_shutdown;// 设置平台驱动程序的关机函数

将指向平台驱动程序关机函数 platform_drv_shutdow() 的指针赋值给平台驱动程序的 shutdown 成员。 这样做是为了指定当系统关机时, 要调用的驱动程序关机函数。

c
	return driver_register(&drv->driver);// 将平台驱动程序注册到内核

调用 driver_register() 函数, 将平台驱动程序的 driver 成员注册到内核中。 该函数负责将驱动程序注册到相应的总线上, 并在注册成功时返回 0, 注册失败时返回一个负数错误码。

1.3.3 总结

通过上面哪些操作, __platform_driver_register() 函数将平台驱动程序与内核关联起来, 并确保内核能够正确识别和调用驱动程序的各种回调函数, 以实现与平台设备的交互和管理。 函数的返回值表示注册过程的执行状态, 以便在需要时进行错误处理。

1.4 platform_driver_unregister()

1.4.1 函数说明

platform_driver_unregister() 函数定义如下:

c
// #include <linux/platform_device.h>
/**
 * platform_driver_unregister - unregister a driver for platform-level devices
 * @drv: platform driver structure
 */
void platform_driver_unregister(struct platform_driver *drv)
{
	driver_unregister(&drv->driver);
}
EXPORT_SYMBOL_GPL(platform_driver_unregister);

void driver_unregister(struct device_driver *drv)
{
	if (!drv || !drv->p) {// 检查传入的设备驱动程序指针和 p 成员是否有效
		WARN(1, "Unexpected driver unregister!\n");
		return;
	}
	driver_remove_groups(drv, drv->groups); // 移除与设备驱动程序关联的属性组
	bus_remove_driver(drv);// 从总线中移除设备驱动程序
}
EXPORT_SYMBOL_GPL(driver_unregister);

函数用于从内核中注销平台驱动。 通过调用该函数, 可以将指定的平台驱动从系统中移除。

参数说明

返回值

1.4.2 函数分析

platform_driver_unregister() 函数最后调用的是 driver_unregister() :

image-20250118134437039
c
	if (!drv || !drv->p) {
		WARN(1, "Unexpected driver unregister!\n");
		return;
	}

检查传入的设备驱动程序指针 drv 是否为空, 或者驱动程序的 p 成员是否为空。如果其中任何一个条件为真, 表示传入的参数无效, 会发出警告并返回。

c
	driver_remove_groups(drv, drv->groups);

调用 driver_remove_groups() 函数, 用于从内核中移除与设备驱动程序关联的属性组。 drv-> groups 是指向属性组的指针, 指定了要移除的属性组列表。

c
	bus_remove_driver(drv);

调用 bus_remove_driver() 函数, 用于从总线中移除设备驱动程序。 该函数会执行以下操作:

(1) 从总线驱动程序列表中移除指定的设备驱动程序。

(2) 调用与设备驱动程序关联的 remove 回调函数( 如果有定义) 。

(3) 释放设备驱动程序所占用的资源和内存。

(4) 最终销毁设备驱动程序的数据结构。

1.4.3 总结

通过调用 driver_unregister() 函数, 可以正确地注销设备驱动程序, 并在注销过程中进行必要的清理工作。 这样可以避免资源泄漏和其他问题。 在调用该函数后, 应避免继续使用已注销的设备驱动程序指针, 因为该驱动程序已不再存在于内核中。

2. 注册平台驱动 demo

2.1 demo 源码

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/platform_device.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

#define PLATFORM_DRV_MATCH_NAME "sdrv" // 驱动名称,和设备名称相同时可以匹配成功
                                       // 会在 /sys/bus/platform/drivers 中创建对应目录,即/sys/bus/platform/drivers/driver-name


/**
 * @brief  sdrv_probe()
 * @note   平台设备的探测函数
 * @param [in]
 * @param [out]
 * @retval 
 */
static int sdrv_probe(struct platform_device *pdev)
{
    PRT("probing platform device & driver!pdev->name=%s\n", pdev->name);
    // 添加设备特定的操作
    return 0;
}

/**
 * @brief  sdrv_remove()
 * @note   平台设备的移除函数
 * @param [in]
 * @param [out]
 * @retval 
 */
static int sdrv_remove(struct platform_device *pdev)
{
    PRT("removing platform driver!pdev->name=%s\n", pdev->name);
    // 清理设备特定的操作
    return 0;
}

/**
 * 定义平台驱动结构体
 */
static struct platform_driver g_sdrv_platform = {
    .probe = sdrv_probe,   // 平台设备的探测函数指针
    .remove = sdrv_remove, //  平台设备的移除函数指针
    .driver = {
        .name = PLATFORM_DRV_MATCH_NAME, // 和设备名称相同时,可以匹配成功
                                         // 会在 /sys/bus/platform/drivers 中创建对应目录,即/sys/bus/platform/drivers/driver-name
        .owner = THIS_MODULE,
    },
};

/**
 * @brief  sdrv_demo_init
 * @note   调用 driver_register 函数注册我们的驱动
 * @param [in]
 * @param [out]
 * @retval 
 */
static __init int sdrv_demo_init(void)
{
    int ret = 0;
	printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);

	// 注册平台驱动
    ret = platform_driver_register(&g_sdrv_platform);
    if (ret) 
    {
        PRT("Failed to register platform driver!ret=%d\n", ret);
        goto err_platform_driver_register;
    }
    PRT("sdrv_demo module init success!\n");
	return 0;

err_platform_driver_register:
    return ret;
}

/**
 * @brief  sdrv_demo_exit
 * @note   注销驱动以及驱动属性文件
 * @param [in]
 * @param [out]
 * @retval 
 */
static __exit void sdrv_demo_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&g_sdrv_platform);
    PRT("sdrv_demo module exit!\n");
}

module_init(sdrv_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sdrv_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo xxx.ko 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

2.2 开发板验证

将编译好的 sdriver_demo.ko 拷贝到开发板,然后加载驱动:

shell
insmod sdriver_demo.ko

然后就会看到在 /sys/bus/platform/drivers/ 中生成了对应的驱动目录:

image-20250120101202662