本文主题

  1. Nordic MCU的GPIO硬件简介、GPIOTE是什么、PPI是什么
  2. Zephyr中GPIO的使用、与外设引脚复用的方法(pinctrl)

声明:本文在解释硬件方面会比较详细,其目的是让读者在遇到问题时方便查阅,并debug底层寄存器信号。并非是推荐开发者直接进行寄存器开发,大多数情况下直接使用Nordic提供的外设API进行开发即可,可参考本文第3、4、5章。

1. GPIO硬件介绍

在介绍NCS中的GPIO和引脚复用(pinctrl)之前,有必要先介绍Nordic平台的GPIO相关硬件。

1.1. GPIO编号与分配表

每个Port上最多有32个GPIO,编号为0 ~ 31。Port从0开始,根据芯片封装的不同,可能还会有Port 1。例如,在代码中,P0.12对应的引脚编号就是12,而P1.12对应的引脚编号就是32+12,也就是44。

在每个手册的“Hardware and layout”章节,有不同MCU封装的GPIO功能说明,我们点进去可以看到每个引脚的用途。不仅是GPIO,还有一些电源和晶振引脚也包含其中:

image-20240122163623759

image-20240122164712134

image-20240122164636172

image-20240122164939613

这里面值得一提的一些信息有:

  1. 模拟引脚是固定的,标有Analog input的引脚才能作为模拟输入。
  2. 外设的数字引脚基本上是可以任意分配的。但有些外设会有推荐的引脚,例如上图中的QSPI。
  3. 某些引脚只能配置为Standard drive,无法作为高驱动模式。因此不适合高速数据传输的外设引脚。

1.2. GPIO硬件

下图来自于nRF52833 Product Specification。

image-20240122162605569

从框图可以看出,GPIO可以作为模拟输入,也可以作为数字输入和输出。

只有部分GPIO可以作为模拟输入,见1.2小节

寄存器介绍

image-20240130104903444

Nordic平台的GPIO,每个port最多有32个引脚。

  • OUT:32bit寄存器,bit写1使对应的GPIO输出高,写0使对应的GPIO输出低
  • OUTSET:32bit寄存器,bit写1使对应的GPIO输出高,写0不影响对应的GPIO的状态
  • OUTCLR:32bit寄存器,bit写1使对应的GPIO输出低,写0不影响对应的GPIO的状态
  • IN:32bit寄存器,读取每个bit即为读取每个GPIO的状态
  • DIR:32bit寄存器,bit写1使对应的GPIO配置为输出模式,写0使对应的GPIO配置为输入模式
  • DIRSET:32bit寄存器,bit写1使对应的GPIO配置为输出模式,写0不影响对应的GPIO的模式
  • DIRCLR:32bit寄存器,bit写1使对应的GPIO配置为输入模式,写0不影响对应的GPIO的模式
  • LATCH:与休眠唤醒配置有关,见下方
  • DETECTMODE:与休眠唤醒配置有关,见下方
  • PIN_CONF:32个32bit寄存器。单独配置每个GPIO的输入输出(可覆盖DIR寄存器),输出模式、输出驱动能力、内部上下拉。

GPIO输出状态与驱动能力配置

Introduction to GPIO - General Purpose I/O - NerdyElectronics

我们知道GPIO的输出电路内部是两个MOS管作为开关,也就是说,GPIO其实有三种状态:

  • 输出高电平:上管导通、下管关断。通常代表逻辑1。
  • 输出低电平:上管关断,下管导通。通常代表逻辑0。
  • 输出高阻态:上下管均关断。可以代表逻辑0或逻辑1,取决于电路设计者自身的定义。

但我们控制GPIO时,代码中只会写0和1两种状态。这就要求我们提前配置好GPIO的输出模式(推挽、漏极开路、源极开路),每一种都代表不同的高、低电平或高阻态的组合:

image-20240122170147234

以上8种状态,包含了推挽、漏极开路、源极开路的状态。此外,当GPIO输出高电平或低电平时,还有标准驱动能力(Standard)和高驱动能力(High drive)两种选择。

举例来说,S0S1就是推挽(Push-Pull)输出;而S0D1就是开漏(Open-Drain)输出。我们知道开漏输出是为了做“线与”操作的,I2C协议就需要这种配置。同理,D0S1就是源极开路输出,可以实现“线或”操作。

线与:相连的GPIO中只要有一个输出低电平,则整个线保持低电平,且不能出现短路;

线或:相连的GPIO中只要有一个输出高电平,则整个线保持高电平,且不能出现短路。

这里除了标准驱动能力(Standard)之外,还有高驱动能力(High drive)可以选择。它相比于Standard可以输出更高的电流:

image-20240122171447781

上图中,GPIO的Electrical specification章节记录了GPIO的电气特性。可以看到标准输出和高驱输出时,拉电流与灌电流的承受范围。

GPIO输出电压的变化,本质是给线上的等效电容充电或放电。因此GPIO输入输出电流的能力越强,则输出高频信号的能力越强。

GPIO数字输入

关于输入,值得一提的是Nordic的输入是可以断开的(从框图中也能看出)。因此只要不使能输入和输出,GPIO内部就是断开的,不用担心漏电导致功耗问题。

GPIO内部上拉/下拉电阻

导线高低电平的电磁学本质,是把导线和地平面之间看作一个微小的电容。输出高电平即为给电容充电,输出低电平即为给电容放电。

上/下拉电阻的作用是,当导线上的所有GPIO都处于高阻态时,通过这个上/下拉电阻给导线充、放电,使得导线的电平处于一个确定的状态。

例如I2C总线,线路上所有的IO都是开漏输出,因此需要一个上拉电阻。当所有IO都输出逻辑1(高阻态)时,能通过这个上拉电阻给导线电容充电,使的线路电平被拉高。

如果PCB上没有上拉电阻,就需要MCU配置内部上拉。配置相关的寄存器如下:

image-20240130111651505

使用内部上拉时,需要注意电阻的阻值,典型值为13千欧。线路上的RC值影响线路上电平变化的速度。当无外挂电容,只考虑线路寄生电容时,使用此内部上拉电阻,I2C总线最高只能配置为100kbps。若想要配置到400kbps,请使用外部4.7千欧或更低的上拉电阻。

image-20240130111825537

除了有提高电平变化速度的场景,还有需要降低电平变化速度的场景。例如,一些通过边沿触发的GPIO中断、或者Reset引脚的触发等。不要认为有了上拉电阻,线路的电压就会稳定不受干扰。因为如果线路上电容值很小,微小的电荷变化就会引起巨大的电压变化。因此线路要保持稳定的电平与上拉电阻关系不太大,反而与线路上的电容关系很大。

1.3. GPIO复用

Nordic平台的外设配置GPIO时,基本上是可以任意选择的。并且,外设的配置可以自动覆盖(Override)GPIO的输入输出方向、输出值等配置。见本文1.2.小节框图中的几个OVERRIDE信号。但是,GPIO的上下拉、输出模式等配置,还是要在GPIO的寄存器中进行配置。

数字复用

要配置一个外设所使用的GPIO,只需直接在这个外设对应的寄存器中进行配置。例如,下图是PWM外设中的PSEL(Pin Select)寄存器,就是可以选择任意一个port的任意一个pin作为输出引脚。当你在PWM外设的寄存器中配置引脚之后,会自动按照本文1.2小节框图中的OVERRIDE信号来覆盖对应GPIO的。

image-20240130114131417

模拟复用

模拟复用不能选择任意的GPIO,只能选择具有Analog Input功能的GPIO。以SAADC为例,这里只能选择AINx或者内部VDD、内部VDD/5作为输入,而不是GPIO的引脚编号。

image-20240130114325318

特殊GPIO(RESET和NFC Tag)

Nordic平台具有UICR寄存器,这是一个flash之外的掉电不丢失区域,用于存储一些用户配置,可擦写。其中具有RESET和NFC Tag引脚的配置:

image-20240131100105700

image-20240131100201965

对于reset引脚来说,PSELRESET[0]PSELREET[1]的值都是PIN=18,PORT=0,CONNECT=0的情况下,P0.18才会作为Reset引脚使用。否则,P0.18作为普通GPIO使用。Reset信号无法映射到其他GPIO

软件控制reset引脚作为普通GPIO使用:

  • 在nRF5 SDK中,不要设置全局宏定义CONFIG_GPIO_AS_PINRESET

  • 在NCS中:CONFIG_GPIO_AS_PINRESET=n

NFC引脚是固定的两个,对于nRF52833来说是P0.09和P0.10。默认情况下这两个IO是GPIO,只有UICR中对应的bit写1之后,这两个IO才能作为NFC来工作。

软件控制NFC引脚作为普通GPIO使用:

  • 在nRF5 SDK中,在Keil/SES/Makefile中设置全局宏定义CONFIG_NFCT_PINS_AS_GPIOS
  • 在NCS中,在prj.conf或其他配置文件中,添加CONFIG_NFCT_PINS_AS_GPIOS=y

添加后,系统启动时会自动擦写UICR并重启。

1.4. GPIO的Sense机制

image-20240415155234948

从GPIO的框图中我们可以看出,每个GPIO在处于输入模式的情况下,有一个SENSE信号。它可以被每个引脚的PIN_CNF寄存器中对应的bit位控制。可以配置为高电平触发或低电平触发。

image-20240415155410982

所有引脚的Sense信号会汇聚成一个DETECT信号。这个DETECT信号有两种作用:

  • 使系统从System Off模式中唤醒(也就是GPIO唤醒休眠)
  • 在GPIOTE外设中产生Port中断(后续在GPIOTE章节中介绍)

这个DETECT信号本身又有两种模式,通过DETECTMODE寄存器进行配置。

第一种是单纯的把所有引脚的PINx.DETECT信号进行逻辑或运算,也就是标准的DETECT信号。

另一种是在逻辑或之前加了一个锁存器(Latch),当PINx.DETECT置1时,相当于RS锁存器的Set端写1,LATCH寄存器中的对应Bit会被写1;当PINx.DETECT置0时,LATCH寄存器中的对应Bit会被锁存,不会变化。LATCH寄存器中对应的bit只有被CPU显式地写1时才会清0,相当于RS锁存器的Clear端写1。这个叫做LDETCT信号。

2. GPIOTE与PPI介绍

对于第一次接触Nordic平台的开发者,首先要明白一个概念:GPIOTE和GPIO是完全不同的外设。要理解为什么是这样,需要先理解Nordic外设接口(Peripheral interface)。

2.1. 外设接口(Peripheral interface)

在Nordic平台中,无论是什么外设,都遵循类似的外设接口,其框图如下:

image-20240415161448616

整个框图代表外设,这个外设可以是Timer、串口、ADC等等。接下来详细解释内部框图的意思,框图中展示的都是所有外设都有的共通的部分。

TASK寄存器

TASK寄存器代表这个外设的输入。例如Timer计时的开始、结束、清零;ADC采样的开始、结束;串口的发送开始、结束等等。只要CPU给对应的TASK寄存器写1,外设就会去执行对应的动作。

EVENT寄存器与INTEN寄存器

EVENT寄存器代表这个外设的输出。例如串口DMA缓存接收满、ADC采样完成等等。这些事件(EVENT)可以用来触发CPU中断,只需要在INTEN寄存器中使能某个EVENT对应的中断,那么这个EVENT就能触发IRQ信号到NVIC模块。

SHORTS寄存器

Shorts意为短路。它可以让某个外设的EVENT自动触发自己的TASK,从而实现自动循环执行,无需CPU的干预。

这个短路路径是预设好的,固定的几条,不能自由搭配。以定时器为例:

image-20240415162841413

定时器最多有6个比较(Compare)通道。当定时器中的count值增长到某个通道的compare值时,触发compare事件。这里可以通过使能对应的SHORT寄存器,让这个COMPARE EVENT去触发定时器的CLEAR TASK,从而实现自动循环计数。也可以让这个COMPARE EVENT去触发定时器的STOP TASK,从而实现单次计数。

这里是无法把compare event连接到start task的(虽然这种连接本身没有意义),因为SHORTS寄存器里的路径是预设好的。

2.2. PPI (Programmable Peripheral Interconnect)

在2.1章节中我们了解了外设的接口。从框图中可以看到TASK和EVENT寄存器上,还连接了PPI。这个PPI本身也是一个外设,它可以让你把一个外设的EVENT寄存器直接连接到另一个外设的TASK寄存器上,从而实现外设之间的自动联动,无需CPU参与处理

image-20240415164008994

图中的竖线,代表PPI的通道(Channel),nRF52833共有32条通道,其中前20条可以自由配置。每个通道可以连接1个EVENT,和2个TASK。(把EVENT寄存器的地址写入到CH[n].EEP寄存器;然后把想触发的TASK写到CH[n].TEP或者FORK[n].TEP中即可)。

举一个实际的例子,就是《Zephyr驱动与设备树实战——串口》中的提到高速异步串口:首先,Nordic的串口硬件具有DMA的功能,可以直接把数据从串口搬运到内存;然后,Nordic的串口驱动软件具有空闲计时的功能,当一定时间没有收到数据,DMA缓存还没存满的时候,就直接不等DMA中断了,直接产生串口回调函数,让CPU提前处理。

这里就产生了一个问题:此时DMA缓存未满,CPU只知道首地址,如何知道数据的长度呢?毕竟串口外设本身可没有计数功能。(RXD.AMOUNT寄存器只有DMA传输完毕才能读,这种提前读取的场景是不知道有多少的)。

一个纯软件的方法,就是每读到1个字节,就进入CPU中断,把一个变量+1。当传输完成时,读取这个变量,就知道一共收到了多少字节了。但是这种方法非常消耗CPU资源,且功耗高。当串口波特率达到921600时,CPU几乎无法做别的事情了。

Nordic的驱动代码采用的是PPI的方法。每收到一个字节(EVENTS_RXDRDY),就通过PPI让Timer的计数器+1(TASKS_COUNT)。等到传输完毕时,直接读取Timer的计数值即可。整个传输过程中CPU都处于休眠状态,只有串口、timer、总线、内存等在工作。从而实现高性能、低功耗。

同理,带有流控的串口发送也是如此。串口正在发送时,突然收到流控制停止发送的信号,这时串口DMA立即停止发送。当重新恢复发送时,如何知道该从第几个字节开始重新发?也是一样使用Timer进行计数。

PPI的使能与分组

PPI每个通道可以单独使能或关闭。通过CHEN寄存器写1使能,写0关闭。或者通过CHENSET和CHENCLR寄存器这种类似于SR锁存器的操作方式进行使能或关闭。

nRF52833的PPI外设还有6个通道组。每个通道组有32个bit,对应32条通道。只要某个Bit置1,那么对应的通道就会被包含在这个通道组里:

image-20240415171821442

image-20240415171810227

通道组的作用仅仅只是让你可以同时使能或者关闭一组通道。

固定的PPI通道

对于nRF52833来说,20~31号通道是不可编程的,它的连接是固定的。通常用于连接RADIO、加密、RTC等外设。Nordic提供的无线协议栈(例如SoftDevice蓝牙低功耗协议栈)内部会用到这些PPI。

image-20240415172000272

分布式PPI(DPPI)

从nRF5340、nRF9160开始,Nordic内部的PPI升级为DPPI。从而把1对2的PPI通道升级为多对多的PPI通道。

每个外设的TASK寄存器会有对应的SUBSCRIBE(订阅)寄存器;EVENT寄存器会有对应的PUBLISH(发布)寄存器。通过发布-订阅不同的DPPI通道,实现了多对多的外设事件传输。

具体可以参考nRF5340或nRF9160的手册。

NCS中的PPI代码

前面都是讲解原理,比较详细。实际到NCS的代码中,PPI并不那么复杂。只需记住两个原则:

  1. NCS中无论是PPI还是DPPI,都被封装为gppi接口,因此开发者无需关注它们的区别;
  2. 永远不要自己指定某个具体的通道号来使用。因为Nordic的很多驱动代码里都用到了PPI,如果自己指定,很有可能和驱动中已经使用的通道冲突。因此我们应该用API来自动分配PPI通道,而不是自己指定。
#include <helpers/nrfx_gppi.h>
#include <nrfx_timer.h>
#include <nrfx_gpiote.h>

...

// 分别获取两个外设的特定EVENT和TASK地址
uint32_t EVENT = nrfx_timer_compare_event_address_get(&timer_inst, NRF_TIMER_CC_CHANNEL0);
uint32_t TASK = nrfx_gpiote_out_task_address_get(&gpiote_inst, OUTPUT_PIN));

// 自动分配一个空闲的PPI通道
uint8_t gppi_channel;
nrfx_gppi_channel_alloc(&gppi_channel);

// 使用此通道连接一个EVENT寄存器和一个TASK寄存器
nrfx_gppi_channel_endpoints_setup(gppi_channel, EVENT, TASK);

// 使能通道
nrfx_gppi_channels_enable(BIT(gppi_channel));

PPI例程位置:${NCS}/modules/hal/nordic/nrfx/samples/src/nrfx_gppi

2.3. GPIOTE (GPIO Tasks & Events)

GPIOTE和GPIO是不同的外设。通过第一章的介绍,我们知道GPIO作为输入输出,可以被CPU和其他外设使用。但是GPIO本身并不具有TASK和EVENT寄存器,因此无法与我们第二章介绍的PPI联动起来。

GPIOTE: Pin Task, Pin Event

GPIOTE也有很多通道(Channels),对于nRF52833来说有8个,每一个通道可以连接1个GPIO。给这个GPIO扩展出TASK和EVENT寄存器,分别是:

  • TASKS_SET:使对应的GPIO输出高电平
  • TASKS_CLR:使对应的GPIO输出低电平
  • TASKS_OUT:使对应的GPIO输出一个预设的行为(在GPIOTE->CONFIG寄存器的POLARITY bits中配置,这个预设的行为可以是输出高、输出低、翻转)
  • EVENTS_IN:当对应的GPIO检测到预设的波形时,产生一个EVENT(同样在GPIOTE->CONFIG寄存器的POLARITY bits中配置。这个预设的行为可以是上升沿、下降沿、双边沿)

你可以用这些通道连接一个具体的GPIO,这样,本来不能产生中断的GPIO就可以通过EVENT寄存器产生中断了。

要查看具体的代码,同样可以查看${NCS}/modules/hal/nordic/nrfx/samples/src/nrfx_gppi例程。注意到GPIOTE的通道也是一种可以分配的资源。和PPI类似,使用时,不要自己指定具体的通道号,而应该用nrfx_gpiote_channel_alloc()函数来申请一个空闲的通道,以免和Nordic驱动代码中已经使用的GPIOTE通道冲突。

GPIOTE: Port Event

GPIOTE还有一个EVENT寄存器叫做EVENTS_PORT。在第一章节讲述GPIO时,提到GPIO有一个SENSE机制,全体GPIO的SENSE信号进行或运算后,会得到DETECT信号。

image-20240122162605569

这里GPIOTE的EVENTS_PORT就是用来把这个DETECT信号变成一个Events寄存器,从而可以用来产生中断,或者连接PPI。

注意,DETCT信号虽然不是一个EVENT,但是DETECT信号本身就能把CPU从System Off模式唤醒,无需GPIOTE。

3. 在Zephyr系统中使用GPIO

前面两章详细介绍了GPIO、GPIOTE和PPI的硬件,目的是让开发者在遇到问题时可以知道该从哪里去Debug,该看什么寄存器。但在一开始软件开发时,不需要关心这么多细节。只需调用现成的驱动API即可。

3.1. 在Zephyr DeviceTree中配置GPIO

由于Zephyr所有硬件操作都在DeviceTree中完成,故需要先配置DeviceTree。下图演示了如何在一个node中写gpio:

n: node {
foo-gpios = <&gpio0 1 GPIO_ACTIVE_LOW>,
<&gpio1 2 GPIO_ACTIVE_LOW>;
}

首先,由于GPIO的配置是一个属性,因此必须写在一个节点(Node)内,例如led_0内。

《详解Zephyr设备树(DeviceTree)与驱动模型》一文中,我们知道DeviceTree的节点不能自己随便增加,每个节点都有对应的compatible,而compatible又必须有对应的Device Binding yaml文件,以及对应的驱动文件。现在问题是,如果我只想单纯的添加一个自由的GPIO,不使用任何led或者button驱动程序,该如何做?

你可以把gpio放在/zephyr,user节点下。这是一个自由的节点,就是用来绕过Device Binding,专门放开发者一些自由的device tree属性的,想在里面写什么都可以。

/{
zephyr,user{
my-gpios = <&gpio0 12 (GPIO_ACTIVE_HIGH|GPIO_PUSH_PULL|GPIO_PULL_DOWN)>;
};
};

然后是属性的名字,属性的名称必须以gpios结尾,也可以只写gpios。这样它才能被编译系统识别。

然后是属性的值,这是一个phandle-array类型的属性,可以写很多组。每个元素都是由三个部分组成:

  • GPIO Controller:也就是我们俗称的port。这里可以直接引用label,例如&gpio0
  • GPIO Pin Number:这个就是引脚编号。P0.12的编号就是12。
  • GPIO配置:激活状态、输入输出、上下拉等等。可以在这里配置,也可以后续在应用代码里配置修改。

注意,部分开发者会有误解。

激活状态GPIO_ACTIVE_LOW的意思是“逻辑1 = 低电平”;GPIO_ACTIVE_HIGH的意思是“逻辑1 = 高电平”。这是用于配置激活状态的参数,而不是部分人误解的配置默认输出高低电平的参数。

GPIO_ACTIVE_LOW常见于LED灯。因为MCU gpio灌电流能力比拉电流能力强,因此LED电路往往是电流流入GPIO,也就是“低电平 = LED灯亮”。

更多GPIO配置的参数选项,请参考文档:https://docs.zephyrproject.org/latest/hardware/peripherals/gpio.html

3.2. 在代码中控制GPIO

首先,需要在conf文件中使能GPIO的驱动(大多数例程都是默认使能的):

CONFIG_GPIO=y

在代码中,首先包含头文件:

#include <zephyr/devicetree.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>

使用device tree中定义的gpios来控制

在main函数中创建一个gpio_dt_sepc结构体,这个是操作一个单独GPIO的句柄:

const struct gpio_dt_spec my_gpio = GPIO_DT_SPEC_GET(DT_PATH(zephyr_user), my_gpios);
  1. device tree中的内容都不可更改,故用cosnt变量存储最好
  2. GPIO_DT_SPEC_GET()可以直接从device tree中读取到一个结构体的值
  3. 第一个参数是node_id,由于我们放在/zephyr,user节点下,故可以用绝对路径来指明这个节点,DT_PATH(zephyr_user)。其中逗号是名称的一部分,在C语言中要变成下划线,才能当作名称的一部分。
  4. 第二个参数是device tree的属性,也就是my-gpios。在C语言中,-需要变成下划线。

然后就可以配置、读写该GPIO

// write
gpio_pin_configure_dt(&my_gpio, GPIO_OUTPUT);
gpio_pin_set_dt(&my_gpio, 1);

//read
gpio_pin_configure_dt(&my_gpio, GPIO_INPUT);
int val = gpio_pin_get_dt(&my_gpio);

绕过device tree配置,直接控制GPIO

使用port控制不需要像前面一样给单独的pin编写device tree,适合快速写一些测试用的代码。但它的缺点是,你使用的所有GPIO都不会在DeviceTree中有提示,如果有GPIO使用冲突,编译时无法帮你检查出来。

//获取GPIO Port的句柄
const struct device *dev_gpio0 = DEVICE_DT_GET(DT_NODELABEL(gpio0));

gpio_pin_configure(dev_gpio0, 12, GPIO_OUTPUT);
gpio_pin_set(dev_gpio0, 12, 1);

gpio_pin_configure(dev_gpio0, 12, GPIO_INPUT);
int val = gpio_pin_get(dev_gpio0, 12);

更多API,请参考Zephyr GPIO文档

配置IO口电流驱动能力

从第1章我们知道,Nordic MCU的IO口驱动能力是可以配置的,这个是Nordic独有的功能,与Zephyr无关,具体参数为:

/** Standard drive for '0' (default, used with GPIO_OPEN_DRAIN) */
#define NRF_GPIO_DRIVE_S0 (0U << 8U)
/** High drive for '0' (used with GPIO_OPEN_DRAIN) */
#define NRF_GPIO_DRIVE_H0 (1U << 8U)
/** Standard drive for '1' (default, used with GPIO_OPEN_SOURCE) */
#define NRF_GPIO_DRIVE_S1 (0U << 9U)
/** High drive for '1' (used with GPIO_OPEN_SOURCE) */
#define NRF_GPIO_DRIVE_H1 (1U << 9U)
/** Standard drive for '0' and '1' (default) */
#define NRF_GPIO_DRIVE_S0S1 (NRF_GPIO_DRIVE_S0 | NRF_GPIO_DRIVE_S1)
/** Standard drive for '0' and high for '1' */
#define NRF_GPIO_DRIVE_S0H1 (NRF_GPIO_DRIVE_S0 | NRF_GPIO_DRIVE_H1)
/** High drive for '0' and standard for '1' */
#define NRF_GPIO_DRIVE_H0S1 (NRF_GPIO_DRIVE_H0 | NRF_GPIO_DRIVE_S1)
/** High drive for '0' and '1' */
#define NRF_GPIO_DRIVE_H0H1 (NRF_GPIO_DRIVE_H0 | NRF_GPIO_DRIVE_H1)

需要包含头文件,才可以使用这些参数

#include <zephyr/dt-bindings/gpio/nordic-nrf-gpio.h>

...

gpio_pin_configure_dt(&my_gpio, GPIO_OUTPUT | GPIO_OPEN_DRAIN | NRF_GPIO_DRIVE_H0);
// 开漏输出,且低电平为高电流驱动能力

3.3. 使用GPIO中断

使用GPIO输入中断也很简单,参考${NCS}/zephyr/samples/basic/button即可。具体步骤为:

void button_pressed(const struct device *dev, struct gpio_callback *cb,uint32_t pins)
{
printk("Button pressed at %lu \n", k_cycle_get_32());
}

void main()
{
...

// get the gpio dt specifier
const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios);

// configure pin
gpio_pin_configure_dt(&button, GPIO_INPUT);

// configure interrupt: rising edge
gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE);

// init and add your callbacks
static struct gpio_callback button_cb_data;
gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin));
gpio_add_callback(button.port, &button_cb_data);

...
}

注意,不要真的拿这个代码去处理按钮。因为这个是最底层的GPIO中断,并没有按键消抖功能。

4. 在Zephyr中分配外设引脚

从第一章我们知道,Nordic的引脚基本上可以任意分配给所有外设的。某些外设的驱动程序在读取device tree时,就是按照前面第3章描述的gpio的写法进行读取的,例如SPI的片选引脚。

除了使用gpios这种属性去配置外,我们在device tree中看到更多的是使用pinctrl去控制。

&spi3 {
status = "okay";
cs-gpios = <&arduino_header 16 GPIO_ACTIVE_LOW>; /* D10 */
pinctrl-0 = <&spi3_default>;
pinctrl-1 = <&spi3_sleep>;
pinctrl-names = "default", "sleep";
};

&pinctrl{
spi3_default: spi3_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 15)>, // P1.15
<NRF_PSEL(SPIM_MISO, 1, 14)>, // P1.14
<NRF_PSEL(SPIM_MOSI, 1, 13)>; // P1.13
};
};

spi3_sleep: spi3_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 15)>, // P1.15
<NRF_PSEL(SPIM_MISO, 1, 14)>, // P1.14
<NRF_PSEL(SPIM_MOSI, 1, 13)>; // P1.13
low-power-enable;
};
};
}

这个其实不用太深入理解,改引脚时照葫芦画瓢即可。例如,以上代码定义了两种状态,分别叫”default”和”sleep”,两种状态的GPIO配置并不相同。那么在厂商提供的驱动代码中,只需要一行pinctrl_apply_state()函数,就能把所有的GPIO同时切换到另一种状态。这个是方便了驱动代码的编写。

只需注意,外设的引脚也是可以配置IO口电流驱动能力、上下拉的,例如:

i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 0, 26)>,
<NRF_PSEL(TWIM_SCL, 0, 27)>;
nordic,drive-mode = <NRF_DRIVE_S0D1>; // standard 0, disconnect 1
bias-pull-up; // internal pull-up
};
};

具体可配置的参数,大家可以Ctrl+鼠标左键,先跳转到pinctrl节点。然后再Ctrl+鼠标左键,点进”nordic,nrf-pinctrl”,查看DeviceBinding文件即可。

image-20240416171800803

image-20240416172102262

5. LED与Button库

前面都是GPIO的基础用法。如果你需要的只是驱动LED或者Button,可以直接使用Nordic现成的驱动API。

DK Libary

文档:https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/libraries/others/dk_buttons_and_leds.html

这是Nordic为开发板(Development Kit)提供的一个简易的库,支持4个以内的LED和Button。其中Button已经做了去抖。

我们在开发板默认的Device Tree中看到的led和button节点就是为这个库服务的。许多简单的例程就是用它来控制GPIO。

# 需要开启的配置
CONFIG_DK_LIB=y

通用应用程序框架(Common Application Framework, CAF)

CAF是Nordic为商业级应用程序开发的一个框架库。里面有蓝牙、功耗管理、SMP DFU等等模组,其中当然也包含按钮和LED。

CAF: LEDS库提供了基本的GPIO LED和PWM LED功能,并且可以配置灯效

CAF: Buttons库除了提供基本的Button去抖功能以外,还支持低功耗(不用时把按钮disable掉),并且支持矩阵键盘