Peripheral Drivers in ESP-IDF(1)——GPIO
GPIO作为最基本的外设,它的构造和使用还是相对简单的,大部分情况下只需要写入对应管脚的配置寄存器即可改变对应GPIO管脚的属性。然而,GPIO的使用经常会和一些其他组件关联起来,例如GPIO交换矩阵IO多路复用器中断交换矩阵等。本篇博文对有关GPIO的一些基本操作进行了简要的介绍,并对有关GPIO的中断处理程序的基本工作流程做了较为详细的介绍。
0.Briefly Speaking
这个系列的博客将聚焦于一些常见外设的驱动源码分析,首先会以ESP32系列的MCU(我手头的开发平台是ESP32-C6,这里是技术参考文档)为研究对象,详细分析在开源的物联网开发框架ESP-IDF中控制外设的驱动源码及其控制逻辑,之后会逐步过渡到Linux内核中的驱动代码,并将两者进行对比研究。
本篇博客主要聚焦的外设是GPIO,详细的源码位于IDF的以下目录:
- esp-idf/components/esp_driver_gpio
- ESP32C6技术参考手册
- ESP-IDF编程指南——GPIO & RTC GPIO
在上述的目录中,可以参考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的基本步骤如下,在此作为总结列出,这些基本步骤也将作为后文的小标题出现,点此下方链接可以直接跳转到对应函数的解析:
- 配置GPIO的基本特性(gpio_config)
- 设置输出电平(gpio_set_level)
- 设置GPIO中断触发条件(gpio_set_intr_type)
- 安装并注册GPIO整个外设的中断服务程序(gpio_install_isr_service)
- 将某一个管脚与其特定中断服务程序关联起来(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_REG的GPIO_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的中断处理程序的基本工作流程做了较为详细的介绍。
更多推荐



所有评论(0)