LV06-03-chrdev-09-点亮LED-02-LED驱动框架

怎么写一个LED驱动程序?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、一个基本LED驱动程序

1. demo源码

可以直接看这里:04_chrdev_basic/11_chrdev_led · 苏木/imx6ull-driver-demo - 码云 - 开源中国

这个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

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

1
2
3
4
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

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

1
2
3
4
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类型变量,这个变量中的函数指针就指向目标函数:

1
2
3
4
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文件了,但是其实没必要,我们定义一个统一的函数,返回这个变量的地址就是了:

1
2
3
4
struct led_operations *get_board_led_opt(void)
{
return &board_demo_led_opt;
}

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

2.2 控制函数的实现

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

1
2
3
4
5
6
7
8
9
10
11
/* 初始化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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 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, 可以这样指定:

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

4. demo实现

可以看这里:04_chrdev_basic/13_led_board_template · 苏木/imx6ull-driver-demo - 码云 - 开源中国

三、驱动的设计思想

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:

1
2
3
struct led_resource {
int gpiox_pinx;
};

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

1
2
3
4
5
/* 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的话就可以这样写:

1
2
3
4
5
6
7
8
9
10
11
// 定义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资源的时候就可以这样写:

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

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

1
2
3
4
struct led_resource *get_led_resouce(void)
{
return &board_A_led;
}

3. chip_demo_gpio.c

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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 · 苏木/imx6ull-driver-demo - 码云 - 开源中国,大概会有这些文件:

image-20250105093856037

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

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资源即可。

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