在前面章节,我们有过使用寄存器去编写字符设备的经历了。这种直接在驱动代码中,通过寄存器映射来对外设进行使用的编程方式,从驱动开发者的角度可以说是灾难。因为每当芯片的寄存器发生了改动,那么底层的驱动几乎得重写。
那么在这个问题上,我们更进了一步,学会了使用设备树来描述外设的各种信息(比如寄存器地址),而不是将寄存器的这些内容放在驱动代码里。这样即使设备信息修改了,我们还是可以通过设备树的接口函数,去灵活的获取设备的信息。极大得提高了驱动的复用能力。
现在我们可以通过在驱动程序代码里使用设备树接口,来获取到外设的信息了。但是,在前面的设备树演示中,我们还是将寄存器操作具体细节体现在了驱动中,比如置位操作。
那么,在驱动中有没有更通用的方法,可以不涉及到具体的寄存器操作的内容呢?对于有些外设,是具备抽象条件的,也就是说我们可以将对这些外设的操作统一起来。
比如本章中将为大家介绍的pinctrl 子系统和GPIO 子系统。
本章将会使用GPIO 子系统实现LED 驱动,GPIO 子系统要用到pinctrl 子系统,所以本章将pinctrl子系统和GPIO 子系统放在一块讲解。
pinctrl 子系统
pinctrl 子系统主要用于管理芯片的引脚。stm32mp1 芯片拥有众多的片上外设,大多数外设需要通过芯片的引脚与外部设备(器件)相连实现相对应的控制,例如我们熟悉的I2C、SPI、LCD、USDHC 等等。而我们知道芯片的可用引脚(除去电源引脚和特定功能引脚)数量是有限的,芯片的设计厂商为了提高硬件设计的灵活性,一个芯片引脚往往可以做为多个片上外设的功能引脚,如下图所示。
PZ3、PI9 的功能引脚不单单只可以使用在UART 上,也可以作为多个外设的功能引脚,如普通的GPIO 引脚,串口的接收发送引脚等,在设计硬件时我们可以根据需要灵活的选择其中的一个。设计完硬件后每个引脚的功能就确定下来了,假设我们将上面的两个引脚连接到其他用串口控制的外部设备上,那么这两个引脚功能就做为了UART 的接收、发送引脚。在编程过程中,无论是裸机还是驱动,一般首先要设置引脚的复用功能并且设置引脚的PAD 属性(驱动能力、上下拉等等)。
在驱动程序中我们需要手动设置每个引脚的复用功能,不仅增加了工作量,编写的驱动程序不方便移植,可重用性差等。更糟糕的是缺乏对引脚的统一管理,容易出现引脚的重复定义。假设我们在I2C 的驱动中将UART_RX_DATA 引脚和UART_TX_DATA 引脚复用为SCL 和SDA,恰好在编写UART 驱动驱动时没有注意到UART_RX_DATA 引脚和UART_TX_DATA 引脚已经被使用,在驱动中又将其初始化为UART_RX 和UART_TX,这样IIC 驱动将不能正常工作,并且这种错误很难被发现。
pinctrl 子系统是由芯片厂商来实现的,简单来说用于帮助我们管理芯片引脚并自动完成引脚的初始化,而我们要做的只是在设备树中按照规定的格式写出想要的配置参数即可。
pinctrl 子系统编写格式以及引脚属性详解
首先我们在/ebf_linux_kernel/arch/arm/boot/dts/stm32mp157-pinctrl.dtsi 文件中,可以看到如下定义
列表1: stm32mp157-pinctrl.dtsi
soc {
pinctrl: pin-controller@50002000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "st,stm32mp157-pinctrl";
ranges = <0 0x50002000 0xa400>;
interrupt-parent = <&exti>;
st,syscfg = <&exti 0x60 0xff>;
hwlocks = <&hsem 0>;
pins-are-numbered;
}
/* 剩下内容省略*/
};
- compatible:修饰的是与平台驱动做匹配的名字, 这里则是与pinctrl 子系统的平台驱动做匹配。
- reg:表示的是引脚配置寄存器的基地址。
stm32mp157-pinctrl.dtsi 这个文件是芯片厂商官方将芯片的通用的部分单独提出来的一些设备树配置。在soc 节点中汇总了所需引脚的配置信息,pinctrl 子系统存储使用着的节点信息。
我们的设备树主要的配置文件在/arch/arm/boot/dts/stm32mp157a-basic.dts 中,打开stm32mp157abasic.dts,在文件中搜索“&pinctrl”找到设备树中引用“pinctrl”节点的位置如下所示。
列表2: stm32mp157a-basic.dts 中&pinctrl 部分内容
&pinctrl{
fmc_pins_a: fmc-0 {
pins1 {
pinmux = <STM32_PINMUX('D', 4, AF12)>, /* FMC_NOE */
<STM32_PINMUX('D', 5, AF12)>, /* FMC_NWE */
<STM32_PINMUX('D', 11, AF12)>, /* FMC_A16_FMC_CLE */
<STM32_PINMUX('D', 12, AF12)>, /* FMC_A17_FMC_ALE */
<STM32_PINMUX('D', 14, AF12)>, /* FMC_D0 */
<STM32_PINMUX('D', 15, AF12)>, /* FMC_D1 */
<STM32_PINMUX('D', 0, AF12)>, /* FMC_D2 */
<STM32_PINMUX('D', 1, AF12)>, /* FMC_D3 */
<STM32_PINMUX('E', 7, AF12)>, /* FMC_D4 */
<STM32_PINMUX('E', 8, AF12)>, /* FMC_D5 */
<STM32_PINMUX('E', 9, AF12)>, /* FMC_D6 */
<STM32_PINMUX('E', 10, AF12)>, /* FMC_D7 */
<STM32_PINMUX('G', 9, AF12)>; /* FMC_NE2_FMC_NCE */
bias-disable;
drive-push-pull;
slew-rate = <1>;
};
pins2 {
pinmux = <STM32_PINMUX('D', 6, AF12)>; /* FMC_NWAIT */
bias-pull-up;
};
};
fmc_sleep_pins_a: fmc-sleep-0 {
pins {
pinmux = <STM32_PINMUX('D', 4, ANALOG)>, /* FMC_NOE */
<STM32_PINMUX('D', 5, ANALOG)>, /* FMC_NWE */
<STM32_PINMUX('D', 11, ANALOG)>, /* FMC_A16_FMC_CLE */
<STM32_PINMUX('D', 12, ANALOG)>, /* FMC_A17_FMC_ALE */
<STM32_PINMUX('D', 14, ANALOG)>, /* FMC_D0 */
<STM32_PINMUX('D', 15, ANALOG)>, /* FMC_D1 */
<STM32_PINMUX('D', 0, ANALOG)>, /* FMC_D2 */
<STM32_PINMUX('D', 1, ANALOG)>, /* FMC_D3 */
<STM32_PINMUX('E', 7, ANALOG)>, /* FMC_D4 */
<STM32_PINMUX('E', 8, ANALOG)>, /* FMC_D5 */
<STM32_PINMUX('E', 9, ANALOG)>, /* FMC_D6 */
<STM32_PINMUX('E', 10, ANALOG)>, /* FMC_D7 */
<STM32_PINMUX('D', 6, ANALOG)>, /* FMC_NWAIT */
<STM32_PINMUX('G', 9, ANALOG)>; /* FMC_NE2_FMC_NCE */
};
};
usbotg_hs_pins_a: usbotg_hs-0 {
pins {
pinmux = <STM32_PINMUX('A', 10, ANALOG)>; /* OTG_ID */
};
};
/* 剩下内容省略*/
};
在这里通过“&pinctrl”在“pinctrl”节点下追加内容。结合设备树源码介绍如下:
-
第2行:如第一个“fmc_pins_a”节点中,从名字来看我们可以知道该节点大概是描述“fmc”外设的引脚功能,在它的第一个子节点“pins1”中,使用了pinmux指定的一组引脚的复用功能。该外设的一些其他引脚使用的电气特性与第一组不同,所以可以用第二个子节点“pins2”来描述剩余的一下引脚。
-
第17-19 行,指定了“pins1”子节点使用的引脚的电气特性。
-
其余源码都是pinctrl 下的子节点了,各自描述了一些外设的使用到的引脚及与之对应的复用功能,它们都是按照一定的格式规范来编写。
那么我们会在什么情况下使用到pinctrl 呢?我们以&sdmmc1 这个外设的节点来看。
&sdmmc1 {
pinctrl-names = "default", "opendrain", "sleep";
pinctrl-0 = <&sdmmc1_b4_pins_a>;
pinctrl-1 = <&sdmmc1_b4_od_pins_a>;
pinctrl-2 = <&sdmmc1_b4_sleep_pins_a>;
broken-cd;
st,neg-edge;
bus-width = <4>;
vmmc-supply = <&v3v3>;
status = "okay";
};
- pinctrl-names:描述了sdmmc1 外设会使用到的三种引脚状态,分别是default、opendrain、sleep。
- pinctrl-0: 当外设处于default 状态下, 则使用pinctrl-0 中引用的引脚配置&sdmmc1_b4_pins_a。
- pinctrl-1: 当外设处于opendrain 状态下, 则使用pinctrl-1 中引用的引脚配置&sdmmc1_b4_od_pins_a。
- pinctrl-2: 当外设处于sleep 状态下, 则使用pinctrl-2 中引用的引脚配置&sdmmc1_b4_sleep_pins_a。
这样以来,我们就指定了这个外设使用到的引脚及其状态。
pinctrl 子节点编写格式
那么按照“&pinctrl”下节点的描述形式,我们也可以自己描述一下某个外设的pinctrl。
列表3: 举例说明
&pinctrl {
xxx: xxx {
pins {
pinmux = <STM32_PINMUX('A', 10, ANALOG)>;
};
};
};
如上述的一个外设xxx,其使用的引脚为GPIOA10,复用功能为ANALOG,电气特性没有指定,则为默认。
这里我们需要知道每个芯片厂商的pinctrl 子节点的编写格式并不相同,这不属于设备树的规范,是芯片厂商自定义的。如果我们想添加自己的pinctrl 节点,只要依葫芦画瓢按照上面的格式编写即可。
关于pinctrl 节点如何去描述,我们可以在内核文档目录中查找芯片产商给出的文档。如ST 官方的pinctrl 文档目录如下:
Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.txt
将RGB 灯引脚添加到pinctrl 子系统
本小节假设没有看过裸机部分RGB 灯章节,我们从看原理图开始,一步步将RGB 灯用到的三个引脚添加到pinctrl 子系统中。
查找RGB 灯使用的引脚
RGB 灯对应的原理图如下所示。
根据网络名在核心板上找到对应的引脚,如下。
rgb_led_red: GPIOA13
rgb_led_green: GPIOG2
rgb_led_blue: GPIOB5
在pinctrl 节点中添加pinctrl 子节点
添加子节点很简单,我们只需要将引脚信息以一定格式,写入到stm32mp157a-basic.dts 设备树文件中的pinctrl 子节点即可,添加完成后如下所示。
列表4: 新增pinctrl 子节点
&pinctrl {
/*----------新添加的内容--------------*/
pinctrl_rgb_led: rgb_led{
pins {
pinmux = <STM32_PINMUX('A', 13, GPIO)>,
<STM32_PINMUX('G', 2, GPIO)>,
<STM32_PINMUX('B', 5, GPIO)>;
drive-push-pull;
};
};
};
新增的节点名为“rgb_led”,名字任意选取,长度不要超过32 个字符,最好能表达出节点的信息。“pinctrl_rgb_led”节点标签,“pinctrl_”是固定的格式,后面的内容自定义的,我们将通过这个标签引用这个节点。
pins 的内容中,我们将LED 使用到的GPIO 引脚功能配置好了,因为pinctrl 各家芯片厂商各异,这里我们就不展开,具体大家可以参考官方的
Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.txt
文档,在添加完pinctrl 子节点后,系统会根据我们添加的配置信息将引脚初始化为GPIO 功能。
到这里关于pinctrl 子系统的使用就已经讲解完毕了,接下来介绍GPIO 子系统相关的内容。
GPIO 子系统
在没有使用GPIO 子系统之前,如果我们想点亮一个LED,首先要得到led 相关的配置寄存器,再手动地读、改、写这些配置寄存器实现控制LED 的目的。有了GPIO 子系统之后这部分工作由GPIO 子系统帮我们完成,我们只需要调用GPIO 子系统提供的API 函数即可完成GPIO 的控制动作。
在stm32mp157-pinctrl.dtsi 文件中的pinctrl 子节点记录着GPIO 控制器的寄存器地址,下面我们以GPIOA 为例介绍GPIOA 子节点相关内容
列表5: stm32mp157-pinctrl.dtsi 中GPIOA 节点内容
/ {
soc {
pinctrl: pin-controller@50002000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "st,stm32mp157-pinctrl";
ranges = <0 0x50002000 0xa400>;
interrupt-parent = <&exti>;
st,syscfg = <&exti 0x60 0xff>;
hwlocks = <&hsem 0>;
pins-are-numbered;
gpioa: gpio@50002000 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
reg = <0x0 0x400>;
clocks = <&rcc GPIOA>;
st,bank-name = "GPIOA";
status = "disabled";
};
/* 剩余内容省略*/
};
};
- compatible :与GPIO 子系统的平台驱动做匹配。
- ranges :GPIO 外设寄存器的基地址,在gpioa 的reg 属性中GPIOA 的寄存器组的映射地址为50002000,范围为0x400。
- interrupt-parent :表示中断控制器是exti 外设
- clocks :初始化GPIO 外设时钟信息
- gpio-controller :表示gpioa 是一个GPIO 控制器
- #gpio-cells :表示有多少个cells 来描述GPIO 引脚
- #interrupt-controller : 是中断控制器
- #interrupt-cells : 表示用多少个cells 来描述一个中断
大家大致有个了解就可以了,一般芯片产商会将这部分信息完善好。
gpioa 这个节点对整个gpioa 进行了描述。使用GPIO 子系统时需要往设备树中添加设备节点,在驱动程序中使用GPIO 子系统提供的API 实现控制GPIO 的效果。
在设备树中添加RGB 灯的设备树节点
相比之前led 灯的设备树节点(没有使用GPIO 子系统),这里只需要增加GPIO 属性定义,基于GPIO 子系统的rgb_led 设备树节点添加到“/arch/arm/boot/dts/stm32mp157a-basic.dts”设备树的根节点内。添加完成后的设备树如下所示。
列表6: 设备树中添加rgb_led 节点
/* 添加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 = <&gpioa 13 GPIO_ACTIVE_LOW>;
rgb_led_green = <&gpiog 2 GPIO_ACTIVE_LOW>;
rgb_led_blue = <&gpiob 5 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”。
编译、下载设备树验证修改结果
本章前两小节我们分别在设备树中将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- stm32mp157_ebf_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
编译成功后会在“./arch/arm/boot/dts”目录下生成“stm32mp157a-basic.dtb”文件,将其替换掉板子/boot/dtbs/目录下的stm32mp157a-basic.dtb 文件并重启开发板。
如用户有nfs 环境,可以参考如下传输dtb 文件的方法:
# 这里操作命令仅作为参考,实际根据自己电脑情况进行修改
# 将生成的设备树拷贝到共享文件夹
cp arch/arm/boot/dts/stm32mp157a-basic.dtb /home/Embedfire/wokdfir
# 挂载nfs 共享文件夹(在开发板上)
sudo mount -f nfs 192.168.0.231:/home/Embedfire/wokdfir /mnt
# 复制设备树到共享文件夹(在开发板上)
cp /mnt/stm32mp157a-basic.dtb /boot/dtbs/
# 重启开发板
reboot
使用新的设备树重新启动之后正常情况下会在开发板的“/proc/device-tree”目录下生成“rgb_led”设备树节点。如下所示。
至此,我们的设备已经添加到了系统中,下面我们可以尝试编写驱动来使用我们的LED 设备了。不过在这之前,我们需要先学习GPIO 子系统的一些函数。
GPIO 子系统常用API 函数讲解
之前两小节我们修改设备树并编译、下载到开发板。设备树部分已经完成了,这里介绍GPIO 子系统常用的几个API 函数,然后就可以使用GPIO 子系统编写RGB 驱动了。
- 获取GPIO 编号函数of_get_named_gpio
GPIO 子系统大多数API 函数会用到GPIO 编号。GPIO 编号可以通过of_get_named_gpio 函数从设备树中获取。
列表7: 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 编号是根据引脚属性生成的一个非负整数),
- 失败: 返回负数。
- GPIO 申请函数gpio_request
列表8: gpio_request 函数(内核源码drivers/gpio/gpioliblegacy.
c)
static inline int gpio_request(unsigned gpio, const char *label);
参数:
- gpio: 要申请的GPIO 编号,该值是函数of_get_named_gpio 的返回值。
- label: 引脚名字,相当于为申请得到的引脚取了个别名。
返回值:
- 成功: 返回0,
- 失败: 返回负数。
- GPIO 释放函数
列表9: gpio_free 函数(内核源码drivers/gpio/gpioliblegacy.c)
static inline void gpio_free(unsigned gpio);
gpio_free 函数与gpio_request 是一对相反的函数,一个申请,一个释放。一个GPIO 只能被申请一次,当不再使用某一个引脚时记得将其释放掉。
参数:
- gpio:要释放的GPIO 编号。
返回值:无
- GPIO 输出设置函数gpio_direction_output
用于将引脚设置为输出模式。
列表10: gpio_direction_output 函数(内核源码include/asm-generic/gpio.h)
static inline int gpio_direction_output(unsigned gpio , int value);
函数参数:
- gpio: 要设置的GPIO 的编号。
- value: 输出值,1,表示高电平。0 表示低电平。
返回值:
- 成功: 返回0
- 失败: 返回负数。
- GPIO 输入设置函数gpio_direction_input
用于将引脚设置为输入模式。
列表11: gpio_direction_input 函数(内核源码include/asmgeneric/gpio.h)
static inline int gpio_direction_input(unsigned gpio)
函数参数:
- gpio: 要设置的GPIO 的编号。
返回值:
- 成功: 返回0
- 失败: 返回负数。
- 获取GPIO 引脚值函数gpio_get_value
用于获取引脚的当前状态。无论引脚被设置为输出或者输入都可以用该函数获取引脚的当前状态。
列表12: gpio_get_value 函数(内核源码include/asmgeneric/gpio.h)
static inline int gpio_get_value(unsigned gpio);
函数参数:
- gpio: 要获取的GPIO 的编号。
返回值:
- 成功: 获取得到的引脚状态
- 失败: 返回负数
- 设置GPIO 输出值gpio_set_value
该函数只用于那些设置为输出模式的GPIO.
列表13: gpio_direction_output 函数(内核源码include/asm-generic/gpio.h)
static inline int gpio_direction_output(unsigned gpio, int value);
函数参数
-
gpio:设置的GPIO 的编号。
-
value:设置的输出值,为1 输出高电平,为0 输出低电平。
返回值:
-
成功: 返回0
-
失败: 返回负数
实验说明与代码讲解
硬件介绍
本节实验使用到STM32MP1 开发板上的RGB 彩灯
硬件原理图分析
参考”字符设备驱动–点亮LED 灯”章节
实验代码讲解
本章的示例代码目录为:linux_driver/gpio_subsystem_rgb_led
程序包含两个C 语言文件,一个是驱动程序,驱动程序在平台总线基础上编写。另一个是一个简单的测试程序,用于测试驱动是否正常。
驱动程序讲解
驱动程序大致分为三个部分,第一部分,编写平台设备驱动的入口和出口函数。第二部分,编写平台设备的.probe 函数, 在probe 函数中实现字符设备的注册和RGB 灯的初始化。第三部分,编写字符设备函数集,实现open 和write 函数。
平台驱动入口和出口函数实现
源码如下:
列表14: 平台驱动框架
/*------------------第一部分----------------*/
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");
/* 删除设备*/
device_destroy(class_led, led_devno); //清除设备
class_destroy(class_led); //清除类
cdev_del(&led_chr_dev); //清除设备号
unregister_chrdev_region(led_devno, DEV_CNT); //取消注册字符设备
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 等的控制。函数源码如下所示。
列表15: probe 函数实现
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_operations
led_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;
}
- 第10-14 行:使用of_find_node_by_path 函数找到并获取rgb_led 在设备树中的设备节点。参数“/rgb_led”是要获取的设备树节点在设备树中的路径,如果要获取的节点嵌套在其他子节点中需要写出节点所在的完整路径。
- 第17-22 行:使用函数of_get_named_gpio 函数获取GPIO 号,读取成功则返回读取得到的GPIO 号。“rgb_led_red”指定GPIO 的名字,这个参数要与rgb_led 设备树节点中GPIO 属性名对应,参数“0”指定引脚索引,我们的设备树中一条属性中只定义了一个引脚,我们只有一个所以设置为0。
- 第25-27 行,将GPIO 设置为输出模式,默认输出电平为高电平。
- 第32-65 行,字符设备相关内容,这部分内容在字符设备章节已经详细介绍这里不再赘述。
实现字符设备函数
字符设备函数我们只需要实现open 函数和write 函数。函数源码如下。
列表16: 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;
}
/* 设置GPIOA_13 输出电平*/
if(write_data & 0x04)
{
gpio_direction_output(rgb_led_red, 0); // GPIOA_13 引脚输出低电平,红灯亮
}
else
{
gpio_direction_output(rgb_led_red, 1); // GPIOA_13 引脚输出高电平,红灯灭
}
/* 设置GPIOG_2 输出电平*/
if(write_data & 0x02)
{
gpio_direction_output(rgb_led_green, 0); // GPIOG_2 引脚输出低电平,绿灯亮
}
else
{
gpio_direction_output(rgb_led_green, 1); // GPIOG_2 引脚输出高电平,绿灯灭
}
/* 设置GPIOB_5 输出电平*/
if(write_data & 0x01)
{
gpio_direction_output(rgb_led_blue, 0); // GPIOB_5 引脚输出低电平,蓝灯亮
}
else
{
gpio_direction_output(rgb_led_blue, 1); // GPIOB_5 引脚输出高电平,蓝灯灭
}
return 0;
}
- 代码3-8 行: 定义字符设备操作函数集,这里主要实现open 和write 函数即可。
- 代码12-16 行:实现open 函数,在平台驱动的prob 函数中已经初始化了GPIO, 这里不用做任何操作
- 代码20-60 行:write 函数实现也很简单,首先使用“copy_from_user”函数将来自应用层的数据“拷贝”内核层。得到命令后就依次检查后三位,根据命令值使用“gpio_direction_output”函数控制RGB 灯的亮灭。
应用程序讲解
应用程序编写比较简单,我们只需要打开设备节点文件,写入命令然后关闭设备节点文件即可。源码如下所示。
列表17: Makefile 文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[])
{
printf("led_tiny test\n");
/* 判断输入的命令是否合法*/
if(argc != 2)
{
printf(" command error ! \n");
printf(" usage : sudo test_app num [num can be 0~7]\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 command = atoi(argv[1]); //将受到的命令值转化为数字;
/* 写入命令*/
int error = write(fd,&command,sizeof(command));
if(error < 0)
{
printf("write file error! \n");
close(fd);
/* 判断是否关闭成功*/
}
/* 关闭文件*/
error = close(fd);
if(error < 0)
{
printf("close file error! \n");
}
return 0;
}
结合代码各部分说明如下:
- 代码10-15 行:判断命令是否有效。再运行应用程序时我们要传递一个控制命令,所以参数长度是2。
- 代码19-24 行:打开设备文件。参数“/dev/rgb-leds”用于指定设备节点文件,设备节点文件名是在驱动程序中设置的,这里保证与驱动一致即可。
- 代码26-43 行:由于从main 函数中获取的参数是字符串,这里首先要将其转化为数字。最后条用write 函数写入命令然后关闭文件即可。
实验准备
在板卡上的部分GPIO 可能会被系统占用,在使用前请根据需要修改/boot/uEnv.txt 文件,可注释掉某些设备树插件的加载,重启系统,释放相应的GPIO 引脚。
如本节实验中,可能在鲁班猫系统中默认使能了LED 的设备功能,用在了LED 子系统。引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源。
方法参考如下:
取消LED 设备树插件,以释放系统对应LED 资源,操作如下:
如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象,请按上述情况检查并按上述步骤操作。
如出现Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root 用户权限,简单的解决方案是在执行语句前加入sudo 或以root 用户运行程序。
Makefile 修改说明
修改Makefile 并编译生成驱动程序
Makefile 程序并没有大的变化,修改后的Makefile 如下所示。
列表18: Makefile 文件
KERNEL_DIR=../ebf_linux_kernel/build_image/build
ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH CROSS_COMPILE
obj-m := rgb-leds.o
out = rgb_leds_app
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
$(CROSS_COMPILE)gcc -o $(out) rgb_leds_app.c
.PHONY:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
- 代码第2 行:变量“KERNEL_DIR”保存的是内核所在路径,这个需要根据自己内核所在位置设定。
- 代码第4 行:“obj-m := rgb-leds.o”中的“rgb-leds.o”要与驱动源码名对应。Makefile 修改完成后执行如下命令编译驱动。
命令:
make
正常情况下会在当前目录生成.ko 驱动文件和应用程序。
下载验证
前两小节我们已经编译出了.ko 驱动和应用程序,将驱动程序和应用程序添加到开发板中(推荐使用之前讲解的NFS 共享文件夹),驱动程序和应用程序在开发板中的存放位置没有限制。
执行如下命令加载驱动:
命令:
insmod ./rgb-leds.ko
正常情况下输出结果如下所示。
在驱动程序中,我们在.probe 函数中注册字符设备并创建了设备文件,设备和驱动匹配成功后.probe 函数已经执行,所以正常情况下在“/dev/”目录下已经生成了“rgb-leds”设备节点,如下所示。
驱动加载成功后直接运行应用程序如下所示。
命令:
./rgb_leds_app < 命令>
执行结果如下:
命令是一个“unsigned char”型数据,只有后三位有效,每一位代表一个灯,从高到低依次代表红、绿、蓝,1 表示亮,0 表示灭。例如命令=4 则亮红灯,命令=7 则三个灯全亮。
参考资料:嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列