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…...
uni-app快速入门(八)--常用内置组件(上)
uni-app提供了一套基础组件,类似HTML里的标签元素,不推荐在uni-app中使用使用div等HTML标签。在uni-app中,对应<div>的标签是view,对应<span>的是text,对应<a>的是navigator,常用uni-app…...
基于Amazon Bedrock:一站式多模态数据处理新体验
目录 引言 关于Amazon Bedrock 基础模型体验 1、进入环境 2、发现模型及快速体验 3、打开 Amazon Bedrock 控制台 4、通过 Playgrounds 体验模型 (1)文本生成 (2)图片生成 关于资源清理 结束语 引言 在云计算和人工智能…...
FAX动作文件优化脚本(MAX清理多余关键帧插件)
大较好,为大家介绍一个节省FBX容量的插件!只保留有用的动画轴向,其他不参与动画运动的清除! 一.插件目的:: 1.我们使用的U3D引擎产生的游戏资源包容量太大,故全方位优化动画资源; 2.在max曲线编辑器内,点取轴向太过麻烦,费事,直观清除帧大大提高效率。 如: 二:…...
Chapter 2 - 16. Understanding Congestion in Fibre Channel Fabrics
Transforming an I/O Operation to FC frames A read or write I/O operation (Figure 2-28) between an initiator and a target undergoes a series of transformations before being transmitted on a Fibre Channel link. 启动程序和目标程序之间的读取或写入 I/O 操作(图…...
mysql数据库(六)pymysql、视图、触发器、存储过程、函数、流程控制、数据库连接池
pymysql、视图、触发器、存储过程、函数、流程控制、数据库连接池 文章目录 pymysql、视图、触发器、存储过程、函数、流程控制、数据库连接池一、pymysql二、视图三、触发器四、存储过程五、函数六、流程控制七、数据库连接池 一、pymysql 可以使用pip install pymysql安装py…...
RFdiffusion EuclideanDiffuser类解读
EuclideanDiffuser 是 RFdiffusion 中的一个关键类,专门设计用于对**三维空间中的点(如蛋白质的原子坐标)**进行扩散处理。它通过逐步向这些点添加噪音来实现扩散过程,从而为扩散模型提供输入数据,并通过逆扩散还原这些数据。 get_beta_schedule函数源代码 def get_beta…...
Flutter实现气泡提示框学习
前置知识点学习 GlobalKey GlobalKey 是 Flutter 中一个非常重要的概念,它用于唯一标识 widget 树中的特定 widget,并提供对该 widget 的访问。这在需要跨越 widget 树边界进行交互或在 widget 树重建时保持状态时尤其有用。 GlobalKey 的作用 唯一标…...
vue3 路由守卫
在Vue 3中,路由守卫是一种控制和管理路由跳转的机制。它允许你在执行导航前后进行一些逻辑处理,比如权限验证、数据预取等,从而增强应用的安全性和效率。路由守卫分为几种不同的类型,每种类型的守卫都有其特定的应用场景。 其实路…...
【MATLAB源码-第218期】基于matlab的北方苍鹰优化算法(NGO)无人机三维路径规划,输出做短路径图和适应度曲线.
操作环境: MATLAB 2022a 1、算法描述 北方苍鹰优化算法(Northern Goshawk Optimization,简称NGO)是一种新兴的智能优化算法,灵感来源于北方苍鹰的捕猎行为。北方苍鹰是一种敏捷且高效的猛禽,广泛分布于北…...
如何控制自己玩手机的时间?两台苹果手机帮助自律
对一些人来说,被智能手机“绑架”是一件心甘情愿的事,和它相处的一天中,不必面对现实的压力,它就像个“舒适区”。这是因为在使用手机的过程中,应用程序(尤其是游戏和社交媒体应用)会不断刺激大…...
杰奇网站地图怎么做/百度外推排名
下面是两个Widget: 蓝图: 这里一定要是这个才正常,我找了一天的原因。 4.12 时,设为Visible也是正常的,4.13要是使用了WidgetInteration,就一定这样才行。 切记切记。...
宜宾市政府网站建设/津seo快速排名
随时随地阅读更多技术实战干货,获取项目源码、学习资料,请关注源代码社区公众号(ydmsq666) 转自:http://blog.csdn.net/eager3466/article/details/42931017 Eclipse中安卓开发遇到parseSdkContent failed Could not initialize class Andro…...
wordpress搜索插件/黑帽seo排名技术
导读:数字藏品,是指使用区块链技术,对应特定的作品、艺术品生成的唯一数字凭证,在保护其数字版权的基础上,实现真实可信的数字化发行、购买、收藏和使用。数字建筑的本质是通过融合新一代信息技术和先进制造理念&#…...
做网站的生产方式/外贸网站建设公司哪家好
2021.9.5 很多地方有改动,添加了自己的理解!!! 0 前言 用Chisel编写的CPU,比如Rocket-Chip、RISCV-Mini等,都有一个特点,就是可以用一个配置文件来裁剪电路,其实它就使用到了我们说…...
承接网站建设广告语/一键关键词优化
20165107 《网络对抗技术》Exp1 PC平台逆向破解 实践内容 本次实践的对象是一个名为pwn1的linux可执行文件,该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。该程序同时包含另一个代码片段,getShell࿰…...
厦门定制型网站建设/万网域名注册官网阿里云
内联函数 以inline修饰的函数叫做内联函数,编译时C编译器会在调用内联函数的地方展开,没有函数压 栈的开销,内联函数提升程序运行的效率 #define Fun(x,y) x*y #define N 10 int Mul(int x, int y) {return x*y; } int main() {cout <<…...