Linux驱动开发(9):pinctrl子系统和gpio子系统--led实验
在前面章节,我们有过使用寄存器去编写字符设备的经历了。这种直接在驱动代码中, 通过寄存器映射来对外设进行使用的编程方式,从驱动开发者的角度可以说是灾难。 因为每当芯片的寄存器发生了改动,那么底层的驱动几乎得重写。
那么在这个问题上,我们更进了一步,学会了使用设备树来描述外设的各种信息(比如寄存器地址), 而不是将寄存器的这些内容放在驱动代码里。 这样即使设备信息修改了,我们还是可以通过设备树的接口函数,去灵活的获取设备的信息。 极大得提高了驱动的复用能力。
现在我们可以通过在驱动程序代码里使用设备树接口,来获取到外设的信息了。 但是,在前面的设备树演示中,我们还是将寄存器操作具体细节体现在了驱动中,比如置位操作。
那么,在驱动中有没有更通用的方法,可以不涉及到具体的寄存器操作的内容呢? 对于有些外设,是具备抽象条件的,也就是说我们可以将对这些外设的操作统一起来。
比如本章中将为大家介绍的pinctrl子系统和GPIO子系统。
本章将会使用GPIO子系统实现LED驱动,GPIO子系统要用到pinctrl子系统所以本章将pinctrl子系统和GPIO子系统放在一块讲解。
1.1. pinctrl子系统
pinctrl子系统主要用于管理芯片的引脚。imx6ull芯片拥有众多的片上外设, 大多数外设需要通过芯片的引脚与外部设备(器件)相连实现相对应的控制,例如我们熟悉的I2C、SPI、LCD、USDHC等等。 而我们知道芯片的可用引脚(除去电源引脚和特定功能引脚)数量是有限的,芯片的设计厂商为了提高硬件设计的灵活性, 一个芯片引脚往往可以做为多个片上外设的功能引脚,以IIC1所对应的引脚为例,如下图所示。

I2C1的SCL和SDA的功能引脚不单单只可以使用在I2C上,也可以作为多个外设的功能引脚,如普通的GPIO引脚,串口的接收发送引脚等, 在设计硬件时我们可以根据需要灵活的选择其中的一个。设计完硬件后每个引脚的功能就确定下来了,假设我们将上面的两个引脚连接 到其他用串口控制的外部设备上,那么这两个引脚功能就做为了UART4的接收、发送引脚。在编程过程中,无论是裸机还是驱动, 一般首先要设置引脚的复用功能并且设置引脚的PAD属性(驱动能力、上下拉等等)。
在驱动程序中我们需要手动设置每个引脚的复用功能,不仅增加了工作量,编写的驱动程序不方便移植, 可重用性差等。更糟糕的是缺乏对引脚的统一管理,容易出现引脚的重复定义。 假设我们在I2C1的驱动中将UART4_RX_DATA引脚和UART4_TX_DATA引脚复用为SCL和SDA, 恰好在编写UART4驱动驱动时没有注意到UART4_RX_DATA引脚和UART4_TX_DATA引脚已经被使用, 在驱动中又将其初始化为UART4_RX和UART4_TX,这样IIC1驱动将不能正常工作,并且这种错误很难被发现。
pinctrl子系统是由芯片厂商来实现的,简单来说用于帮助我们管理芯片引脚并自动完成引脚的初始化, 而我们要做的只是在设备树中按照规定的格式写出想要的配置参数即可。
1.1.1. pinctrl子系统编写格式以及引脚属性详解
1.1.1.1. iomuxc节点介绍
首先我们在内核源码/arch/arm/boot/dts/imx6ull.dtsi文件中查找iomuxc节点,可以看到如下定义
imx6ull.dtsi中iomuxc部分:
iomuxc: iomuxc@20e0000 {compatible = "fsl,imx6ul-iomuxc";reg = <0x20e0000 0x4000>;};
-
compatible: 修饰的是与平台驱动做匹配的名字,这里则是与pinctrl子系统的平台驱动做匹配。
-
reg: 表示的是引脚配置寄存器的基地址。
imx6ull.dtsi这个文件是芯片厂商官方将芯片的通用的部分单独提出来的一些设备树配置。 在iomuxc节点中汇总了所需引脚的配置信息,pinctrl子系统存储使用着iomux节点信息。
我们的设备树主要的配置文件在./arch/arm/boot/dts/imx6ull-mmc-npi.dts中, 打开imx6ull-mmc-npi.dts,在文件中搜索“&iomuxc”找到设备树中引用“iomuxc”节点的位置如下所示。
imx6ull-mmc-npi.dts中&iomuxc部分内容:
&iomuxc {pinctrl-names = "default";pinctrl-0 = <&pinctrl_hog_1>;pinctrl_hog_1: hoggrp-1 {fsl,pins = <MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 /* SD1 CD */MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059 /* SD1 VSELECT */MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059 /* SD1 RESET */>;};pinctrl_enet1: enet1grp {fsl,pins = <MX6UL_PAD_ENET1_RX_EN__ENET1_RX_EN 0x1b0b0MX6UL_PAD_ENET1_RX_ER__ENET1_RX_ER 0x1b0b0/*------------以下省略-----------------*/>;};pinctrl_enet2: enet2grp {fsl,pins = <MX6UL_PAD_GPIO1_IO07__ENET2_MDC 0x1b0b0MX6UL_PAD_GPIO1_IO06__ENET2_MDIO 0x1b0b0/*------------以下省略-----------------*/>;};pinctrl_uart1: uart1grp {fsl,pins = <MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1>;};
在这里通过“&iomuxc”在“iomuxc”节点下追加内容。结合设备树源码介绍如下:
-
第2-3行:“pinctrl-names”标识,指定PIN的状态列表,默认设置为“default”。 “pinctrl-0 = <&pinctrl_hog_1>”的意思的在默认设置下,将使用pinctrl_hog_1这个设备节点来设置我们的GPIO端口状态, pinctrl_hog_1内容是支持热插拔相关的我们暂时不用理会。一个引脚可能有多种状态,以上面串口举例, 在正常使用的时候我们将引脚设置为发送引脚、接收引脚,而在系统进入休眠模式时, 为了节省功耗,我们可以将这两个引脚设置为其他模式,如设置为GPIO功能并设置为高电平等。如下代码所示。
-
其余源码都是pinctrl子节点,它们都是按照一定的格式规范来编写。
举例说明:
&iomuxc {pinctrl-names = "default","sleep","init";pinctrl-0 = <&pinctrl_uart1>;pinctrl-1 =<&xxx>;pinctrl-2 =<&yyy>;
...pinctrl_uart1: uart1grp {fsl,pins = <MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1>;};xxx: xxx_grp {...这里设置将引脚设置为其他模式}yyy: yyy_grp {...这里设置将引脚设置为其他模式}...
}
1.1.1.2. pinctrl子节点编写格式
接下来以“pinctrl_uart1”节点源码为例介绍pinctrl子节点格式规范编写:

pinctrl子节点格式规范,格式框架如下:
pinctrl_自定义名字: 自定义名字 {fsl,pins = <引脚复用宏定义 PAD(引脚)属性引脚复用宏定义 PAD(引脚)属性>;
};
这里我们需要知道每个芯片厂商的pinctrl子节点的编写格式并不相同,这不属于设备树的规范,是芯片厂商自定义的。 如果我们想添加自己的pinctrl节点,只要依葫芦画瓢按照上面的格式编写即可。 接下来我们重点讲解上图的标号3处的内容,也是我们编写的主要内容–添加引脚配置信息。
1.1.1.3. 引脚配置信息介绍
引脚的配置信息一眼看去由两部分组成,一个宏定义和一个16进制数组成。这实际上定义已经配置 控制引脚所需要用到的各个寄存器的地址及应写入寄存器值的信息,以上图的第一条配置信息为例说明。
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 是定义在“./arch/arm/boot/dts/imx6ul-pinfunc.h”文件内的一个宏定义。

从上图可以看出以“MX6UL_PAD_UART1_TX_DATA__xxx”命名的宏定义共有8个, 之前我们讲过引脚复用功能选择寄存器,很容易联想到这8个宏就是用来定义“UART1_TX_DATA”引脚的8个复用功能。 宏定义“MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX”将“UART1_TX_DATA”引脚复用为UART1的TX引脚。 每个宏定义后面有5个参数,名字依次为 mux_reg、conf_reg、input_reg、mux_mode、input_val。
1 2 | <mux_reg conf_reg input_reg mux_mode input_val> 0x0084 0x0310 0x0000 0x0 0x0 |
如果将宏定义展开则在设备树中每条配置信息实际是6个参数,由于第6个参数设置较为复杂需要根据实际需要设置 因此并没有把它放到宏定义里面。以MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX为例,宏定义中5个参数参数介绍如下:
1. mux_reg 和 mux_mode :mux_reg是引脚复用选择寄存器偏移地址,mux_mode是引脚复用选择寄存器模式选择位的值。 UART1_TX引脚复用选择寄存器IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA定义如下所示。
mux_reg = 0x0084与IM6ULL用户手册偏移地址一致, mux_mode = 0。 设置复用选择寄存器IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA[MUX_MODE] = 0,将其复用为UART1_TX功能。
2. conf_reg ,引脚(PAD)属性控制寄存器偏移地址。与引脚复用选择寄存器不同, 引脚属性寄存器应当根据实际需要灵活的配置,所以它的值并不包含在宏定义中, 它的值是我们上面所说的“第六个”参数。

从上图可以看到conf_reg = 0x0310对应UART1_TX引脚的引脚属性寄存器的偏移地址。而这个寄存器包含很多配置项 (上图中是部分配置项),这些配置项在裸机部分已有详细介绍,忘记的朋友可以回去再看下裸机部分详细解释。
3. input_reg 和 input_val ,input_reg暂且称为输入选择寄存器偏移地址。input_val是输入选择寄存器的值。 这个寄存器只有某些用作输入的引脚才有,正如本例所示,UART1_TX用作输出,所以这两个参数都是零。 “输入选择寄存器”理解稍微有点复杂,结合下图介绍如下。

从上图可以看出,如果引脚用作输出,我们我们只需要配置引脚复用选择寄存器和引脚PAD属性设置寄存器。 如果用作输入时还增加了引脚输入选择寄存器,输入选择寄存器的作用也很明显,在多个可选输入中选择一个连接到片上外设。
引脚(PAD)属性值
在pinctrl子系统中一条配置信息由一个宏定义和一个参数组成,将宏定义展开就是六个参数。 结合上面分析我们知道这6个参数就是IOMUX相关的三个寄存器偏移地址和寄存器的值(引脚用作输出时实际只有四个有效, 输入选择寄存器偏移地址和它的值全为0),至于为什么要将pad属性寄存器的值单独列出,前面也说过了,pad属性配置选项非常多, 配置灵活。在pinctrl子系统中添加的PAD属性值就是引脚(PAD)属性设置寄存器的值(16进制)。 有关PAD属性设置内容已经在裸机部分GPIO章节详细介绍,忘记的同学可以回去再回顾下,这里便不再赘述了。
1.1.2. 将RGB灯引脚添加到pinctrl子系统
本小节假设没有看过裸机部分RGB灯章节,我们从看原理图开始,一步步将RGB灯用到的三个引脚添加到pinctrl子系统中。
1.1.2.1. 查找RGB灯使用的引脚
RGB灯对应的原理图如下所示。

根据网络名在核心板上找到对应的引脚,如下。
rgb_led_red: GPIO1_IO04
rgb_led_green: CSI_HSYNC
rgb_led_blue: CSI_VSYNC
1.1.2.2. 找到引脚配置宏定义
这些引脚都将被复用为GPIO,用作驱动LED灯。首先要在“./arch/arm/boot/dts/imx6ul-pinfunc.h”文件内找到对应的宏定义, 以CSI_HSYNC引脚为例,在imx6ul-pinfunc.h中直接搜索“CSI_HSYNC”找到如下结果,

同一个引脚的可选复用功能是连续排布的,我们要将其复用为GPIO,所以选择“MX6UL_PAD_CSI_HSYNC__GPIO4_IO20”即可。
其他的两个引脚最终得到的结果如下:
CSI_HSYNC:MX6UL_PAD_CSI_HSYNC__GPIO4_IO20
CSI_VSYNC:MX6UL_PAD_CSI_VSYNC__GPIO4_IO19
1.1.2.3. 设置引脚属性
我们要写入到设备树中的引脚属性实际就是引脚属性设置寄存器的值。引脚属性配置项很多,以GPIO1_IO04为例如下所示。

实际编程中我们几乎不会手动设置每一个配置项然后再将其组合成一个16进制数,通常情况下我们直接参照官方的设置, 设备树中其他的pinctrl子节点的配置就是很好的一个参考,如果有需要再对个别参数进行修改。 通常情况下用作GPIO的引脚PAD属性设置为“0x000010B1”
1.1.2.4. 在iomuxc节点中添加pinctrl子节点
添加子节点很简单,我们只需要将前面选择好的配置信息按照之前讲解的格式写入到设备树中即可,添加完成后如下所示。
&iomuxc {pinctrl-names = "default";pinctrl-0 = <&pinctrl_hog_1>;/*----------新添加的内容--------------*/pinctrl_rgb_led:rgb_led{fsl,pins = <MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0x000010B1MX6UL_PAD_CSI_HSYNC__GPIO4_IO20 0x000010B1MX6UL_PAD_CSI_VSYNC__GPIO4_IO19 0x000010B1>;
};
新增的节点名为“rgb_led”,名字任意选取,长度不要超过32个字符,最好能表达出节点的信息。 “pinctrl_rgb_led”节点标签,“pinctrl_”是固定的格式,后面的内容自定义的,我们将通过这个标签引用这个节点。 在添加完pinctrl子节点后,系统会根据我们添加的配置信息将引脚初始化为GPIO功能。 到这里关于pinctrl子系统的使用就已经讲解完毕了,接下来介绍GPIO子系统相关的内容。
1.2. GPIO子系统
在没有使用GPIO子系统之前,如果我们想点亮一个LED,首先要得到led相关的配置寄存器,再手动地读、改、写这些配置寄存器实现 控制LED的目的。有了GPIO子系统之后这部分工作由GPIO子系统帮我们完成,我们只需要调用GPIO子系统提供的API函数即可完成GPIO的 控制动作。
在imx6ull.dtsi文件中的GPIO子节点记录着GPIO控制器的寄存器地址,下面我们以GPIO4为例介绍GPIO子节点相关内容:
gpio4: gpio@20a8000 {compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";reg = <0x20a8000 0x4000>;interrupts = <GIC_SPI 72 IRQ_TYPE_LEVEL_HIGH>,<GIC_SPI 73 IRQ_TYPE_LEVEL_HIGH>;clocks = <&clks IMX6UL_CLK_GPIO4>;gpio-controller;#gpio-cells = <2>;interrupt-controller;#interrupt-cells = <2>;gpio-ranges = <&iomuxc 0 94 17>, <&iomuxc 17 117 12>;
};
-
compatible :与GPIO子系统的平台驱动做匹配。
-
reg :GPIO寄存器的基地址,GPIO4的寄存器组是的映射地址为0x20a8000-0x20ABFFF
-
interrupts :描述中断相关的信息
-
clocks :初始化GPIO外设时钟信息
-
gpio-controller :表示gpio4是一个GPIO控制器
-
#gpio-cells :表示有多少个cells来描述GPIO引脚
-
interrupt-controller :表示gpio4也是个中断控制器
-
#interrupt-cells :表示用多少个cells来描述一个中断
-
gpio-ranges :将gpio编号转换成pin引脚,<&iomuxc 0 94 17>,表示将gpio4的第0个引脚引脚映射为94, 17表示的是引脚的个数。
gpio4这个节点对整个gpio4进行了描述。使用GPIO子系统时需要往设备树中添加设备节点,在驱动程序中使用GPIO子系统提供的API 实现控制GPIO的效果。
1.2.1. 在设备树中添加RGB灯的设备树节点
相比之前led灯的设备树节点(没有使用GPIO子系统),这里只需要增加GPIO属性定义,基于GPIO子系统的rgb_led设备树节点 添加到“./arch/arm/boot/dts/imx6ull-mmc-npi.dtb”设备树的根节点内。 添加完成后的设备树如下所示。
/*添加rgb_led节点*/
rgb_led{#address-cells = <1>;#size-cells = <1>;pinctrl-names = "default";compatible = "fire,rgb-led";pinctrl-0 = <&pinctrl_rgb_led>;rgb_led_red = <&gpio1 4 GPIO_ACTIVE_LOW>;rgb_led_green = <&gpio4 20 GPIO_ACTIVE_LOW>;rgb_led_blue = <&gpio4 19 GPIO_ACTIVE_LOW>;status = "okay";
};
-
第6行,设置“compatible”属性值,与led的平台驱动做匹配。
-
第7行,指定RGB灯的引脚pinctrl信息,上一小节我们定义了pinctrl节点,并且标签设置为“pinctrl_rgb_led”, 在这里我们引用了这个pinctrl信息。
-
第8-10行,指定引脚使用的哪个GPIO,编写格式如下所示。

-
标号①,设置引脚名字,如果使用GPIO子系统提供的API操作GPIO,在驱动程序中会用到这个名字,名字是自定义的。
-
标号②,指定GPIO组。
-
标号③,指定GPIO编号。
-
编号④,这是一个宏定义,指定有效电平,低电平有效选择“GPIO_ACTIVE_LOW”高电平有效选择“GPIO_ACTIVE_HIGH”。
1.2.2. 编译、下载设备树验证修改结果
前两小节我们分别在设备树中将RGB灯使用的引脚添加到pinctrl子系统,然后又在设备树中添加了rgb_led设备树节点。 这一小节将会编译、下载修改后的设备树,用新的设备树启动系统,然后检查是否有rgb_led设备树节点产生。
编译内核时会自动编译设备树,我们可以直接重新编译内核,这样做的缺点是编译时间会很长。 在内核目录下(~/ebf_linux_kernel)执行如下命令,只编译设备树:
命令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
如果执行了“make distclean”清理了内核,那么就需要在内核目录下执行如下命令重新配置内核 (如果编译设备树出错也可以先清理内核然后执行如下命令尝试重新编译)。
命令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- npi_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
编译成功后会在“./arch/arm/boot/dts”目录下生成“imx6ull-mmc-npi.dtb”文件,将其替换掉板子/usr/lib/linux-image-4.19.35-imx6/ 目录下的imx6ull-mmc-npi.dtb文件并输入 sudo reboot 重启开发板。
#这里操作命令仅作为参考,实际根据自己电脑情况进行修改#将生成的设备树拷贝到共享文件夹
cp arch/arm/boot/dts/imx6ull-mmc-npi.dtb /home/Embedfire/wokdfir#挂载nfs共享文件夹(在开发板上)
sudo mount -f nfs 192.168.0.231:/home/Embedfire/wokdfir /mnt#复制设备树到共享文件夹(在开发板上)
cp /mnt/imx6ull-mmc-npi.dtb /usr/lib/linux-image-4.19.35-imx6/#重启开发板
sudo reboot
使用新的设备树重新启动之后正常情况下会在开发板的“/proc/device-tree”目录下生成“rgb_led”设备树节点。如下所示。

1.2.3. GPIO子系统常用API函数讲解
之前两小节我们修改设备树并编译、下载到开发板。设备树部分已经完成了,这里介绍GPIO子系统常用的几个API函数, 然后就可以使用GPIO子系统编写RGB驱动了。
1. 获取GPIO编号函数of_get_named_gpio
GPIO子系统大多数API函数会用到GPIO编号。GPIO编号可以通过of_get_named_gpio函数从设备树中获取。
of_get_named_gpio函数(内核源码include/linux/of_gpio.h):
static inline int of_get_named_gpio(struct device_node *np, const char *propname, int index)
参数:
-
np: 指定设备节点。
-
propname: GPIO属性名,与设备树中定义的属性名对应。
-
index: 引脚索引值,在设备树中一条引脚属性可以包含多个引脚,该参数用于指定获取那个引脚。
返回值:
-
成功: 获取的GPIO编号(这里的GPIO编号是根据引脚属性生成的一个非负整数),
-
失败: 返回负数。
2. GPIO申请函数gpio_request
gpio_request函数(内核源码drivers/gpio/gpiolib-legacy.c):
static inline int gpio_request(unsigned gpio, const char *label);
参数:
-
gpio: 要申请的GPIO编号,该值是函数of_get_named_gpio的返回值。
-
label: 引脚名字,相当于为申请得到的引脚取了个别名。
返回值:
-
成功: 返回0,
-
失败: 返回负数。
3. GPIO释放函数
gpio_free函数(内核源码drivers/gpio/gpiolib-legacy.c):
static inline void gpio_free(unsigned gpio);
gpio_free函数与gpio_request是一对相反的函数,一个申请,一个释放。一个GPIO只能被申请一次, 当不再使用某一个引脚时记得将其释放掉。
4. GPIO输出设置函数gpio_direction_output
用于将引脚设置为输出模式。
gpio_direction_output函数(内核源码include/asm-generic/gpio.h):
static inline int gpio_direction_output(unsigned gpio , int value);
函数参数:
-
gpio: 要设置的GPIO的编号。
-
value: 输出值,1,表示高电平。0表示低电平。
返回值:
-
成功: 返回0
-
失败: 返回负数。
5. GPIO输入设置函数gpio_direction_input
用于将引脚设置为输入模式。
gpio_direction_input函数(内核源码include/asm-generic/gpio.h):
static inline int gpio_direction_input(unsigned gpio);
6. 获取GPIO引脚值函数gpio_get_value
用于获取引脚的当前状态。无论引脚被设置为输出或者输入都可以用该函数获取引脚的当前状态。
gpio_get_value函数(内核源码include/asm-generic/gpio.h)
static inline int gpio_get_value(unsigned gpio);
7. 设置GPIO输出值gpio_set_value
该函数只用于那些设置为输出模式的GPIO.
gpio_direction_output函数(内核源码include/asm-generic/gpio.h):
static inline int gpio_direction_output(unsigned gpio, int value);
函数参数
-
gpio: 设置的GPIO的编号。
-
value: 设置的输出值,为1输出高电平,为0输出低电平。
返回值:
-
成功: 返回0
-
失败: 返回负数
根据上面这些函数我们就可以在驱动程序中控制IO口了。
1.3. 实验说明与代码讲解
硬件介绍
本节实验使用到 EBF6ULL-PRO 开发板上的 RGB 彩灯
硬件原理图分析
参考”字符设备驱动–点亮LED灯”章节
1.3.1. 实验代码讲解
本章的示例代码目录为:linux_driver/gpio_subsystem_rgb_led
程序包含两个C语言文件,一个是驱动程序,驱动程序在平台总线基础上编写。 另一个是一个简单的测试程序,用于测试驱动是否正常。
1.3.1.1. 驱动程序讲解
驱动程序大致分为三个部分,第一部分,编写平台设备驱动的入口和出口函数。第二部分,编写平台设备的.probe函数, 在probe函数中实现字符设备的注册和RGB灯的初始化。第三部分,编写字符设备函数集,实现open和write函数。
平台驱动入口和出口函数实现
平台驱动框架:
/*------------------第一部分----------------*/
static const struct of_device_id rgb_led[] = {
{ .compatible = "fire,rgb-led"},{ /* sentinel */ }
};/*定义平台驱动结构体*/
struct platform_driver led_platform_driver = {.probe = led_probe,.driver = {.name = "rgb-leds-platform",.owner = THIS_MODULE,.of_match_table = rgb_led,}
};/*------------------第二部分----------------*/
/*驱动初始化函数*/
static int __init led_platform_driver_init(void)
{int error;error = platform_driver_register(&led_platform_driver);printk(KERN_EMERG "\tDriverState = %d\n",error);return 0;
}/*------------------第三部分----------------*/
/*驱动注销函数*/
static void __exit led_platform_driver_exit(void)
{printk(KERN_EMERG "platform_driver_exit!\n");platform_driver_unregister(&led_platform_driver);
}module_init(led_platform_driver_init);
module_exit(led_platform_driver_exit);MODULE_LICENSE("GPL");
-
第2-15行:为代码的第一部分,仅实现.probe函数和.driver,当驱动和设备匹配成功后会执行该函数, 这个函数的函数实现我们在后面介绍。.driver描述这个驱动的属性,包括.name驱动的名字,.owner驱动的所有者, .of_match_table驱动匹配表,用于匹配驱动和设备。驱动设备匹配表定义为“rgb_led”在这个表里只有一个匹配值 “.compatible = “fire,rgb-led” ”这个值要与我们在设备树中rgb_led设备树节点的“compatible”属性相同。
-
第17-40行:第二、三部分是平台设备的入口和出口函数,函数实现很简单,在入口函数中注册平台驱动,在出口函数中注销平台驱动。
平台驱动.probe函数实现
当驱动和设备匹配后首先会probe函数,我们在probe函数中实现RGB的初始化、注册一个字符设备。 后面将会在字符设备操作函数(open、write)中实现对RGB等的控制。函数源码如下所示。
static int led_probe(struct platform_device *pdv)
{unsigned int register_data = 0; //用于保存读取得到的寄存器值int ret = 0; //用于保存申请设备号的结果printk(KERN_EMERG "\t match successed \n");/*------------------第一部分---------------*//*获取RGB的设备树节点*/rgb_led_device_node = of_find_node_by_path("/rgb_led");if(rgb_led_device_node == NULL){printk(KERN_EMERG "\t get rgb_led failed! \n");}/*------------------第二部分---------------*/rgb_led_red = of_get_named_gpio(rgb_led_device_node, "rgb_led_red", 0);rgb_led_green = of_get_named_gpio(rgb_led_device_node, "rgb_led_green", 0);rgb_led_blue = of_get_named_gpio(rgb_led_device_node, "rgb_led_blue", 0);printk("rgb_led_red = %d,\n rgb_led_green = %d,\n rgb_led_blue = %d,\n", rgb_led_red,\rgb_led_green,rgb_led_blue);/*------------------第三部分---------------*/gpio_direction_output(rgb_led_red, 1);gpio_direction_output(rgb_led_green, 1);gpio_direction_output(rgb_led_blue, 1);/*------------------第四部分---------------*//*---------------------注册 字符设备部分-----------------*///第一步//采用动态分配的方式,获取设备编号,次设备号为0,//设备名称为rgb-leds,可通过命令cat /proc/devices查看//DEV_CNT为1,当前只申请一个设备编号ret = alloc_chrdev_region(&led_devno, 0, DEV_CNT, DEV_NAME);if(ret < 0){printk("fail to alloc led_devno\n");goto alloc_err;}//第二步//关联字符设备结构体cdev与文件操作结构体file_operationsled_chr_dev.owner = THIS_MODULE;cdev_init(&led_chr_dev, &led_chr_dev_fops);//第三步//添加设备至cdev_map散列表中ret = cdev_add(&led_chr_dev, led_devno, DEV_CNT);if(ret < 0){printk("fail to add cdev\n");goto add_err;}//第四步/*创建类 */class_led = class_create(THIS_MODULE, DEV_NAME);/*创建设备*/device = device_create(class_led, NULL, led_devno, NULL, DEV_NAME);return 0;add_err://添加设备失败时,需要注销设备号unregister_chrdev_region(led_devno, DEV_CNT);printk("\n error! \n");
alloc_err:return -1;
}
实现字符设备函数
字符设备函数我们只需要实现open函数和write函数。函数源码如下。
/*------------------第一部分---------------*/
/*字符设备操作函数集*/
static struct file_operations led_chr_dev_fops =
{.owner = THIS_MODULE,.open = led_chr_dev_open,.write = led_chr_dev_write,
};/*------------------第二部分---------------*/
/*字符设备操作函数集,open函数*/
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{printk("\n open form driver \n");return 0;
}/*------------------第三部分---------------*/
/*字符设备操作函数集,write函数*/
static ssize_t led_chr_dev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{unsigned char write_data; //用于保存接收到的数据int error = copy_from_user(&write_data, buf, cnt);if(error < 0) {return -1;}/*设置 GPIO1_04 输出电平*/if(write_data & 0x04){gpio_direction_output(rgb_led_red, 0); // GPIO1_04引脚输出低电平,红灯亮}else{gpio_direction_output(rgb_led_red, 1); // GPIO1_04引脚输出高电平,红灯灭}/*设置 GPIO4_20 输出电平*/if(write_data & 0x02){gpio_direction_output(rgb_led_green, 0); // GPIO4_20引脚输出低电平,绿灯亮}else{gpio_direction_output(rgb_led_green, 1); // GPIO4_20引脚输出高电平,绿灯灭}/*设置 GPIO4_19 输出电平*/if(write_data & 0x01){gpio_direction_output(rgb_led_blue, 0); // GPIO4_19引脚输出低电平,蓝灯亮}else{gpio_direction_output(rgb_led_blue, 1); // GPIO4_19引脚输出高电平,蓝灯灭}return 0;
}
-
代码3-8行:定义字符设备操作函数集,这里主要实现open和write函数即可。
-
代码12-16行:实现open函数,在平台驱动的prob函数中已经初始化了GPIO,这里不用做任何操作
-
代码20-60行:write函数实现也很简单,首先使用“copy_from_user”函数将来自应用层的数据“拷贝”内核层。 得到命令后就依次检查后三位,根据命令值使用“gpio_direction_output”函数控制RGB灯的亮灭。
1.3.1.2. 应用程序讲解
应用程序编写比较简单,我们只需要打开设备节点文件,写入命令然后关闭设备节点文件即可。源码如下所示。
int main(int argc, char *argv[])
{/*判断输入的命令是否合法*/if(argc != 2){printf(" commend error ! \n");return -1;}/*打开文件*/int fd = open("/dev/rgb-leds", O_RDWR);if(fd < 0){printf("open file : %s failed !\n", argv[0]);return -1;}unsigned char commend = atoi(argv[1]); //将受到的命令值转化为数字;/*判断命令的有效性*//*写入命令*/int error = write(fd,&commend,sizeof(commend));if(error < 0){printf("write file error! \n");close(fd);/*判断是否关闭成功*/}/*关闭文件*/error = close(fd);if(error < 0){printf("close file error! \n");}return 0;
}
结合代码各部分说明如下:
-
代码4-8行:判断命令是否有效。再运行应用程序时我们要传递一个控制命令,所以参数长度是2。
-
代码11-16行:打开设备文件。参数“/dev/rgb-leds”用于指定设备节点文件,设备节点文件名是在驱动程序中设置的, 这里保证与驱动一致即可。
-
代码18-35行:由于从main函数中获取的参数是字符串,这里首先要将其转化为数字。最后条用write函数写入命令然后关闭文件即可。
1.3.2. 实验准备
1.3.2.1. Makefile修改说明
修改Makefile并编译生成驱动程序
Makefile程序并没有大的变化,修改后的Makefile如下所示。
Makefile文件
KERNEL_DIR=../ebf_linux_kernel/build_image/buildARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH CROSS_COMPILEobj-m := rgb-leds.oapp_in = rgb_leds_app.c
app_out = rgb_leds_appall:$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules$(CROSS_COMPILE)gcc -o $(app_out) $(app_in).PHONY:clean
clean:$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) cleanrm $(app_out)
-
代码第1行:变量“KERNEL_DIR”保存的是内核所在路径,这个需要根据自己内核所在位置设定。
-
代码第7行:“obj-m := rgb-leds.o”中的“rgb-leds.o”要与驱动源码名对应。Makefiel 修改完成后执行如下命令编译驱动。
-
代码第9行:“rgb_leds_app.c” 是需要编译的应用程序。
-
代码第10行:“rgb_leds_app” 是编译应用程序后输出的应用程序可以执行文件。
命令:
make
正常情况下会在当前目录生成.ko驱动文件和rgb_leds_app应用程序可执行文件。
1.3.3. 下载验证
前两小节我们已经编译出了.ko驱动和应用程序,将驱动程序和应用程序添加到开发板中

执行如下命令加载驱动:
命令:
insmod ./rgb-leds.ko
正常情况下输出结果如下所示。

在驱动程序中,我们在.probe函数中注册字符设备并创建了设备文件,设备和驱动匹配成功后.probe函数已经执行, 所以正常情况下在“/dev/”目录下已经生成了“rgb-leds”设备节点,如下所示。

驱动加载成功后直接运行应用程序如下所示。
./rgb_leds_app <命令>
执行结果如下:

命令是一个“unsigned char”型数据,只有后三位有效,每一位代表一个灯,从高到低依次代表红、绿、蓝,1表示亮,0表示灭。 例如命令=4 则亮红灯,命令=7则三个灯全亮。
相关文章:
Linux驱动开发(9):pinctrl子系统和gpio子系统--led实验
在前面章节,我们有过使用寄存器去编写字符设备的经历了。这种直接在驱动代码中, 通过寄存器映射来对外设进行使用的编程方式,从驱动开发者的角度可以说是灾难。 因为每当芯片的寄存器发生了改动,那么底层的驱动几乎得重写。 那么…...
用sqlmap工具打sqli-labs前20关靶场
这个星期我们用手动注入打了前20关靶场,今天我们用sqlmap直接梭哈前20关 1.介绍sqlmap sqlmap是一个自动化的SQL注入工具,其主要功能是扫描,发现并利用给定的URL和SQL注入漏洞。 2.下载和使用sqlmap 官方下载地址:GitHub - sq…...
代码随想录算法训练营第二十一天 | 93.复原IP地址 | 78.子集
Day 20 总结 自己实现中遇到哪些困难 一句话讲明白问题分类 组合问题和分割问题都是收集树的叶子节点,子集问题是找树的所有节点!切割字符串问题回顾 昨天的切割回文子串,和今天的切割ip地址,都是需要将字符串拆分成 n 份。只不过…...
#Uniapp篇:支持纯血鸿蒙发布适配UIUI
uni-ui梳理 组件生命周期 https://uniapp.dcloud.net.cn/tutorial/page.html#componentlifecycle 页面生命周期 https://uniapp.dcloud.net.cn/collocation/App.html#applifecycle onLaunch 当uni-app 初始化完成时触发(全局只触发一次),…...
边缘提取函数 [OPENCV--2]
OPENCV中最常用的边界检测是CANNY函数 下面展示它的用法 通常输入一个灰度图像(边界一般和颜色无关)这样也可以简化运算cv::Canny(inmat , outmat , therhold1, therhold2 ) 第一个参数是输入的灰度图像,第二个是输出的图像这两个参数都是引用…...
插值原理(数值计算方法)
插值原理(数值计算方法) 一. 原理介绍二. 图例三. 唯一性表述 一. 原理介绍 在数学中,插值(Interpolation)是指通过已知的离散数据点,构造一个连续的函数,该函数能够精确地通过这些数据点&#…...
【Pikachu】SSRF(Server-Side Request Forgery)服务器端请求伪造实战
尽人事以听天命 1.Server-Side Request Forgery服务器端请求伪造学习 SSRF(服务器端请求伪造)攻击的详细解析与防范 SSRF(Server-Side Request Forgery,服务器端请求伪造) 是一种安全漏洞,它允许攻击者通…...
IDEA怎么定位java类所用maven依赖版本及引用位置
在实际开发中,我们可能会遇到需要搞清楚代码所用依赖版本号及引用位置的场景,便于排查问题,怎么通过IDEA实现呢? 可以在IDEA中打开项目,右键点击maven的pom.xml文件,或者在maven窗口下选中项目,…...
Discuz论坛网站管理员的默认用户名admin怎么修改啊?
当我们在某个论坛注册账号后,处于某种原因想要修改用户名,该如何修改? Discuz论坛网站管理员处于安全性或某种原因想要修改默认用户名admin该如何修改?驰网飞飞和你分享 其实非常简单,但是普通用户没有修改权限&…...
BIO、NIO、AIO的区别?
文章目录 BIO、NIO、AIO的区别?为什么不使用java 原生nio哪些项目使用了netty BIO阻塞I/O存在问题 NIO(nonblocking IO)Java NIO channel(通道)、buffer、selector(选择器) AIO(Asynchronous I/O) BIO、NIO…...
音视频入门基础:MPEG2-TS专题(7)——FFmpeg源码中,读取出一个transport packet数据的实现
一、引言 从《音视频入门基础:MPEG2-TS专题(3)——TS Header简介》可以知道,TS格式有三种:分别为transport packet长度固定为188、192和204字节。而FFmpeg源码中是通过read_packet函数从一段MPEG2-TS传输流/TS文件中读…...
Flutter中sqflite的使用案例
目录 引言 安装sqflite 创建表 查询数据 添加数据 删除数据 更新数据 完整使用案例 引言 随着移动应用的发展,本地数据存储成为了一个不可或缺的功能。在Flutter中,sqflite 是一个非常流行且强大的SQLite插件,它允许开发者在移动设备…...
【2024 Optimal Control 16-745】【Lecture 2】integrators.ipynb功能分析
代码功能分析 导入库和项目设置 import Pkg; Pkg.activate(__DIR__); Pkg.instantiate()功能:激活当前文件夹为 Julia 项目环境,并安装当前项目中缺失的依赖包。 import Pkg: 导入 Julia 的包管理模块 Pkg,用于管理项目依赖。 …...
【linux】ubuntu下常用快捷键【笔记】
环境 硬件:通用PC 系统:Ubuntu 20.04 软件 : 打开终端窗口:Ctrl Alt T 关闭当前窗口:Alt F4 改变窗口大小:Alt F8 移动窗口: Alt F7 配合 “←”、“→”、“↑”、“↓”来移动窗口 …...
【Linux】常用命令练习
一、常用命令 1、在/hadoop目录下创建src和WebRoot两个文件夹 分别创建:mkdir -p /hadoop/src mkdir -p /hadoop/WebRoot 同时创建:mkdir -p /hadoop/{src,WebRoot}2、进入到/hadoop目录,在该目录下创建.classpath和README文件 分别创建&am…...
力扣-Hot100-数组【算法学习day.37】
前言 ###我做这类文档一个重要的目的还是给正在学习的大家提供方向(例如想要掌握基础用法,该刷哪些题?)我的解析也不会做的非常详细,只会提供思路和一些关键点,力扣上的大佬们的题解质量是非常非常高滴&am…...
表格不同类型的数据如何向量化?
在进行机器学习项目时,首先需要获取数据,这些数据可以来自数据库、API、网络抓取,或从CSV、Excel等文件中读取。数据可能包含数值、文本和类别等多种特征,但原始数据通常无法直接用于训练模型。 数据预处理包括清洗、填补缺失值和…...
成都栩熙酷,电商服务新选择
在当今数字经济蓬勃发展的时代,电商平台已成为推动商业创新、促进消费升级的重要力量。抖音小店,作为短视频与电商深度融合的产物,凭借其独特的社交属性和内容营销优势,迅速吸引了大量用户和商家的关注。在这场变革中,…...
【java基础】微服务篇
参考黑马八股视频。 目录 Spring Cloud 5大组件 注册中心 负载均衡 限流 CAP和BASE 分布式事务解决方案 分布式服务的接口幂等性 分布式任务调度 Spring Cloud 5大组件 注册中心 Eureka的作用 健康监控 负载均衡 限流 漏桶固定速率,令牌桶不限速 CAP和BA…...
【LLM训练系列02】如何找到一个大模型Lora的target_modules
方法1:观察attention中的线性层 import numpy as np import pandas as pd from peft import PeftModel import torch import torch.nn.functional as F from torch import Tensor from transformers import AutoTokenizer, AutoModel, BitsAndBytesConfig from typ…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...
Git常用命令完全指南:从入门到精通
Git常用命令完全指南:从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...
go 里面的指针
指针 在 Go 中,指针(pointer)是一个变量的内存地址,就像 C 语言那样: a : 10 p : &a // p 是一个指向 a 的指针 fmt.Println(*p) // 输出 10,通过指针解引用• &a 表示获取变量 a 的地址 p 表示…...
