当前位置: 首页 > article >正文

第二课 — 读取按钮状态用以控制LED闪烁

(一)概述

在本节课,我们将探讨nRF Connect SDK中硬件的描述方式,这种描述方式在开发套件(DK)、片上系统(SoC)、封装系统(SiP)和模块中都适用。设备驱动程序实现应用程序与硬件之间的交互,本节课程将了解到nRF Connect SDK中使用的设备驱动程序。我们将以通用输入/输出(General-Purpose Input/Output, GPIO)硬件外设及其驱动程序为例,逐行分析前一节课中我们在板上下载的blinky示例。

在本节课的练习部分,我们将学习如何使用GPIO外设来控制LED并读取按钮状态,包括轮询和中断两种方法。

(二)目标

  • 检查devicetree API <zephyr/devicetree.h>;
  • 检查板级 (board-level) 的device tree.dts;
  • 检查片级 (SoC-level) 的device tree.dtsi;
  • 熟悉devicetree绑定文件(.yaml)的作用及其兼容属性;
  • 熟悉设备驱动 <zephyr/device.h>;
  • 分析设备驱动程序API与设备驱动程序实现之间的解耦以及使用一个设备指针的必要性;
  • 检查GPIO APIs <zephyr/drivers/gpio.h>;
  • 通过实际操作练习配置GPIO引脚,学习如何读取/写入GPIO引脚以及如何实现GPIO引脚的设置中断操作。

(三)课程内容

3.1 Devicetree

在嵌入式系统固件开发中,硬件传统上是在头文件(.h或.hh)中描述的。nRF Connect SDK从Zephyr RTOS借鉴使用一种更结构化和模块化的方法来描述硬件,我们称之为设备树(Devicetree)。

设备树是一种用于描述硬件的层次数据结构。所描述的硬件可以是开发套件、SoC、SiP、模块,包含从开发套件中LED的GPIO配置到外设的内存映射位置的一切。设备树采用由连接在一起的节点组成的特定格式,每个节点包含一组属性。

以下内容来源于the Zephyr project官网。

3.1.1 Devicetree基础

很直白,Devicetree的字面意思指明了其像树一样的结构。.DTS文件是Devicetree的文件格式,DTS是devicetree source的缩写。下面是一个DTS文件的示例:

/dts-v1/;
/{
	a-node{
		subnode_label: a-sub-node{
			foo = <3>;
		};
	};
};

上面这个示例中包含三个节点(nodes):

  1. 一个根节点: /
  2. 一个命名为a-node的子节点
  3. 一个命名为a-sub-node的子节点的子节点

节点可以被赋予标签 (label),这些标签是唯一的简写,可以在设备树的其他地方用来指代已标记的节点。示例中,子节点a的标签是subnode_label。一个节点可以有一个或多个节点标签,也可以没有。

Devicetree节点也可以具有属性。属性是名称/值键值对。属性值可以是字符串、字节、数字或任何类型混合的数组。

节点a-sub-node有一个名为foo的属性,其值是3。foo值的大小和类型由DTS中的括号(<和>)隐含表示。如果传达的是布尔值,属性可以为空值。在这种情况下,属性的存在与否已足够描述。

设备树节点具有标识其在树中位置的路径。类似于Unix文件系统路径,设备树路径是由斜杠(/)分隔的字符串,根节点的路径是一个单独的斜杠:/。除此之外,每个节点的路径是通过将节点祖先的名字与其自身名称连接而成,中间用斜杠分隔。例如,子节点a的完整路径是/a-node/a-sub-node。

3.1.2 设备树绑定(YAML文件)

YAML文件定义了兼容属性。它声明了设备树节点内容的要求,并提供了有效节点内容的语义信息。Zephyr的设备树绑定被定义为YAML文件。每个设备树节点都必须有一个兼容属性。设备树节点通过其在YAML文件中的定义与兼容属性进行匹配。

下面是定义名为nordic的兼容属性、nrf-sample的设备树绑定文件(.yaml)的一个示例,该设备树绑定文件包含一个名为num-sample的必需属性,其类型为整型。

compatible: "nordic, nrf-sample"
properties:
	num-sample:
		type: int
		required: true

下面是示例DTS文件(.dts),其中节点node0设置为兼容的nordic、nrf-sample。这意味着node0必须具有所需的属性num-sample,并且该属性必须被赋值一个整数值。否则,构建将无效。

node0 {
	compatible = "nordic, nrf-sample";
	num-sample = <3>;
};

设备树绑定随SDK一起提供在<install_path>\zephyr\dts\bindings中(参见Nordic公司的设备树绑定)。在某些情况下,您需要自定义YAML文件,例如创建自定义驱动程序时。这在nRF Connect SDK中级课程中有详细讲解。

3.1.3 别名Aliases

通常会在设备树中添加别名。/aliases节点包含别名属性,其中属性的名称是该别名的名称,属性的值是设备树中某个节点的引用,见下面的示例。

/{
	aliases {
		subnode_alias = &subnode_label;
	};
};

上述代码片段将节点a-sub-node,通过其标签subnode_label赋值到别名subnode_alias。这里的目的在于你的C/C++应用程序代码(例如:main.c)将使用该别名。在板卡的dts文件中定义固定别名(例如:led0表示板上的第一个LED),可以使应用程序代码更加可移植,因为它可以避免硬编码不同的设备节点名称,并使应用程序代码对所用板卡的变化更加灵活。

3.1.4 访问设备树Accessing the devicetree

要获取源代码中特定设备树节点的信息,需要一个节点标识符。这只是一个引用该节点的C宏。获取节点标识符的方法有很多。

两种常见的方法是利用节点标签通过宏DT_NODELABEL(),以及通过别名通过宏DT_ALIAS()。

例如,要获取子节点a的节点标识符:

DT_NODELABEL(subnode-label)

要获取分配给某个设备树属性的值,可以使用宏DT_PROP()。例如,要获取分配给foo属性的值:

DT_PROP(DT_NODELABEL(subnode-label, foo))

3.1.5 Devicetree示例

让我们通过一个实际例子来更好地理解这些概念。nRF52833 DK配备了用户可配置的四个LED(PCB标记为LED1至LED4),连接到GPIO引脚P0.13至P0.16,如下图所示,该图来自nRF52833 DK的原理图(可在开发套件页面的下载->硬件文件中获取)。

img

img

[!NOTE]

nRF54系列DK(例如nRF54L15 DK)上的LED和按钮的PCB标签“丝网印刷在PCB上”现在与设备树中的定义一致。

例如,PCB标签“LED0”对应于devicetree中的“led0”。这与我们之前的开发工具包不同,之前使用“LED1”PCB标签来指代devicetree中的“led0”。

3.1.6 DK设备树文件

这些硬件细节都包含在nRF52833 DK的设备树文件中,它位于<install_path>\zephyr\boards\nordic\nrf52833dk\nrf52833dk_nrf52833.dts

img

  1. DK的设备树文件包括开发工具包中使用的特定SoC变体的设备树。对于nRF52833 DK,该文件是位于<install_path>\zephyr\dts\arm\nordic目录下的nrf52833_qiaa.dtsi文件。此文件被使用是因为它对应于nRF52833 DK上所使用的SoC的封装变体和功能变体。DTSI中的I表示包含。dtsi文件通常包含SoC级别定义。它还包含了引脚映射,定义在nrf52833dk_nrf52833-pinctrl.dtsi中。
  2. nRF52833 DK上的LED1(请参阅上面的信息说明)的节点名称为led_0,节点标签为led0。节点标签通常用于指代节点,例如&led0。
  3. led_0有两个属性:gpios和label;
  4. 你可以看到属性gpios通过&符号引用了节点gpio0。gpio0在SoC设备树中定义,我们将在下一段中看到。LED1在套件上的GPIO引脚连接到nRF52833 SoC的位置被定义为GPIO 0,即第13引脚(P0.13),并且是低电平有效。

[!IMPORTANT]

节点通常具有节点标签,但也可以有一个名为“标签”的属性。例如,节点led_0具有节点标签led0和一个值为“绿色LED 0”的属性标签。在此背景下,“标签”属性添加了一个描述LED的人类可读字符串。使用“标签”属性(如“绿色LED 0”)来获取节点标识符是不推荐的,也不应使用。相反,建议使用节点标签(led0)来获取节点标识符。

别名节点也定义在DK设备树文件中,参见下图。

img

我们从DK设备树的/aliases节点中可以看到,节点led_0通过其节点标签&led0被赋予了别名led0。这看起来可能多余;然而,这样做是为了确保所有带有LED节点的板卡都能为其LED设置一个常量别名(例如,第一个LED的别名是led0),这样应用程序代码(例如main.c)就可以在不同的板卡上编译,而无需手动检查DTS文件并确定不同板卡上LED使用的节点标签。

如果我们查看一些其他外围节点,您会注意到各种pinctrl属性(在<install_path>\zephyr\dts\bindings\pinctrl\pinctrl-device.yaml).中定义)。例如,&uart0,描述UART0外围节点的节点具有pinctrl-0、pinctrl-1和pinctrl-names属性。

img

这是基于Zephyr的引脚控制,并通过在引脚控制设备树文件中定义的&pinctrl节点将特定的引脚配置分配给设备树节点。

3.1.7 固定控制设备树文件Pin contorl devicetree

设备树文件依赖于引脚控制设备树文件来映射各个节点(不包括LED和按钮)。如上图所示,&uart0节点引用了在此文件中定义的&uart0_default和&uart0_sleep节点。引脚控制设备树文件位于<install_path>\zephyr\boards\arm\nrf52833dk_nrf52833\nrf52833dk_nrf52833-pinctrl.dtsi.

img

  1. &pinctrl节点(在<install_path>\zephyr\dts\bindings\pinctrl\nordic,nrf-pinctrl.yaml中定义)包含其子节点中的所有设备引脚配置。
  2. 节点uart0_default编码了UART0外围设备默认状态下的引脚配置。引脚控制API允许根据状态为外围设备分配不同的引脚;两种标准状态是默认状态和睡眠状态。
  3. 每个子节点中的引脚配置按组组织,其中每个组在psels属性中指定一个引脚功能选择列表。group1节点是这些组之一,指定了UART_TX和UART_RTS的引脚配置。
  4. 组还可以指定所有指定引脚的共同的共享引脚属性。group2节点指定了UART_RX和UART_CTS的引脚配置,并为它们都设置了偏置上拉属性。

3.1.8 SoC变体设备树文件

现在,检查目录<install_path>\zephyr\dts\arm\nordic中可用的SoC变体设备树nrf52833_qiaa.dtsi。

img

  1. SoC变体设备树包括基本SoC设备树nrf52833.dtsi,它在同一个目录中。
  2. 它包含与SoC变体(版本)相关的信息,例如RAM和FLASH基础地址和大小。

3.1.9 基本SoC设备树文件

SoC设备树包含所有外设和系统模块的SoC级硬件描述。检查目录<install_path>\zephyr\dts\arm\nordic中可用的基本SoC设备树nrf52833.dtsi。

img

  1. 节点gpio0的定义在这里;
  2. 节点通过兼容性属性指定它所代表的硬件。驱动程序使用此属性来选择它支持的节点;
  3. 它还定义了节点的地址空间。

这一章节的内容应该使您对如何使用设备树描述和呈现硬件有一个基本的概览。要获取有关设备树的更多信息,您可以在此处下载设备树规范。

3.2 设备驱动程序模型(Device driver model)

为了与硬件外设或系统模块交互,我们需要使用设备驱动程序(简称驱动程序),这是一种处理硬件配置低级细节的软件。在nRF Connect SDK中,驱动程序实现与其API高度解耦。这就意味着我们可以在不修改应用程序的情况下切换低级驱动程序实现,因为我们可以使用相同的通用API。

这种解耦有许多好处,包括高度的可移植性,因为它使得可以在不同的板上使用相同的代码,而不需要手动修改底层驱动程序实现。

应用程序利用通用API与硬件交互,通过使用宏DEVICE_DT_GET()或相关宏获取所涉及硬件的设备指针。

[!NOTE]

也可以通过device_get_binding()获取设备指针,不过这已不再推荐。

与以前使用device_get_binding()检索设备指针的做法相反,使用DEVICE_DT_GET()的好处是如果设备未由驱动程序分配,则在构建时失败,例如,如果它不存在于设备树中或具有禁用状态。

此外,与device_get_binding()不同的是,它不执行运行时字符串比较,在某些情况下可能会对性能产生影响。

Zephyr设备模型负责通用API和设备驱动程序实现之间的关联。

img

宏DEVICE_DT_GET()具有如下所示的签名:

img

要获取设备指针,你需要传递设备树节点标识符。如在设备树部分所述,获取节点标识符的方法有很多。两种常用方法是通过宏DT_NODELABEL()获取节点标签和通过宏DT_ALIAS()获取别名。

在使用设备指针之前,应使用device_is_ready()进行检查,其签名如下:

img

device_is_ready()将检查设备是否准备好使用,例如,是否已正确初始化。

以下代码片段将使用DT_NODELABEL()返回的设备树节点标识符并返回指向设备对象的指针。然后device_is_ready()验证设备是否已准备好使用,即处于可以使用其标准API的状态。

const struct device *dev;
dev = DEVICE_DT_GET(DT_NODELABEL(uart0));

if (!device_is_ready(dev)) {
    return;
}

要使用设备驱动程序通用API,您必须有一个类型为const struct device的指针指向其实现。您需要针对每个外设实例这样做。例如,如果您有两个UART外设(&uart0和&uart1),并且希望同时使用它们,那么您必须有两个独立的类型为const struct device的指针。

也就是说,您需要对DEVICE_DT_GET()进行两次不同的调用。

[!IMPORTANT]

大多数外围API都将具有与DEVICE_DT_GET()和device_is_ready()等效的、特定于外围设备的方法。例如,对于GPIO外围设备,有GPIO_DT_SPEC_GET()和gpio_is_ready_dt()。

这些是推荐的外围设备使用方法,因为它们从设备树结构中收集更多关于外围设备的信息,并减少了在应用程序代码中添加外围设备配置的需求。

3.3 GPIO通用API

要与通用输入输出(GPIO)外设交互,我们可以使用通用API <zephyr/drivers/gpio.h>,该API提供了用户友好的功能,用于与GPIO外设进行交互。GPIO外设可用于与各种外部组件互动,如开关、按钮和LED。

在使用Zephyr中的任何驱动程序时,第一步是通过获取设备指针来初始化它。对于GPIO引脚,接下来的必要步骤是将引脚配置为输入或输出引脚。然后可以向输出引脚写入数据或从输入引脚读取数据。在以下段落中,这四个步骤将详细说明。

3.3.1 初始化API

Zephyr中的一些通用API拥有特定于API的结构体,其中包含前面提到的设备指针以及其他关于设备的信息。在GPIO API中,这是gpio_dt_spec结构。该结构体包括设备指针常量结构体device * port,以及设备上的引脚编号gpio_pin_t引脚和设备的配置标志gpio_dt_flags_t dt_flags。

端口是控制引脚的GPIO设备。引脚通常被分组并由单个GPIO端口控制。在大多数Nordic SoC上,有一个或两个GPIO控制器,分别命名为GPIO0或GPIO1。

您可以在SoC产品规范中查看此信息。例如,参见nRF52833产品规范GPIO—通用输入/输出部分。

img

要检索此结构,我们需要使用特定于API的函数GPIO_DT_SPEC_GET(),其签名如下:

img

与DEVICE_DT_GET()类似,GPIO_DT_SPEC_GET()也获取设备树节点标识符。它还获取节点的属性名称。该函数将返回一个gpio_dt_spec类型的变量,其中包含设备指针以及引脚编号和配置标志。

这种特定API的结构的优势在于,它将使用设备所需的所有信息封装在一个变量中,而不是必须逐行从设备树中提取这些信息。

以led_0为例,其设备树实现如下所示:

img

从上图中,我们可以看到包含所有这些信息的属性称为gpios,它是传递给GPIO_DT_SPEC_GET()的属性名称:

static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);

此函数将返回一个类型为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;
	}

3.3.2 配置单个pin

通过调用函数gpio_pin_configure_dt()来实现,其签名如下:

img

通过此函数,您可以通过下面示例中所示的第二个参数标志将引脚配置为输入GPIO_INPUT或输出GPIO_OUTPUT。

以下行配置与gpio_dt_spec引脚关联的引脚,可以表示为led.pin,作为输出引脚:

gpio_pin_configure_dt(&led, GPIO_OUTPUT);

您还可以指定其他硬件特性到引脚,例如驱动强度、上拉/下拉电阻、不同的硬件特性可以通过|运算符组合。同样,这是通过参数标志来实现的。

以下行配置了引脚led.pin为低电平有效的输出。

gpio_pin_configure_dt(&led, GPIO_OUTPUT | GPIO_ACTIVE_LOW);

所有GPIO标志均在此处记录。

3.3.3 对一个输出引脚进行“写”操作

使用函数gpio_pin_set_dt(),可以非常简单地将数据写入输出引脚,其签名如下:

img

例如,以下行将与gpio_dt_spec led关联的引脚设置为逻辑1“激活状态”,该引脚可以表示为led.pin:

gpio_pin_set_dt(&led, 1);

例如,对于nRF52833DK上的节点led_0,这将把引脚13设置为逻辑1“激活状态”。

您还可以使用gpio_pin_toggle_dt()函数切换输出引脚。

img

例如,以下行将切换引脚led.pin。每当调用此API时,该行都会切换引脚led.pin。

gpio_pin_toggle_dt(&led);

3.3.4 对一个输入引脚进行“读”操作

读取配置为输入的引脚不像写入配置为输出的引脚那样简单。有两种方法可以读取输入引脚的状态:

(1)轮询方式(polling method)

轮询意味着持续读取引脚的状态,以检查是否已更改。要读取引脚的当前状态,您只需调用gpio_pin_get_dt()函数,其签名如下:

img

例如,下面的行读取led.pin的当前状态,并将其保存到名为val的变量中。

val = gpio_pin_get_dt(&led); 

轮询方法的缺点是,必须反复调用gpio_pin_get_dt()来跟踪一个引脚的状态。从性能和功耗的角度来看,这通常不是最优的,因为它需要CPU持续关注。这是一种简单的方法,但并不节能。

我们将使用本课练习1中的方法进行演示。

(2)中断方式(Interrupt method)

在此方法中,一旦引脚状态发生变化,硬件会通知CPU。这是推荐的读取输入引脚的方式,因为它可以减轻CPU反复轮询引脚状态的负担。您可以让CPU进入休眠状态,仅在有变化时唤醒它。我们将在本课的练习2中使用这种方法。

[!NOTE]

只能在配置为输入的GPIO引脚上配置中断

以下是设置GPIO引脚中断所需的通用步骤。

Step 1. 在引脚上配置中断。

通过调用函数gpio_pin_interrupt_configure_dt()来实现,其签名如下所示:

img

通过第二个参数<flags标志>,可以配置是否要在上升沿、下降沿或同时触发中断。或者改为逻辑电平1、逻辑电平0或同时触发中断。

以下行将配置在dev.pin上的中断,以将逻辑级别更改为1。

gpio_pin_interrupt_configure_dt(&button,GPIO_INT_EDGE_TO_ACTIVE);

所有中断标志 (flags) 选项均记录在GPIO中断配置标志下。

Step 2. 定义回调处理程序函数pin_isr()。

让我们定义一个回调处理程序函数,当触发中断时将调用该函数。

[!IMPORTANT]

Callback handler function回调处理函数:也称为中断处理程序或中断服务例程(ISR)。它在硬件或软件中断时异步运行。通常,ISR的优先级高于所有线程(详见第7节)。它会抢占当前线程的执行,使某个操作立即发生。只有当所有ISR工作完成之后,线程执行才会恢复。

回调处理程序函数的签名(原型)如下所示:

void pin_isr(const struct device *dev, struct gpio_callback *cb, gpio_port_pins_t pins);

此签名在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);
}

Step 3. 定义一个类型为static struct的变量gpio_callback,如下代码行所示。

static struct gpio_callback pin_cb_data;

pin_cb_data gpio回调变量将保存中断发生时要调用的引脚号和函数(回调函数)等信息。

Step 4. 使用gpio_init_callback()初始化gpio回调变量pin_cb_data。

此gpio_callback结构变量存储回调函数地址以及与该引脚相关的位。使用gpio_init_callback()函数进行此初始化。

img

例如,下面的行将用回调函数pin_isr和pin dev.pin的位掩码初始化pin_cb_data变量。注意使用宏BIT(n),它只是获取一个位位置为n的无符号整数。

gpio_init_callback(&pin_cb_data, pin_isr, BIT(dev.pin));

Step 5. 最后一步是通过函数gpio_add_callback()添加回调函数。

img

例如,下面的行添加了我们在前面步骤中设置的回调函数。

gpio_add_callback(button.port, &pin_cb_data);

GPIO通用接口的完整API文档可在此处获取。

3.4. 剖析闪烁示例

现在我们已经检查了设备树、设备驱动程序模型和GPIO通用API,让我们剖析blinky示例程序来了解它是如何工作的。在以下段落中,我们将逐行检查blinky示例代码,以了解该程序的工作原理。

blinky示例代码随nRF Connect SDK一起提供,可在以下位置找到:<install_path>\zephyr\samples\basic\blinky。

img

3.4.1 包含模块

闪烁示例在nRF Connect SDK中使用以下模块:

  • 用于睡眠功能k_msleep()的内核服务<zephyr/kernel.h>。此头文件称为公共内核API头文件。

  • 用于结构gpio_dt_spec的通用GPIO接口<zephyr/drivers/gpio.h>、宏GPIO_DT_SPEC_GET()以及函数gpio_is_ready_dt()、gpio_pin_configure_dt()和gpio_pin_toggle_dt()。

    #include <zephyr/kernel.h>
    #include <zephyr/drivers/gpio.h>
    

3.4.2 定义节点标识符

下面的代码使用了设备树宏DT_ALIAS()来获取节点标识符符号LED0_NODE,该符号将表示LED1(节点led_0)。请记住,在设备树部分中定义了DK的设备树中的led_0节点。现在,LED0_NODE是代表LED1硬件的源代码符号。

DT_ALIAS()宏从节点的别名中获取节点标识符,如我们在Devicetree部分看到的,它是led0。

#define LED0_NODE DT_ALIAS(led0) // LED0_NODE = led0 defined in the .dts file

[!NOTE]

检索节点标识符的方法有很多。宏DT_PATH()、DT_NODELABEL()、DT_ALIAS()和DT_INST()都根据不同的参数返回节点标识符。

3.4.3 获取设备指针、针号和配置标志

宏调用GPIO_DT_SPEC_GET()返回了结构体gpio_dt_spec,其中包含了节点led_0的设备指针以及引脚编号和相关配置标志。节点标识符LED0_NODE在前一步中定义,其gpios属性中嵌入了这些信息。请注意第二个参数gpios,即包含所有这些信息的属性名称。

static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);

接下来,到了main()函数。

3.4.4 验证设备是否准备好了

正如我们之前提到的,我们必须将gpio_dt_spec的设备指针,在这种情况下是led,传递给gpio_is_ready_dt(),以验证设备是否已准备好使用。

if (!gpio_is_ready_dt(&led)) {
		return 0;
	}

3.4.5 配置GPIO引脚

GPIO通用API函数gpio_pin_configure_dt()用于将与led关联的GPIO引脚配置为输出(低电平有效),并将其初始化为逻辑1,如GPIO通用API部分所述。

int ret;

ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
    return;
}

3.4.6 持续切换GPIO引脚

最后,blinky的main函数将进入一个无限循环,在这个循环中,我们使用gpio_pin_toggle_dt()不断切换GPIO引脚。请注意,在每次迭代中,我们还会调用内核服务函数k_msleep(),这会使main函数休眠1秒,从而实现每隔1秒一次的闪烁效果。

while (1) {
    ret = gpio_pin_toggle_dt(&led);
    if (ret < 0) {
        return;
    }
    k_msleep(SLEEP_TIME_MS);
}

3.5 练习1-通过按键来控制一个LED灯(轮询方式)

在本练习中,我们将修改blinky示例,使LED1仅在按下button1时才打开。

[!NOTE]

在nRF54L15 DK上,板上的LED和按钮用PCB标签(PCB丝网印刷)标记,从0开始(LED0-LED3)和(BUTTON0-BUTTON3)。在前一代开发套件中,索引从1开始(LED1-LED4)。因此,在nRF54L14 DK上,我们将使用LED0和BUTTON0。

为了实现这一点,我们将使用轮询方法,如3.3《GPIO通用API》章节中所述。这是通过持续轮询CPU来检查按钮是否被按下,并相应地更新LED来完成的。在接下来的练习2中,我们将学习如何使用GPIO中断,这比轮询更节能。

如果我们回顾nRF52833 DK的原理图,可以看到有四个按钮连接到与LED相同的GPIO外设,即GPIO0(节点标签:gpio0)。这可以从GPIO引脚映射中看出,例如,注意按钮1连接到P0.11。P0中的0表示它是&gpio0。

img

img

这就是为什么LED和按钮需要相同的驱动程序。从DK的设备树文件nrf52833dk_nrf52833.dts也可以看出这一点。

	buttons {
		compatible = "gpio-keys";
		button0: button_0 { 
			gpios = <&gpio0 11 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
			label = "Push button switch 0";
		};
		button1: button_1 {
			gpios = <&gpio0 12 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
			label = "Push button switch 1";
		};
/* ... */
		};
	};

/* ... */
	/* These aliases are provided for compatibility with samples */
	aliases {
/* ... */
		sw0 = &button0;
		sw1 = &button1;
		sw2 = &button2;
		sw3 = &button3;
		bootloader-led0 = &led0;
	};
};

nRF52833 DK上的按钮1被赋予了节点名称button_0,该节点具有别名(sw0)和节点标签(button0),并连接到&gpio0的第11引脚。请记住,在设备树文件中索引总是从0开始,这就是为什么板上的按钮1被称为button0。

gpios = <&gpio0 11 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;

[!NOTE]

如果按钮连接到与LED不同的GPIO端口(例如gpio1),则需要一个新的设备指针。例如,nRF54L15 DK在gpio1和gpio2上都有LED,而按钮连接到gpio0和gpio1。

Step 1 克隆nRF Connect SDK基础知识GitHub仓库。我们有两个选项。我们可以使用Visual Studio Code中的图形用户界面(在步骤1.1中描述)或命令行(在步骤1.2中描述)来克隆GitHub仓库。请选择1.1或1.2。

Step 1.1 复制到存储库的链接,然后使用VS Code的命令面板克隆存储库。

转到View ->命令调色板->键入Git Clone并粘贴到仓库链接中。将仓库保存在接近根目录的地方。

Step 1.2 或者,在根目录附近创建一个文件夹,然后在nRF Connect终端中运行以下命令克隆存储库。

git clone https://github.com/NordicDeveloperAcademy/ncs-fund.git

发出命令后,课程代码库将克隆到当前工作目录中。

[!NOTE]

请确保路径中没有空格或特定字符。

Step 2. 在VS code中打开本练习的练习代码库

Step 2.1 在nRF Connect扩展的欢迎视图中,单击打开现有应用程序。

img

选择该练习的基本代码,该代码在ncs-fund/v2.8.x-v2.7.0/l2/l2_e1中找到,然后单击“选择文件夹”。

Step 2.2 选择该练习的main代码,该代码在ncs-fund/v2.8.x-v2.7.0/l2/l2_e1中找到,然后单击“选择文件夹”。

img

Step 3 初始化硬件上的按钮

Step 3.1 通过别名sw0获取按钮1的节点标识符。

回想一下Devicetree章节的内容,DK Devicetree中的/aliases节点定义了节点&button0的别名sw0。下面的行使用宏调用DT_ALIAS()通过别名访问节点标识符。

在main.c,搜索Step 3.1然后复制粘贴下面代码:

#define SW0_NODE	DT_ALIAS(sw0) 

这样做是为了确保不同硬件之间的兼容性。

Stpe 3.2 通过gpio_dt_spec获取设备指针、引脚编号和引脚配置标志。

GPIO API有一个名为gpio_dt_spec的结构,它封装了所有这些信息,并且可以使用GPIO_DT_SPEC_GET()检索。

在main.c中,查找STEP 3.2并添加(复制和粘贴)以下行:

	static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(SW0_NODE, gpios);

此行使用宏GPIO_DT_SPEC_GET()为gpio_dt_spec变量的成员填充以下内容:

  • 设备指针:const struct device *port;
  • 引脚编号:gpio_pin_t引脚;
  • 引脚的flags:gpio_dt_flags_t dt_flags;

Step 4. 通过调用device_is_ready()来确认设备是否已经准备好了。

img

在main.c中,查找STEP 4并添加(复制和粘贴)以下行:

if (!device_is_ready(button.port)) {
	return -1;
}

Step 5. 配置连接按键的引脚为输入引脚,同时设置它的硬件规格

在main()中,查找STEP 5并添加以下行:

ret = gpio_pin_configure_dt(&button, GPIO_INPUT);
if (ret < 0) {
	return -1;
}

Step 6. 在main循环中,我们将无限期地轮询CPU以读取按钮的状态(按下=1,未按下= 0),并更新LED到按钮的状态。

Step 6.1 读取按键状态并存储

使用gpio_pin_get_dt(),读取button.pin的当前状态,并将其保存在变量val中。

搜索Step 6.1并在无限循环中添加以下代码:

bool val = gpio_pin_get_dt(&button);

Step 6.2 将LED更新为按钮状态

使用gpio_pin_set_dt()更新LED,以反映在上一步中保存在变量val中的按钮的当前状态。

搜索step 6.2,并在无限循环中添加以下代码:

 gpio_pin_set_dt(&led,val);

Step 7. 将睡眠时间从1000 ms更改为100 ms。100 ms是主线程进入睡眠状态后,仍然能够在按键被按下时及时响应的时间。

Step 8. 添加构建配置,就像我们在第1课练习2中所做的那样。

Step 9. 构建练习并将其显示在板上,就像我们在上一课中所做的那样。观察到当按下按钮1时,LED1被打开。(记住在nRF54L15 DK上,这将是按钮0和LED0)

此练习的解决方案可以在GitHub存储库中找到,即l2/l2_e1_sol。

3.6练习2 — 通过中断方式控制LED灯

在这个练习中,我们采用更节能的基于中断的方法修改应用程序,就像我们在《GPIO通用API》中说明的那样。首先,配置按钮使其每次被按下时生成一个中断;然后,在按钮的中断处理程序(回调函数)中,我们将切换LED。

Step 1. 在VS Code中,选择打开现有应用程序,就像我们在前面的练习中所做的那样。

Step 2. 在本课程的GitHub存储库中,打开本练习的练习代码库,该代码库位于2.8.x-v2.7.0目录的l2/l2_e2中。

Step 3. 在按钮的引脚上配置中断。

通过调用函数gpio_pin_interrupt_configure_dt()配置与按钮关联的引脚上的中断。

在main.c中搜索STEP 3并添加以下代码:

ret = gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE);

Step 4. 定义回调函数button_pressed()。

定义当按下按钮切换LED时将运行的回调函数。

搜索Step 4并添加以下代码:

void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
    gpio_pin_toggle_dt(&led);
}

Step 5. 定义类型为static struct的变量gpio_callback。

在main.c中搜索STEP 5,并添加以下行:

static struct gpio_callback button_cb_data;

Step 6. 初始化静态结构变量gpio_callback。

通过将此变量以及回调函数和GPIO pin按钮的位掩码pin传递给gpio_init_callback()来初始化结构gpio_callback button_cb_data。

搜索步骤6并添加以下行:

gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin)); 	

Step 7. 通过调用gpio_add_callback()添加回调函数。

搜索步骤7并添加以下行:

gpio_add_callback(button.port, &button_cb_data);

Step 8. 我们将从main循环中删除所有的轮询代码,并且我们将保持对函数k_msleep()的调用,以使主线程(在第7课中讨论)长时间休眠。
在这里插入图片描述

Step 9. 将main函数中的睡眠时间从100 ms增加到10分钟。这是通过将宏SLEEP_TIME_MS值从100变为10601000来实现的。

[!CAUTION]

在这个例子中,没有其他高优先级线程在运行。因此,空闲线程会自动被调用以使CPU进入休眠状态。每当按下按钮时,CPU就会唤醒,它会调用ISR函数,从而切换LED。然后执行恢复到无限循环,再次调用休眠功能。由于main函数除了配置GPIO和回调函数外没有任何功能,我们通过调用k_msleep()让它空闲,这样RTOS调度器可以选择一个就绪的线程来运行。更好的方法实际上是在线程循环中调用k_yield()。第7课将介绍不同的Zephyr系统线程。

10.构建练习并将其下载到板上。观察到当按下按钮1时,LED1被切换(在nRF54 DK中,是按钮0和LED0)。

此练习的解决方案可以在GitHub仓库l2/l2_2_sol目录v2.8.x-v2.7.0中找到


http://www.kler.cn/a/557460.html

相关文章:

  • 【JavaScript进阶】构造函数数据常用函数
  • [ Vim ] 常用命令 and 配置
  • 从0开始:OpenCV入门教程【图像处理基础】
  • open webui 部署 以及解决,首屏加载缓慢,nginx反向代理访问404,WebSocket后端服务器链接失败等问题
  • 记录一下_treafik使用Gateway-APi使用的细节参数
  • [ComfyUI]Recraft贴图开源方案,实现服装印花自由
  • P2865 [USACO06NOV] Roadblocks G 与最短路的路径可重复的严格次短路
  • Spring Boot中整合Flink CDC 数据库变更监听器来实现对MySQL数据库
  • 工业级无人机手持地面站技术详解
  • 基于SpringBoot+vue+uniapp的智慧旅游小程序+LW示例参考
  • DirectX SDK(June 2010)安装报错:S1023
  • 0222-leetcode-1768.交替合并字符串、389找不同、
  • 0基础学Linux系统(准备1)
  • Java试题:进制转换
  • SQL Server 创建用户并授权
  • 【部署优化篇十三】深度解析《DeepSeek API网关:Kong+Nginx配置指南》——从原理到实战的超详细手册
  • 3.3.2 交易体系构建——缠论操作思路
  • Git常见命令--助力开发
  • C++ 设计模式-中介者模式
  • Python采用DeepSeekR1本地部署+本地API接口实现简单对话