NodeMCU ESP8266 GPIO 和 PWM 学习

前言

入门任何一款 MCU,GPIO 是必须掌握的基础知识,虽然每款 MCU GPIO 的数量和能力各不一样,但是使用用法是基本一样的。本文将通过两个小实验去理解 GPIO 中的核心知识,通过 ESP8266 了解一下 GPIO 中的基本概念,从而更好的掌握 ESP8266 其他的功能。通用输入/输出 (GPIO) 是集成电路上的一个引脚功能,它既可以是输入引脚,也可以是输出引脚,这些功能均可以在编写程序时进行控制。

ESP8266 与 NodeMCU 管脚

ESP8266 管脚定义

ESP8266 共有16个通用 IO,管脚的位置和管脚的分别为:

管脚 名称 类型 功能
1 VDDA P 模拟电源 3.0V ~ 3.6V
2 LNA I/O 射频天线接口,芯片输出阻抗为 50Ω。无需对芯片进行匹配,但建议保留π型匹配网络对天线进行匹配。
3 VDD3P3 P 功放电源 3.0V ~ 3.6V
4 VDD3P3 P 功放电源 3.0V ~ 3.6V
5 VDD_RTC P NC(1.1V)
6 TOUT I ADC端(芯片内部ADC端口),可用于检测VDD3P3(Pin3、Pin4)电源电压和TOUT(Pin6)的输入电压。(二者不可同时使用)
7 CHIP_PU I 芯片使能端。高电平:有效,芯片正常工作;低电平:芯片关闭,电流很小
8 XPD_DCDC I/O 深度睡眠唤醒;GPIO16
9 MTMS I/O GPOIO14;HSPI_CLK
10 MTDI I/O GPIO12;HSPI_MISO
11 VDDPST P 数字/IO电源(1.8V ~ 3.3V)
12 MTCK I/O GPIO13;HSPI_MOSI;UART0_CTS
13 MTDO I/O GPIO15;HSPI_CS;UART0_RTS
14 GPIO2 I/O 可用作烧写Flash时UART1_TX;GPIO2
15 GPIO0 I/O GPIO0;SPI_CS2
16 GPIO4 I/O GPIO4
17 VDDPST P 数字/IO电源(1.8V ~ 3.3V)
18 SDIO_DATA_2 I/O 连接到SD_D2(串联200Ω);PIHD;HSPIHD;GPIO9
19 SDIO_DATA_3 I/O 连接到SD_D3(串联200Ω);SPIWP;HSPIWP;GPIO10
20 SDIO_CMD I/O 连接到SD_CMD(串联200Ω);SPI_CS0;GPIO11
21 SDIO_CLK I/O 连接到SD_CLK(串联200Ω);SPI_CKL;GPIO6
22 SDIO_DATA_0 I/O 连接到SD_D0(串联200Ω);SPI_MSIO;GPIO7
23 SDIO_DATA_1 I/O 连接到SD_D1(串联200Ω);SPI_MOSI;GPIO8
24 GPIO5 I/O GPIO5
25 U0RXD I/O 可用作烧写Flash时UART Rx;GPIO3
26 U0TXD I/O 可用作烧写Flash时UART Tx;GPIO1;SPI_CS1
27 XTAL_OUT I/O 连接晶振输出端,也可以用于提供BT的时钟输入
28 XTAL_IN I/O 连接晶振输入端
29 VDDD P 模拟电源 3.0V ~ 3.6V
30 VDDA P 模拟电源 3.0V ~ 3.6V
31 RES12K I 串联12kΩ电阻到地
32 EXT_RSTB I 外部重置信号(低电平有效)

其中,在四线 (QUAD) 模式 flash 下,有六个 IO 于 flash 通讯;在两线 (DUAL) 模式 flash 下,有四个 IO 于与 flash 通讯。

NodeMCU 管脚映射图

| Label | GPIO | Input | Output | Notes |

D0 GPIO16 no interrupt no PWM or I2C support HIGH at boot used to wake up from deep sleep
D1 GPIO5 OK OK often used as SCL (I2C)
D2 GPIO4 OK OK often used as SDA (I2C)
D3 GPIO0 pulled up OK connected to FLASH button, boot fails if pulled LOW
D4 GPIO2 pulled up OK HIGH at boot connected to on-board LED, boot fails if pulled LOW
D5 GPIO14 OK OK SPI (SCLK)
D6 GPIO12 OK OK SPI (MISO)
D7 GPIO13 OK OK SPI (MOSI)
D8 GPIO15 pulled to GND OK SPI (CS) Boot fails if pulled HIGH
RX GPIO3 OK RX pin HIGH at boot
TX GPIO1 TX pin OK HIGH at boot debug output at boot, boot fails if pulled LOW
A0 ADC0 Analog Input X

NodeMCU 开发板引脚的编号与 NodeMCU 的内部 GPIO 编号不是一个编号,例如板上的 D4 对应的是 ESP8266 的 GPIO 引脚 2,可以通过 GPIO_Pin_2 变量引用。 根据 ESP8266 的系统 (SoC) 设计,其内部包含了处理器芯片等组件,处理器大约有 16 条 GPIO 线路,其中一些 GPIO 规定默认用于与其他内部组件进行通信,比如与内部闪存的通信,GPIO6 至 GPIO11 通常连接到 ESP8266 板上的 Flash 芯片,不建议使用这些引脚,这样我们大约还有 11 个GPIO引脚可按常规 GPIO 进行使用,在这 11 个针脚中,又有 2 个针脚预留给串口 RX 和 TX。因此最后只剩下 9 个通用 I/O 引脚,即 D0 到 D8。

在实际使用中,从上图我们可以看到一些 GPIO 引脚同时兼备了其他功能,如 RX, TX, SD2, SD3,这些引脚大多不作为 GPIO 使用,因为它们可用于其他进程。极端情况下,可使用 SD3 (D12) 引脚,D12 引脚主要用于响应 GPIO/PWM/中断等功能。需要注意的是,D0|GPIO16 引脚只能作为 GPIO 读/写使用,不支持任何特殊功能。

点亮 NodeMCU 开发板上的 LED

GPIO2 引脚电路图

点亮开发板上的 LED 灯是嵌入式编程开发中的 “Hello, World”。 NodeMCU ESP8266 开发板有两个 LED 灯,一个 LED 位于 ESP-12 模组的 PCB 上;一个 LED 位于 NodeMCU 的 PCB 上。

ESP-12 模组板载 LED 引脚对应 GPIO2/TXD1,电路图如下:

NodeMCU 板载 LED 引脚对应 GPIO16,电路图如下:

两个 LED 都在反向模式下工作,关于引脚电平-当引脚高时,LED是关闭的;当引脚低时,LED是亮的。GPIO 0-15 引脚都配有内置上拉电阻,可以通过 GPIO 配置使能上拉。

配置 GPIO2 引脚

GPIO 的初始化通过一个结构体来配置,如下:

typedef struct {
    uint16           GPIO_Pin;      /**< GPIO pin:配置某一号管脚 */
    GPIOMode_TypeDef GPIO_Mode;     /**< GPIO mode:设置输入输出模式 */
    GPIO_Pullup_IF   GPIO_Pullup;   /**< GPIO pullup:使能上拉 */
    GPIO_INT_TYPE    GPIO_IntrType; /**< GPIO interrupt type:是否使能中断,并配置中断模式 */
} GPIO_ConfigTypeDef;

中断模式配置结构体:

typedef enum {
    GPIO_PIN_INTR_DISABLE = 0,      /**< disable GPIO interrupt 失能中断 */
    GPIO_PIN_INTR_POSEDGE = 1,      /**< GPIO interrupt type : rising edge 上升沿触发 */
    GPIO_PIN_INTR_NEGEDGE = 2,      /**< GPIO interrupt type : falling edge 下降沿触发 */
    GPIO_PIN_INTR_ANYEDGE = 3,      /**< GPIO interrupt type : bothe rising and falling edge 双边沿触发 */
    GPIO_PIN_INTR_LOLEVEL = 4,      /**< GPIO interrupt type : low level 输入低电平触发 */
    GPIO_PIN_INTR_HILEVEL = 5       /**< GPIO interrupt type : high level 输入高电平触发 */
} GPIO_INT_TYPE;

输入输出模式配置结构体:

typedef enum {
    GPIO_Mode_Input = 0x0,          /**< GPIO mode : Input */
    GPIO_Mode_Out_OD,               /**< GPIO mode : Output_OD */
    GPIO_Mode_Output ,              /**< GPIO mode : Output */
    GPIO_Mode_Sigma_Delta ,         /**< GPIO mode : Sigma_Delta */
} GPIOMode_TypeDef;

下面我们以控制 GPIO2 连接的 LED 灯为例加以说明。设置 GPIO2 为不开启中端,输出端口并使能上拉:

#include "gpio.h"

void led_init(void){
    GPIO_ConfigTypeDef gpio_in_cfg;                       // 定义 GPIO 初始化结构体
    gpio_in_cfg.GPIO_IntrType = GPIO_PIN_INTR_DISABLE;    // 是否使能中断,并配置中断模式
    gpio_in_cfg.GPIO_Mode = GPIO_Mode_Output;             // 设置 GPIO2 口为输出端口
    gpio_in_cfg.GPIO_Pullup = GPIO_PullUp_EN;             // 设置 GPIO2 口上拉有效
    gpio_in_cfg.GPIO_Pin = GPIO_Pin_2;                    // 使能 GPIO2 口
    gpio_config(&gpio_in_cfg);                            // GPIO 初始化
}

void user_init(void){
    led_init();
}

GPIO2 LED 灯闪烁实例

如果我们想实现 LED 闪烁的效果,我们可以定义一个任务来实现:

/**
 * 创建一个任务,参数分别为:
 * - pvTaskCode:任务函数
 * - pcName: 任务名称
 * - usStackDepth: 任务堆栈
 * - pvParameters: 任务函数的参数
 * - uxPriority: 任务优先级
 * - pvCreatedTask: 任务句柄
 */
xTaskCreate(led_toggle_task, "led_toggle_task", 256, NULL, 1, NULL);

任务函数实现:

void led_init(void){
    GPIO_ConfigTypeDef gpio_in_cfg;                       // 定义 GPIO 初始化结构体
    gpio_in_cfg.GPIO_IntrType = GPIO_PIN_INTR_DISABLE;    // 是否使能中断,并配置中断模式
    gpio_in_cfg.GPIO_Mode = GPIO_Mode_Output;             // 设置 GPIO 口为输出端口 /
    gpio_in_cfg.GPIO_Pullup = GPIO_PullUp_EN;             // 设置 GPIO 口上拉有效
    gpio_in_cfg.GPIO_Pin = GPIO_Pin_2;                    // 使能 GPIO IO 口
    gpio_config(&gpio_in_cfg);                            // GPIO 初始化
}

void led_toggle(void){
    // 获取 GPIO2 管脚的电平状态
    uint32_t bit = GPIO_INPUT_GET(2);
    // 设置 GPIO2 管脚输出电平
    GPIO_OUTPUT(GPIO_Pin_2, bit^1);
    printf("led toggle \n");
}

void led_toggle_task(void *pvParameters){
    led_init();
    for( ;; ){
        led_toggle();
        // 延时 500 ms
        vTaskDelay(500 / portTICK_RATE_MS);
    }
    vTaskDelete(NULL);
}

制作 RGB 三色 LED 呼吸灯

PWM 实现呼吸灯

什么是呼吸灯?

呼吸灯最早是由苹果公司发明并应用于笔记本睡眠提示上,一经展出,立刻吸引众多科技厂商争相效仿。呼吸灯是指灯在微控器控制之下完成由暗到亮的逐渐变化,再由亮到暗的逐渐变化,亮暗的节奏感觉像是人在呼吸。 用微控器做呼吸灯是利用频率来控制呼吸灯的呼吸时间,用占空比来控制灯的亮度。也就是采用 PWM 的方式,在固定的频率下,采用占空比的方式来实现 LED 亮度的变化。占空比为 0,LED 灯不亮,占空比为 100%,则 LED 灯最亮。所以将占空比从 0 到 100%,再从 100% 到 0 不断变化,就可以实现 LED 灯实现特效呼吸。

什么是 PWM 控制信号?

PWM(pulse-width modulation)脉冲宽度调制,MCU(微控制器)通过对开关器件的通断进行控制,使输出端得到一系列幅值相等的脉冲,用这些脉冲来代替正弦波或所需的波形。当我们需要连续控制电压变化,实现呼吸灯或者电机转速的时候,就要用到PWM,如下图所示。

电路连接图

NodeMCU 套件上的 PWM 引脚如下图所示:

下面我们用 NodeMCU D5 (GPIO14)、D6 (GPIO12)、D7 (GPIO13) 这三个支持 PWM 功能的引脚去控制 RGB 三色 LED 灯。电路连接图如下:

我们按照电路图接好电路会发现 RGB 三色灯均发亮了,这是因为 GPIO12、GPIO13、GPIO14 UBOOT电平默认是高电平。

呼吸灯代码实现

PWM 驱动接⼝函数不能跟 hw_timer.c 的接⼝同时使⽤,因为它们共⽤了同⼀个硬件定时器。

#define PWM_0_OUT_IO_MUX PERIPHS_IO_MUX_MTDI_U
#define PWM_0_OUT_IO_NUM 12
#define PWM_0_OUT_IO_FUNC FUNC_GPIO12
#define PWM_1_OUT_IO_MUX PERIPHS_IO_MUX_MTCK_U
#define PWM_1_OUT_IO_NUM 13
#define PWM_1_OUT_IO_FUNC FUNC_GPIO13
#define PWM_2_OUT_IO_MUX PERIPHS_IO_MUX_MTMS_U
#define PWM_2_OUT_IO_NUM 14
#define PWM_2_OUT_IO_FUNC FUNC_GPIO14

LOCAL os_timer_t rgb_light_timer;
/** PWM占空比变量 */
LOCAL u8 set_duty = 0;
/** PWM占空比加减标志 */
LOCAL bool flag = true;

void ESP8266_PWM_RUN(void)
{
    if (flag)
    {
        if (++set_duty >= 100)
        {
            flag = false;
        }
    }
    else
    {
        if (--set_duty <= 0)
        {
            flag = true;
        }
    }

    /** 更新 PWM 通道的占空比 */
    pwm_set_duty(set_duty, 0);
    pwm_set_duty(set_duty, 1);
    pwm_set_duty(set_duty, 2);
    pwm_start();
}

void rgb_light_pwm_init(void)
{
    uint32 period = 1000;
    uint32 duty[3] = {0};
    uint32 io_info[3][3] = {
        {PWM_0_OUT_IO_MUX, PWM_0_OUT_IO_FUNC, PWM_0_OUT_IO_NUM}, // GPIO12
        {PWM_1_OUT_IO_MUX, PWM_1_OUT_IO_FUNC, PWM_1_OUT_IO_NUM}, // GPIO13
        {PWM_2_OUT_IO_MUX, PWM_2_OUT_IO_FUNC, PWM_2_OUT_IO_NUM}  // GPIO14
    };
    // 配置 PWM 的周期为 1000us,占空比为 0,PWM 通道数量为 3
    pwm_init(period, duty, 3, io_info);
    os_timer_disarm(&rgb_light_timer);
    os_timer_setfn(&rgb_light_timer, (os_timer_func_t *)(ESP8266_PWM_RUN), NULL);
    os_timer_arm(&rgb_light_timer, 20, true);
}

实现效果如下:

参考

写这些代码也许就一两个小时的事,写一篇大家好接受的文章需要几天的酝酿,如果文章对您有帮助请我喝杯咖啡吧!