1. Pinctrl子系统和GPIO子系统–led实验

在前面章节,我们有过使用寄存器去编写字符设备的经历了。这种直接在驱动代码中, 通过寄存器映射来对外设进行使用的编程方式,从驱动开发者的角度可以说是灾难。 因为每当芯片的寄存器发生了改动,那么底层的驱动几乎得重写。

那么在这个问题上,我们更进了一步,学会了使用设备树来描述外设的各种信息(比如寄存器地址), 而不是将寄存器的这些内容放在驱动代码里。 这样即使设备信息修改了,我们还是可以通过设备树的接口函数,去灵活的获取设备的信息。 极大得提高了驱动的复用能力。

现在我们可以通过在驱动程序代码里使用设备树接口,来获取到外设的信息了。 但是,在前面的设备树演示中,我们还是将寄存器操作具体细节体现在了驱动中,比如置位操作。

那么,在驱动中有没有更通用的方法,可以不涉及到具体的寄存器操作的内容呢? 对于有些外设,是具备抽象条件的,也就是说我们可以将对这些外设的操作统一起来。

比如本章中将为大家介绍的pinctrl子系统和GPIO子系统。

本章将会使用GPIO子系统实现LED驱动,GPIO子系统要用到pinctrl子系统,所以本章将pinctrl子系统和GPIO子系统放在一块讲解。

1.1. pinctrl子系统

pinctrl子系统主要用于管理芯片的引脚。stm32mp1芯片拥有众多的片上外设, 大多数外设需要通过芯片的引脚与外部设备(器件)相连实现相对应的控制,例如我们熟悉的I2C、SPI、LCD、USDHC等等。 而我们知道芯片的可用引脚(除去电源引脚和特定功能引脚)数量是有限的,芯片的设计厂商为了提高硬件设计的灵活性, 一个芯片引脚往往可以做为多个片上外设的功能引脚,如下图所示。

总线结构02|

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子系统是由芯片厂商来实现的,简单来说用于帮助我们管理芯片引脚并自动完成引脚的初始化, 而我们要做的只是在设备树中按照规定的格式写出想要的配置参数即可。

1.1.1. pinctrl子系统编写格式以及引脚属性详解

1.1.1.1. pinctrl节点介绍

首先我们在/ebf_linux_kernel/arch/arm/boot/dts/stm32mp157-pinctrl.dtsi文件中,可以看到如下定义

stm32mp157-pinctrl.dtsi
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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中, 打开stm32mp157a-basic.dts,在文件中搜索“&pinctrl”找到设备树中引用“pinctrl”节点的位置如下所示。

stm32mp157a-basic.dts中&pinctrl部分内容
 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
&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这个外设的节点来看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
&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。

这样以来,我们就指定了这个外设使用到的引脚及其状态。

1.1.1.2. pinctrl子节点编写格式

那么按照“&pinctrl”下节点的描述形式,我们也可以自己描述一下某个外设的pinctrl。

举例说明
 &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

1.1.2. 将RGB灯引脚添加到pinctrl子系统

本小节假设没有看过裸机部分RGB灯章节,我们从看原理图开始,一步步将RGB灯用到的三个引脚添加到pinctrl子系统中。

1.1.2.1. 查找RGB灯使用的引脚

RGB灯对应的原理图如下所示。

总线结构08|

根据网络名在核心板上找到对应的引脚,如下。

rgb_led_red: GPIOA13

rgb_led_green: GPIOG2

rgb_led_blue: GPIOB5

1.1.2.2. 在pinctrl节点中添加pinctrl子节点

添加子节点很简单,我们只需要将引脚信息以一定格式, 写入到stm32mp157a-basic.dts设备树文件中的pinctrl子节点即可,添加完成后如下所示。

新增pinctrl子节点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
&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;
        };
    };
};
broken

新增的节点名为“rgb_led”,名字任意选取,长度不要超过32个字符,最好能表达出节点的信息。 “pinctrl_rgb_led”节点标签,“pinctrl_”是固定的格式,后面的内容自定义的,我们将通过这个标签引用这个节点。

pins的内容中,我们将LED使用到的GPIO引脚功能配置好了,因为pinctrl各家芯片厂商各异,这里我们就不展开, 具体大家可以参考官方的

Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.txt

文档,在添加完pinctrl子节点后,系统会根据我们添加的配置信息将引脚初始化为GPIO功能。

到这里关于pinctrl子系统的使用就已经讲解完毕了,接下来介绍GPIO子系统相关的内容。

1.2. GPIO子系统

在没有使用GPIO子系统之前,如果我们想点亮一个LED,首先要得到led相关的配置寄存器,再手动地读、改、写这些配置寄存器实现 控制LED的目的。有了GPIO子系统之后这部分工作由GPIO子系统帮我们完成,我们只需要调用GPIO子系统提供的API函数即可完成GPIO的 控制动作。

在stm32mp157-pinctrl.dtsi文件中的pinctrl子节点记录着GPIO控制器的寄存器地址,下面我们以GPIOA为例介绍GPIOA子节点相关内容

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的效果。

1.2.1. 在设备树中添加RGB灯的设备树节点

相比之前led灯的设备树节点(没有使用GPIO子系统),这里只需要增加GPIO属性定义,基于GPIO子系统的rgb_led设备树节点 添加到“/arch/arm/boot/dts/stm32mp157a-basic.dts”设备树的 根节点内 。 添加完成后的设备树如下所示。

设备树中添加rgb_led节点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*添加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";
};
broken
  • 第6行,设置“compatible”属性值,与led的平台驱动做匹配。

  • 第7行,指定RGB灯的引脚pinctrl信息,上一小节我们定义了pinctrl节点,并且标签设置为“pinctrl_rgb_led”, 在这里我们引用了这个pinctrl信息。

  • 第8-10行,指定引脚使用的哪个GPIO,编写格式如下所示。

总线结构12|
  • 标号①,设置引脚名字,如果使用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- 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”设备树节点。如下所示。

总线结构13|

至此,我们的设备已经添加到了系统中,下面我们可以尝试编写驱动来使用我们的LED设备了。 不过在这之前,我们需要先学习GPIO子系统的一些函数。

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)
1
 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)
1
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)
1
static inline void gpio_free(unsigned gpio);

gpio_free函数与gpio_request是一对相反的函数,一个申请,一个释放。一个GPIO只能被申请一次, 当不再使用某一个引脚时记得将其释放掉。

参数:

  • gpio: 要释放的GPIO编号。

返回值:

4. GPIO输出设置函数gpio_direction_output

用于将引脚设置为输出模式。

gpio_direction_output函数(内核源码include/asm-generic/gpio.h)
1
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)
1
static inline int gpio_direction_input(unsigned gpio)

函数参数:

  • gpio: 要设置的GPIO的编号。

返回值:

  • 成功: 返回0

  • 失败: 返回负数。

6. 获取GPIO引脚值函数gpio_get_value

用于获取引脚的当前状态。无论引脚被设置为输出或者输入都可以用该函数获取引脚的当前状态。

gpio_get_value函数(内核源码include/asm-generic/gpio.h)
1
static inline int gpio_get_value(unsigned gpio);

函数参数:

  • gpio: 要获取的GPIO的编号。

返回值:

  • 成功: 获取得到的引脚状态

  • 失败: 返回负数

7. 设置GPIO输出值gpio_set_value

该函数只用于那些设置为输出模式的GPIO.

gpio_direction_output函数(内核源码include/asm-generic/gpio.h)
1
static inline int gpio_direction_output(unsigned gpio, int value);

函数参数

  • gpio: 设置的GPIO的编号。

  • value: 设置的输出值,为1输出高电平,为0输出低电平。

返回值:

  • 成功: 返回0

  • 失败: 返回负数

根据上面这些函数我们就可以在驱动程序中控制IO口了。

1.3. 实验说明与代码讲解

硬件介绍

本节实验使用到 STM32MP1 开发板上的 RGB 彩灯

硬件原理图分析

参考”字符设备驱动–点亮LED灯”章节

1.3.1. 实验代码讲解

本章的示例代码目录为:linux_driver/gpio_subsystem_rgb_led

程序包含两个C语言文件,一个是驱动程序,驱动程序在平台总线基础上编写。 另一个是一个简单的测试程序,用于测试驱动是否正常。

1.3.1.1. 驱动程序讲解

驱动程序大致分为三个部分,第一部分,编写平台设备驱动的入口和出口函数。第二部分,编写平台设备的.probe函数, 在probe函数中实现字符设备的注册和RGB灯的初始化。第三部分,编写字符设备函数集,实现open和write函数。

平台驱动入口和出口函数实现

源码如下:

平台驱动框架
 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
/*------------------第一部分----------------*/
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等的控制。函数源码如下所示。

probe函数实现
 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
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函数。函数源码如下。

open函数和write函数实现
 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
/*------------------第一部分---------------*/
/*字符设备操作函数集*/
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灯的亮灭。

1.3.1.2. 应用程序讲解

应用程序编写比较简单,我们只需要打开设备节点文件,写入命令然后关闭设备节点文件即可。源码如下所示。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#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函数写入命令然后关闭文件即可。

1.3.2. 实验准备

在板卡上的部分GPIO可能会被系统占用,在使用前请根据需要修改 /boot/uEnv.txt 文件, 可注释掉某些设备树插件的加载,重启系统,释放相应的GPIO引脚。

如本节实验中,可能在鲁班猫系统中默认使能了 LED 的设备功能, 用在了LED子系统。引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源。

方法参考如下:

broken

取消 LED 设备树插件,以释放系统对应LED资源,操作如下:

broken

如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象, 请按上述情况检查并按上述步骤操作。

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

1.3.2.1. Makefile修改说明

修改Makefile并编译生成驱动程序

Makefile程序并没有大的变化,修改后的Makefile如下所示。

Makefile文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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驱动文件和应用程序。

1.3.3. 下载验证

前两小节我们已经编译出了.ko驱动和应用程序,将驱动程序和应用程序添加到开发板中(推荐使用之前讲解的NFS共享文件夹), 驱动程序和应用程序在开发板中的存放位置没有限制。

总线结构14|

执行如下命令加载驱动:

命令:

1
insmod ./rgb-leds.ko

正常情况下输出结果如下所示。

总线结构15|

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

总线结构16|

驱动加载成功后直接运行应用程序如下所示。

命令:

1
./rgb_leds_app <命令>

执行结果如下:

总线结构17|

命令是一个“unsigned char”型数据,只有后三位有效,每一位代表一个灯,从高到低依次代表红、绿、蓝,1表示亮,0表示灭。 例如命令=4 则亮红灯,命令=7则三个灯全亮。