Linux驱动讲究驱动分离与分层,pinctrl和gpio子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架

pinctrl简单介绍

pinctrl子系统主要工作内容如下:

  • 获取设备树中pin信息。
  • 根据获取到的pin信息来设置pin的复用功能
  • 根据获取到的pin信息来设置pin的电气特性,如驱动能力。

要使用pinctrl 子系统,我们需要在设备树里面设置PIN的配置信息,毕竟pinctrl子系统要根据你提供的信息来配置 PIN 功能,一般会在设备树里面创建一个节点来描述 PIN 的配置信息。

pinctrl: pinctrl {
		compatible = "rockchip,rk3568-pinctrl";
		rockchip,grf = <&grf>;
		rockchip,pmu = <&pmugrf>;
		#address-cells = <2>; // 地址占两个位置
		#size-cells = <2>;	  // 长度占两个位置
		ranges;

		gpio0: gpio0@fdd60000 {
			compatible = "rockchip,gpio-bank";
			reg = <0x0 0xfdd60000 0x0 0x100>;
			interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
			clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;

			gpio-controller;
			#gpio-cells = <2>;
			interrupt-controller;
			#interrupt-cells = <2>;
		};

		// .......

		gpio4: gpio4@fe770000 {
			compatible = "rockchip,gpio-bank";
			reg = <0x0 0xfe770000 0x0 0x100>;
			interrupts = <GIC_SPI 37 IRQ_TYPE_LEVEL_HIGH>;
			clocks = <&cru PCLK_GPIO4>, <&cru DBCLK_GPIO4>;

			gpio-controller;
			#gpio-cells = <2>;
			interrupt-controller;
			#interrupt-cells = <2>;
		};
	};

而描述PIN相关的具体配置,rk3568-pinctrl.dtsi (编译内核后得到)

// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * Copyright (c) 2020 Rockchip Electronics Co., Ltd.
 */

#include <dt-bindings/pinctrl/rockchip.h>
#include "rockchip-pinconf.dtsi"

/*
 * This file is auto generated by pin2dts tool, please keep these code
 * by adding changes at end of this file.
 */
&pinctrl {
	acodec {
		acodec_pins: acodec-pins {
			rockchip,pins =
				/* acodec_adc_sync */
				<1 RK_PB1 5 &pcfg_pull_none>,
				/* acodec_adcclk */
				<1 RK_PA1 5 &pcfg_pull_none>,
				/* acodec_adcdata */
				<1 RK_PA0 5 &pcfg_pull_none>,
				/* acodec_dac_datal */
				<1 RK_PA7 5 &pcfg_pull_none>,
				/* acodec_dac_datar */
				<1 RK_PB0 5 &pcfg_pull_none>,
				/* acodec_dacclk */
				<1 RK_PA3 5 &pcfg_pull_none>,
				/* acodec_dacsync */
				<1 RK_PA5 5 &pcfg_pull_none>;
		};
	};
	// ......
	cam {
		cam_pins: cam-pins {
			rockchip,pins =
				/* cam_clkout0 */
				<4 RK_PA7 1 &pcfg_pull_none>,
				/* cam_clkout1 */
				<4 RK_PB0 1 &pcfg_pull_none>;
		};
	};
	can0 {
		can0m0_pins: can0m0-pins {
			rockchip,pins =
				/* can0_rxm0 */
				<0 RK_PB4 2 &pcfg_pull_none>,
				/* can0_txm0 */
				<0 RK_PB3 2 &pcfg_pull_none>;
		};
		can0m1_pins: can0m1-pins {
			rockchip,pins =
				/* can0_rxm1 */
				<2 RK_PA2 4 &pcfg_pull_none>,
				/* can0_txm1 */
				<2 RK_PA1 4 &pcfg_pull_none>;
		};
	};

	spi0 {
		spi0clkm0: spi0clkm0 {
			rockchip,pins =
				<0 RK_PB5 2 &pcfg_pull_none>;
		};
		spi0cs0m0: spi0cs0m0 {
			rockchip,pins =
				<0 RK_PC6 2 &pcfg_pull_none>;
		};
		spi0cs1m0: spi0cs1m0 {
			rockchip,pins =
				<0 RK_PC4 2 &pcfg_pull_none>;
		};
		spi0misom0: spi0misom0 {
			rockchip,pins =
				<0 RK_PC5 2 &pcfg_pull_none>;
		};
		spi0mosim0: spi0mosim0 {
			rockchip,pins =
				<0 RK_PB6 2 &pcfg_pull_none>;
		};
		spi0clkm0_hs: spi0clkm0-hs {
			rockchip,pins =
				<0 RK_PB5 2 &pcfg_pull_up_drv_level_1>;
		};
		spi0misom0_hs: spi0misom0-hs {
			rockchip,pins =
				<0 RK_PC5 2 &pcfg_pull_up_drv_level_1>;
		};
		spi0mosim0_hs: spi0mosim0-hs {
			rockchip,pins =
				<0 RK_PB6 2 &pcfg_pull_up_drv_level_1>;
		};
		spi0clkm1: spi0clkm1 {
			rockchip,pins =
				<2 RK_PD3 3 &pcfg_pull_none>;
		};
		spi0cs0m1: spi0cs0m1 {
			rockchip,pins =
				<2 RK_PD2 3 &pcfg_pull_none>;
		};
		spi0misom1: spi0misom1 {
			rockchip,pins =
				<2 RK_PD0 3 &pcfg_pull_none>;
		};
		spi0mosim1: spi0mosim1 {
			rockchip,pins =
				<2 RK_PD1 3 &pcfg_pull_none>;
		};
		spi0clkm1_hs: spi0clkm1-hs {
			rockchip,pins =
				<2 RK_PD3 3 &pcfg_pull_up_drv_level_1>;
		};
		spi0misom1_hs: spi0misom1-hs {
			rockchip,pins =
				<2 RK_PD0 3 &pcfg_pull_up_drv_level_1>;
		};
		spi0mosim1_hs: spi0mosim1-hs {
			rockchip,pins =
				<2 RK_PD1 3 &pcfg_pull_up_drv_level_1>;
		};
	};
	// ........
	uart0 {
		uart0_xfer: uart0-xfer {
			rockchip,pins =
				/* uart0_rx */
				<0 RK_PC0 3 &pcfg_pull_up>,
				/* uart0_tx */
				<0 RK_PC1 3 &pcfg_pull_up>;
		};
		uart0_ctsn: uart0-ctsn {
			rockchip,pins =
				<0 RK_PC7 3 &pcfg_pull_none>;
		};
		uart0_rtsn: uart0-rtsn {
			rockchip,pins =
				<0 RK_PC4 3 &pcfg_pull_none>;
		};
	};
	// ........
	gmac-txc-level2 {
		gmac0_rgmii_clk_level2: gmac0-rgmii-clk-level2 {
			rockchip,pins =
				/* gmac0_rxclk */
				<2 RK_PA5 2 &pcfg_pull_none>,
				/* gmac0_txclk */
				<2 RK_PB0 2 &pcfg_pull_none_drv_level_2>;
		};
		gmac1m0_rgmii_clk_level2: gmac1m0-rgmii-clk-level2 {
			rockchip,pins =
				/* gmac1_rxclkm0 */
				<3 RK_PA7 3 &pcfg_pull_none>,
				/* gmac1_txclkm0 */
				<3 RK_PA6 3 &pcfg_pull_none_drv_level_2>;
		};
		gmac1m1_rgmii_clk_level2: gmac1m1-rgmii-clk-level2 {
			rockchip,pins =
				/* gmac1_rxclkm1 */
				<4 RK_PA3 3 &pcfg_pull_none>,
				/* gmac1_txclkm1 */
				<4 RK_PA0 3 &pcfg_pull_none_drv_level_2>;
		};
	};
};

不同的外设使用的PIN不同、其配置也不同,因此一个萝卜一个坑,将某个外设所使用的所有PIN都组织在一个子节点里面。绑定文档Documentation/devicetree/bindings/pinctrl/rockchip,pinctrl.txt描述了如何在设备树中设置rk3568的PIN信息。

每个pincrtl节点必须至少包含一个子节点来存放pincrtl相关信息,也就是pinctrl集,这个集合里面存放当前外设用到哪些引脚(PIN)、复用配置、上下拉、驱动能力等。一般这个存放pincrtl集的子节点名字是“rockchip,pins”

引脚复用设置的格式如下

rockchip,pins = <PIN_BANK PIN_BANK_IDX MUX &phandle>
  1. PIN_BANK: RK3568一共有5组PIN:GPIO0~GPIO4,分别对应0 ~ 4
  2. PIN_BANK_IDX: 组内的编号,以GPIO0组为例,一共有A0 ~ A7、B0 ~ B7、C0 ~ C7、D0 ~ D7,这32个PIN
28 #define RK_PA0       0 
29 #define RK_PA1       1 
30 #define RK_PA2       2 
31 #define RK_PA3       3 
32 #define RK_PA4       4 
33 #define RK_PA5       5 
...... 
54 #define RK_PD2       26 
55 #define RK_PD3       27 
56 #define RK_PD4       28 
57 #define RK_PD5       29 
58 #define RK_PD6       30 
59 #define RK_PD7       31
  1. MUX: 就是设置PIN的复用功能,一个PIN最多有16个复用功能
61 #define RK_FUNC_GPIO 0 
62 #define RK_FUNC_0    0 
63 #define RK_FUNC_1    1 
64 #define RK_FUNC_2    2 
65 #define RK_FUNC_3    3 
66 #define RK_FUNC_4    4 
67 #define RK_FUNC_5    5 
68 #define RK_FUNC_6    6 
69 #define RK_FUNC_7    7 
70 #define RK_FUNC_8    8 
71 #define RK_FUNC_9    9 
72 #define RK_FUNC_10   10 
73 #define RK_FUNC_11   11 
74 #define RK_FUNC_12   12 
75 #define RK_FUNC_13   13 
76 #define RK_FUNC_14   14 
77 #define RK_FUNC_15   15
  1. phandle: 用来描述一些引脚的通用配置信息
5   &pinctrl { 
6  
7       /omit-if-no-ref/ 
8       pcfg_pull_up: pcfg-pull-up { 
9           bias-pull-up; 
10      }; 
11 
12      /omit-if-no-ref/ 
13      pcfg_pull_down: pcfg-pull-down { 
14          bias-pull-down; 
15      }; 
16 
17      /omit-if-no-ref/ 
18      pcfg_pull_none: pcfg-pull-none { 
19          bias-disable; 
20      }; 
21 
22      /omit-if-no-ref/ 
23      pcfg_pull_none_drv_level_0: pcfg-pull-none-drv-level-0 { 
24          bias-disable; 
25          drive-strength = <0>; 
26      }; 
...... 
344 };

添加一个pinctrl节点

比如我们需要将GPIO0_D1这个PIN复用为UART2_TX引脚,pinctrl节点添加过程如下

  1. 创建对应的节点
1 &pinctrl { 
2   uart2 { 
3       /omit-if-no-ref/ 
4       uart2m0_xfer: uart2m0-xfer { 
5           /* 具体的PIN信息 */ 
6       }; 
7   }; 
8 };

在pinctrl节点下添加一个“uart2”子节点,然后在uart2节点里面在创建一个“uart2m0_xfer: uart2m0-xfer”子节点

对于 /omit-if-no-ref/ gpt给出的解释是 如果这个节点最终没有被任何地方引用,就在编译 DTB 时把它自动省略掉。

  1. 添加 ”rockchip,pins“ 属性
1  &pinctrl { 
2   uart2 { 
3       /omit-if-no-ref/ 
4       uart2m0_xfer: uart2m0-xfer { 
5           rockchip,pins = 
6               /* uart2_tx_m1 */ 
7               <0 RK_PD1 1 &pcfg_pull_up>; 
8       }; 
9   }; 
10 }

按道理来讲,当我们将一个PIN用作GPIO功能的时候也需要创建对应的pinctrl节点,并 且将所用的PIN复用为GPIO功能,但是!对于RK3568而言,如果一个PIN用作GPIO功能的时候不需要创建对应的pinctrl节点!

GPIO子系统

我们以前几个章节控制LED的引脚GPIO0_C0这个引脚,我们可以在rk3568.dtsi设备树下找到对应控制节点

3540 gpio0: gpio@fdd60000 { 
3541    compatible = "rockchip,gpio-bank"; # 属性值 对应内核gpio驱动源文件
3542    reg = <0x0 0xfdd60000 0x0 0x100>;  # gpio对应寄存器地址, 以及大小
3543    interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>; # GPIO0 对应中断信息
3544    clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;  # GPIO0 对应的时钟信息
3545  
3546    gpio-controller;  # 表示gpio0 是个GPIO控制器 每个GPIO控制器节点必须包含 这个属性
3547    #gpio-cells = <2>; # 表示一共有两个cell,比如“&gpio0 RK_PC0”就表示GPIO0_C0 第二个cell表示GPIO 极性
3548    gpio-ranges = <&pinctrl 0 0 32>; 
3549    interrupt-controller; 
3550    #interrupt-cells = <2>; 
3551 };

当某个具体的引脚作为GPIO使用的时候还需要进一步设置,例如GPIO3_B6 作为摄像头的RESET引脚,GPIO3_B6复用为GPIO功能,这里需要设备树来告诉驱动,在设备树中的CSI1摄像头节点下添加一个属性来描述摄像头的RESET引脚

585         imx415: imx415@1a { 
586             status = "okay"; 
587             compatible = "sony,imx415"; 
588             reg = <0x1a>; 
589             clocks = <&cru CLK_CIF_OUT>; 
590             clock-names = "xvclk"; 
591             power-domains = <&power RK3568_PD_VI>; 
592             pinctrl-names = "rockchip,camera_default"; 
593             pinctrl-0 = <&cif_clk>; 
594             reset-gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_LOW>; 
595             power-gpios = <&gpio4 RK_PB4 GPIO_ACTIVE_HIGH>; 
596             rockchip,camera-module-index = <0>; 
597             rockchip,camera-module-facing = "back"; 
598             rockchip,camera-module-name = "CMK-OT1522-FG3"; 
599             rockchip,camera-module-lens-name = "CS-P1150-IRC-8M-FAU"; 
...... 
606         };

从上述的设备描述将gpio引脚行单独抽离出来进行解释

reset-gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_LOW>;
  • &gpio3: 表示属于GPIO3 组
  • RK_PB6: 表示该组的PB6引脚
  • GPIO_ACTIVE_LOW:引脚默认极性 低电平有效

而对于GPIO引脚状态枚举如下

8  enum gpio_lookup_flags { 
9    GPIO_ACTIVE_HIGH     = (0 << 0),  // GPIO 高电平有效
10   GPIO_ACTIVE_LOW      = (1 << 0),  // GPIO 低电平有效
	 // 输出电气模式
11   GPIO_OPEN_DRAIN     = (1 << 1),   // GPIO 开漏输出
12   GPIO_OPEN_SOURCE     = (1 << 2),  // GPIO 开源输出
  	 // 休眠复位后的状态保持
13   GPIO_PERSISTENT      = (0 << 3),  // GPIO 默认状态
14   GPIO_TRANSITORY      = (1 << 3),  // GPIO 临时态
15 };

gpio子系统api函数

1、gpio_request 函数

int gpio_request(unsigned gpio, const char *label);
  • gpio:要申请的 GPIO 标号,可通过 of_get_named_gpio() 从设备树获取。
  • label:给 GPIO 设置的名称。
  • 返回值:0:申请成功;其他值:申请失败。

2、gpio_free 函数

void gpio_free(unsigned gpio);
  • gpio:要释放的 GPIO 标号。
  • 返回值:无。

3、gpio_direction_input 函数

int gpio_direction_input(unsigned gpio);
  • gpio:要设置为输入的 GPIO 标号。
  • 返回值:0:设置成功;负值:设置失败。

4、gpio_direction_output 函数

int gpio_direction_output(unsigned gpio, int value);
  • gpio:要设置为输出的 GPIO 标号。
  • value:GPIO 的默认输出值。
  • 返回值:0:设置成功;负值:设置失败。

5、gpio_get_value 函数

int gpio_get_value(unsigned int gpio);
  • gpio:要获取值的 GPIO 标号。
  • 返回值:非负值:获取到的 GPIO 值;负值:获取失败。

6、gpio_set_value 函数

void gpio_set_value(unsigned int gpio, int value);
  • gpio:要设置值的 GPIO 标号。
  • value:要设置的值。
  • 返回值:无。

与gpio相关的OF函数

1、of_gpio_named_count 函数

int of_gpio_named_count(struct device_node *np, const char *propname);
  • np:设备节点。
  • propname:要统计 GPIO 数量的属性名。
  • 返回值:统计到的 GPIO 数量;负值表示失败。

2、of_gpio_count 函数

int of_gpio_count(struct device_node *np);
  • np:设备节点。
  • 返回值:gpios 属性中统计到的 GPIO 数量;负值表示失败。

3、of_get_named_gpio 函数

int of_get_named_gpio(struct device_node *np, const char *propname, int index);
  • np:设备节点。
  • propname:包含要获取的 GPIO 信息的属性名。
  • index:GPIO 索引,指定获取属性中哪个 GPIO 的编号;只有一个 GPIO 时为 0。
  • 返回值:获取到的 GPIO 编号;负值表示失败。

pinctrl 实验

现在使用pinctrl的方法给led绑定gpio,同样还是GPIO0_C0 引脚,在根节点 ”/“下创建一个新的gpio_led 节点,但是同时要注意,由于正点原子的RK3568开发板默认以该引脚会系统心跳灯,因此需要将心跳灯的设备节点给status设置为disabled

	// 心跳灯占用
	leds: leds {
		compatible = "gpio-leds";
		work_led: work {
			gpios = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;
			linux,default-trigger = "heartbeat";
			status = "disabled";
		};
	};

	gpio_led {
		compatible = "alientek, led";
		led-gpio = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;
		status = "okay";
	};

对内核进行重编译,并重新烧录boot.img 到开发板,观察节点是否添加成功

root@ATK-DLRK3568:/proc/device-tree# ls -lh gpio_led/
total 0
-r--r--r-- 1 root root 14 Sep 29 18:25 compatible
-r--r--r-- 1 root root 12 Sep 29 18:25 led-gpio
-r--r--r-- 1 root root  9 Sep 29 18:25 name
-r--r--r-- 1 root root  5 Sep 29 18:25 status
root@ATK-DLRK3568:/proc/device-tree# cd gpio_led/
root@ATK-DLRK3568:/proc/device-tree/gpio_led# cat compatible
alientek, ledroot

可以发现gpio_led节点已经成功挂载到根节点下,接下来就是编写驱动代码,参考FUCK7的代码进行改写。

驱动代码修改部分

  // 获取设备树中的属性数据
  dtsled.nd = of_find_node_by_path(NODE_PATH);
  if (dtsled.nd == NULL)
  {
      printk("%s node not find \r\n", NODE_PATH);
      goto fail_find_node;
  }
  else
  {
      printk("find node %s", NODE_PATH);
  }   

  // 读取status 
  ret = of_property_read_string(dtsled.nd, "status", &str);
  if (ret < 0)
  {
      printk("read status failed \r\n");
      return -EINVAL;
  }
  // 判断节点状态是否为okay
  if (strcmp("okay", str))
  {
      printk("status: %s", str);
      return -EINVAL;
  }

  // 获取compatible属性值进行匹配
  ret = of_property_read_string(dtsled.nd, "compatible", &str);
  if (ret < 0)
  {
      printk("failed to read compatible \r\n");
      return -EINVAL;
  }
  if (strcmp(str,  "alientek, led"))
  {
      printk("compatible match failed: %s \r\n", str);
      return -EINVAL;
  }

  // 获取设备树中gpio属性 
  dtsled.led_gpio = of_get_named_gpio(dtsled.nd, "led-gpio", 0);
  if (dtsled.led_gpio < 0)
  {
      printk("failed to get gpio \r\n");
      return -EINVAL;
  }
  printk("led gpio: %d \r\n", dtsled.led_gpio);
  // 向gpio子系统申请使用gpio 
  ret = gpio_request(dtsled.led_gpio, "led_dts_gpio");
  if (ret)
  {
      printk(KERN_ERR"failed to request led gpio \r\n");
      return ret;
  }
  // 设置gpio输出模式, 输出低电平
  ret = gpio_direction_output(dtsled.led_gpio, 0);
  if (ret < 0)
  {
      printk("cant set gpio \r\n");
  }

在alloc_err 以及 卸载模块的时候由iounmap变为gpio_free函数

alloc_err:
    gpio_free(dtsled.led_gpio);

对dtsled的结构体增加gpio编号

struct led_dts_type
{
    struct cdev cdev;      // 字符设备结构体
    dev_t dev_id;          // 设备号
    struct class *class;   // 类
    struct device *device; // 设备
    int major;             // 主设备号
    int minor;             // 从设备号
    struct device_node *nd;// 设备节点
    int led_gpio;          // gpio编号
};

对fp操作的写函数改为内核对于gpio操作的api函数

ssize_t fp_led_write (struct file *filp, const char __user *buf, size_t cnt, loff_t *loff)
{
    int ret = 0;
    struct led_dts_type *dev = filp->private_data;
    get_user(ret, buf);
    printk("ret: %d \r\n", ret);
    gpio_set_value(dev->led_gpio, ret);
    return 0;
}

使用FUCK7的测试app最后实现的效果与FUCK5 最后呈现的视频效果一样,一秒间隔的led灯闪烁

root@ATK-DLRK3568:/home/ftp# cd /lib/modules
root@ATK-DLRK3568:/lib/modules# ls
5.10.160        calculate.ko          led_dts_gpio_module.ko  RTL8189FS.ko
8852be.ko       chardev_module.ko     led_dts_module.ko       RTL8189FU.ko
8852bs.ko       hci_uart.ko           parameter.ko            RTL8723DS.ko
bcmdhd.ko       hello.ko              rtk_btusb.ko            RTL8821CS.ko
bcmdhd_pcie.ko  led_driver_module.ko  rtkm.ko                 RTL8822CS.ko
root@ATK-DLRK3568:/lib/modules# insmod led_dts_gpio_module.ko
[ 2768.540302] find node /gpio_led
[ 2768.540371] led gpio: 16
[ 2768.540582] major: 511 minor: 0
[ 2768.541078] dts led module init success
root@ATK-DLRK3568:/lib/modules# cd /home/ftp/
root@ATK-DLRK3568:/home/ftp# ls /dev/gpioled
/dev/gpioled
root@ATK-DLRK3568:/home/ftp# ./led_dts_test_app /dev/gpio
gpiochip0  gpiochip1  gpiochip2  gpiochip3  gpiochip4  gpiochip5  gpioled
root@ATK-DLRK3568:/home/ftp# ./led_dts_test_app /dev/gpioled
led_dts_test_app
[ 2797.835108] fp dts led open
LED ON
[ 2797.835184] ret: 1
[ 2798.835696] ret: 0
LED OFF
[ 2799.836261] ret: 1
LED ON
[ 2800.836688] ret: 0
LED OFF
LED ON
[ 2801.837197] ret: 1
[ 2802.837571] ret: 0
LED OFF
[ 2803.838092] ret: 1
LED ON
[ 2804.838509] ret: 0
LED OFF

总结一下

Linux 字符设备驱动控制 GPIO 常见有两种思路:学习阶段可以通过 ioremap 直接映射 SoC 寄存器,理解 GPIO、IOMUX 等底层机制;实际工程开发更推荐使用设备树 + pinctrl + GPIO 子系统,因为可移植性、可维护性以及与 Linux 驱动框架的兼容性更好。