esp32系列(3):GPIO信号传输(以简单GPIO输入输出、ADC、DAC为例)

ESP32学习记录:

  1. esp32系列(1):Hello world, 初识esp32
    搭建VScode下的esp32开发环境,hello esp32。
  2. esp32系列(2):工程结构学习,从新建工程到烧写程序
    熟悉工程的文件结构,各文件的含义,编译流程。以及VScode的具体操作。
  3. esp32系列(3):GPIOGPIO信号传输(以简单GPIO输入输出、ADC、DAC为例)
    学习ESP32 GPIO与外设的几种输入输出方式,通过两个简单的例子学习简单GPIO与RTC GPIO输入输出的代码实现。

1 ESP32 GPIO基本概念

ESP32 芯片有 34 个物理 GPIO pad(GPIO PAD号:0­-19, 21-­23, 25­-27, 32­-39。其中 GPIO 34­-39 仅用作输入管脚,其他的既可以作为输入又可以作为输出管脚。)。每个 pad 都可用作一个通用 IO,或连接一个内部的外设信号。

IO_MUX、RTC IO_MUX 和 GPIO 交换矩阵用于将信号从外设传输至 GPIO pad。这些模块共同组成了芯片的 IO 控
制。

在这里插入图片描述

  1. IO_MUX 选择GPIO pad配置为GPIO(与交换矩阵连接)或者直连(更好的高频数字特性,用于高速信号)
  2. GPIO 交换矩阵 进行【外设输入输出】与【pad信号】之间的全交换。其实就是完pad的输入与输出信号的选择。
  3. RTC IO_MUX 控制GPIO pad的低功耗和模拟功能。

1.1 通过 GPIO 交换矩阵的外设输入

34个GPIO(X: 0-19,21-23,25-27,32-39)
获取
外设输入信号的索引号(Y: 0-18,23-36,39-58,61-90,95-124,140-155,164-181,190-195,198-206)

在这里插入图片描述

重点:把某个外设信号 Y 绑定到某个 GPIO pad X 的配置过程为:

  1. 在 GPIO 交换矩阵中配置外设信号YGPIO_FUNCy_IN_SEL_CFG 寄存器:
    • 设置 GPIO_FUNCy_IN_SEL 字段为要读取的 GPIO pad X 的值。清零其他 GPIO pad 的其他字段。
  2. 在 GPIO 交换矩阵中配置 GPIO pad XGPIO_FUNCx_OUT_SEL_CFG 寄存器、清零 GPIO_ENABLE_DATA[x] 字段:
    • 要强制管脚的输出状态始终由 GPIO_ENABLE_DATA[x] 字段决定,则将 GPIO_FUNCx_OUT_SEL_CFG 寄存器的 GPIO_FUNCx_OEN_SEL 字段位置为 1。
    • GPIO_ENABLE_DATA[x] 字段在 GPIO_ENABLE_REG (GPIOs 0-31) 或 GPIO_ENABLE1_REG (GPIOs 32-39) 中,清零此位可以关闭 GPIO pad 的输出
  3. 配置 IO_MUX 寄存器来选择 GPIO 交换矩阵。配置 GPIO pad X 的 IO_MUX_x_REG 的过程如下:
    • 设置功能字段 (MCU_SEL) 为 GPIO X 的 IO_MUX 功能(所有管脚的 Function 2,数值为 2)。
    • 置位 FUN_IE 使能输入。
    • 置位或清零 FUN_WPU 和 FUN_WPD 位,使能或关闭内部上拉/下拉电阻器。
      说明:
  • 同一个输入 pad 上可以同时绑定多个内部 input_signals。
  • 置位 GPIO_FUNCy_IN_INV_SEL 可以把输入的信号取反。
  • 无需将输入信号绑定到一个 pad 也可以使外设读取恒低或恒高电平的输入值。实现方式为选择特定的
    GPIO_FUNCy_IN_SEL 输入值而不是一个 GPIO 序号:
    • 当 GPIO_FUNCy_IN_SEL 是 0x30 时, input_signal_x 始终为 0。
    • 当 GPIO_FUNCy_IN_SEL 是 0x38 时, input_signal_x 始终为 1。
      例子:把 RMT 外设通道 0 的输入信号 RMT_SIG_IN0_IDX(信号索引号 83)绑定到 GPIO15,请按照以下步骤操作(请注意 GPIO15 也叫做 MTDO 管脚):
  1. 将 GPIO_FUNC83_IN_SEL_CFG 寄存器的 GPIO_FUNC83_IN_SEL 字段设置为 15。
  2. 因为此信号是纯输入信号,置位 GPIO_FUNC15_OUT_SEL_CFG_REG 寄存器中的 GPIO_FUNC15_OEN_SEL 位。
  3. 清零 GPIO_ENABLE_REG 寄存器的 bit 15(GPIO_ENABLE_DATA[15] 字段)。
  4. 配置 IO_MUX_GPIO15 寄存器的 MCU_SEL 字段为 2 (GPIO function),同时置位 FUN_IE(使能输入模式)。

简单的GOIO输入:

  • GPIO_IN_REG/GPIO_IN1_REG 寄存器存储着每一个 GPIO pad 的输入值。
  • 任意 GPIO pin 的输入值都可以随时读取而无需为某一个外设信号配置 GPIO 交换矩阵。但是需要为 pad X 的
    IO_MUX_x_REG 寄存器配置 FUN_IE 位以使能输入。

1.2 通过 GPIO 交换矩阵的外设输出

28 个 GPIO (X: 0-19, 21-23, 25-27, 32-33)
获取
外设信号(Y: 0-18, 23-37, 61-121,140-215, 224-228)

在这里插入图片描述

重点:输出外设信号 Y 到某一 GPIO pad X 的步骤为:

  1. 在 GPIO 交换矩阵里配置 GPIO X 的 GPIO_FUNCx_OUT_SEL_CFG 寄存器和 GPIO_ENABLE_DATA[x] 字段:
    • 设置 GPIO_FUNCx_OUT_SEL_CFG 寄存器的 GPIO_FUNCx_OUT_SEL 字段为外设输出信号 Y 的索引号 (Y)。
    • 要将信号强制使能为输出模式,将 GPIO pad X 的 GPIO_FUNCx_OUT_SEL_CFG 寄存器的GPIO_FUNCx_OEN_SEL 置位,并且将 GPIO_ENABLE_REG 寄存器的 GPIO_ENABLE_DATA[x] 字段置位。或者,将 GPIO_FUNCx_OEN_SEL 清零,此时输出使能信号由内部逻辑功能决定。
    • GPIO_ENABLE_DATA[x] 字段在 GPIO_ENABLE_REG (GPIOs 0-31) 或 GPIO_ENABLE1_REG (GPIOs32-39) 中,清零此位可以关闭 GPIO pad 的输出。
  2. 要选择以开漏方式输出,可以设置 GPIO X 的 GPIO_PINx 寄存器中的 GPIO_PINx_PAD_DRIVER 位。
  3. 配置 IO_MUX 寄存器来选择 GPIO 交换矩阵。配置 GPIO pad X 的 IO_MUX_x_REG 的过程如下:
    • 设置功能字段 (MCU_SEL) 为 GPIO X 的 IO_MUX 功能(所有管脚的 Function 2,数值为 2)。
    • 设置 FUN_DRV 字段为特定的输出强度值 (0-3),值越大,输出驱动能力越强。
    • 在开漏模式下,通过置位/清零 FUN_WPU 和 FUN_WPD 使能或关闭上拉/下拉电阻器。

说明:

  • 某一个外设的输出信号可以同时从多个 pad 输出。
  • 置位 GPIO_FUNCx_OUT_INV_SEL 可以把输出的信号取反。

简单的GOIO输出:

  • GPIO 交换矩阵也可以用于简单 GPIO 输出。设置 GPIO_OUT_DATA 寄存器中某一位的值可以写入对应的 GPIO pad。
  • 为实现某一 pad 的 GPIO 输出,设置 GPIO 交换矩阵 GPIO_FUNCx_OUT_SEL 寄存器为特定的外设索引值 256(0x100)。

1.3 直接I/O

从之前的总体输入输出结构框图可以看出,IO_MUX还有一些直接I/O信号。快速信号如以太网、 SDIO、 SPI、 JTAG、 UART 等会旁路 GPIO 交换矩阵以实现更好的高频数字特性。

1.4 RTC IO_MUX 输入输出

18 个 GPIO 管脚具有低功耗(低功耗 RTC)性能和模拟功能,这些功能不经过IO_MUX 和 GPIO 交换矩阵,而是使用 RTC_MUX 将 I/O 指向 RTC 子系统。

当这些管脚被配置为 RTC GPIO 管脚,作为输出管脚时仍然能够在芯片处于 Deep-sleep 睡眠模式下保持输出
电平值或者作为输入管脚使用时可以将芯片从 Deep-sleep 中唤醒。

2 代码实现

2.1 简单 GPIO 输入输出实现

实际的实现,ESP-IDF官方文档中将GPIO的设置封装成函数:gpio_config。

@path: gpio.h  
@brief: GPIO通用配置函数,配置 GPIO 的模式,上拉,下拉,中断   
@param: 一个指向** GPIO 配置结构体**的指针 pGPIOConfig   
@return: ESP_OK:成功;ESP_ERR_INVALID_ARG:参数错误    

esp_err_t gpio_config(const gpio_config_t *pGPIOConfig);

GPIO 配置结构体:

/**
 * @brief Configuration parameters of GPIO pad for gpio_config function
 */
typedef struct {
    uint64_t pin_bit_mask;          /*!< GPIO pin: set with bit mask, each bit maps to a GPIO */
    gpio_mode_t mode;               /*!< GPIO mode: set input/output mode                     */
    gpio_pullup_t pull_up_en;       /*!< GPIO pull-up                                         */
    gpio_pulldown_t pull_down_en;   /*!< GPIO pull-down                                       */
    gpio_int_type_t intr_type;      /*!< GPIO interrupt type                                  */
} gpio_config_t;

以配置GPIO1为输出、GPIO3为输入为例。代码为:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "driver/gpio.h"  
#include "freertos/FreeRTOS.h" //portTICK_RATE_MS
#include "freertos/task.h" //vTaskDelay
/**
 * Brief:
 * 基本的GPIO输入输出学习
 *
 * GPIO status:
 * GPIO1: output
 * GPIO3:  input
 *
 * Test:
 * Connect GPIO1 with GPIO3
 * Generate pulses on GPIO3, that connect to GPIO3
 *
 */ 
#define GPIO_2   2
#define GPIO_4   4

void app_main(void)
{
    // GPIO1
    gpio_config_t io_conf = {};                 //新建配置GPIO pad的gpio_config功能参数的结构体  
    io_conf.pin_bit_mask = (1ULL << GPIO_2);    //设置GPIO2的掩码为1
    io_conf.mode = GPIO_MODE_OUTPUT;            //设置GPIO1 为输出模式
    io_conf.pull_up_en = 0;                     //不上拉
    io_conf.pull_down_en = 0;                   //不下拉  
    io_conf.intr_type = 0;                      //禁用GPIO2中断  
    esp_err_t result;
    result = gpio_config(&io_conf);             //配置GPIO2  
    if (result == ESP_OK)
        printf("gpio2_config succeed \n"); 
    else 
        printf("gpio2_config failed \n"); 
    
    // GPIO3  
    io_conf.pin_bit_mask = (1ULL << GPIO_4);    //设置GPIO3的掩码为1
    io_conf.mode = GPIO_MODE_INPUT;             //设置GPIO3 为输入模式  
    result = gpio_config(&io_conf);             //配置GPIO3   
    if (result == ESP_OK)
        printf("gpio4_config succeed \n"); 
    else 
        printf("gpio4_config failed \n");  

    int cnt = 0;
    int value = 0;
    while(1)
    {   
        cnt++;
        vTaskDelay(1000 / portTICK_RATE_MS); 
        gpio_set_level(GPIO_2, cnt % 2); 
        printf("GPIO_2_output: %d\n", cnt % 2);
        value = gpio_get_level(GPIO_4);
        printf("GPIO_4_input: %d\n", value);
    }
}

将开发板的GPIO2与GPIO4连接在一起
在这里插入图片描述

运行结果:
在这里插入图片描述

2.2 RTC GPIO 输入输出实现

以DAC1通过GPIO25输出、ADC1通过GPIO33输入为例。

首先查找乐鑫的《esp32技术参考手册》的 4.10 章节找到ADC与DAC的管脚映射:

在这里插入图片描述

可以看到 DAC_1 对应 GPIO25, ADC1_CH5 对应 GPIO33 。

对于这类外设输入输出,官方给定的外设驱动文件给出了相关的配置函数,我们使用起来也很方便,这里以当前的dac、adc应用为例,介绍一下相关的函数:

  • esp_adc_cal_check_efuse
    检查当前参考电压是否熔断到对应位置。
  • adc1_config_width
    设置ADC1采集位宽
  • adc1_config_channel_atten
    配置ADC1的通道,即对应的 GPIO 。以及这个通道的衰减。
  • esp_adc_cal_characterize
    使用输入的参数配置ADC
  • esp_adc_cal_raw_to_voltage
    根据采样值以及adc的配置特性计算电压值。

可见,有了官方的驱动,可以省去繁杂的寄存器配置过程。除了ad和da,其他的外设使用也是一样的方便。

测试代码:

#include <stdio.h>  
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "esp_adc_cal.h"      //esp_adc_cal_check_efuse
#include "driver/adc.h"  

#include "driver/dac.h"

#define DEFAULT_VREF    1100  // 参考电压  
#define NO_OF_SAMPLES   64    // 采样次数  

#define AMP_DAC         255   // DAC输出幅度 

static const adc_unit_t unit = ADC_UNIT_1;              // ADC1
static const adc_channel_t channel = ADC_CHANNEL_5;     // ADC1 GPIO33
static const adc_bits_width_t width = ADC_WIDTH_BIT_12; // ESP32 集成 12-bit SAR ADC
static const adc_atten_t atten = ADC_ATTEN_DB_11;       // 衰减 11 dB (3.55 x)
static esp_adc_cal_characteristics_t *adc_chars;        // ADC的结构存储特性
static const dac_channel_t dac_chan = DAC_CHANNEL_1;

void app_main(void)
{
    //Check if TP is burned into eFuse
    if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP) == ESP_OK) {
        printf("eFuse Two Point: Supported\n");
    } else {
        printf("eFuse Two Point: NOT supported\n");
    }

    //Check Vref is burned into eFuse
    if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_VREF) == ESP_OK) {
        printf("eFuse Vref: Supported\n");
    } else {
        printf("eFuse Vref: NOT supported\n");
    }  

    // 配置位宽
    if (adc1_config_width(width) == ESP_OK ) {   
        printf("adc1_config_width: ESP_OK\n");
    } else {
        printf("adc1_config_width: ESP_ERR_INVALID_ARG\n");
    }  

    // 在ADC1上设置特定通道的衰减,并配置其相关的GPIO RTC_MUX。
    if (adc1_config_channel_atten(channel, atten) == ESP_OK ) { 
        printf("adc1_config_channel_atten: ESP_OK\n");
    } else {
        printf("adc1_config_channel_atten: ESP_ERR_INVALID_ARG\n");
    }  

    // 构造ADC的结构存储特性
    adc_chars = calloc(1, sizeof(esp_adc_cal_characteristics_t)); //分配一个内存块。
    esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, atten, width, DEFAULT_VREF, adc_chars);

    // 使能DAC输出  
    if (dac_output_enable(dac_chan) == ESP_OK ) { 
        printf("dac_output_enable: ESP_OK\n");
    } else {
        printf("dac_output_enable: ESP_ERR_INVALID_ARG\n");
    } 

    int cnt = 0;
    while(1)
    {
        uint32_t adc_reading = 0;  

        cnt++; 

        // dac 输出方波 
        dac_output_voltage(dac_chan, ((cnt % 2) * 200));  
        printf("dac Voltage: %d mV\n", ((cnt % 2) * 200) * 3300 / 255 ); 

        // 采样64次取平均值  
        for (int i = 0; i < NO_OF_SAMPLES; i++)
        {
            adc_reading += adc1_get_raw((adc1_channel_t)channel);
        }
        adc_reading /= NO_OF_SAMPLES;  

        // 采样值转换为电平值  
        uint32_t voltage = esp_adc_cal_raw_to_voltage(adc_reading, adc_chars); 
        printf("Raw: %d\tVoltage: %dmV\n", adc_reading, voltage);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

连接GPIO25与GPIO33。
在这里插入图片描述

调试结果:

在这里插入图片描述

值得注意的是,ESP32 ADC 不同的衰减值有对应的输入电压支持范围,所以这里0mV测不准是正常的。

衰减配置值衰减(dB)支持的范输入电压范围(mV)
ADC_ATTEN_DB_00100 ~ 950
ADC_ATTEN_DB_2_52.5100 ~ 1250
ADC_ATTEN_DB_66150 ~ 1750
ADC_ATTEN_DB_1111150 ~ 2450
  • 8
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lu-ming.xyz

觉得有用的话点个赞吧 :)

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值