Skip to content

LV105-LED驱动框架

一、一个基本 LED 驱动程序

1. demo 源码

可以直接看这里:04_chrdev_basic/11_chrdev_led

这个 demo 就实现了一个基本的 LED 驱动程序,在内部实现了 drv_open()、drv_write() .... 等函数。

image-20250103094033354

驱动层访问硬件外设寄存器依靠的是 ioremap() 函数去映射到寄存器地址,然后开始控制寄存器。

2. 怎么写驱动程序?

(1)确定主设备号,也可以让内核分配;

(2)定义自己的 file_operations 结构体,主要是定义文件操作函数集;

(3)实现对应的 drv_open()/drv_read()/drv_write() 等函数,填入 file_operations 结构体;

(4)把 file_operations 结构体告诉内核:通过 register_chrdev() 函数;

(5)谁来注册驱动程序?需要一个入口函数:安装驱动程序时,就会去调用这个 入口函数;

(6)有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev();

(7)其它:提供设备信息,自动创建设备节点,class_create()、device_create();

3. 我们想要什么样的接口?

对于 LED 驱动,我们想要什么样的接口?

image-20250103101036993

对于用户来说,我们能看到的就是 app_demo、/dev/led_node 这两个东西,我们通过 app_demo 去操作/dev/led_node 这个节点,控制 LED 灯的亮灭。至少我们需要实现 drv_open()、和 drv_write()两个函数。

二、LED 驱动分层?

1. 有不同的板子怎么办?

想一个问题,现在我们有两块板子 board A 和 board B,board A 上 LED 控制引脚是 GPIO1_IO03,board B 上 LED 控制引脚是 GPIO1_IO05,他们的驱动方式肯定是一模一样的,驱动框架也是一模一样的。其实是没有必要写两个驱动的,要是写在一个驱动程序中,里面要写两块板子的东西,这样就有很多不必要的信息。

其实我们完全可以把驱动框架放一个文件(led_drv.c),把各个板子中对 led 灯的初始化和亮灭控制放在一个文件(board_X.c),这样我们使用 board A 的时候就编译 board_A.c 和 led_drv.c,使用 board B 的时候就编译 board_B.c 和和 led_drv.c。所以我们把驱动拆分为通用的框架(led_drv.c)、具体的硬件操作(board_X.c)后,调用关系如下:

image-20250103131909787

我们可以用面向对象的思想,抽象出一个结构体:

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

每个单板相关的 board_X.c 实现自己的 led_operations 结构体,供上层 的 led_drv.c 调用:

image-20250103132643949

2. 分层的实现

2.1 struct led_operations

上面已经说过了,可以抽象出一个结构体来管理这些初始化、控制函数:

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

那么这个时候,board_X.c 中就可以定义一个 struct led_operations 类型变量,这个变量中的函数指针就指向目标函数:

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

这个时候就有另一个问题了,怎么在 led_drv.c 中使用这个变量?可以去掉 static 然后 extern 过去,不过这样干的话,当修改这个变量名的时候就需要修改 led_drv.c 文件了,但是其实没必要,我们定义一个统一的函数,返回这个变量的地址就是了:

c
struct led_operations *get_board_led_opt(void)
{
	return &board_demo_led_opt;
}

这样我们在 led_drv.c 中调用这个函数就可以了。

2.2 控制函数的实现

接下来就是 LED 的初始化函数,控制函数的实现了,这里就不再详细说了,都一样的:

c
/* 初始化 LED, which-哪个 LED */	
static int board_demo_led_init (int which)    
{
	return 0;
}

/* 控制 LED, which-哪个 LED, status: 1-亮,0-灭 */
static int board_demo_led_ctl (int which, char status) 
{
	return 0;
}

3. Makefile?

这样的话,一个驱动就包含了多个源文件了,怎么编译?我们可以参考这个 Makefile - drivers/char/ipmi/Makefile

makefile
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for the ipmi drivers.
#

ipmi_si-y := ipmi_si_intf.o ipmi_kcs_sm.o ipmi_smic_sm.o ipmi_bt_sm.o \
	ipmi_si_hotmod.o ipmi_si_hardcode.o ipmi_si_platform.o \
	ipmi_si_port_io.o ipmi_si_mem_io.o
ifdef CONFIG_PCI
ipmi_si-y += ipmi_si_pci.o
endif
ifdef CONFIG_PARISC
ipmi_si-y += ipmi_si_parisc.o
endif

obj-$(CONFIG_IPMI_HANDLER) += ipmi_msghandler.o
obj-$(CONFIG_IPMI_DEVICE_INTERFACE) += ipmi_devintf.o
obj-$(CONFIG_IPMI_SI) += ipmi_si.o
obj-$(CONFIG_IPMI_DMI_DECODE) += ipmi_dmi.o
obj-$(CONFIG_IPMI_SSIF) += ipmi_ssif.o
obj-$(CONFIG_IPMI_POWERNV) += ipmi_powernv.o
obj-$(CONFIG_IPMI_WATCHDOG) += ipmi_watchdog.o
obj-$(CONFIG_IPMI_POWEROFF) += ipmi_poweroff.o
obj-$(CONFIG_IPMI_KCS_BMC) += kcs_bmc.o
obj-$(CONFIG_ASPEED_BT_IPMI_BMC) += bt-bmc.o
obj-$(CONFIG_ASPEED_KCS_IPMI_BMC) += kcs_bmc_aspeed.o
obj-$(CONFIG_NPCM7XX_KCS_IPMI_BMC) += kcs_bmc_npcm7xx.o

也就是说# 要想把 a.c, b.c 编译成 ab.ko, 可以这样指定:

makefile
ab-y  := a.o b.o
obj-m += ab.o

4. demo 实现

可以看这里:04_chrdev_basic/13_led_board_template

三、驱动的设计思想

1. 面向对象

  • 字符设备驱动程序抽象出一个 file_operations 结构体;

  • 我们写的程序针对硬件部分抽象出 led_operations 结构体。

2. 分层

上下分层,比如我们前面写的 LED 驱动程序就分为 2 层:

(1)上层实现硬件无关的操作,比如注册字符设备驱动:leddrv.c

(2)下层实现硬件相关的操作,比如 board_A.c 实现单板 A 的 LED 操作,board_B.c 实现单板 B 的 LED 操作。

image-20250104092337246

3. 分离?

还能不能改进?或者说,如果硬件上更换一个引脚来控制 LED 怎么办?我们要去修改上面结构体中的 init、ctl 函数,但是呢,其实每一款芯片它的 GPIO 操作都是类似的。比如:GPIO1_3、 GPIO5_4 这 2 个引脚接到 LED:

  • GPIO1_3 属于第 1 组,即 GPIO1。

(1)有方向寄存器 DIR、数据寄存器 DR 等,基础地址是 addr_base_addr_gpio1。

(2)设置为 output 引脚:修改 GPIO1 的 DIR 寄存器的 bit3。

(3)设置输出电平:修改 GPIO1 的 DR 寄存器的 bit3。

  • GPIO5_4 属于第 5 组,即 GPIO5。

(1)有方向寄存器 DIR、数据寄存器 DR 等,基础地址是 addr_base_addr_gpio5。

(2)设置为 output 引脚:修改 GPIO5 的 DIR 寄存器的 bit4。

(3)设置输出电平:修改 GPIO5 的 DR 寄存器的 bit4。

既然引脚操作有规律,并且这是跟主芯片相关的,那完全可以针对该芯片写 出比较通用的硬件操作代码。比如 board_A.c 使用芯片 chipY,那就可以写出:chipY_gpio.c,它实现 芯片 Y 的 GPIO 操作,适用于芯片 Y 的所有 GPIO 引脚。使用时,我们只需要在 board_A_led.c 中指定使用哪一个引脚即可。程序结构就会变成下面这样:

image-20250104095920210

以面向对象的思想,在 board_A.c 中实现 led_resouce 结构体,它定 义“资源”——即要使用哪一个引脚。 在 chipY_gpio.c 中仍是实现 led_operations 结构体,它要写得更完善, 支持所有可能会用到的 GPIO 的初始化以及高低电平控制等操作。

四、LED 驱动分离

接下来看一下怎么进一步改进上面已经上下分层的 LED 驱动框架。

程序仍分为上下两层:上层 led_drv.c 向内核注册 file_operations 结构体;下层 chip_demo_gpio.c 提供 led_operations 结构体来操作硬件。

下层的代码分为 2 个源文件实现:chip_demo_gpio.c 实现通用的 GPIO 操作, board_A_led.c 指定使用哪个 GPIO,即“资源”。

1. struct led_resource

我们定义一个描述 GPIO 的结构体,用与单板指定使用哪一个 GPIO:

c
struct led_resource {
	int gpiox_pinx;
};

gpiox_pinx 这个成员怎么赋值?它需要包含 GPIO 的组号和组内的编号,由于我们定义的是一个 int 类型的引脚号,我们可以将 GPIO 的组号和编号进行组合,用高 16 位表示组号,用低 16 位表示组内编号,我们可以定义几个宏:

c
/* bit [31:16] = group */
/* bit [15:0]  = which pin */
#define GET_GPIO_GROUP(x)       (x>>16)
#define GET_GPIO_PIN(x)         (x&0xFFFF)
#define MK_GPIOX_PINX(g, p)     ((g << 16) | (p))

例如,现在使用的是 imx6ull 的 GPIO1_IO03,那这里指定 GPIO 的话就可以这样写:

c
// 定义 GPIO 引脚资源
static struct led_resource board_xxx_led = {
	.gpiox_pinx = MK_GPIOX_PINX(1,3),
};

// 获取组号
GET_GPIO_GROUP(board_xxx_led.gpiox_pinx)

// 获取组内的编号
GET_GPIO_PIN(board_xxx_led.gpiox_pinx)

2. board_A_led.c

所以我们在 board_A_led.c 中定义 GPIO 资源的时候就可以这样写:

c
static struct led_resource board_A_led = {
	.gpiox_pinx = GROUP_PIN(1,3),
};

资源定义好了,我们还需要提供一个接口来获取这个变量的地址:

c
struct led_resource *get_led_resouce(void)
{
	return &board_A_led;
}

3. chip_demo_gpio.c

接下来是通用的 GPIO 初始化和控制,和前面其实是一样的,只是多了一个调用,需要在对应的函数中确定要初始化和控制哪个引脚:

c
static struct led_resource *led_rsc = NULL;
/* 初始化 LED, which-哪个 LED */
static int board_demo_led_init (int which)  
{	
	if (!led_rsc)
	{
		led_rsc = get_led_resouce();
	}
	
	printk("init gpio: group %d, pin %d\n", GROUP(led_rsc->pin), PIN(led_rsc->pin));
	switch(GROUP(led_rsc->pin))
	{
		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;
}

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

	switch(GROUP(led_rsc->pin))
	{
		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_opt = {
	.init = board_demo_led_init,
	.ctl  = board_demo_led_ctl,
};

struct led_operations *get_board_led_opt(void)
{
	return &board_demo_led_opt;
}

4. led_drv.c

这个没啥好说的,就是字符设备的基本框架,然后调用一下 chip_demo_gpio.c 中相关的函数实现 gpio 的控制和初始化。

5. demo 实现

可以看这里:04_chrdev_basic/14_led_board_seperate_template,大概会有这些文件:

image-20260119110122737

怎么个结构呢?我这里画了个图:

image-20250105100415414

五、总结

上面其实我们以 LED 驱动框架为例一共写了三个驱动的框架,乍一看,一个比一个复杂,最开始只有一个源文件,到上下分层变成 2 个源文件,到左右分离,变成了 3 个源文件,代码框架越来越复杂。但是,会发现,去适配不同的板子的时候,越来越方便的。

第一种,每适配一块单板,都要去改 led_drv.c 文件,一不小心改错地方了,可能就出现了其他问题了。

第二种,针对不同的板子,我们可以设计不同的 board_X.c 文件,驱动出现问题就查 led_drv.c,led 控制出现问题就查 board_X.c。但是这个时候当我们修改 LED 引脚的时候,就要重新去初始化其他的引脚了。

第三种:有了第二种的优点,而且实现了针对芯片的 GPIO 操作通用函数,当需要更换引脚的时候,我们直接修改 board_X_led.c 中的 led 资源即可。

那还有没有更好更方便的方式?当然有啦,就是后面会学习到的总线设备驱动、设备树等。这些后面再学习。