imx6ull-驱动开发篇11——gpio子系统
目录
前言
gpio 子系统简介
I.MX6ULL 的 gpio 子系统驱动
设备树中的 gpio 信息
GPIO 驱动程序简介
of_device_id 匹配表
gpio-mxc.c文件
mxc_gpio_probe函数
mxc_gpio_port 结构体
mxc_gpio_get_hw 函数
imx35_gpio_hwdata结构体
bgpio_init函数
gpio_chip 结构体
gpiochip_add函数
gpio 子系统 API 函数
设备树中添加 gpio 节点模板
创建 test 设备节点
添加 pinctrl 信息
添加 GPIO 属性信息
前言
在上一讲内容里,驱动开发篇10——pinctrl 子系统,我们已经详细地讲解了pinctrl子系统。
这一讲,我们继续学习gpio子系统,熟悉原理、掌握常用的API函数。
gpio 子系统简介
GPIO子系统架构GPIO子系统采用分层设计,主要包含以下组件:
- GPIO芯片驱动层:直接操作硬件寄存器
- GPIO核心层:提供通用接口和框架
- GPIO用户接口层:向用户空间和内核其他模块提供API
gpio 子系统,用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,读取 GPIO 的值等。
// 申请GPIO
int gpio_request(unsigned gpio, const char *label);// 设置方向
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);// 读写操作
int gpio_get_value(unsigned gpio);
void gpio_set_value(unsigned gpio, int value);// 释放GPIO
void gpio_free(unsigned gpio);
gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO,。
Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大地方便了驱动开发者使用 GPIO。
I.MX6ULL 的 gpio 子系统驱动
设备树中的 gpio 信息
我们使用正点原子的I.MX6ULL-ALPHA 开发板,以SD 卡的检测引脚为例:
打开 imx6ull-alientek-emmc.dts,有如下代码:
pinctrl_hog_1: hoggrp-1 {fsl,pins = <MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 /* SD1 CD */
......>;
};
将 UART1_RTS_B 这个 PIN 复用为 GPIO1_IO19,并且设置电气属性。
SD 卡连接在I.MX6ULL 的 usdhc1 接口上,在 imx6ull-alientek-emmc.dts 中找到名为“usdhc1”的节点,这个节点就是 SD 卡设备节点,如下所示:
&usdhc1 {/* 引脚控制状态名称,对应不同时钟频率下的配置 */pinctrl-names = "default", "state_100mhz", "state_200mhz";/* 各状态对应的具体引脚配置组 */pinctrl-0 = <&pinctrl_usdhc1>; // 默认状态引脚配置pinctrl-1 = <&pinctrl_usdhc1_100mhz>; // 100MHz时钟下的引脚配置pinctrl-2 = <&pinctrl_usdhc1_200mhz>; // 200MHz时钟下的引脚配置// pinctrl-3 = <&pinctrl_hog_1>; // 注释掉的备用配置/* SD卡检测信号配置 */cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>; // 使用GPIO1_IO19作为卡检测引脚,低电平有效/* 电源管理相关配置 */keep-power-in-suspend; // 在挂起状态保持电源enable-sdio-wakeup; // 允许SDIO唤醒系统/* 电源供应配置 */vmmc-supply = <®_sd1_vmmc>; // 指定3.3V电源调节器/* 设备状态 */status = "okay"; // 启用该设备
};
其中,属性“cd-gpios”描述了 SD 卡的 CD 引脚使用的哪个 IO。
/* SD卡检测信号配置 */cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>; // 使用GPIO1_IO19作为卡检测引脚,低电平有效
- “&gpio1”表示 CD 引脚所使用的 IO 属于 GPIO1 组,
- “19” 表示 GPIO1 组的第 19 号 IO,通过这两个值 SD 卡驱动程序就知道 CD 引脚使用了 GPIO1_IO19这 GPIO。
- “GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH”就表示高电平有效。
根据上面这些信息, SD 卡驱动程序就可以使用 GPIO1_IO19 来检测 SD 卡的 CD 信号。
打开 imx6ull.dtsi,关于GPIO1节点有如下所示内容:
gpio1: gpio@0209c000 {compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio"; // 设备兼容性标识,支持i.MX6UL和i.MX35的GPIO驱动reg = <0x0209c000 0x4000>; // 寄存器物理地址范围:基地址0x0209c000,长度16KB// 中断配置:两个中断号,均采用高电平触发interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, // 第一个中断,SPI中断号66<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>; // 第二个中断,SPI中断号67gpio-controller; // 声明为GPIO控制器#gpio-cells = <2>; // 使用GPIO时需要2个参数:引脚号和标志位interrupt-controller; // 同时作为中断控制器#interrupt-cells = <2>; // 中断说明符需要2个参数:GPIO号和中断类型
};
gpio1 节点信息描述了 GPIO1 控制器的所有信息,重点就是 GPIO1 外设寄存器基地址以及兼容 属性 。
关 于 I.MX 系 列 SOC 的 GPIO 控 制 器 绑 定 信 息 请 查 看 文档: Documentation/devicetree/bindings/gpio/ fsl-imx-gpio.txt。
reg 属性设置了 GPIO1 控制器的寄存器基地址为 0X0209C000:
reg = <0x0209c000 0x4000>;
“#gpio-cells”属性:有两个 cell:
#gpio-cells = <2>;
- 第一个 cell 为 GPIO 编号,比如“&gpio1 3”就表示 GPIO1_IO03。
- 第二个 cell 表示GPIO 极 性 , 如 果 为 0(GPIO_ACTIVE_HIGH) 的 话 表 示 高 电 平 有 效 , 如 果 为1(GPIO_ACTIVE_LOW)的话表示低电平有效。
GPIO 驱动程序简介
of_device_id 匹配表
I.MX6ULL的 GPIO 驱动文件drivers/gpio/gpio-mxc.c,有如下所示 of_device_id 匹配表:
/* * i.MX系列GPIO控制器兼容性匹配表* 用于设备树匹配和驱动初始化*/
static const struct of_device_id mxc_gpio_dt_ids[] = {/* i.MX1系列GPIO兼容性定义 */{ .compatible = "fsl,imx1-gpio", // 设备树兼容性字符串.data = &mxc_gpio_devtype[IMX1_GPIO], // 指向IMX1的GPIO设备类型数据},/* i.MX21系列GPIO兼容性定义 */ {.compatible = "fsl,imx21-gpio", // i.MX21兼容字符串.data = &mxc_gpio_devtype[IMX21_GPIO], // IMX21设备类型数据指针},/* i.MX31系列GPIO兼容性定义 */{.compatible = "fsl,imx31-gpio", // i.MX31兼容字符串.data = &mxc_gpio_devtype[IMX31_GPIO], // IMX31设备类型数据指针},/* i.MX35系列GPIO兼容性定义 */{.compatible = "fsl,imx35-gpio", // i.MX35兼容字符串.data = &mxc_gpio_devtype[IMX35_GPIO], // IMX35设备类型数据指针},/* 结束标记 */{ /* sentinel */ }
};
其中,compatible 值为“fsl,imx35-gpio”的代码,和 gpio1 的 compatible 属性匹配,因此 gpio-mxc.c 就是 I.MX6ULL 的 GPIO 控制器驱动文件。
gpio-mxc.c文件
在 gpio-mxc.c 文件中有如下所示内容:
/** i.MX GPIO平台驱动注册结构体* 用于向内核注册GPIO控制器驱动*/
static struct platform_driver mxc_gpio_driver = {.driver = {.name = "gpio-mxc", /* 驱动名称,用于/sys/bus/platform/drivers/目录 */.of_match_table = mxc_gpio_dt_ids, /* 设备树匹配表,指向之前定义的兼容性表 */},.probe = mxc_gpio_probe, /* 设备探测函数,匹配成功后调用 */.id_table = mxc_gpio_devtype, /* 设备ID表,用于非设备树的平台设备匹配 */
};
GPIO 驱动也是个平台设备驱动,因此当设备树中的设备节点与驱动的of_device_id 匹配以后 probe 函数就会执行,在这里就是 mxc_gpio_probe 函数。
mxc_gpio_probe函数
mxc_gpio_probe函数就是I.MX6ULL 的 GPIO 驱动入口函数。
/** i.MX GPIO控制器探测函数* 完成GPIO控制器的硬件初始化和驱动注册*/
static int mxc_gpio_probe(struct platform_device *pdev)
{struct device_node *np = pdev->dev.of_node; // 获取设备树节点struct mxc_gpio_port *port; // GPIO端口数据结构struct resource *iores; // IO资源指针int irq_base; // 中断号基址int err; // 错误码// 获取硬件类型信息mxc_gpio_get_hw(pdev);// 分配端口内存空间port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);if (!port)return -ENOMEM;/*----- 硬件资源初始化 -----*/// 获取并映射寄存器资源iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);port->base = devm_ioremap_resource(&pdev->dev, iores);if (IS_ERR(port->base))return PTR_ERR(port->base);// 获取中断资源port->irq_high = platform_get_irq(pdev, 1); // 高16位GPIO中断port->irq = platform_get_irq(pdev, 0); // 基础中断if (port->irq < 0)return port->irq;/*----- 硬件初始化 -----*/// 禁用中断并清除状态writel(0, port->base + GPIO_IMR); // 中断屏蔽寄存器writel(~0, port->base + GPIO_ISR); // 中断状态寄存器/*----- 中断处理配置 -----*/if (mxc_gpio_hwtype == IMX21_GPIO) {// i.MX21系列的特殊处理irq_set_chained_handler(port->irq, mx2_gpio_irq_handler);} else {// i.MX3及后续系列的通用处理irq_set_chained_handler(port->irq, mx3_gpio_irq_handler);irq_set_handler_data(port->irq, port);// 高16位GPIO中断配置if (port->irq_high > 0) {irq_set_chained_handler(port->irq_high, mx3_gpio_irq_handler);irq_set_handler_data(port->irq_high, port);}}/*----- GPIO控制器初始化 -----*/// 使用gpiolib的通用后端初始化err = bgpio_init(&port->bgc, &pdev->dev, 4,port->base + GPIO_PSR, // 引脚状态寄存器port->base + GPIO_DR, // 数据寄存器NULL,port->base + GPIO_GDIR, // 方向寄存器NULL, 0);if (err)goto out_bgio;// 设置GPIO芯片特性port->bgc.gc.to_irq = mxc_gpio_to_irq; // 转换GPIO到IRQ的函数port->bgc.gc.base = (pdev->id < 0) ? // GPIO编号基址of_alias_get_id(np, "gpio") * 32 : pdev->id * 32;// 注册GPIO控制器err = gpiochip_add(&port->bgc.gc);if (err)goto out_bgpio_remove;/*----- 中断域配置 -----*/// 分配中断描述符irq_base = irq_alloc_descs(-1, 0, 32, numa_node_id());if (irq_base < 0) {err = irq_base;goto out_gpiochip_remove;}// 创建IRQ域port->domain = irq_domain_add_legacy(np, 32, irq_base, 0,&irq_domain_simple_ops, NULL);if (!port->domain) {err = -ENODEV;goto out_irqdesc_free;}// 初始化通用中断芯片mxc_gpio_init_gc(port, irq_base);// 将端口添加到全局列表list_add_tail(&port->node, &mxc_gpio_ports);return 0;/*----- 错误处理路径 -----*/...
}
让我们来详细分析一下这段代码:
mxc_gpio_port 结构体
定义一个结构体指针 port,结构体类型为 mxc_gpio_port。
struct mxc_gpio_port *port; // GPIO端口数据结构
gpio-mxc.c 的重点工作就是维护 mxc_gpio_port, mxc_gpio_port 就是对 I.MX6ULL GPIO 的抽象。
mxc_gpio_port 结构体定义如下:
/** i.MX GPIO端口数据结构* 描述一个GPIO控制器的所有硬件和软件状态*/
struct mxc_gpio_port {struct list_head node; // 链表节点,用于将多个GPIO端口串联到全局列表void __iomem *base; // GPIO寄存器基地址指针(I/O映射后的虚拟地址)int irq; // 主中断号(处理GPIO0-15中断)int irq_high; // 高16位中断号(处理GPIO16-31中断,可选)struct irq_domain *domain; // IRQ域,用于GPIO中断号映射管理struct bgpio_chip bgc; // 通用GPIO控制器数据结构,包含:// - gpio_chip 基础结构// - 寄存器访问方法// - 状态缓存等u32 both_edges; // 双边沿触发状态标志位,用于:// - 记录配置为双边沿触发的GPIO位// - 实现模拟双边沿触发功能
};/* * 结构体功能详解:* 1. 硬件资源管理:* - base: 提供对GPIO控制寄存器的访问* - irq/irq_high: 管理两组GPIO中断线** 2. 中断系统集成:* - domain: 将GPIO引脚映射到Linux中断号* - both_edges: 特殊处理双边沿触发模式** 3. GPIO核心框架:* - bgpio_chip: 集成到Linux GPIO子系统* * 包含gpio_chip基本操作* * 实现方向控制/数值读写等标准接口** 4. 系统管理:* - node: 允许系统遍历所有已注册的GPIO控制器** 典型使用场景:* - 每个i.MX处理器的GPIO bank对应一个port实例* - 通过base+偏移访问具体寄存器* - 通过bgc提供标准GPIO操作接口*/
mxc_gpio_get_hw 函数
调用 mxc_gpio_get_hw 函数获取 gpio 的硬件相关数据,其实就是 gpio 的寄存器组。
// 获取硬件类型信息mxc_gpio_get_hw(pdev);
函数 mxc_gpio_get_hw 里面有如下代码:
/** mxc_gpio_get_hw - 获取i.MX GPIO控制器硬件类型和配置数据* @pdev: 平台设备指针** 根据设备树匹配结果确定具体的GPIO硬件类型,并设置对应的硬件配置数据。* 该函数在probe阶段被调用,用于初始化硬件相关参数。*/
static void mxc_gpio_get_hw(struct platform_device *pdev)
{// 通过设备树兼容性字符串匹配硬件类型const struct of_device_id *of_id = of_match_device(mxc_gpio_dt_ids, &pdev->dev);enum mxc_gpio_hwtype hwtype;/* * 硬件类型判断逻辑:* 根据of_match_table中设置的.data指针确定具体型号*/if (of_id && of_id->data) {hwtype = (enum mxc_gpio_hwtype)of_id->data;} else {/* 默认回退到最基础的类型 */hwtype = IMX1_GPIO;}// 根据硬件类型选择对应的配置数据结构if (hwtype == IMX35_GPIO)mxc_gpio_hwdata = &imx35_gpio_hwdata; // i.MX35系列专用配置else if (hwtype == IMX31_GPIO)mxc_gpio_hwdata = &imx31_gpio_hwdata; // i.MX31系列专用配置elsemxc_gpio_hwdata = &imx1_imx21_gpio_hwdata; // i.MX1/i.MX21通用配置// 设置全局硬件类型标识mxc_gpio_hwtype = hwtype;
}
mxc_gpio_hwdata 是个全局变量,如果硬件类型是 IMX35_GPIO 的话,设置mxc_gpio_hwdat 为 imx35_gpio_hwdata。
对于 I.MX6ULL 而言,硬件类型就是 IMX35_GPIO。
imx35_gpio_hwdata结构体
imx35_gpio_hwdata 是个结构体变量,描述了 GPIO 寄存器组,内容如下:
/** i.MX35系列GPIO硬件配置数据结构* 定义i.MX35处理器的GPIO控制器寄存器布局和中断触发类型配置*/
static struct mxc_gpio_hwdata imx35_gpio_hwdata = {/* GPIO寄存器偏移地址定义 */.dr_reg = 0x00, // 数据寄存器:存储GPIO输入/输出值.gdir_reg = 0x04, // 方向寄存器:配置GPIO输入/输出模式.psr_reg = 0x08, // 引脚状态寄存器:读取GPIO当前电平状态.icr1_reg = 0x0c, // 中断配置寄存器1:配置GPIO0-15中断触发方式.icr2_reg = 0x10, // 中断配置寄存器2:配置GPIO16-31中断触发方式.imr_reg = 0x14, // 中断屏蔽寄存器:使能/禁用GPIO中断.isr_reg = 0x18, // 中断状态寄存器:查看触发的中断源.edge_sel_reg = 0x1c, // 边沿选择寄存器:用于双边沿触发配置/* 中断触发类型配置值 */.low_level = 0x00, // 低电平触发配置值.high_level = 0x01, // 高电平触发配置值.rise_edge = 0x02, // 上升沿触发配置值.fall_edge = 0x03, // 下降沿触发配置值
};
imx35_gpio_hwdata 结构体就是 GPIO 寄存器组结构。
这样我们后面就可以通过mxc_gpio_hwdata 这个全局变量来访问 GPIO 的相应寄存器了。
bgpio_init函数
bgpio_init 函数主要任务就是初始化 bgc->gc 。 bgpio_init 里 面 有 三 个 setup 函 数 : bgpio_setup_io 、bgpio_setup_accessors 和 bgpio_setup_direction。
这三个函数就是初始化 bgc->gc 中的各种有关GPIO 的操作,比如输出,输入等等。
/*----- GPIO控制器初始化 -----*/// 使用gpiolib的通用后端初始化err = bgpio_init(&port->bgc, &pdev->dev, 4,port->base + GPIO_PSR, // 引脚状态寄存器port->base + GPIO_DR, // 数据寄存器NULL,port->base + GPIO_GDIR, // 方向寄存器NULL, 0);
bgpio_init 函数第一个参数为 bgc,是 bgpio_chip 结构体指针。
bgc 既有对 GPIO 的操作函数,又有 I.MX6ULL 有关 GPIO的寄存器,那么只要得到 bgc 就可以对 I.MX6ULL 的 GPIO 进行操作。
gpio_chip 结构体
bgpio_chip结构体有个 gc 成员变量, gc 是个 gpio_chip 结构体类型的变量。 gpio_chip 结构体是抽象出来的GPIO 控制器。
gpio_chip 结构体如下所示(有缩减):
/** GPIO控制器抽象接口* 定义GPIO控制器的标准操作方法和属性*/
struct gpio_chip {const char *label; /* 控制器标签/名称,用于标识 */struct device *dev; /* 关联的设备结构体 */struct module *owner; /* 所属模块(用于引用计数) */struct list_head list; /* 全局GPIO控制器链表节点 *//*----- 核心操作函数集 -----*/int (*request)(struct gpio_chip *chip, unsigned offset); /* GPIO引脚申请 */void (*free)(struct gpio_chip *chip, unsigned offset); /* GPIO引脚释放 *//* 方向控制 */int (*get_direction)(struct gpio_chip *chip, unsigned offset); /* 获取方向 */int (*direction_input)(struct gpio_chip *chip, unsigned offset); /* 设为输入 */int (*direction_output)(struct gpio_chip *chip,unsigned offset, int value); /* 设为输出 *//* 数值操作 */ int (*get)(struct gpio_chip *chip, unsigned offset); /* 获取引脚值 */void (*set)(struct gpio_chip *chip, unsigned offset, int value); /* 设置引脚值 */......
};
可以看出, gpio_chip 大量的成员都是函数,这些函数就是 GPIO 操作函数。
gpiochip_add函数
调用函数gpiochip_add向Linux内核注册gpio_chip,也就是 port->bgc.gc。
// 注册GPIO控制器err = gpiochip_add(&port->bgc.gc);
注册完成以后我们就可以在驱动中使用 gpiolib 提供的各个 API 函数。
gpio 子系统 API 函数
对于驱动开发人员,设置好设备树以后就可以使用 gpio 子系统提供的 API 函数来操作指定的 GPIO, gpio 子系统向驱动开发人员屏蔽了具体的读写寄存器过程。
gpio 子系统提供的常用的 API 函数有下面几个:
函数名 | 功能描述 |
---|---|
gpio_request | 申请GPIO引脚使用权(使用前必须调用) |
gpio_free | 释放已申请的GPIO资源 |
gpio_direction_input | 将GPIO配置为输入模式 |
gpio_direction_output | 将GPIO配置为输出模式(可设置初始输出值) |
gpio_get_value | 读取GPIO当前输入电平值 |
gpio_set_value | 设置GPIO输出电平值 |
// 1. GPIO申请(必须首先调用)
int gpio_request(unsigned int gpio, const char *label);
/* 功能:申请GPIO引脚使用权* 参数:* gpio - 要申请的GPIO编号(通过of_get_named_gpio()从设备树获取)* label - 自定义标识字符串(出现在/sys/kernel/debug/gpio中)* 返回:* 0成功,非零失败(-EBUSY表示已被占用)*/// 2. GPIO释放
void gpio_free(unsigned int gpio);
/* 功能:释放GPIO资源* 参数:* gpio - 要释放的GPIO编号*/// 3. 设置为输入模式
int gpio_direction_input(unsigned int gpio);
/* 功能:配置GPIO为输入方向* 参数:* gpio - 要配置的GPIO编号* 返回:* 0成功,负值表示错误*/// 4. 设置为输出模式(带初始值)
int gpio_direction_output(unsigned int gpio, int value);
/* 功能:配置GPIO为输出方向并设置初始电平* 参数:* gpio - 要配置的GPIO编号* value - 初始输出值(0/1)* 返回:* 0成功,负值表示错误*/// 5. 读取GPIO值(输入模式使用)
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned int gpio);
/* 功能:读取GPIO当前电平值* 参数:* gpio - 要读取的GPIO编号* 返回:* 0/1 - 实际读取的电平* 负值 - 读取失败*/// 6. 设置GPIO值(输出模式使用)
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned int gpio, int value);
/* 功能:设置GPIO输出电平* 参数:* gpio - 要设置的GPIO编号* value - 要设置的值(0/1)*/
使用举例:
#include <linux/gpio.h>// 假设从设备树获取的GPIO编号
#define LED_GPIO 123 // 输出用GPIO
#define KEY_GPIO 456 // 输入用GPIO// GPIO初始化
void gpio_init(void)
{// 1. 申请GPIOgpio_request(LED_GPIO, "my_led");gpio_request(KEY_GPIO, "my_key");// 2. 设置方向gpio_direction_output(LED_GPIO, 0); // LED初始低电平gpio_direction_input(KEY_GPIO); // 按键输入模式
}// GPIO使用示例
void gpio_usage(void)
{// 3. 输出控制(LED闪烁)gpio_set_value(LED_GPIO, 1); // LED亮gpio_set_value(LED_GPIO, 0); // LED灭// 4. 输入读取(按键检测)if (gpio_get_value(KEY_GPIO)) {printk("Key pressed!\n"); }
}// GPIO释放
void gpio_release(void)
{gpio_free(LED_GPIO);gpio_free(KEY_GPIO);
}
设备树中添加 gpio 节点模板
我们来学习一下如何创建 test 设备的 GPIO 节点。
创建 test 设备节点
在根节点“/”下创建 test 设备子节点,如下所示:
test {/* 节点内容 */
};
添加 pinctrl 信息
在之前的文章里,pinctrl 子系统,我们创建了 pinctrl_test 节点,此节点描述了 test 设备所使用的 GPIO1_IO00 这个 PIN 的信息。
我们要将这节点添加到 test 设备节点中,如下所示:
test {pinctrl-names = "default";pinctrl-0 = <&pinctrl_test>;/* 其他节点内容 */
};
添加 GPIO 属性信息
在 test 节点中添加 GPIO 属性信息,表明 test 所使用的 GPIO 是哪个引脚。
添加完成以后如下所示:
test {pinctrl-names = "default";pinctrl-0 = <&pinctrl_test>;gpio = <&gpio1 0 GPIO_ACTIVE_LOW>;
};
关于 pinctrl 子系统和 gpio 子系统就讲解到这里。
下一讲内容我们使用 pinctrl 和 gpio 子系统,来驱动 I.MX6ULL-ALPHA 开发板上的 LED 灯。