0%

Pinctrl和GPIO子系统

Pinctrl和GPIO子系统

引言

在Linux系统中Pinctrl子系统和GPIO子系统是非常重要的概念,他们共同负责和管理SoC(system on a Chip)上的引脚。这两个子系统相互协作,为驱动开发提供一个灵活且高效的接口,用于配置和控制硬件引脚。

Pinctrl子系统

Pinctrl,即Pin Conctroller,中文翻译为引脚控制器。它是一个虚拟的概念,主要用与设置IOMUX(input/output Multiplexer),将某个引脚链接到指定的模块,从而实现特定的功能。

Pinctrl的作用

  • 引脚复用:一个引脚可以被配置为多种功能(如GPIO、UART、IIC等),Pinctrl子系统负责管理这些服用配置。
  • 引脚配置:设置引脚的电气特性,如输入输出模式、驱动能力、上拉下拉等。、
  • 设备树集成:通过设备树描述硬件,pingctrl子系统更具设备树配置自动完成引脚的初始化。

Pinctrl的工作原理

1、设备树描述:在设备树中,我们为每个引脚配置定义一个pinctrl-name属性,为这个属性 指定该引脚可以具有的各种状态。

2、Pinctrl驱动:pinctrl驱动程序会根据设备树中的配置,初始化pinctrl子系统。

3、驱动程序的使用:当一个设备的驱动程序需要使用某个引脚时,它会通过pinctrl子系统申请并配置引脚。

4、pinctrl子系统的配置:pinctrl子系统会根据驱动程序的请求,设置相应的寄存器,完成引脚的复用和配置。

1、pinctrl节点介绍

首先我们可以在linux-5.4.31/arch/arm/boot/dts/stm32mp151-pinctrl.dtsi文件中找到pinctrl节点。或者也可以使用 grep "pinctrl:" stm32mp15* 在dts目录下搜索。

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;
}
/* 剩下内容省略 */
};
  • soc{…}:这表示pinctrl节点是soc根节点下的一个子节点,在设备树中,soc节点是描述整个片上系统。
  • pinctrl:节点标签,方便其他地方引用节点。
  • #address-cells、#address-cells:定义了地址单元、大小单元的数量。(也就是reg属性里面的数量)。
  • compatible:兼容性字符串,用于驱动的匹配。(很重要)(书写规范:从最具体到最通用的顺序排列,首先是制造商 的名称,然后是设备的型号或者设备类型的描述)
  • ranges:该属性定义了父地址空间和子地址空间的映射关系。0:父地址空间的起始地址偏移量为0;0x50002000:子地址空间的起始地址;0xa400:子地址空间的大小。
    • rangs与reg两个的区别:reg属性时用来描述父节点的地址空间的地址和大小,是物理地址直接链接在内存总线上的。而rangs属性是用与父节点和子节点之间建立隐射关系的。
  • interrupt-parent:中断的父节点。
  • st,syscfg:stm特定的属性,用于配置系统配置寄存器的。
  • hwlocks:该节点的硬件锁,&hsem,0:表示使用hsem节点的第一个硬件信号量。
  • pins-are-numbered:这个是一个标记属性,表示该引脚控制器使用数字编号来表示引脚。

该文件是由芯片厂商官方将芯片的通用部分单独提出来的一些设备树的配置信息,在soc节点中汇总了所需的配置信息。

在设备树的只要配置文件的stm32mp15-pinctrl.dtsi文件中,我们可以搜索到很多的配置信息。大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
&pinctrl {
adc1_in6_pins_a: adc1-in6 {
pins {
pinmux = <STM32_PINMUX('F', 12, ANALOG)>;
};
};

adc12_ain_pins_a: adc12-ain-0 {
pins {
pinmux = <STM32_PINMUX('C', 3, ANALOG)>, /* ADC1 in13 */
<STM32_PINMUX('F', 12, ANALOG)>, /* ADC1 in6 */
<STM32_PINMUX('F', 13, ANALOG)>, /* ADC2 in2 */
<STM32_PINMUX('F', 14, ANALOG)>; /* ADC2 in6 */
};
};

  • &pinctrl:这是一个引用。相当一在上面结果的pinctrl节点中添加的子节点。
  • adc1_in6_pins_a: adc1-in6 { … }:标签和名称。
  • pins{…}:pins节点包含具体的引脚配置。
  • pinmux:用于配置引脚的复用功能。
  • <STM32_PINMUX(‘F’, 12, ANALOG)>:这行代码的含义是将 PF12 引脚配置为 ADC1 的输入通道 6。

GPIO子系统

1、GPIO子系统的作用

GPIO(通用输入/输出)是微控制器或者片上系统(soc)上最基本的外设之一。GPIO引脚乐意配置为输入或者输出,用于与外部设备进行数字信号的交互。

2、GPIO子系统的组成部分

  • gpiolib核心:GPIO子系统的核心框架,提供了一组同的API函数,共驱动使用。
  • GPIO控制器驱动:每一个soc都有自己的GPIO控制器,需要相应的驱动程序来管理这些控制器。
  • 设备树绑定:设备树用于描述硬件信息。

3、GPIO子系统的API

GPIO有两套API:

  • 基于描述符的API:新的API,函数以gpiod—为开头,它使用gpio-desc结构体表示一个gpio引脚。
  • 传统的API:函数以gpio-为开头,使用一个整数来表示一个gpio引脚。

实验代码

功能实现:

  • 使用pinctrl子系统和GPIO子系统。
  • 使用设备树。
  • 点亮led灯。

子系统的讲解:

  • 在内核代码的/home/h/h/linux-5.4.31/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts目录下,添加设备树的节点。

    1
    2
    3
    4
    5
    6
    7
    8
    led_test2{
    compatible = "h,led2";
    led_name = "led02";
    led_minor = <10>;
    gpios = <&gpioz 5 GPIO_ACTIVE_HIGH>,
    <&gpioz 6 GPIO_ACTIVE_HIGH>,
    <&gpioz 7 GPIO_ACTIVE_HIGH>;
    };
  • s其中gpios是在dts/stm32mp151.dtsi文件中的gpioz节点中追加。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    gpioz: gpio@54004000 {
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    reg = <0 0x400>;
    clocks = <&scmi0_clk CK_SCMI0_GPIOZ>;
    st,bank-name = "GPIOZ";
    st,bank-ioport = <11>;
    status = "disabled";
    };
  • 重新编译设备树。运行下面命令。

    1
    2
    3
    h@h-virtual-machine:~/h/linux-5.4.31$ make -j4 ARCH=arm dtbs LOADADDR=0xC2000040
    DTC arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb
    h@h-virtual-machine:~/h/linux-5.4.31$ cp -raf arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb /tftpboot/
  • 这样我们编写的设备树文件会重新编译并在重新启动板子的时候加载到芯片中。

平台驱动程序讲解:

  • 编写驱动框架代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    static int __init led_pdrv_init(void)
    {
    printk("-------%s----------\n",__FUNCTION__);
    return 0;
    }

    static void __exit led_pdrv_exit(void)
    {
    printk("-------%s----------\n",__FUNCTION__);
    }

    module_init(led_pdrv_init);
    module_exit(led_pdrv_exit);
    MODULE_LICENSE("GPL");
  • 框架搭好的了之后,开始写注册平台驱动。

    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
    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/miscdevice.h>
    #include <linux/platform_device.h>
    #include <linux/slab.h>
    #include <linux/fs.h>
    #include <linux/io.h>
    #include <linux/uaccess.h>
    #include <linux/mod_devicetable.h>
    #include <linux/of.h>
    #include <linux/of_gpio.h>
    #include <linux/gpio.h>

    int led_probe(struct platform_device *pdev)
    {
    printk("-------%s----------\n",__FUNCTION__);
    return 0;
    }
    int led_remove(struct platform_device *pdev)
    {
    printk("-------%s----------\n",__FUNCTION__);
    return 0;
    }


    struct of_device_id led_match_table[] = {
    {
    .compatible = "h,led2",
    },
    {
    },
    };


    struct platform_driver led_pdrv = {
    .probe = led_probe,
    .remove = led_remove,
    .driver = {
    .name = "led_pdrv",
    .of_match_table = led_match_table,
    },
    };


    static int __init led_pdrv_init(void)
    {
    printk("-------%s----------\n",__FUNCTION__);
    return platform_driver_register(&led_pdrv);
    }

    static void __exit led_pdrv_exit(void)
    {
    printk("-------%s----------\n",__FUNCTION__);
    platform_driver_unregister(&led_pdrv);
    }

    module_init(led_pdrv_init);
    module_exit(led_pdrv_exit);
    MODULE_LICENSE("GPL");
    • 其中struct platform_driver led_pdrv 结构体是很重要的,probe函数是在驱动和设备树上的节点匹配之后,内核自动调用的。remove是在内核卸载是调用的。driver中的name是平台驱动的名字。of_match_table的作用是与设备树进行匹配用的,通过compatible进行匹配。
  • 平台驱动,probe函数的实现

    • 为自己创建的结构体申请空间

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      struct stm32mp157_led {
      struct miscdevice misc; // 直接定义miscdevice结构体
      struct device_node *np;
      int gpio_5;
      int gpio_6;
      int gpio_7;
      };
      struct stm32mp157_led *led_t;

      led_t = kzalloc(sizeof(*led_t), GFP_KERNEL);
      if (!led_t) {
      printk(KERN_ERR "led kzalloc failed\n");
      return -ENOMEM;
      }
    • 获取设备树节点属性,为创建杂项设备做准备

      1
      2
      3
      led_t->np = pdev->dev.of_node;
      of_property_read_u32(led_t->np, "led_minor", &minor);
      of_property_read_string(led_t->np, "led_name", (const char **)&name);
    • 初始化杂项设备和注册。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      //初始化杂项设备
      led_t->misc.minor = minor;
      led_t->misc.name = name;
      led_t->misc.fops = &led_fops;

      // 注册杂项设备
      ret = misc_register(led_t->misc);
      if(ret)
      {
      printk("misc register faile\n");
      goto err_misc_register;
      }
    • 申请gpio资源

      1
      2
      3
      4
      // 申请gpio资源
      led_t->gpio_5 = of_get_gpio(led_t->np, 0);
      led_t->gpio_6 = of_get_gpio(led_t->np, 1);
      led_t->gpio_7 = of_get_gpio(led_t->np, 2);
    • 错误跳转

      1
      2
      3
      err_misc_register:
      kfree(led_t);
      return ret;
  • 平台驱动的remove函数实现

    • 注销和卸载

      1
      2
      misc_deregister(led_t->misc);
      kfree(led_t);
  • 文件操作函数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
    ssize_t led_write (struct file *file, const char __user *buf, size_t size, loff_t *loff)
    {
    int ret = 0;
    int value;
    printk("-------%s----------\n",__FUNCTION__);
    // 将应用数据转换为内核数据
    ret = copy_from_user(&value, buf, size);
    if (ret) {
    printk("copy from user error\n");
    return -EAGAIN;
    }

    gpio_request(led_t->gpio_5, "led_5");
    gpio_request(led_t->gpio_6, "led_6");
    gpio_request(led_t->gpio_7, "led_7");

    if (value) { // 开灯
    gpio_direction_output(led_t->gpio_5, 1);
    gpio_direction_output(led_t->gpio_6, 1);
    gpio_direction_output(led_t->gpio_7, 1);
    } else { // 关灯
    gpio_direction_output(led_t->gpio_5, 0);
    gpio_direction_output(led_t->gpio_6, 0);
    gpio_direction_output(led_t->gpio_7, 0);
    }

    gpio_free(led_t->gpio_5);
    gpio_free(led_t->gpio_6);
    gpio_free(led_t->gpio_7);

    return size;
    }

应用程序led_app.c

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,char **argv)
{
int fd;
int on;

fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("open faile\n");
exit(1);
}
while(1)
{
on = 1;
write(fd, &on, sizeof(on));
sleep(1);
on = 0;
write(fd, &on, sizeof(on));
sleep(1);
}
close(fd);

return 0;
}

Makefile文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
KERN_DIR = /home/h/h/linux-5.4.31
DRV_DIR := $(shell pwd) # 获取当前工作目录
MODULE_NAME = led_pdrv
APP_NAME = led_app

all:
make -C $(KERN_DIR) M=$(DRV_DIR) modules
$(CC) -o $(APP_NAME) $(APP_NAME).c

clean:
make -C $(KERN_DIR) M=$(DRV_DIR) clean
rm -rf $(APP_NAME)

install:
cp -raf *.ko /opt/myrootfs/drv_module
cp -raf $(APP_NAME) /opt/myrootfs/drv_module

obj-m += $(MODULE_NAME).o

下载验证

  • 在文件目录下,输入make 命令,进行编译,成功后,再输入make install 将编译的文件加载到内核中。

  • 重新启动下载板子。

  • 在串口助手的端口中找到存放代码的文件,可以发现有两个文件。

    1
    led_app led_pdrv.ko
  • 输入insmod led_pdrv.ko 命令加载驱动。

  • 成功后,可以在/dev目录下找平台设备文件。

    1
    2
    3
    4

    # ls /dev/led02 -n
    crw-rw---- 1 0 0 10, 10 Jan 5 14:04 /dev/led02

  • 然后加载应用程序就可以了。

    1
    ./led_app /dev/led02