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() …. 等函数。
驱动层访问硬件外设寄存器依靠的是 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 驱动,我们想要什么样的接口?
对于用户来说,我们能看到的就是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)后,调用关系如下:
我们可以用面向对象的思想,抽象出一个结构体:
1 | struct led_operations { |
每个单板相关的 board_X.c 实现自己的 led_operations 结构体,供上层 的 led_drv.c 调用:
2. 分层的实现
2.1 struct led_operations
上面已经说过了,可以抽象出一个结构体来管理这些初始化、控制函数:
1 | struct led_operations { |
那么这个时候,board_X.c中就可以定义一个struct led_operations类型变量,这个变量中的函数指针就指向目标函数:
1 | static struct led_operations board_demo_led_opt = { |
这个时候就有另一个问题了,怎么在led_drv.c中使用这个变量?可以去掉static然后extern过去,不过这样干的话,当修改这个变量名的时候就需要修改led_drv.c文件了,但是其实没必要,我们定义一个统一的函数,返回这个变量的地址就是了:
1 | struct led_operations *get_board_led_opt(void) |
这样我们在led_drv.c中调用这个函数就可以了。
2.2 控制函数的实现
接下来就是LED的初始化函数,控制函数的实现了,这里就不再详细说了,都一样的:
1 | /* 初始化LED, which-哪个LED */ |
3. Makefile?
这样的话,一个驱动就包含了多个源文件了,怎么编译?我们可以参考这个 Makefile - drivers/char/ipmi/Makefile:
1 | # SPDX-License-Identifier: GPL-2.0 |
也就是说# 要想把 a.c, b.c编译成ab.ko, 可以这样指定:
1 | ab-y := a.o b.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 操作。
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 中指定使用哪一个引脚即可。程序结构就会变成下面这样:
以面向对象的思想,在 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 | struct led_resource { |
gpiox_pinx这个成员怎么赋值?它需要包含GPIO的组号和组内的编号,由于我们定义的是一个int类型的引脚号,我们可以将GPIO的组号和编号进行组合,用高16位表示组号,用低16位表示组内编号,我们可以定义几个宏:
1 | /* bit[31:16] = group */ |
例如,现在使用的是imx6ull的GPIO1_IO03,那这里指定GPIO的话就可以这样写:
1 | // 定义GPIO引脚资源 |
2. board_A_led.c
所以我们在board_A_led.c中定义GPIO资源的时候就可以这样写:
1 | static struct led_resource board_A_led = { |
资源定义好了,我们还需要提供一个接口来获取这个变量的地址:
1 | struct led_resource *get_led_resouce(void) |
3. chip_demo_gpio.c
接下来是通用的GPIO初始化和控制,和前面其实是一样的,只是多了一个调用,需要在对应的函数中确定要初始化和控制哪个引脚:
1 | static struct led_resource *led_rsc = NULL; |
4. led_drv.c
这个没啥好说的,就是字符设备的基本框架,然后调用一下chip_demo_gpio.c中相关的函数实现gpio的控制和初始化。
5. demo实现
可以看这里:04_chrdev_basic/14_led_board_seperate_template · 苏木/imx6ull-driver-demo - 码云 - 开源中国,大概会有这些文件:
怎么个结构呢?我这里画了个图:
五、总结
上面其实我们以LED驱动框架为例一共写了三个驱动的框架,乍一看,一个比一个复杂,最开始只有一个源文件,到上下分层变成2个源文件,到左右分离,变成了3个源文件,代码框架越来越复杂。但是,会发现,去适配不同的板子的时候,越来越方便的。
第一种,每适配一块单板,都要去改led_drv.c文件,一不小心改错地方了,可能就出现了其他问题了。
第二种,针对不同的板子,我们可以设计不同的board_X.c文件,驱动出现问题就查led_drv.c,led控制出现问题就查board_X.c。但是这个时候当我们修改LED引脚的时候,就要重新去初始化其他的引脚了。
第三种:有了第二种的优点,而且实现了针对芯片的GPIO操作通用函数,当需要更换引脚的时候,我们直接修改board_X_led.c中的led资源即可。
那还有没有更好更方便的方式?当然有啦,就是后面会学习到的总线设备驱动、设备树等。这些后面再学习。