一、GPIO接口的使用:
? ? ? ? 前文以key按键控制led灯的亮灭翻转,介绍了GPIO的输入模式和输出模式。其中在初始化GPIO端口时使用的方法是:
void key_init()
{
led_init(); //初始化LED
//选则GPIO端口
gpio_pad_select_gpio(KEY);
//设置GPIO为输入模式
gpio_set_direction(KEY,GPIO_MODE_INPUT);
}
void led_init(void)
{
//选择GPIO端口
gpio_pad_select_gpio(BLINK_GPIO);
//设置GPIO端口为输出模式
gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);
}
除了这种初始化GPIO的方法之外,还可以通过结构体,对GPIO端口进行初始化:
结构体成员变量 | 类型 | 涵义 | pin_bit_mask | uint64_t | GPIO pin: set with bit mask, each bit maps to a GPIO | mode | gpio_mode_t | GPIO mode: set input/output mode | pull_up_en | gpio_pullup_t | GPIO pull-up? | pull_down_en | gpio_pulldown_t | GPIO pull-down? | intr_type | gpio_int_type_t | GPIO interrupt type |
在gpio_config_t结构体中,pin_bit_mask是一个uint64_t类型的变量,具有 64个二进制位,其中的每个位对应于配置哪个GPIO端口。例如:
pin_bit_mask = 0b0100? 表示此配置对端口GPIO2生效。因为pin_bit_mask的第二位为1。
#define BLINK_GPIO 2
#define GPIO_OUTPUT_PIN_SEL (1ULL<<BLINK_GPIO) // 配置GPIO 2口。
//配置GPIO 18口和19口
//#define GPIO_OUTPUT_PIN_SEL (1ULL<<18)|(1ULL<<19)
void led_init(void)
{
gpio_config_t io_conf; //定义gpio_config类型的结构体;
//GPIO中断类型
io_conf.intr_type = GPIO_PIN_INTR_DISABLE; //禁止中断
io_conf.mode = GPIO_MODE_OUTPUT; //设置GPIO为输出模式
io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL; //配置GPIO_OUT寄存器
io_conf.pull_down_en = 0; //禁止下拉
io_conf.pull_up_en = 0; //禁止上拉
gpio_config(&io_conf); //配置使能
}
两者可以达到相同的效果,而使用结构体使代码更加有条理。
二、GPIO中断模式
? ? ? ? 前文中采用while循环的方式,检测key按键的按下,从而实现LED灯的亮灭,虽然这样也可以达到目的,但是,主程序一直循环检测,非常耗费资源,而且也会出现检测不到的情况。 除了这种方式之外,我们还可以使用中断的方式,为GPIO0注册一个下降沿触发事件,在事件触发后,会向RTOS队列写入数据。同时创建一个任务,从该消息队列中获取新加入的事件,并执行相应的代码。
interrupt.h代码如下:
/*
* interrupt.h
*
* Created on: 2021年12月7日
* Author: wangy
*/
#ifndef COMPONENTS_INTERRUPT_INCLUDE_INTERRUPT_H_
#define COMPONENTS_INTERRUPT_INCLUDE_INTERRUPT_H_
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "led.h"
#define gpio_pin 0
#define GPIO_INPUT_PIN_SEL (1ULL<<gpio_pin)
/**
* 引脚配置函数
*/
void inter_init(void);
/*任务函数*/
void gpio_task_example(void* arg);
/*gpio中断回调函数*/
void IRAM_ATTR gpio_isr_handler(void* arg);
#endif /* COMPONENTS_INTERRUPT_INCLUDE_INTERRUPT_H_ */
interrupt.c代码如下:
/*
* interrupt.c
*
* Created on: 2021年12月7日
* Author: wangy
*/
#include "interrupt.h"
//声明一个队列句柄,队列句柄可以理解为一个队列的标记,不同的队列具有不同的标记
xQueueHandle gpio_evt_queue = NULL;
int led_flag = 0;
/**
* 初始化中断
*/
void inter_init(void){
/**
* 定义并配置GPIO口
*/
gpio_config_t io_conf;
io_conf.intr_type = GPIO_INTR_NEGEDGE; //下降沿中断
io_conf.mode = GPIO_MODE_INPUT; //输入模式
io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL; //配置要设置的引脚
io_conf.pull_down_en = 0; //禁止下拉
io_conf.pull_up_en = 1; //引脚电平上拉
//配置gpio
gpio_config (&io_conf);
/**
* 创建一个队列
* 参数1:队列深度(队列能够存储的最大单元数目)
* 参数2:队列单元数据的size,以字节为单位
* 返回值:NULL表示没有足够的堆空间给队列而导致创建失败。
* 非NULL,队列创建成功,返回队列句柄。
*/
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
/**
* 创建新的任务(任务是freeRTOS系统调度的一个单位)
* 参数1: 任务的入口函数,任务必须执行,并且永不返回。(既,无线循环)
* 参数2: 描述任务的名字
* 参数3: 指定任务堆栈的大小 ,堆栈能保护变量的数目- 不是字节数.
* 例如,如果堆栈为16位宽度,usStackDepth定义为 100, 200 字节,这些将分配给堆栈。
* 堆栈嵌套深度(堆栈宽度)不能超多最大值——包含了size_t类型的变量
* 参数4: 指针,用于作为一个参数,传向创建的任务。
* 参数5:任务运行时的优先级
* 参数6:用于传递一个处理--引用创建的任务。
* 返回值: pdPASS 任务创建成功并且添加到就绪列表
*
*/
xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);
//中断配置
/**
* 注册中断服务
*/
gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1); //注册中断服务
//移除相应引脚的中断处理程序
gpio_isr_handler_remove(gpio_pin);
/**
* 为相应的引脚添加中断处理程序
* 参数1:GPIO引脚
* 参数2:中断处理程序(中断回调函数)
* 参数3:用于中断处理程序的参数。
*/
gpio_isr_handler_add(gpio_pin, gpio_isr_handler, (void*) gpio_pin);
}
/*gpio中断回调函数*/
void IRAM_ATTR gpio_isr_handler(void* arg)
{
uint32_t gpio_num = (uint32_t) arg;
/**
* 将消息存储到队列
* 参数1:目标队列句柄
* 参数2:发送数据的指针。
* 其指向将要复制到目标队列中的数据单元。由于在创建队列时设置了队列中数据单元的长度,所以会从该指针指向的空间复制对应长度的数据到队列的存储区域。
*
* 返回值:pdPASS 数据被成功写入队列
* errQUEUE_FULL 由于队列已经满了,无法将数据写入
*/
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
/*任务函数*/
void gpio_task_example(void* arg)
{
uint32_t io_num;
for(;;) {
/**
* 函数xQueueReceive从队列中取出内容(数据单元)
* 参数1:被读队列的句柄
* 参数2:接收缓存指针。其指向一段内存区域,用于接收从队列中拷贝来的数据。
* 数据单元的长度在创建队列时就已经被设定,所以该指针指向的内存区域大小应当足够保存一个数据单元。
* 参数3:阻塞超时时间:(1)如果在接收时队列为空,则这个时间是任务处于阻塞状态以等待队列数据有效的最长等待时间。
* (2)如果xTicksToWait 设为0,并且队列为空,则xQueueRecieve()与xQueuePeek()均会立即返回。阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统心跳频率。常量portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。
* (3)如果把xTicksToWait 设置为portMAX_DELAY , 并且在FreeRTOSConig.h 中设定INCLUDE_vTaskSuspend 为1,那么阻塞等待将没有超时限制。
* 返回值: pdPASS 在超时到来前能够从队列中成功读取数据,函数则会返回pdPASS。
* errQUEUE_FULL 如果设定了阻塞超时时间(xTicksToWait 非0),在函数返回之前任务将被转移到阻塞态以等待队列数据有效。但直到超时也没有其它任务或是中断服务例程往队列中写入数据,函数则会返回errQUEUE_FULL。
*/
if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)==pdPASS) {
if(led_flag==0){
led_flag =1;
led_on();
}else{
led_flag=0;
led_off();
}
}
}
}
?在进行中断配置时,使用到的函数主要有:
- gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1)? 注册一个中断服务
- gpio_isr_handler_add(gpio_pin, gpio_isr_handler, (void*) gpio_pin)? 将相应引脚和中断处理服务关联起来
? ? ? ? 此外,中断程序中使用到了FreeRtos操作系统的响应内容,FreeRtos操作系统是应用于嵌入式实时操作系统。?此处,对基于FreeRtos的中断程序作简单介绍。
? ? ? ? 程序逻辑为:首先,配置好ESP32相应引脚的中断服务,同时,基于FreeRtos创建一个任务,以及一个消息队列。该任务负责从消息队列中获取数据(中断触发消息),当从队列获取到一个数据单元,执行相应的操作,此处为LED电平翻转。而在中断服务中,我们只做一件事情,就是向一个提前创建好的消息队列中发送消息,表明中断已经被触发。
? ? ? ? 使用FreeRtos操作系统,可以很好的实现多任务,在一个任务阻塞时,其他任务可以执行,以及并发同时执行多个任务等。此处并发同时并不一定是真正的同时,只是操作系统在不同任务之间切换的非常快,上一刻执行任务A,下一刻可能就执行任务B,给人的感觉好像任务A和任务B在同时执行一样。
? ? ? ? 在基于FreeRtos操作系统的中断服务中,中断程序不宜做太多的事情,最好只是传递中断触发的消息,具体的任务执行由操作系统去调度,这样可以尽可能的保证实时性,发挥出FreeRtos操作系统的优势。
main函数:
#include "interrupt.h"
void app_main(void)
{
/**
* 初始化GPIO
*/
led_init();
inter_init();
while(1) {
//必须加延时,任务不能没有延时,否则导致任务无法切换.
vTaskDelay(1000 / portTICK_RATE_MS);
}
}
三、结束
? ? ? ? 本文介绍了GPIO的使用方式,主要介绍了GPIO中断,其中牵扯到一些FreeRtos操作系统,该操作系统会在后续进行详细的介绍。
? ? ? ? 本文源码:ESP32_GPIO中断.zip-硬件开发文档类资源-CSDN下载
|