为了与通用输入/输出 (GPIO) 外设进行交互,我们可以使用通用 API <zephyr/drivers/gpio.h>,它提供了与 GPIO 外设交互的用户友好型函数。GPIO 外设可用于与各种外部组件(如开关、按钮和 LED)进行交互。
在 Zephyr 中使用任何驱动程序时,第一步都是通过检索设备指针来对其进行初始化。对于 GPIO 引脚,之后第一个必要的步骤是将引脚配置为输入或输出引脚。然后,您可以向输出引脚写入数据或从输入引脚读取数据。在接下来的段落中,将详细介绍这四个步骤。
Zephyr 中的一些通用 API 具有特定的 API 结构体,其中包含前面提到的设备指针以及有关设备的其他一些信息。在 GPIO API 中,该结构体为 gpio_dt_spec。该结构体包含设备指针 const struct device * port,以及设备上的引脚编号 gpio_pin_t pin 和设备的配置标志 gpio_dt_flags_t dt_flags。
端口是控制引脚的 GPIO 设备。引脚通常由单个 GPIO 端口分组和控制。在大多数 Nordic SoC 上,有一个或两个 GPIO 控制器,命名为 GPIO0 或 GPIO1。
您可以在 SoC 产品规范中查看此信息。例如,请参阅 nRF52833 产品规范中的 GPIO — 通用输入/输出章节。

为了检索此结构体,我们需要使用特定于 API 的函数 GPIO_DT_SPEC_GET(),其函数原型如下:

与 DEVICE_DT_GET() 类似,GPIO_DT_SPEC_GET() 也接收设备树(devicetree)节点标识符。它还接收节点的属性名称。该函数将返回一个类型为 gpio_dt_spec 的变量,其中包含设备指针以及引脚编号和配置标志。
这种特定于 API 的结构体的优势在于,它将使用设备所需的所有信息封装在单个变量中,而无需逐行从设备树中提取信息。
让我们以 led_0 为例,其设备树实现如下所示:

从上图中我们可以看到,包含所有这些信息的属性称为 gpios,它是传递给 GPIO_DT_SPEC_GET() 的属性名称:
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
C此函数将返回一个类型为 gpio_dt_spec 的结构体,其中包含 GPIO 控制器的设备指针 &gpio0、引脚编号 led.pin = 13 和标志 led.dt_flags = GPIO_ACTIVE_LOW。
在使用 gpio_dt_spec led 中包含的设备指针之前,我们需要使用 gpio_is_ready_dt() 检查它是否已就绪。
if (!gpio_is_ready_dt(&led)) {
return 0;
}C这是通过调用函数 gpio_pin_configure_dt() 完成的,其函数原型如下:

通过此函数,您可以通过第二个参数 flags 将引脚配置为输入 GPIO_INPUT 或输出 GPIO_OUTPUT,如下面的示例所示。
以下行将与 gpio_dt_spec led 相关联的引脚(可表示为 led.pin)配置为输出引脚:
gpio_pin_configure_dt(&led, GPIO_OUTPUT);C您还可以为引脚指定其他硬件特性,例如驱动强度、上拉/下拉电阻、高电平有效或低电平有效。不同的硬件特性可以通过 | 运算符进行组合。同样,这是使用参数 flags 完成的。
以下行将引脚 led.pin 配置为低电平有效的输出。
gpio_pin_configure_dt(&led, GPIO_OUTPUT | GPIO_ACTIVE_LOW);C所有 GPIO flags 均在 此处 有文档说明。
使用函数 gpio_pin_set_dt() 向输出引脚写入数据非常简单,其函数原型如下:

例如,以下行将与 gpio_dt_spec led 相关联的引脚(可表示为 )设置为逻辑 1 “有效状态”:led.pin
gpio_pin_set_dt(&led, 1);C例如,对于 nRF52833DK 上的节点 led_0,这会将引脚 13 设置为逻辑 1 “有效状态”。
您还可以使用 gpio_pin_toggle_dt() 函数来翻转输出引脚的状态。

例如,每当调用此 API 时,以下行都会翻转引脚 led.pin 的状态。
gpio_pin_toggle_dt(&led);C读取配置为输入的引脚并不像向配置为输出的引脚写入数据那样简单。有两种可能的方法来读取输入引脚的状态:
轮询意味着持续读取引脚的状态以检查其是否发生了变化。要读取引脚的当前状态,您只需调用函数 gpio_pin_get_dt(),其函数原型如下:

例如,以下行读取 led.pin 的当前状态并将其保存在名为 val 的变量中。
val = gpio_pin_get_dt(&led); C轮询法的缺点是您必须重复调用 gpio_pin_get_dt() 以跟踪引脚的状态。从性能和功耗的角度来看,这通常不是最优的,因为它需要 CPU 的持续关注。这是一种简单的方法,但功耗效率不高。
为了演示目的,我们将在本课的练习 1 中使用此方法。
在这种方法中,一旦引脚状态发生变化,硬件就会通知 CPU。这是读取输入引脚的推荐方式,因为它减轻了 CPU 重复轮询引脚状态的负担。您可以让 CPU 进入睡眠状态,仅在发生变化时将其唤醒。我们将在本课的练习 2 中使用此方法。
您只能在配置为输入的 GPIO 引脚上配置中断。
以下是在 GPIO 引脚上设置中断所需的一般步骤。
1. 在引脚上配置中断。
这是通过调用函数 gpio_pin_interrupt_configure_dt() 完成的,其函数原型如下所示:

通过第二个参数 flags,您可以配置是要在上升沿、下降沿还是两者都触发中断。或者更改为逻辑电平 1、逻辑电平 0 或两者。
以下行将在 button.pin (gpio_dt_spec.pin) 变为逻辑电平 1 时配置中断。
gpio_pin_interrupt_configure_dt(&button,GPIO_INT_EDGE_TO_ACTIVE);C所有中断标志选项均在 此处 的 GPIO 中断配置标志 下有文档说明。
2. 定义一个回调处理函数,称为 pin_isr()。
让我们定义在触发中断时将调用的回调处理函数。
回调处理函数:也称为中断处理程序或中断服务程序 (ISR)。它异步运行以响应硬件或软件中断。通常,ISR 的优先级高于所有线程(在第 7 课中介绍)。它会抢占当前线程的执行,从而允许立即采取行动。只有在所有 ISR 工作完成后,线程执行才会恢复。
回调处理函数的函数原型如下所示:
void pin_isr(const struct device *dev, struct gpio_callback *cb, gpio_port_pins_t pins);C此原型定义在 gpio_callback_handler_t() 中。
您在 ISR 体内放入的内容高度取决于应用程序。例如,以下 ISR 在每次触发中断时都会翻转 LED 的状态。
void pin_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
gpio_pin_toggle_dt(&led);
}C3. 定义一个类型为 static struct 的变量 gpio_callback,如下面的代码行所示。
static struct gpio_callback pin_cb_data;Cpin_cb_data gpio 回调变量将保存诸如引脚编号和发生中断时要调用的函数(回调函数)等信息。
4. 使用 gpio_init_callback() 初始化 gpio 回调变量 pin_cb_data。
此 gpio_callback 结构体变量存储回调函数的地址和与引脚相关的位掩码。使用函数 gpio_init_callback() 来执行此初始化。

例如,以下行将使用回调函数 pin_isr 和引脚 button.pin 的位掩码来初始化 pin_cb_data 变量。注意宏 BIT(n) 的使用,它只是获取一个设置了位位置 n 的无符号整数。
gpio_init_callback(&pin_cb_data, pin_isr, BIT(button.pin));C5. 最后一步是通过函数 gpio_add_callback() 添加回调函数。

例如,以下行添加了我们在前面步骤中设置的回调函数。
gpio_add_callback(button.port, &pin_cb_data);CGPIO 通用接口的完整 API 文档可在 此处 找到。