0.Briefly Speaking

这个系列的博客将聚焦于一些常见外设的驱动源码分析,首先会以ESP32系列的MCU(我手头的开发平台是ESP32-C6,这里是技术参考文档)为研究对象,详细分析在开源的物联网开发框架ESP-IDF控制外设的驱动源码及其控制逻辑,之后会逐步过渡到Linux内核中的驱动代码,并将两者进行对比研究。

本篇博客主要聚焦的外设是GPIO,详细的源码位于IDF的以下目录:

在上述的目录中,可以参考test_apps中对GPIO的测试代码,里面包含了对GPIO的简单操作示例,借助这些简单示例一方面可以快速掌握一些常见外设的调用API,还可以一层层深入下去,直至看到ESP-IDF底层对GPIO的完整封装和调用逻辑

顺带一提的是,我会在这个系列的博客中插入一些“离题点”(Digression Point),因为我的思维很多时候有些发散,经常会联想起相关的一些知识和问题,然后会开始思考它们之间可能存在的内在逻辑,或者顺着一件小事揪出背后一整套的逻辑链条。为了不影响这些思维发散过度使本文脱离主旨,我会将它们标记为Digression,如果你感兴趣可以展开阅读它们,暂时跳过这些也不会影响对后文的理解 😃

在说到ESP32系列MCU的GPIO时,绕不开的两个概念还有GPIO交换矩阵和IO多路选择器(IO_MUX),这里简单提一嘴:这是因为现代MCU中的GPIO管脚资源是非常有限的,因此要尽可能以有限的管脚数目实现尽可能丰富的功能。这就需要经常复用有限的GPIO针脚,GPIO交换矩阵是一个GPIO和外设信号的全交换矩阵,基于此GPIO可以连接到任何一个ESP32 MCU外设信号。而IO_MUX则顾名思义,它用来选择当前IO针脚的功能,是作为GPIO使用(此时可以经由GPIO交换矩阵路由到某一外设信号),还是专用于特定的外设功能(此时可以旁路交换矩阵,获得更好的高频性能)。

引子——如何使用一个GPIO

components/esp_driver_gpio/test_apps/gpio/main/test_gpio.c中包含了大量用Unity框架封装的针对ESP32系列MCU GPIO进行基本功能测试的代码,以下是其中最简单的一个示例:使用GPIO触发一个中断,并调用中断处理程序。结合这个基本示例,可以基本理清GPIO的基本使用方法,随后逐层深入,勾勒出整个GPIO驱动的蓝图

使用一个GPIO的基本步骤如下,在此作为总结列出,这些基本步骤也将作为后文的小标题出现,点此下方链接可以直接跳转到对应函数的解析:

  1. 配置GPIO的基本特性(gpio_config)
  2. 设置输出电平(gpio_set_level)
  3. 设置GPIO中断触发条件(gpio_set_intr_type)
  4. 安装并注册GPIO整个外设的中断服务程序(gpio_install_isr_service)
  5. 将某一个管脚与其特定中断服务程序关联起来(gpio_isr_handler_add)
TEST_CASE("GPIO_rising_edge_interrupt_test", "[gpio]")
{
    edge_intr_times = 0; 
	// 1.配置GPIO的基本特性(输入输出、上下拉等)
    test_gpio_config_mode_input_output(TEST_GPIO_INPUT_OUTPUT_IO1);
	// 2.设置输出电平
    TEST_ESP_OK(gpio_set_level(TEST_GPIO_INPUT_OUTPUT_IO1, 0));

    // 3.设置GPIO中断触发条件
    TEST_ESP_OK(gpio_set_intr_type(TEST_GPIO_INPUT_OUTPUT_IO1, GPIO_INTR_POSEDGE));
	
	// 4.安装并注册GPIO整个外设的中断服务程序(ISR)
    TEST_ESP_OK(gpio_install_isr_service(0));
	
	// 5.将某一个管脚与其特定中断服务程序关联起来
    gpio_isr_handler_add(TEST_GPIO_INPUT_OUTPUT_IO1, gpio_isr_edge_handler, (void *) TEST_GPIO_INPUT_OUTPUT_IO1);
	
	// 模拟触发一个上升沿,检查中断程序是否正常触发
    TEST_ESP_OK(gpio_set_level(TEST_GPIO_INPUT_OUTPUT_IO1, 1));
    vTaskDelay(100 / portTICK_PERIOD_MS);
    TEST_ASSERT_EQUAL_INT(1, edge_intr_times);
	
	// 6.卸载ISR
    gpio_isr_handler_remove(TEST_GPIO_INPUT_OUTPUT_IO1);
    gpio_uninstall_isr_service();
}

下面按照上述使用一个GPIO的方法,逐层解析ESP-IDF中对GPIO的驱动和封装。

1 配置GPIO的基本特性

在使用GPIO IO的开始,需要首先对GPIO的基本属性进行配置。从软件的角度而言,需要对物理层面上的GPIO管脚可能拥有的特性进行封装和抽象,并提供可配置的接口给用户。在上面的示例中的test_gpio_config_mode_input_output函数就完成了对GPIO属性的基本配置,它的调用关系如下:

/* <简单的调用关系图示例,缩进表示调用关系> */
test_gpio_config_mode_input_output(gpio_num_t num);
	// do some configuration
	gpio_config(const gpio_config_t *);

1.1 gpio_config_t结构体——gpio的属性抽象

可以看到上述函数中gpio_config完成了最终对GPIO的配置,它的传入参数是一个gpio_config_t类型的变量。IDF中将GPIO可能拥有的属性抽象为了一个gpio_config_t结构体,它的定义如下所示:

typedef struct {
	// 针脚掩码(表示选中的是哪一个GPIO pin)
    uint64_t pin_bit_mask;
    
	// 管脚模式设定:输入/输出/双向/开漏等         
    gpio_mode_t mode;
    
	// 上下拉使能               
    gpio_pullup_t pull_up_en;       
    gpio_pulldown_t pull_down_en;
    
	// 中断触发时机(高电平触发/上升沿/下降沿触发)  
    gpio_int_type_t intr_type;  

// 是否支持PIN脚“输入迟滞”
// 有关于输入迟滞的介绍,请参考以下链接
// https://electronics.stackexchange.com/questions/12312/what-is-input-hysteresis    
#if SOC_GPIO_SUPPORT_PIN_HYS_FILTER
    gpio_hys_ctrl_mode_t hys_ctrl_mode;       
#endif
} gpio_config_t;

1.2 gpio_config函数——完成一个GPIO的配置

下面的代码首先确定要配置的GPIO是否合法,随后对GPIO的属性进行一系列的配置,源码逻辑如下,基本上是一系列的gpio_xxx_enable/disable函数堆砌起来的逻辑,并在外部添加一个while循环来逐个扫描每一个可能需要配置的GPIO引脚

esp_err_t gpio_config(const gpio_config_t *pGPIOConfig)
{
	// 获取一个GPIO的pin_mask
	// 哪个bit是1,就说明选中了哪个gpio针脚
    uint64_t gpio_pin_mask = (pGPIOConfig->pin_bit_mask);
    uint32_t io_num = 0;
    uint8_t input_en = 0;
    uint8_t output_en = 0;
    uint8_t od_en = 0;
    uint8_t pu_en = 0;
    uint8_t pd_en = 0;
	
	// 对pin_bit_mask做合法性检查
	// 如果为0,表示没有选中
    if (pGPIOConfig->pin_bit_mask == 0 ||
            pGPIOConfig->pin_bit_mask & ~SOC_GPIO_VALID_GPIO_MASK) {
        ESP_LOGE(GPIO_TAG, "GPIO_PIN mask error ");
        return ESP_ERR_INVALID_ARG;
    }
	
	// 合法性检查
	// 有些GPIO只能用作输入,这里检查是否错误地指定为输出
    if (pGPIOConfig->mode & GPIO_MODE_DEF_OUTPUT &&
            pGPIOConfig->pin_bit_mask & ~SOC_GPIO_VALID_OUTPUT_GPIO_MASK) {
        ESP_LOGE(GPIO_TAG, "GPIO can only be used as input mode");
        return ESP_ERR_INVALID_ARG;
    }
	
    do {
    	// 如果当前gpio被选中
        if (((gpio_pin_mask >> io_num) & BIT(0))) {

// 检查当前gpio管脚是否为LP gpio
// 如果是,将其重新设定为DIGITAL gpio
#if SOC_RTCIO_PIN_COUNT > 0
            if (rtc_gpio_is_valid_gpio(io_num)) {
                rtc_gpio_deinit(io_num);
            }
#endif
			/* 一系列gpio_xxx_enable_disable函数 */
			/*** 根据传入的配置对gpio进行设定 ***/
			// 输入使能
            if ((pGPIOConfig->mode) & GPIO_MODE_DEF_INPUT) {
                input_en = 1;
                gpio_input_enable(io_num);
            } else {
                gpio_input_disable(io_num);
            }
					
			// 是否使能开漏模式
            if ((pGPIOConfig->mode) & GPIO_MODE_DEF_OD) {
                od_en = 1;
                gpio_od_enable(io_num);
            } else {
                gpio_od_disable(io_num);
            }
					
			// 是否使能输出模式 
            if ((pGPIOConfig->mode) & GPIO_MODE_DEF_OUTPUT) {
                output_en = 1;
                gpio_output_enable(io_num);
            } else {
                gpio_output_disable(io_num);
            }
					 
			// 是否上拉
            if (pGPIOConfig->pull_up_en) {
                pu_en = 1;
                gpio_pullup_en(io_num);
            } else {
                gpio_pullup_dis(io_num);
            }
					 
			// 是否下拉
            if (pGPIOConfig->pull_down_en) {
                pd_en = 1;
                gpio_pulldown_en(io_num);
            } else {
                gpio_pulldown_dis(io_num);
            }
					 
			// 打印一条信息,列出当前GPIO的配置属性
            ESP_LOGI(GPIO_TAG, "GPIO[%"PRIu32"]| InputEn: %d| OutputEn: %d| OpenDrain: %d| Pullup: %d| Pulldown: %d| Intr:%d ", io_num, input_en, output_en, od_en, pu_en, pd_en, pGPIOConfig->intr_type);
            gpio_set_intr_type(io_num, pGPIOConfig->intr_type);
					 
			// 使能gpio中断
            if (pGPIOConfig->intr_type) {
                gpio_intr_enable(io_num);
            } else {
                gpio_intr_disable(io_num);
            }

// 关于输入迟滞的配置
#if SOC_GPIO_SUPPORT_PIN_HYS_FILTER
            if (pGPIOConfig->hys_ctrl_mode == GPIO_HYS_SOFT_ENABLE) {
                gpio_hysteresis_enable(io_num);
            } else if (pGPIOConfig->hys_ctrl_mode == GPIO_HYS_SOFT_DISABLE) {
                gpio_hysteresis_disable(io_num);
            }
#if SOC_GPIO_SUPPORT_PIN_HYS_CTRL_BY_EFUSE
            else {
                gpio_hysteresis_by_efuse(io_num);
            }
#endif
#endif  //SOC_GPIO_SUPPORT_PIN_HYS_FILTER

            /* By default, all the pins have to be configured as GPIO pins. */
            // 确保针脚功能为GPIO
            gpio_hal_func_sel(gpio_context.gpio_hal, io_num, PIN_FUNC_GPIO);
        }
			 
		// 检查下一个针脚
        io_num++;
    } while (io_num < GPIO_PIN_COUNT);

    return ESP_OK;
}

对于gpio_xxx_enable/disable系列的接口,它们的作用如同名字指出的那样,专门对GPIO的一些特性进行设置,下面将会简单地展开其中的一些函数,这会涉及到esp32系列MCU的其他一些硬件组件的细节,对提升其他部分组件的理解会有益处:

1.2.1 gpio_output_enable——使能gpio的输出

首先给出gpio_output_enable函数的调用关系图,对这个函数完成的任务做一个宏观上的概况:

/* 函数调用图2: gpio_output_enable */
gpio_config(const gpio_config_t *config);
    // 以gpio_output_enable函数为例
	gpio_output_enable(gpio_num_t gpio_num);
		// hal: 配置gpio交换矩阵
		gpio_hal_matrix_out_default(gpio_dev_t *hw, uint32_t gpio_num);
		// ll: 指向特定芯片目标的配置寄存器的动作
		// -> 表示宏替换,这个函数和gpio_ll_matrix_out_default本质上是同级
		-> gpio_ll_matrix_out_default(gpio_dev_t *hw. uint32_t gpio_num);
		// 使能gpio输出功能
		gpio_hal_output_enable(gpio_dev_t* hw, uint32_t gpio_num);
		-> gpio_ll_output_enable(gpio_dev_t *hw, uint32_t gpio_num);

下面开始进入gpio_output_enable函数的细节,这个函数的作用是将一个GPIO管脚设置为输出模式,源代码如下:

esp_err_t gpio_output_enable(gpio_num_t gpio_num)
{
    // 首先检查当前gpio是否可以被配置为输出
    GPIO_CHECK(GPIO_IS_VALID_OUTPUT_GPIO(gpio_num), "GPIO output gpio_num error", ESP_ERR_INVALID_ARG);
    
    // No peripheral output signal routed to the pin, just as a simple GPIO output
    // 设置gpio交换矩阵,保证当前GPIO输出为默认来源,而不是来自于其他外设
    gpio_hal_matrix_out_default(gpio_context.gpio_hal, gpio_num);
	  
	// 使能GPIO输出 
    gpio_hal_output_enable(gpio_context.gpio_hal, gpio_num);
    return ESP_OK;
}

可以看到这个函数只调用了两个硬件抽象层的函数(Hardware Abstraction Layer, HAL)

  • gpio_hal_matrix_out_default (设置GPIO交换矩阵,保持输出来自于GPIO本身,而非某一外设)

这个函数用来设置GPIO交换矩阵的信号来源,GPIO交换矩阵可以简单理解为一个GPIO管脚和外设信号之间的全交换矩阵(请联想一个棋盘格)。通过将信号的编码(定义在gpio_sig_map.h)写入GPIO_FUNCn_OUT_SEL_CFG_REG寄存器中,即可实现让 G P I O n GPIO_n GPIOn输出对应外设的信号。而gpio_hal_matrix_out_default函数最终会被定向到gpio_ll_matrix_out_default,代码如下所示,这个函数选择了SIG_GPIO_OUT_IDX(128, 0x80)作为输出信号来源,表明输出信号来源于GPIO自身,输出为高还是低将最终由GPIO_OUT_REG寄存器中对应的位决定。

/**
  * @brief Disconnect any peripheral output signal routed via GPIO matrix to the pin
  *
  * @param hal Context of the HAL layer
  * @param gpio_num GPIO number
  */
/* gpio_hal_matrix_out_default函数就是对gpio_ll_matrix_out_default的简单封装 */
#define gpio_hal_matrix_out_default(hal, gpio_num) gpio_ll_matrix_out_default((hal)->dev, gpio_num)

/* 以下是gpio_ll_matrix_out_default的具体定义,本质就是对寄存器的写入动作 */
__attribute__((always_inline))
static inline void gpio_ll_matrix_out_default(gpio_dev_t *hw, uint32_t gpio_num)
{
	/* gpio_func_out_sel_cfg_reg_t是定义在soc/gpio_struct.h中的结构体,本质上是寄存器的结构体形式的描述 */ 
    gpio_func_out_sel_cfg_reg_t reg = {
       /* SIG_GPIO_OUT_IDX(128)定义在gpio_sig_map.h文件中 */
      .out_sel = SIG_GPIO_OUT_IDX,
    };
    hw->func_out_sel_cfg[gpio_num].val = reg.val;
}

有关这段操作的说明,技术参考手册也给出了对应的描述:
在这里插入图片描述

  • gpio_hal_output_enable (使能GPIO的输出)
    在设置GPIO输出信号来源之后,接下来就可以设置GPIO输出使能了,这里的逻辑是通过写入GPIO_ENABLE_W1TS寄存器(W1TS means writing 1 to set)来完成的,这个寄存器位于GPIO交换矩阵中:
/**
  * @brief Enable output mode on GPIO.
  *
  * @param hal Context of the HAL layer
  * @param gpio_num GPIO number
  */
/* gpio_hal_output_enable是对gpio_ll_output_enable的简单封装 */
#define gpio_hal_output_enable(hal, gpio_num) gpio_ll_output_enable((hal)->dev, gpio_num)

// 使能的动作就是向GPIO_ENABLE_W1TS寄存器中的对应位写入1
// 这个位会最终传播到GPIO_ENABLE_REG中,并将对应的位置1,从而使能了对应GPIO的管脚输出
__attribute__((always_inline))
static inline void gpio_ll_output_enable(gpio_dev_t *hw, uint32_t gpio_num)
{
    hw->enable_w1ts.enable_w1ts = (0x1 << gpio_num);
}

Q:为什么要有W1TS寄存器,直接写enable寄存器不行吗?会有哪些损害?

A:这个问题很有趣,请尝试自己思考一下,最后再参考这篇问答Why W1TS is better

最后,请注意上述两个驱动函数的直接操作对象是一个叫做gpio_context的结构体。它是一个定义在gpio.c中的静态变量(gpio.c:68),每一次我们对GPIO的操作都会首先被先映射到这个局部静态变量上,随后再操作对应的硬件实体,可以说gpio_context_t就是对当前GPIO设备状态的抽象,这里IDF中称其为上下文(context)

/* gpio_context_t结构体,抽象对GPIO的操作 */
typedef struct {
    /* 要操作的硬件结构实例,是gpio_dev_t类型的变量,定义在gpio_struct.h中 */
    gpio_hal_context_t *gpio_hal;
	/* 自旋锁 */
    portMUX_TYPE gpio_spinlock;
	/* 当前GPIO中断正在被哪个处理器核心响应 */
    uint32_t isr_core_id;
	/* GPIO中断处理程序(ISR)函数指针和ISR句柄 */
	/* 这两者区别请见后面 */
    gpio_isr_func_t *gpio_isr_func;
    gpio_isr_handle_t gpio_isr_handle;
    // for edge-triggered interrupts, interrupt status bits should be cleared before entering per-pin handlers
    // 是否要在进入中断之后立即清空中断标记
    uint64_t isr_clr_on_entry_mask; 
} gpio_context_t;

/* gpio_context_t声明的静态变量实例(初始状态) */
static gpio_context_t gpio_context = {
    .gpio_hal = &_gpio_hal,
    .gpio_spinlock = portMUX_INITIALIZER_UNLOCKED,
    .isr_core_id = GPIO_ISR_CORE_ID_UNINIT,
    .gpio_isr_func = NULL,
    .isr_clr_on_entry_mask = 0,
};


Digression 1: IDF如何组织设备驱动?

根据gpio_output_enable函数的调用链可以窥探到IDF组织设备驱动程序的层次和结构,可以看到ESP-IDF驱动中任何一个动作都要经过这样的一条调用链的转发,如下图所示:

在这里插入图片描述
HAL层的存在有效地屏蔽了不同硬件平台的差异,使得更上一层的封装得以调用统一的接口完成硬件功能的配置


1.2.2 gpio_pullup_en——使能GPIO的上拉

gpio_pullup_en函数的实现如下,主要调用了gpio_hal_pullup_en函数和rtc_gpio_pullup_en函数来完成HP GPIO和LP GPIO的上拉设置,由于LP GPIO的限制,需要额外做一些条件判断

[TODO: 为什么一些LP GPIO不可以设置上下拉?]

esp_err_t gpio_pullup_en(gpio_num_t gpio_num)
{
	// GPIO编号合法性检查
    GPIO_CHECK(GPIO_IS_VALID_OUTPUT_GPIO(gpio_num), "GPIO number error", ESP_ERR_INVALID_ARG);
	
	// 确保当前指定的GPIO管脚不属于LP GPIO范围
	// LP GPIO的上下拉在一些ESP芯片上是无法配置的
    if (!rtc_gpio_is_valid_gpio(gpio_num) || SOC_GPIO_SUPPORT_RTC_INDEPENDENT) {
    	// 在使用上下文之前先获取自旋锁,防止其他任务同时访问上下文(保证访问的原子性)
    	// 为什么gpio_output_enable不用获取自旋锁? :)
        portENTER_CRITICAL(&gpio_context.gpio_spinlock);
		// 设置上拉
        gpio_hal_pullup_en(gpio_context.gpio_hal, gpio_num);
        portEXIT_CRITICAL(&gpio_context.gpio_spinlock);
    } else {
// 如果当前要配置的GPIO管脚属于LP GPIO
// 需要查看LP GPIO是否支持配置上拉
#if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
        rtc_gpio_pullup_en(gpio_num);
#else
// 否则直接终止
        abort(); // This should be eliminated as unreachable, unless a programming error has occurred
#endif
    }
    return ESP_OK;
}

而gpio_hal_pullup_en的实现逻辑也非常简单,它是对gpio_ll_pullup_en函数的简单宏替换,最终只需要配置IO_MUX中的寄存器IO_MUX_GPIOn_REG中的FUN_PU字段即可

/**
  * @brief Enable pull-up on GPIO.
  *
  * @param hal Context of the HAL layer
  * @param gpio_num GPIO number
  */
// gpio_hal_pullup_en是对gpio_ll_pullup_en函数的宏替换
#define gpio_hal_pullup_en(hal, gpio_num) gpio_ll_pullup_en((hal)->dev, gpio_num)

/**
  * @brief Enable pull-up on GPIO.
  *
  * @param hw Peripheral GPIO hardware instance address.
  * @param gpio_num GPIO number
  */
static inline void gpio_ll_pullup_en(gpio_dev_t *hw, uint32_t gpio_num)
{
	// 设置上拉的寄存器位于IO_MUX,定义在io_mux_reg.h中
	// 这里用的是寄存器形式来设置GPIO的弱上拉,而非结构体形式,所以hw实际上没有用到
	// 每一个GPIO管脚在IO_MUX中对应一个4字节的寄存器,因此4 * n对应到GPIOn的寄存器
    REG_SET_BIT(IO_MUX_GPIO0_REG + (gpio_num * 4), FUN_PU);
}

1.2.3 gpio_intr_enable——使能GPIO的中断

gpio_intr_enable函数用来设置使能GPIO的中断,这是GPIO外设端控制的一个中断开关,此开关一旦打开,GPIO中断就可以正常触发并被路由到CPU中断上。这个函数的调用关系图如下:

/* 函数调用图3: gpio_intr_enable */
gpio_intr_enable(gpio_num_t gpio_num);
	// 获取当前的CPU核心ID
	xPortGetCoreID();
	// 打开对应核心上的GPIO中断
	gpio_intr_enable_on_core(gpio_num_t gpio_num, uint32_t core_id);
		gpio_hal_intr_enable_on_core(gpio_hal_context_t *hal, uint32_t gpio_num, uint32_t core_id);
			// 清掉之前可能残存的中断
			gpio_ll_clear_intr_status(gpio_dev_t *hw, uint32_t mask);
			// 打开对应核心上的GPIO中断
			gpio_ll_intr_enable_on_core(gpio_dev_t *hw, uint32_t core_id, uint32_t gpio_num);

最深层的gpio_ll_intr_enable_on_core通过写入寄存器GPIO_PINn_REGGPIO_PINn_INT_ENA域来打开GPIO通往CPU的中断通路,至此GPIO中断可以被中断矩阵(INTMNX)路由到CPU中断上,这一点在后续注册中断处理函数时会进一步地说明。gpio_ll_intr_enable_on_core函数的实现如下

/**
 * @brief  Enable GPIO module interrupt signal
 *
 * @param  hw Peripheral GPIO hardware instance address.
 * @param  core_id Interrupt enabled CPU to corresponding ID
 * @param  gpio_num GPIO number. If you want to enable the interrupt of e.g. GPIO16, gpio_num should be GPIO_NUM_16 (16);
 */
// 其实core_id在除了ESP32以外的其他芯片中根本没有用到,而仅仅是作为一个断言的条件
// 但是为了保证接口上的统一,这里依然保留了core_id这个入口参数
__attribute__((always_inline))
static inline void gpio_ll_intr_enable_on_core(gpio_dev_t *hw, uint32_t core_id, uint32_t gpio_num)
{
	// 对于ESP32C6,core_id只是用来做一个判断,因为它是单核处理器
    HAL_ASSERT(core_id == 0 && "target SoC only has a single core");
    GPIO.pin[gpio_num].int_ena = GPIO_LL_PRO_CPU_INTR_ENA;     //enable pro cpu intr
}

Digression 2: 与GPIO有关的中断状态寄存器

这里值得注意的是,在ESP32C6中,反映GPIO中断状态的寄存器有3组,这里对它们做一些简要的区分

  • GPIO_STATUS_REG (反映当前GPIO是否发生了中断,软件可读写)
  • GPIO_PCPU_INT_REG(反映当前GPIO发生的中断是否被转发到了CPU,因此上述的gpio_ll_intr_enable_on_core函数一旦执行,此寄存器状态将与GPIO_STATUS_REG保持同步,软件只读)
  • GPIO_STATUS_INTERRUPT_NEXT(反映GPIO中断是否发生,这个寄存器反映了硬件客观上是否发生中断,而不受软件的影响,软件只读)

在上述的gpio_ll_intr_enable_on_core函数中,本质上打开的是将GPIO中断转发到CPU的开关


点此跳回顶层目录

2 设置GPIO电平

2.1 gpio_set_level——设置GPIO管脚的输出电平

在上述示例代码的第二步,程序使用gpio_set_level函数设置了GPIO的电平,此函数的调用关系如下,它遵循的调用层次和我们在上面提到的是一致的(API->HAL->LL)

/*函数调用图4: gpio_ll_set_level*/
gpio_set_level(gpio_num_t gpio_num, uint32_t level);
	gpio_hal_set_level(hal, gpio_num, level);
	-> gpio_ll_set_level(gpio_dev_t *hw, uint32_t gpio_num, uint32_t level);

最终配置寄存器的动作是通过gpio_ll_set_level来完成的,这个函数的实现如下,通过写入GPIO交换矩阵的 GPIO_OUT_W1TS_REG或GPIO_OUT_W1TC_REG寄存器来实现设置电平动作,和前述设置输出使能一样,这里用了W1TS/W1TC寄存器来间接地设置/清空GPIO_OUT_REG

/**
 * @brief  GPIO set output level
 *
 * @param  hw Peripheral GPIO hardware instance address.
 * @param  gpio_num GPIO number. If you want to set the output level of e.g. GPIO16, gpio_num should be GPIO_NUM_16 (16);
 * @param  level Output level. 0: low ; 1: high
 */
__attribute__((always_inline))
static inline void gpio_ll_set_level(gpio_dev_t *hw, uint32_t gpio_num, uint32_t level)
{
    if (level) {
        hw->out_w1ts.val = 1 << gpio_num;
    } else {
        hw->out_w1tc.val = 1 << gpio_num;
    }
}

点此跳回顶层目录

3 设置GPIO中断触发条件

gpio_set_intr_type函数用来设置触发GPIO中断的条件,函数调用关系图如下:

/* 函数调用图5: gpio_set_intr_type*/
gpio_set_intr_type(gpio_num_t gpio_num, gpio_int_type_t intr_type);
	gpio_hal_set_intr_type(hal, gpio_num, intr_type);
	->gpio_ll_set_intr_type(gpio_dev_t *hw, uint32_t gpio_num, gpio_int_type_t intr_type);
	/* 设置中断清空条件gpio_context.isr_clr_on_entry_mask */
	/* do something */

整体上gpio_set_intr_type函数主要完成了以下两件事情:

  • 调用gpio_hal_set_intr_type设置GPIO中断触发条件
  • 按照中断触发条件的不同,设置清中断的时机
esp_err_t gpio_set_intr_type(gpio_num_t gpio_num, gpio_int_type_t intr_type)
{
	// 检查入口参数的合法性
    GPIO_CHECK(GPIO_IS_VALID_GPIO(gpio_num), "GPIO number error", ESP_ERR_INVALID_ARG);
    GPIO_CHECK(intr_type < GPIO_INTR_MAX, "GPIO interrupt type error", ESP_ERR_INVALID_ARG);
	// 获取GPIO设置上下文的自旋锁
    portENTER_CRITICAL(&gpio_context.gpio_spinlock);
	// 设置GPIO中断类型
    gpio_hal_set_intr_type(gpio_context.gpio_hal, gpio_num, intr_type);
    
	// 如果中断类型是边缘触发而非电平触发,则在进入中断处理程序时清空中断
	// [边缘触发通常隐含着时间驱动语义,即每出现一次就触发一次]
	// [电平触发通常隐含着事件驱动语义,即当条件满足时触发一次]
	// 注意这里修改的是gpio上下文中的isr_clr_on_entry_mask标志 [为什么不将此属性设置在REG里?]
	// isr_clr_on_entry_mask是管理GPI管脚是否清中断的掩码,为1表示要在进入ISR后要清空中断标记
    if (intr_type == GPIO_INTR_POSEDGE || intr_type == GPIO_INTR_NEGEDGE || intr_type == GPIO_INTR_ANYEDGE) {
        gpio_context.isr_clr_on_entry_mask |= (1ULL << (gpio_num));
    } else {
        gpio_context.isr_clr_on_entry_mask &= ~(1ULL << (gpio_num));
    }
	// 释放自旋锁
    portEXIT_CRITICAL(&gpio_context.gpio_spinlock);
    return ESP_OK;
}

3.1 gpio_hal_set_intr_type——设置GPIO中断触发类型

和前面介绍过的函数别无二致,gpio_hal_set_intr_type设置GPIO中断类型是通过设置GPIO_PINn_REG中的GPIO_PINn_INT_TYPE域来实现的。

/**
 * @brief  GPIO set interrupt trigger type
 *
 * @param  hal Context of the HAL layer
 * @param  gpio_num GPIO number. If you want to set the trigger type of e.g. of GPIO16, gpio_num should be GPIO_NUM_16 (16);
 * @param  intr_type Interrupt type, select from gpio_int_type_t
 */
#define gpio_hal_set_intr_type(hal, gpio_num, intr_type) gpio_ll_set_intr_type((hal)->dev, gpio_num, intr_type)
/**
 * @brief  GPIO set interrupt trigger type
 *
 * @param  hw Peripheral GPIO hardware instance address.
 * @param  gpio_num GPIO number. If you want to set the trigger type of e.g. of GPIO16, gpio_num should be GPIO_NUM_16 (16);
 * @param  intr_type Interrupt type, select from gpio_int_type_t
 */
static inline void gpio_ll_set_intr_type(gpio_dev_t *hw, uint32_t gpio_num, gpio_int_type_t intr_type)
{
    hw->pin[gpio_num].int_type = intr_type;
}

而ESP32系列MCU支持的中断类型有以下几种,被定义在hal/gpio_types.h中,这与TRM中的对应关系是保持一致的

/*  */
typedef enum {
    GPIO_INTR_DISABLE = 0,     /*!< Disable GPIO interrupt                             */
    GPIO_INTR_POSEDGE = 1,     /*!< GPIO interrupt type : rising edge                  */
    GPIO_INTR_NEGEDGE = 2,     /*!< GPIO interrupt type : falling edge                 */
    GPIO_INTR_ANYEDGE = 3,     /*!< GPIO interrupt type : both rising and falling edge */
    GPIO_INTR_LOW_LEVEL = 4,   /*!< GPIO interrupt type : input low level trigger      */
    GPIO_INTR_HIGH_LEVEL = 5,  /*!< GPIO interrupt type : input high level trigger     */
    GPIO_INTR_MAX,
} gpio_int_type_t;

点此跳回顶层目录

4 安装并注册中断服务程序(ISR)

接下来就是GPIO中最重要的一步,在设置完GPIO的各种基本属性之后,最终需要让GPIO中断触发一个中断服务程序(Interrupt Service Routine, ISR)。而这一步就即将要完成中断服务函数的注册与安装动作,gpio_install_isr_service函数用来完成此任务,其函数调用层次关系如下:

/* 函数调用图6: gpio_install_isr_service */
// 分配gpio_isr_func_t空间,并设置到gpio_context中
gpio_install_isr_service(int intr_alloc_flags);
	// 配置gpio_isr_alloc_t入口参数,分配一个isr
	gpio_isr_register(void (*fn)(void *), void *arg, int intr_alloc_flags, gpio_isr_handle_t *handle);
		// 单核情况下调用gpio_isr_register_on_core_static完成中断注册
		gpio_isr_register_on_core_static(void *param);
			// 分配一个CPU中断(define in esp_hw_support)
			esp_intr_alloc(int source, int flags, intr_handler_t handler, void *arg, intr_handle_t *ret_handle);
			-> esp_intr_alloc_intrstatus(...);		

4.1 gpio_install_isr_service——设置并注册GPIO管脚ISR的主函数

gpio_install_isr_service函数是安装GPIO中断处理程序的主函数,它在入口时做了一些合法性检查, 后续为所有GPIO管脚的ISR分配了一段连续的内存空间,并将这段地址空间的头指针放置到了gpio_context结构体中的gpio_isr_func字段中,后续调用gpio_isr_register函数完成了ISR的注册。

esp_err_t gpio_install_isr_service(int intr_alloc_flags)
{
	// 检查GPIO是否已经安装过ISR结构体
    GPIO_CHECK(gpio_context.gpio_isr_func == NULL, "GPIO isr service already installed", ESP_ERR_INVALID_STATE);
	
	// 分配一个gpio_isr_func_t结构体的空间,此结构体存放了ISR对应的函数指针及其参数
	// typedef struct {
    // 		gpio_isr_t fn;   /*!< isr function */
    // 		void *args;      /*!< isr function args */
	//	} gpio_isr_func_t;
    esp_err_t ret = ESP_ERR_NO_MEM;
	// 判断内存要在哪个存储区域分配
    const uint32_t alloc_caps = (intr_alloc_flags & ESP_INTR_FLAG_IRAM) ? MALLOC_CAP_INTERNAL : MALLOC_CAP_DEFAULT;
	// 在内存中连续分配GPIO_NUM_MAX个gpio_isr_func_t结构体,用以存放所有GPIO管脚的ISR
	// 这里相当于为所有GPIO管脚的ISR都留下了位置
	// 有关于IDF的堆内存分配策略,这里暂不展开
    gpio_isr_func_t *isr_func = (gpio_isr_func_t *) heap_caps_calloc(GPIO_NUM_MAX, 
    																 sizeof(gpio_isr_func_t), 
    																 alloc_caps);
   	// 若分配成功
    if (isr_func) {
        portENTER_CRITICAL(&gpio_context.gpio_spinlock);
        // 这段逻辑看上去有些多余,函数入口已经做过检查了...
        if (gpio_context.gpio_isr_func == NULL) {
        	// 将分配的地址写入gpio_context
            gpio_context.gpio_isr_func = isr_func;
            portEXIT_CRITICAL(&gpio_context.gpio_spinlock);
			// 注册整个GPIO的中断服务程序为gpio_intr_service
            ret = gpio_isr_register(gpio_intr_service, 
            						NULL, 
            						intr_alloc_flags, 
            						&gpio_context.gpio_isr_handle);
            if (ret != ESP_OK) {
                // registering failed, uninstall isr service
                gpio_uninstall_isr_service();
            }
        // 冗余: never reached ?
        } else {
            // isr service already installed, free allocated resource
            // 如果ISR已经安装过了,则释放已经分配的资源
            portEXIT_CRITICAL(&gpio_context.gpio_spinlock);
            ret = ESP_ERR_INVALID_STATE;
            free(isr_func);
        }
    }

    return ret;
}

4.2 gpio_isr_register——设置ISR配置,并完成GPIO ISR的注册

此函数在入口处声明了一个gpio_isr_alloc_t类型的局部变量p,这个结构体用来记录一些在ISR分配过程中需要声明的一些必要属性,其声明如下:

// defined in gpio.c
// 记录了ISR的一些必要属性和信息
typedef struct {
    int source;               /* 中断源,定义在interrupts.h */
    int intr_alloc_flags;     /* 标记中断属性的一些标志位 */
    void (*fn)(void *);       /* 中断处理函数  */
    void *arg;                /* 中断处理函数的入口参数 */
    void *handle;             /* 记载此中断信息的一个结构 */
    esp_err_t ret;
} gpio_isr_alloc_t;

gpio_isr_register函数在入口时根据传入的参数初始化了一个gpio_isr_alloc_t类型的结构体p,并将此配置传入gpio_isr_register_on_core_static函数,完成最终的一系列配置动作。

esp_err_t gpio_isr_register(void (*fn)(void *), 		// gpio_intr_service函数指针
							void *arg, 					// NULL
							int intr_alloc_flags, 		// 0
							gpio_isr_handle_t *handle)	// &gpio_context.gpio_isr_handle
{
	// 判断传入的ISR指针是否为空
    GPIO_CHECK(fn, "GPIO ISR null", ESP_ERR_INVALID_ARG);
	// gpio_isr_alloc_t结构体中记录了即将要分配的ISR的各项配置
    gpio_isr_alloc_t p;
    // 中断信号源,定义在interrupts.h中
    // 对于GPIO中断不同芯片有不同的信号源名称
#if CONFIG_IDF_TARGET_ESP32P4  //TODO: IDF-7995
    p.source = ETS_GPIO_INTR0_SOURCE;
#else
    p.source = ETS_GPIO_INTR_SOURCE;
#endif
	// 一些传入的额外标志位
    p.intr_alloc_flags = intr_alloc_flags;
#if SOC_ANA_CMPR_INTR_SHARE_WITH_GPIO
    p.intr_alloc_flags |= ESP_INTR_FLAG_SHARED;
#endif
	// 传入ISR函数以及其对应的参数
    p.fn = fn;
    p.arg = arg;
	
	// ISR的处理句柄,对应到gpio_context.gpio_isr_handle
	// 这会最终存储当前中断和其共享中断的链表入口,详见后续解析
    p.handle = handle;
	
	// 将当前正在处理中断的CPU ID记录在gpio_context中
    portENTER_CRITICAL(&gpio_context.gpio_spinlock);
    if (gpio_context.isr_core_id == GPIO_ISR_CORE_ID_UNINIT) {
        gpio_context.isr_core_id = xPortGetCoreID();
    }
    portEXIT_CRITICAL(&gpio_context.gpio_spinlock);
	
    esp_err_t ret;
// 单核情况
#if CONFIG_FREERTOS_UNICORE
    gpio_isr_register_on_core_static(&p);
    ret = ESP_OK;
// 多核情况
#else /* CONFIG_FREERTOS_UNICORE */
    ret = esp_ipc_call_blocking(gpio_context.isr_core_id, gpio_isr_register_on_core_static, (void *)&p);
#endif /* !CONFIG_FREERTOS_UNICORE */
	// 判断ISR是否注册执行成功
    if (ret != ESP_OK) {
        ESP_LOGE(GPIO_TAG, "esp_ipc_call_blocking failed (0x%x)", ret);
        return ESP_ERR_NOT_FOUND;
    }
    if (p.ret != ESP_OK) {
        ESP_LOGE(GPIO_TAG, "esp_intr_alloc failed (0x%x)", p.ret);
        return ESP_ERR_NOT_FOUND;
    }
    return ESP_OK;
}

Digression3: esp_intr_alloc_intrstatus——将中断服务程序注册到CPU中断上

gpio_isr_register_on_core_static经过简单的函数调用后,最终代码执行流会来到esp_intr_alloc_intrstatus函数中。这个函数实现得有些冗长,下面给出了较为详尽的解读。这一部分稍微脱离了GPIO的主线,因此作为Digression3。为了节省阅读的时间,作为总结,它主要完成了以下几件事情:

  • 入口参数合法性检查
  • 找到一个最合适且未被分配的CPU中断号(get_available_int)
  • 建立并填充新的中断向量描述符(vector_desc_t, vd)
  • 将ISR注册到对应的中断号上,并设置路由矩阵建立中断源到CPU中断号的映射关系

函数中一些更深层次的函数嵌套和调用,只是对其功能做了大体的描述,并没有更进一步的深入,这对于了解外设驱动的组织结构,已经足够。出于方便与前文建立逻辑联系,下文将上述调用链中的入口参数注释在了源代码中

// intr_alloc.c
// gpio_isr_alloc_t p;
// p是gpio_isr_register传入的配置参数,以下函数基于此参数完成分配
esp_err_t esp_intr_alloc_intrstatus(int source, 			 	// p->source
									int flags, 				 	// p->intr_alloc_flags
									uint32_t intrstatusreg,  	// 0
									uint32_t intrstatusmask, 	// 0
									intr_handler_t handler,  	// p->fn (gpio_intr_service函数)
                                    void *arg, 				 	// p->arg 
                                    intr_handle_t *ret_handle) 	// p->handle (出口参数)
{
	// 这个变量将会记载分配的中断向量的具体信息
	// 并最终赋值给ret_handle
	// typedef struct intr_handle_data_t {
    // 		vector_desc_t *vector_desc;
    // 		shared_vector_desc_t *shared_vector_desc;
	//} intr_handle_data_t;
    intr_handle_data_t *ret=NULL;
    int force = -1;
    ESP_EARLY_LOGV(TAG, "esp_intr_alloc_intrstatus (cpu %u): checking args", esp_cpu_get_core_id());
	// 一些入口参数合法性检查,这里暂不展开,它涉及到更多关于中断的分类和细节
    //Shared interrupts should be level-triggered.
    if ((flags & ESP_INTR_FLAG_SHARED) && (flags & ESP_INTR_FLAG_EDGE)) {
        return ESP_ERR_INVALID_ARG;
    }
    //You can't set an handler / arg for a non-C-callable interrupt.
    if ((flags & ESP_INTR_FLAG_HIGH) && (handler)) {
        return ESP_ERR_INVALID_ARG;
    }
    //Shared ints should have handler and non-processor-local source
    if ((flags & ESP_INTR_FLAG_SHARED) && (!handler || source<0)) {
        return ESP_ERR_INVALID_ARG;
    }
    //Statusreg should have a mask
    if (intrstatusreg && !intrstatusmask) {
        return ESP_ERR_INVALID_ARG;
    }
    //If the ISR is marked to be IRAM-resident, the handler must not be in the cached region
    //ToDo: if we are to allow placing interrupt handlers into the 0x400c0000—0x400c2000 region,
    //we need to make sure the interrupt is connected to the CPU0.
    //CPU1 does not have access to the RTC fast memory through this region.
    if ((flags & ESP_INTR_FLAG_IRAM) && handler && !esp_intr_ptr_in_isr_region(handler)) {
        return ESP_ERR_INVALID_ARG;
    }

    // Default to prio 1 for shared interrupts. Default to prio 1, 2 or 3 for non-shared interrupts.
    // 共享中断优先级设置为1,否则可被C语言响应的中断只能为低级别中断
    if ((flags & ESP_INTR_FLAG_LEVELMASK) == 0) {
        if (flags & ESP_INTR_FLAG_SHARED) {
            flags |= ESP_INTR_FLAG_LEVEL1;
        } else {
            flags |= ESP_INTR_FLAG_LOWMED;
        }
    }
    ESP_EARLY_LOGV(TAG, "esp_intr_alloc_intrstatus (cpu %u): Args okay. Resulting flags 0x%X", esp_cpu_get_core_id(), flags);
	
	/* 入口参数合法性检查到此结束 */
	
    // Check 'special' interrupt sources. These are tied to one specific interrupt, so we
    // have to force get_free_int to only look at that.
    // 以下是一些不经过中断矩阵的内部中断源(internal interrupt sources),只能被路由到某一个特定的CPU中断源上
    // 当传入的中断源是内部中断源时,get_available_int函数只能查看 “特定的中断源(force)” 是否空闲,而不能遍历
    // 以下中断源的宏定义位于intr_alloc.c中
    if (source == ETS_INTERNAL_TIMER0_INTR_SOURCE) {
        force = ETS_INTERNAL_TIMER0_INTR_NO;
    }
    if (source == ETS_INTERNAL_TIMER1_INTR_SOURCE) {
        force = ETS_INTERNAL_TIMER1_INTR_NO;
    }
    if (source == ETS_INTERNAL_TIMER2_INTR_SOURCE) {
        force = ETS_INTERNAL_TIMER2_INTR_NO;
    }
    if (source == ETS_INTERNAL_SW0_INTR_SOURCE) {
        force = ETS_INTERNAL_SW0_INTR_NO;
    }
    if (source == ETS_INTERNAL_SW1_INTR_SOURCE) {
        force = ETS_INTERNAL_SW1_INTR_NO;
    }
    if (source == ETS_INTERNAL_PROFILING_INTR_SOURCE) {
        force = ETS_INTERNAL_PROFILING_INTR_NO;
    }

    // Allocate a return handle. If we end up not needing it, we'll free it later on.
    // 分配一块存储区,用来放置出口参数
    ret = heap_caps_malloc(sizeof(intr_handle_data_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
    if (ret == NULL) {
        return ESP_ERR_NO_MEM;
    }
	
	// 获取自旋锁,因此以下代码是原子执行的
    portENTER_CRITICAL(&spinlock);
    uint32_t cpu = esp_cpu_get_core_id();
    // See if we can find an interrupt that matches the flags.
    // get_available_int函数用来寻找满足需求的最优CPU中断号
    // 这个函数内部逻辑有些意思,可以在之后的博客中详细展开一下
    int intr = get_available_int(flags, cpu, force, source);
   	// 如果没能找到合适且空闲的中断号,则直接退出
    if (intr == -1) {
        //None found. Bail out.
        portEXIT_CRITICAL(&spinlock);
        free(ret);
        ESP_LOGE(TAG, "No free interrupt inputs for %s interrupt (flags 0x%X)", esp_isr_names[source], flags);
        return ESP_ERR_NOT_FOUND;
    }
    
    // Get an int vector desc for int.
    // IDF用一个[二维链表]将所有已经分配的中断描述符组织起来,头指针位于vector_desc_head(intr_alloc.c)
    // 每个主链表节点是一个vector_desc_t结构体,它又同时保存了一个shared_vector_desc_t构成的子链表,记录了共享中断信息
    // get_desc_for_int获取已分配中断号的中断描述符,当没有找到时向主链表中插入一个新的描述符vector_desc_t    
    vector_desc_t *vd = get_desc_for_int(intr, cpu);
    if (vd == NULL) {
        portEXIT_CRITICAL(&spinlock);
        free(ret);
        return ESP_ERR_NO_MEM;
    }

    // Allocate that int!
    // 如果当前要分配的中断是一个可共享中断
    if (flags & ESP_INTR_FLAG_SHARED) {
        //Populate vector entry and add to linked list.
        // 分配一块共享中断向量描述符空间
        shared_vector_desc_t *sh_vec = heap_caps_malloc(sizeof(shared_vector_desc_t), 
        												MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
        if (sh_vec == NULL) {
            portEXIT_CRITICAL(&spinlock);
            free(ret);
            return ESP_ERR_NO_MEM;
        }
		
		// 设置共享中断结构体
        memset(sh_vec, 0, sizeof(shared_vector_desc_t));
        sh_vec->statusreg = (uint32_t*)intrstatusreg;
        sh_vec->statusmask = intrstatusmask;
        sh_vec->isr = handler;
        sh_vec->arg = arg;
        // 链表头插法,将sh_vec插入上述主链表的vd节点
        sh_vec->next = vd->shared_vec_info;
        sh_vec->source = source;
        sh_vec->disabled = 0;
        vd->shared_vec_info = sh_vec;
        vd->flags |= VECDESC_FL_SHARED;
        // (Re-)set shared isr handler to new value.
        // 设置共享中断的ISR函数为shared_intr_isr,此函数会遍历并调用自身所在子链表上每个节点的ISR
        esp_cpu_intr_set_handler(intr, (esp_cpu_intr_handler_t)shared_intr_isr, vd);
    } else {
        // Mark as unusable for other interrupt sources. This is ours now!
        // 如果我们申请的是一个独占中断,则将此属性设置到vd的标志位中
        // 现在这个中断号只属于当前中断源
        vd->flags = VECDESC_FL_NONSHARED;
        if (handler) {
#if CONFIG_APPTRACE_SV_ENABLE
// 从略
#else
			// <! 将中断处理函数注册到对应的中断号上>
			// 注意至此,外设中断的ISR就和CPU中断关联了起来
            esp_cpu_intr_set_handler(intr, (esp_cpu_intr_handler_t)handler, arg);
#endif
        }
		
		// 如果是边缘触发的中断,则清空
        if (flags & ESP_INTR_FLAG_EDGE) {
            esp_cpu_intr_edge_ack(intr);
        }
        vd->source = source;
    }
	
	// 如果此ISR位于RAM中,则在flash disable的情况下仍可正常响应
	// 反之不可以
    if (flags & ESP_INTR_FLAG_IRAM) {
        vd->flags |= VECDESC_FL_INIRAM;
        non_iram_int_mask[cpu] &= ~(1<<intr);
    } else {
        vd->flags &= ~VECDESC_FL_INIRAM;
        non_iram_int_mask[cpu] |= (1<<intr);
    }
	// <!如果这是一个外部中断,则将其从中断源路由到对应的cpu中断号intr上>
    if (source>=0) {
        esp_rom_route_intr_matrix(cpu, source, intr);
    }

    //Fill return handle data.
    // 填充一些将要返回的数据
    ret->vector_desc = vd;
    ret->shared_vector_desc = vd->shared_vec_info;

    //Enable int at CPU-level;
    // <! 在CPU端使能中断>
    // 注意与上面的gpio_intr_enable做出区分
    // gpio_intr_enable在GPIO一侧打开了中断,而此处打开了CPU一侧的中断
    ESP_INTR_ENABLE(intr);

    //If interrupt has to be started disabled, do that now; 
    // ints won't be enabled for real until the end
    // of the critical section.
    // 如果仅仅是注册中断,而不使能,则关闭这个中断
    // 1.如果关闭外部中断,只需要关闭中断交换矩阵的路由功能,不让外部中断连接到CPU中断
    // 2.如果是内部中断,只需要关闭CPU对应的中断(ESP_INTR_DISABLE)
    // 中断从外设到CPU,经历了3道开关:外设、中断矩阵、CPU
    if (flags & ESP_INTR_FLAG_INTRDISABLED) {
        esp_intr_disable(ret);
    }

// 做一些中断控制器的配置,使其与此中断配置保持一致
#if SOC_CPU_HAS_FLEXIBLE_INTC
    //Extract the level from the interrupt passed flags
    int level = esp_intr_flags_to_level(flags);
    esp_cpu_intr_set_priority(intr, level);

    if (flags & ESP_INTR_FLAG_EDGE) {
        esp_cpu_intr_set_type(intr, ESP_CPU_INTR_TYPE_EDGE);
    } else {
        esp_cpu_intr_set_type(intr, ESP_CPU_INTR_TYPE_LEVEL);
    }
#endif

/* NOTE: ESP-TEE is responsible for all interrupt-related configurations
 * when enabled. The following code is not applicable in that case */
#if !CONFIG_SECURE_ENABLE_TEE
#if SOC_INT_PLIC_SUPPORTED
    /* Make sure the interrupt is not delegated to user mode (IDF uses machine mode only) */
    RV_CLEAR_CSR(mideleg, BIT(intr));
#endif
#endif

    portEXIT_CRITICAL(&spinlock);

    //Fill return handle if needed, otherwise free handle.
    // 设定返回值,对于GPIO,这个值将被设置到gpio_context中的handle字段
    if (ret_handle != NULL) {
        *ret_handle = ret;
    } else {
        free(ret);
    }

    ESP_EARLY_LOGD(TAG, "Connected src %d to int %d (cpu %"PRIu32")", source, intr, cpu);
    return ESP_OK;
}

4.3 gpio_intr_service——GPIO外设注册的中断处理函数

上述代码是一些具体在ISR分配过程中的一些细节,对于GPIO整个外设而言,上述代码注册的中断处理程序是gpio_intr_service(在gpio_install_isr_service中完成注册),每当GPIO中的任一管脚的中断触发条件满足时,此函数就会被CPU调用,它会扫描每一个GPIO管脚的中断触发状态,并调用对应的ISR

// 请注意此段代码放置在SRAM中
static void IRAM_ATTR gpio_intr_service(void *arg)
{
    // 如果gpio_isr_func还没有被分配,则直接返回
    // 此时没有可用的中断处理函数
    // 回忆:此段内存在gpio_install_isr_service函数中被分配
    if (gpio_context.gpio_isr_func == NULL) {
        return;
    }

    // read status to get interrupt status for GPIO0-31
    // 读取GPIO_PCPU_INT_REG寄存器,获取GPIO0-31的CPU中断状态
    uint32_t gpio_intr_status;
    gpio_hal_get_intr_status(	gpio_context.gpio_hal, 
    							gpio_context.isr_core_id, 
    							&gpio_intr_status);
	
	// 如果有中断需要处理,则进入gpio_isr_loop函数
	// 从低位到高位依次调用每一个需要的中断处理函数,这样即使同时有多个管脚发生中断
	// 它们的ISR也可以依次被调用
    if (gpio_intr_status) {
        gpio_isr_loop(gpio_intr_status, 0);
    }
	
	/* 逻辑和上面一样,只是处理GPIO管脚编号大于32的情况 */
    //read status1 to get interrupt status for GPIO32-39
    uint32_t gpio_intr_status_h;
    gpio_hal_get_intr_status_high(	gpio_context.gpio_hal, 
    								gpio_context.isr_core_id, 
    								&gpio_intr_status_h);

    if (gpio_intr_status_h) {
        gpio_isr_loop(gpio_intr_status_h, 32);
    }
}

扫描并调用对应的中断处理函数的逻辑是在gpio_isr_loop中完成的,这个函数的实现细节如下:

// 此段代码放置在SRAM中
static inline void IRAM_ATTR gpio_isr_loop(uint32_t status, const uint32_t gpio_num_start)
{
	// 中断没有处理完则保持循环
    while (status) {
    	// 找出status不为0的最低位
        int nbit = __builtin_ffs(status) - 1;
		// 清掉status局部变量中对应的标志位,防止死循环
        status &= ~(1 << nbit);
		// 计算出触发中断的GPIO管脚号
        int gpio_num = gpio_num_start + nbit;
        
		// 是否要在进入ISR中清掉中断
        bool intr_status_bit_cleared = false;
        // Edge-triggered type interrupt can clear the interrupt status bit before entering per-pin interrupt handler
        if ((1ULL << (gpio_num)) & gpio_context.isr_clr_on_entry_mask) {
            intr_status_bit_cleared = true;
            gpio_hal_clear_intr_status_bit(gpio_context.gpio_hal, gpio_num);
        }
		
		// <! 调用中断处理函数,处理对应管脚的中断>
        if (gpio_context.gpio_isr_func[gpio_num].fn != NULL) {
            gpio_context.gpio_isr_func[gpio_num].fn(gpio_context.gpio_isr_func[gpio_num].args);
        }

        // If the interrupt status bit was not cleared at the entry, then must clear it before exiting
        // 处理完中断后清空中断标志
        if (!intr_status_bit_cleared) {
            gpio_hal_clear_intr_status_bit(gpio_context.gpio_hal, gpio_num);
        }
    }
}

至此,可以做一个小的总结,理一下GPIO中断相关的代码组织:

GPIO整体作为一个外设,它只映射到一个CPU中断(也就是说所有GPIO管脚本质上是在共用一个CPU中断号)。而作为一个整体,它的中断服务例程在IDF中是确定的,就是上述的gpio_intr_service函数。此函数将会在GPIO中断触发时读取所有GPIO管脚的中断状态,并在循环中调用所有需要调用的中断处理函数

现在还差最后一块拼图,上面我们看到了gpio_context.gpio_isr_func的空间分配动作(在gpio_install_isr_service函数中),也看到gpio_context.gpio_isr_func函数的调用动作(在gpio_intr_service->gpio_isr_loop函数中),但是还没有看见某个GPIO管脚的中断处理函数是何时被放置到gpio_context.gpio_isr_func字段的,这就是最后一个函数gpio_isr_handler_add的作用了。

点此跳回顶层目录

5 将某一个管脚与其特定中断服务程序关联起来

5.1 gpio_isr_handler_add——将GPIO管脚与对应ISR关联起来

gpio_isr_handler_add函数完成的功能很简单,它将用户定义的中断服务函数和参数填充到gpio_context.gpio_isr_func字段,至此之后,当某一个GPIO中断发生时,CPU就可以通过函数gpio_isr_loop中的循环调用到对应用户注册的函数

esp_err_t gpio_isr_handler_add(gpio_num_t gpio_num, gpio_isr_t isr_handler, void *args)
{
	// 检查gpio_context.gpio_isr_func空间是否已经分配
    GPIO_CHECK(gpio_context.gpio_isr_func != NULL, "GPIO isr service is not installed, call gpio_install_isr_service() first", ESP_ERR_INVALID_STATE);
    GPIO_CHECK(GPIO_IS_VALID_GPIO(gpio_num), "GPIO number error", ESP_ERR_INVALID_ARG);
    portENTER_CRITICAL(&gpio_context.gpio_spinlock);
    // 关中断,防止isr注册过程中发生中断
    gpio_intr_disable(gpio_num);
	// 将自定义的,某一个GPIO管脚的中断处理函数和参数写入gpio_context.gpio_isr_func
	// 这样gpio_isr_loop就可以调用到对应管脚的ISR
    if (gpio_context.gpio_isr_func) {
        gpio_context.gpio_isr_func[gpio_num].fn = isr_handler;
        gpio_context.gpio_isr_func[gpio_num].args = args;
    }
    // 开中断
    gpio_intr_enable_on_core(gpio_num, esp_intr_get_cpu(gpio_context.gpio_isr_handle));
    portEXIT_CRITICAL(&gpio_context.gpio_spinlock);
    return ESP_OK;
}

点此跳回顶层目录

6 总结

GPIO作为最基本的外设,它的构造和使用还是相对简单的,大部分情况下只需要写入对应管脚的配置寄存器即可改变对应GPIO管脚的属性。然而,GPIO的使用经常会和一些其他组件关联起来,例如GPIO交换矩阵IO多路复用器中断交换矩阵等。

本篇博文对有关GPIO的一些基本操作进行了简要的介绍,并对有关GPIO的中断处理程序的基本工作流程做了较为详细的介绍。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐