zephyr的GPIOTE驱动开发记录——基于nordic的NCS

简介:

  本次测试了zephyr的中断驱动方式(GPIOTE),在这可以去看zephyr的官方文档对zephyr的中断定义,连接如下,Interrupts — Zephyr Project Documentation (nordicsemi.com) ;版本可能不对应,但是原理是一致的,今天记录的就是其中的零延迟中断,就是减少中断时间,让来自外部的中断能快速响应,进入到我们的中断服务程序中进行快速执行(也就是ISR)。

  注意:本次测试中有两个版本的代码,我会再需要版本区别的时候进行备注,主要区分为v2.0之后(如V2.3,V2.4)以及v2.0之前(如V1.8),实际开发中请自行区别

 根据文档,就作者理解如下,如有更好的理解可以进行指正,有些时候在执行某些线程时对时间有要求或者在临界区进行操作时,不能够被外部中断(ISQ)打断,可以禁止该ISR(中断服务程序)的执行。通过IRQ禁止达到在处理某些线程时不会被打断,但是会让中断处理被延迟,但这时候又出现一个矛盾,有些中断是我想要及时处理的,那么我们需要不被屏蔽掉,就是这个中断中需要处理的事是比前面列举的线程执行更重要的事,那么怎么办,可以直接使用零延迟中断进行定义,让这些中断直接得到响应,在零延迟中断中又分为两种:一是常规的ISR,二是直接的ISR(某些情况下比常规的更快),常规的ISR可能还是会被打断,导致一些开销产生,具体打断情况在zephyr中有4点列举:

如果某个任务完全不想要被打断,快速的执行,那么就可以使用直接ISR(direct ISR)。在作者看来正常情况下(没有其余中断打断的情况下),他们两的时间应该是一致的。具体可以点击文章顶部给出的链接,直接看官方描述。

本次测试采用nordic的硬件与软件进行测试,在开发之前默认你已经配置好nordic的相关开发环境,如果是第一次开发,建议去安装下面给出的官方环境搭建参考文档,或者去哔哩哔哩观看环境搭建的学习视频(VS code),也可以参看我文章中的关于9160开机测试的文章。官方连接如下:开发你的第一个nRF Connect SDK(NCS)/Zephyr应用程序 - iini - 博客园 (cnblogs.com)

参考资料:

  nordic的官方讲解视频,可以在哔哩哔哩上搜索nordic半导体去看关于其中一个视频:zephyr的设备驱动程序模型,中断和电源管理视频,中文讲解( https://www.bilibili.com/video/BV1MU4y177Zhis_story_h5=false&p=1&share_from=ugc&share_medium=android&share_plat=android&share_session_id=c0145896-48dc-4bbf-b938f1f4b3a4644a&share_source=WEIXIN&share_tag=s_i×tamp=1668583626&unique_k=29oQkX4),或者直接参看zephyr的官方文档。

本次测试环境:

VS code、NCS1.8和NCS2.x(也就是2.0以上)

一、建立工程

建立一个zephyr的工程,如果你有NCS,并且已经安装好相关可以进行开发的环境,那么可以打开一个hello Word的工程进行添加,为什么可以依据nordic官方NCS进行开发,因为它也有如STM32等芯片底层文件,nordic只是在zephyr的SDK中加入了自己的产品形成了NCS包,其余zephyr原本有的并没有删减,所以你可以在NCS中建立如STM32芯片的工程进行开发,且上层的驱动都是抽象的,只是对应于硬件的定义换成了具体的芯片定义,我们只用管上层的APP开发,所以一套代码,可以建立成不同芯片的工程,并且在编译下载后依然可以运行,不止局限于nordic的开发(至少保证硬件资源差不多)。

1、zephyr工程建立

对于zephyr可以直接建立一个文件夹,然后再里面包含如下的几个文件就可以进行编译开发了,

1)、其中src中放置我们的.c文件(APP),便于管理;

2)、CMakeLists.txt是工程创建的根本文件,具体内容可以是如下:

#这是cmake的版本
cmake_minimum_required(VERSION 3.20.0)

#添加的库
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})

#建立的工程名字(本次为hello_world,可以改为GPIOTE等)
project(hello_world)

#添加.c文件,稍后在src中建立一个main.c
target_sources(app PRIVATE src/main.c)

3)、prj.conf为配置文件

  很多时候还需要一个overlay文件,可以进行设备树驱动的更改,在zephyr中有一个默认的硬件定义文件,如果自己工程中需要定义更改,就用overlay文件进行实现,这样不会影响都其余工程,值针对于本工程修改。

   由于本次我使用的是nordic的SDK(NCS),我可以在VS code上安装好相关插件,然后直接镜像创建一个工程在其余文件中(根据自己选择,但是保证和NCS处于同一个磁盘中即可),如果不会请参看前面提到的教学文档与教学视频,在观看后,你就可以理解为什么只是这几个文件就可以建立一个工程了。

  因此我们根据NCS中的hello_word建立一个镜像工程,并把该工程的文件夹名字命令为gpiote,且工程也建立为gpiote,,然后建立一个可以跑在nrf5340的应用核的工程。具体如下,该工程主要功能是,通过串口打印出,“hello world+板子信息”。

 2、添加自定义.c文件

原本已经有一个.c文件了,该文件中主要就是串口打印信息,本次测试是需要测试中断,所以我们在定义一个名字为gpiote.c的文件(如下图),添加到我们工程中,然后再进行代码编写。

然后把gpiote.c加入到工程,这就需要我们打开我们的CMakeLists.txt,添加如图所示代码,把gpiote.c加入我们的工程中(相当于keil的源文件添加):

 然后点击全编译,我们就可以看到我们的工程下加入了gpiote.c文件:

全编译如下图所示的按钮:

 3、overlay文件加入

这里有一个隐藏的规则,如果你看了前面推荐的nordic中文官方博客连接的内容,那么应该知道,在工程目录下建立文件名和我们使用的板子一致时,可以不用在CMakeLists.txt中进行文件添加,编译器建立工程时可以识别这overlay文件,知道你要更改默认的devicetree定义,会把你自己工程下的.overlay文件加入进入一起编译生成最终的设备树文件.dts文件,如果不知道请去看下前面给出的链接,那么zephyr定义了那些板子呢,他们的名是什么,可以直接在vs code中进行查看,就行是你建立工程时选择的板子名字的地方:

由于我使用的是nrf5340,那么我就建立一个同名的overlay文件,最后我们工程目录如下,就看我框选部分,其余是建立hello_word镜像工程时产生的:

 二、设备树更改

1、基于NCS1.8版本

这里注意的是我使用了1.8的NCS,如果你使用高版本的NCS如2.1,那么overlay文件会有一点问题,你可以参考其余工程就行修改。

主要是添加一个中断口定义,我们在nrf5340dk_nrf5340_cpuapp.overlay中进行处理,在添加前我们来看一下设备树文件zephyr.dts,建立编译工程后,可以在如下目录找到它:

 可以看到已经有一个buttons的设备定义了,我想自己加一个自己的按键定义,作为中断触发源,我使用的是官方开发板,按键依然是那几个,但是我可以再定义一个,然后起一个其他的名字。具体如下,在overlay中添加代码:

/*参数加入devicetree的位置*/ 
/{
    /*其别名,这主要给test_button其一个别名,然后可以在APP中通过别名gpiote定位到我们定义的按键*/ 
    aliases {    
        gpiote = &test_button;        
    };
    /*在原有的buttons下定义一个测试IO口,并且定位为GPIO0的0x17脚,即P0.23,名字为test_gpiote*/ 
    buttons{
        test_button: test_button {        
            gpios = < &gpio0 0x17 0x11 >;
            label = "test_gpiote";
        };
    };
};

截图如下:

 编译后可以在zephyr.dts中看到本次添加的定义:

2、基于v2.x(x=1,2,3,4,5)之后的版本

2.1、如何定义一个自定义节点

在前一节中是在原有的.dts中加入了一个新的定义,本节是定义一个属于我们自己的设备树节点,然后做按键中断触发;

/*参数加入devicetree的位置*/ 
/{
    /*其别名,这主要给test_button其一个别名,然后可以在APP中通过别名gpiote定位到我们定义的按键,起别名不要有大写和特殊字符*/ 
    aliases {    
        gpiote = &test_button;        
    };
    /*定义一个新的节点,名字为 my_gpio_test,其中定义一个子节点 test_button */ 
    my_gpio_test{
        compatible = "gpio-keys";
        test_button: m_test_button {        
            gpios = < &gpio0 11 GPIO_ACTIVE_HIGH>,
                        <&gpio0 12 GPIO_ACTIVE_HIGH>;
            label = "test_button";
        };
    };
};

  这一段代码的意义是说,在新定义的节点my_gpio_test中定义一个子节点,名字为 m_test_button ,同时定义该子节点的标签名为 test_button,然后在别名aliases中引用这个标签,把其赋值给 gpiote ,在之后的编程中我们只要在节点 aliases 中去查找响应的别名就可以找到我们自定义的节点了。

  其中还有一个很重要的点就是 compatible ,就像上面的 compatible = "gpio-keys";  。每一个节点都要有一个兼容属性,这里有两种情况:

  • 第一种是,我们自定义的节点有父节点,在父节点中有一个compatible 兼容属性了,那么我们可以忽略,不写,因为子节点可以继承父节点的 compatible   

    eg:如图在父节点中 pin-controller 中已经定义了兼容属性,那么该节点下的两个子节点可以不用再定义兼容属性,但是如果 pin-controller 中没有定义,那么就会找到更上一层的父节点,直到找到。如果找到根节点都没有,你们就是没有兼容属性,这会导致在程序中区查询我们的设备树节点时无法找到,使用不了

  •  第二种,父节点没有 compatible 属性,或者父节点有,但是我们想使用另一个兼容属性,那就直接使用 compatible 进行定义,如上面我自定义的 my_gpio_test 节点,是直接放在根节点下的一个自定义节点,如果我不在其中定义 compatible 属性,那么它会去其父节点找,但是父节点就是根节点没有任何的 compatible 属性。所以你会发现如果不定义 compatible 会导致在程序中去找这个节点的时候总是找不到,导致编译失败。

2.2、节点的 compatible 加入注意事项

在选择一个 compatible 后我们需要按照他的规则加入对应的属性才可以编译成功,不然会建立失败:

    my_gpio_test{
        compatible = "gpio-keys";
        test_button: m_test_button {        
            gpios = < &gpio0 11 GPIO_ACTIVE_HIGH>,
                        <&gpio0 12 GPIO_ACTIVE_HIGH>;
            label = "test_button";
        };
    };

对于上面这一段代码,加入的兼容属性是 gpio-keys ,如何看它的规则,需要跳转过去看,在使用VS code开发时,按住 Ctrl后把鼠标移动到 gpio-keys 上单机就跳转过去yaml文件了,其中定义了该兼容属性要使用必须对每一个子节点添加哪些属性。

 如截图,在 gpio-keys 的yaml中只有一个属性是必须的,其他属性可选可不选,也就是 

 required: true

required中文是必须的意思,然后值为true,且他在 gpios 下面,说明如果你使用了兼容属性 gpio-keys ,那么在定义设备树节点时必须有 gpios,不然就会编译报错,除此之外的 label 和 zephyr,code 没有使用 required 进行定义,那就是可选的,换句话说:

 我的设备树定义

 my_gpio_test{
        compatible = "gpio-keys";
        test_button: m_test_button {        
            gpios = < &gpio0 11 GPIO_ACTIVE_HIGH>,
                        <&gpio0 12 GPIO_ACTIVE_HIGH>;
            label = "test_button";
        };
    };

可以替换为:

 my_gpio_test{
        compatible = "gpio-keys";
        test_button: m_test_button {        
            gpios = < &gpio0 11 GPIO_ACTIVE_HIGH>,
                        <&gpio0 12 GPIO_ACTIVE_HIGH>;
        };
    };

2.3、代码中获取设备树节点

代码:

const struct gpio_dt_spec m_pin_1 = GPIO_DT_SPEC_GET_BY_IDX(DT_ALIAS(gpiote),gpios,0);
const struct gpio_dt_spec m_pin_2 = GPIO_DT_SPEC_GET_BY_IDX(DT_ALIAS(gpiote),gpios,1);

在定义的最后面有0和1两个参数分别获取的是 m_test_button 节点中的 两个节点,对应如下:

< &gpio0 11 GPIO_ACTIVE_HIGH>    就是0,使用 GPIO_DT_SPEC_GET_BY_IDX(DT_ALIAS(gpiote),gpios,0)获取到
<&gpio0 12 GPIO_ACTIVE_HIGH>    就是1,使用GPIO_DT_SPEC_GET_BY_IDX(DT_ALIAS(gpiote),gpios,1)获取到,
原因是在这个版本中的将使用两个GPIO口作为中断,所以定义了两个,如果你只定义了一个可以这样定义:
/*参数加入devicetree的位置*/ 
/{
    /*其别名,这主要给test_button其一个别名,然后可以在APP中通过别名gpiote定位到我们定义的按键,起别名不要有大写和特殊字符*/ 
    aliases {    
        gpiote = &test_button;        
    };
    /*定义一个新的节点,名字为 my_gpio_test,其中定义一个子节点 test_button */ 
    my_gpio_test{
        compatible = "gpio-keys";
        test_button: m_test_button {        
            gpios = < &gpio0 11 GPIO_ACTIVE_HIGH>;
            label = "test_button";
        };
    };
};

获取设备树:

const struct gpio_dt_spec m_pin_1 =GPIO_DT_SPEC_GET(DT_ALIAS(gpiote),gpios);

或者:

const struct gpio_dt_spec m_pin_1 = GPIO_DT_SPEC_GET_BY_IDX(DT_ALIAS(gpiote),gpios,0);

其实最终GPIO_DT_SPEC_GET  的实现就是使用了 GPIO_DT_SPEC_GET_BY_IDX,截图如下:

 三、应用代码编写

1、常规方式的ISR

1.1、V1.8版本

在gpiote.c中的代码如下:

#include "device.h"
#include "irq.h"
#include <zephyr.h>
#include <sys/printk.h>
#include <sys/util.h>
#include <device.h>
#include <devicetree.h>
#include <drivers/gpio.h>
#include <nrfx.h>
#include <dk_buttons_and_leds.h>

#define PIN DT_GPIO_PIN(DT_ALIAS(gpiote), gpios)
/* 建立一个gpio引脚的类*/
struct gpio_pin {
    const char * const port;
    const uint8_t number;
};
/* 定义gpio_pin类型的变量,用于读取设备定义信息,这以数组的形式定义,
便于有多个按键时可以直接定义获取,ARRAY_SIZE用于计算大小的函数*/
static const struct gpio_pin init_pin[] ={
    {DT_GPIO_LABEL(DT_ALIAS(gpiote), gpios),
     DT_GPIO_PIN(DT_ALIAS(gpiote), gpios)},
};
/* */
static const struct device * init_device[ARRAY_SIZE(init_pin)];
/* 定义回调类型的变量*/
static struct gpio_callback gpiote_cb;

/*回调函数*/
void gpio_inte_handle(const struct device *port,
                    struct gpio_callback *cb,
                    gpio_port_pins_t pins)
{
 printk("run to gpiote test\n");
}
/*GPIOTE程序*/
void gpiote_test(void)
{
    /*如果同时有多个按键可以增加数组个数*/
    int err;
    uint32_t pin_mask = 0;

    /*获取设备*/
    init_device[0]=device_get_binding(init_pin[0].port);
    if (!init_device[0]) {
        printk("Cannot bind gpio device");
    }
    /*配置gpio口,输入上拉*/
    err = gpio_pin_configure(init_device[0], init_pin[0].number,
                            GPIO_INPUT | GPIO_PULL_UP);
    if (err) {
        printk("Cannot configure button gpio");
    }

    /*中断配置*/
    err = gpio_pin_interrupt_configure(init_device[0],
            init_pin[0].number, GPIO_INT_DISABLE);
    if (err) {
        printk("Cannot disable callbacks()");
    }
    pin_mask |= BIT(init_pin[0].number);
    /*回调设置*/

    pin_mask |= BIT(init_pin[0].number);
    /*回调设置*/
    gpio_init_callback(&gpiote_cb, gpio_inte_handle, pin_mask);

    /*将刚刚绑定的结构添加到向量表中*/
    err = gpio_add_callback(init_device[0], &gpiote_cb);
    if (err) {
        printk("Cannot add callback");
    }
    /*将GPIO中断配置为下降沿触发,并启用它*/
    err = gpio_pin_interrupt_configure(init_device[0],
            init_pin[0].number, GPIO_INT_EDGE_FALLING);
    if (err) {
        printk("Cannot disable callbacks()");
    }
    printk("test start\n");

    while(1)
    {
    }
}

/*创建一个区别于main.c中的线程,用于初始化gpiote功能 */
K_THREAD_DEFINE(gpiote_test_id,1024,gpiote_test,NULL,NULL,NULL,7,0,0);

在此程序的基础上,你可以定义多个按键并放入设备模型数组。虽然我本次测试只使用了一个按键,如果你添加的是多个按键,记得初始化时用for循环,把每一个设备都添加一下,我这只有一个设备所以只使用了数组的第0位的设备(也就是只有一个设备)。

结果:

 

1.2、V2.x版本以上代码编写

检查设备树节点设备是否获取成功:

  int err;
    err = device_is_ready(p_pin.port);
    if (!err) {
        printk("not button\n");
        return err;
    }

配置IO口为输入:

err = gpio_pin_configure_dt(&p_pin, GPIO_INPUT | GPIO_PULL_UP);
    if (err < 0) {
        printk("button input config fail\n");
        return err;
    }

配置引脚为中断模式:

err = gpio_pin_interrupt_configure_dt(&p_pin,GPIO_INT_EDGE_FALLING);
    if (err) {
        printk("Cannot disable callbacks");
        return err;
    }

初始化中断回调:

static struct gpio_callback button_cb_data;
/*回调函数*/
void gpio_inte_handle(const struct device *port,
                    struct gpio_callback *cb,
                    gpio_port_pins_t pins)
{
   
    if(pins & BIT(m_pin_1.pin))
    {
        printk("button 1\n");
    }
    if(pins & BIT(m_pin_2.pin))
    {
        printk("button 2\n");
    }

}
gpio_init_callback(&button_cb_data, gpio_inte_handle, pin_mask);

err = gpio_add_callback(p_pin.port, &button_cb_data);
    if (err) {
        printk("gpio_add_callback fail");
        return err;
    }

因为我定义两个GPIO口,所以在一个回调中要去判断是那个引脚的中断,所以用 pins & BIT(pin_mask) 进行检查,

完整代码如下:

/*
 * Copyright (c) 2012-2014 Wind River Systems, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

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


const struct gpio_dt_spec m_pin_1 = GPIO_DT_SPEC_GET_BY_IDX(DT_ALIAS(gpiote),gpios,0);
const struct gpio_dt_spec m_pin_2 = GPIO_DT_SPEC_GET_BY_IDX(DT_ALIAS(gpiote),gpios,1);

static struct gpio_callback button_cb_data;

/*回调函数*/
void gpio_inte_handle(const struct device *port,
                    struct gpio_callback *cb,
                    gpio_port_pins_t pins)
{
    
    if(pins & BIT(m_pin_1.pin))
    {
        printk("button 1\n");
    }
    if(pins & BIT(m_pin_2.pin))
    {
        printk("button 2\n");
    }

}

int gpiote_init(const struct gpio_dt_spec p_pin)
{
    static uint32_t pin_mask = 0;
    int err;
    err = device_is_ready(p_pin.port);
    if (!err) {
        printk("not button\n");
        return err;
    }

    err = gpio_pin_configure_dt(&p_pin, GPIO_INPUT | GPIO_PULL_UP);
    if (err < 0) {
        printk("button input config fail\n");
        return err;
    }
    
    err = gpio_pin_interrupt_configure_dt(&p_pin,GPIO_INT_EDGE_FALLING);
    if (err) {
        printk("Cannot disable callbacks");
        return err;
    }
    pin_mask |= BIT(p_pin.pin);
    
    gpio_init_callback(&button_cb_data, gpio_inte_handle, pin_mask);

    
    err = gpio_add_callback(p_pin.port, &button_cb_data);
    if (err) {
        printk("gpio_add_callback fail");
        return err;
    }

}

int main(void)
{
    int err;

    printk("Hello World! %s\n", CONFIG_BOARD);

    err = gpiote_init(m_pin_2);
    if (err) {
        return err;
    }
    err = gpiote_init(m_pin_1);
    if (err) {
        return err;
    }
    

    while (1)
    {
        /* code */
        k_msleep(1000);
    }
    return 0;
}

 结果:

 

 2、zephyr中的direct ISR(直接中断模式)

APP我们不用更改,只要把驱动中的IRQ_CONNECT();替换为IRQ_DIRECT_CONNECT();然后再加入zephy官方文档定义的代码:

 你可以在工程的如下地方找到这个文件,然后更改原始定义,更改后如下:

 可以直接替换代码:

#define CONFIG_direct_isr

#ifdef CONFIG_direct_isr
ISR_DIRECT_DECLARE(gpiote_event_handler_direct)
{
   gpiote_event_handler();
   ISR_DIRECT_PM(); /* PM done after servicing interrupt for best latency */
   return 1; /* We should check if scheduling decision should be made */
}
#endif
static int gpio_nrfx_init(const struct device *port)
{
    static bool gpio_initialized;

    if (!gpio_initialized) {
        gpio_initialized = true;
        #ifdef CONFIG_direct_isr
        IRQ_DIRECT_CONNECT(DT_IRQN(GPIOTE_NODE), DT_IRQ(GPIOTE_NODE, priority),
                gpiote_event_handler_direct, 0);

        #else
        IRQ_CONNECT(DT_IRQN(GPIOTE_NODE), DT_IRQ(GPIOTE_NODE, priority),
                gpiote_event_handler, NULL, 0);
        #endif
        irq_enable(DT_IRQN(GPIOTE_NODE));
        nrf_gpiote_int_enable(NRF_GPIOTE, NRF_GPIOTE_INT_PORT_MASK);
    }

    return 0;
}

编译下载即可:

四、中断向量表查看

在如下目录可以看到我们的中断服务程序入口:其中21753就是本次中断ISR的如果地址:

 在这个数组下还有中断向量表,可以自行查看:

 

GPIO测试到此结束。如有错漏欢迎评论指正。

 

 注意事项:

# 研发生产测试方向
1、 预留好射频测试的串口测试点作为串口 TX 和 RX 做 DTM 测试用,任意 GPIO 即可 (首推空引脚),方便研发调试或者方便和工装对接
2、 开发调试的时候使用 Jlink 进行下载代码和 Debug;工厂批量烧录如下,主要是下载速 率和稳定性的问题,因为影响工厂生产效率
3、 芯片射频匹配电路根据芯片型号和封装和线路图参考手册上面的参数进行配置;射频 走线为 50 欧姆,走线光滑过渡自然,背面等有完整的参考地面,走线附近多地孔 等;天线匹配根据实际天线设置匹配参数;研发阶段射频必须做频偏校准,通过调节 高频晶振负载电容控制 CH0 /CH19/CH39 通道的频偏在 0KHz 附近,同时射频区域和晶 振匹配电容的器件选用小封装、高精度、低温飘的器件,以保批量时候的产品一致性
posted @ 2022-11-16 17:30  星辰_stars  阅读(2088)  评论(0编辑  收藏  举报