esp32学习笔记——esp_now control例程分析(一)

前言

关于esp-now的介绍视频建议看官方https://www.bilibili.com/video/BV1gf4y1H7iA
esp-now的官方例程代码仓库在https://github.com/espressif/esp-now
本章着重介绍自己的理解,请大家批判的接受

代码整理

我在分析demo的时候极其不习惯使用官方的工程,我会自己整理一个比较“纯净”的工程利于我自己的学习研究,当然这个只是自己的个人习惯。
这里我把demo变成如下结构:

.
├── components
│   ├── control
│   │   ├── include
│   │   └── src
│   ├── espnow
│   │   ├── include
│   │   └── src
│   ├── led
│   │   ├── include
│   │   └── src
│   └── utils
│       ├── include
│       └── src
└── main

除了led,是在esp-now\examples\dev_kits\common_components\led,其它都是在esp-now\components里面。这样搞好后,我们的外层CMakeLists.txt就可以回归到我们最常用的

cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(main)

整个文件夹显得非常的舒服

源码注释

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"

#include "esp_system.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_netif.h"

#include "esp_utils.h"
#include "espnow.h"
#include "espnow_ctrl.h"

#include "driver/gpio.h"
#include "driver/rmt.h"
#include "led_strip.h"

// 这里为了兼容C3,C3的18、19用于usb所以最好不占用
#ifdef CONFIG_IDF_TARGET_ESP32C3 
#define BOOT_KEY_GPIIO        GPIO_NUM_9
#define CONFIG_LED_STRIP_GPIO GPIO_NUM_8
#else
#define BOOT_KEY_GPIIO        GPIO_NUM_0
#define CONFIG_LED_STRIP_GPIO GPIO_NUM_18
#endif

// 绑定设备的时候需要长按的时间
#define BOOT_KEY_PRESS_TIMEOUT_BIND    1000
// 解绑设备的时候需要长按的时间
#define BOOT_KEY_PRESS_TIMEOUT_UNBIND  5000

// log用的标签
static const char *TAG = "main";
// gpio中断消息
static xQueueHandle gpio_evt_queue = NULL;
// led中断消息
static led_strip_t *g_strip_handle = NULL;

/**
 * @brief wifi初始化
 * 
 */
static void wifi_init()
{
    ESP_ERROR_CHECK(esp_netif_init()); // 初始化底层 TCP/IP 协议栈。

    esp_event_loop_create_default();  // 创建默认事件循环,用于接收处理wifi相关事件
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); // 使用默认参数配置wifi
    ESP_ERROR_CHECK(esp_wifi_init(&cfg)); // 将配置丢进去,初始化wifi
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); // 设置wifi模式为station模式
    ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));// 设置wifi存储位置(存在ram里意味着。断电不保存)
    ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));// 设置当前 WiFi 省电类型(这里为不省电)
    ESP_ERROR_CHECK(esp_wifi_start());// 开启wifi
}

/**
 * @brief gpio中断服务函数,用于第一时间发送中断消息给处理任务去处理
 * 
 */
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
    uint32_t gpio_num = (uint32_t) arg;
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

/**
 * @brief 按键处理任务
 * 
 */
static void initiator_key_task(void *arg)
{
    ESP_LOGI(TAG, "Initiator key handle task is running");

    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_ANYEDGE // 任意边沿中断
        .mode = GPIO_MODE_INPUT, // 输入模式
        .pin_bit_mask = BIT(BOOT_KEY_GPIIO), // 配置成gpio0
        .pull_up_en = true, // 上拉输入
    };

    gpio_config(&io_conf);// gpio初始化
    gpio_install_isr_service(0);// 安装中断驱动
    gpio_isr_handler_add(BOOT_KEY_GPIIO, gpio_isr_handler, (void *) BOOT_KEY_GPIIO);// 给引脚绑定中断服务函数

    uint32_t gpio_num; // 用于接收gpio中断的消息
    uint32_t last_press_timetamp = UINT32_MAX; // 记录最后按下的时间
    gpio_evt_queue = xQueueCreate(1, sizeof(uint32_t)); // 创建gpio队列
    bool status = 0; // 记录goio的状态
    
	// 下面是按钮逻辑处理
    while (xQueueReceive(gpio_evt_queue, &gpio_num, portMAX_DELAY)) {
        bool key_level = gpio_get_level(gpio_num);
        vTaskDelay(pdMS_TO_TICKS(10));
		// 经过10ms查询按钮电平是确定按钮不是处于抖动状态,这一步是为了消抖
        if (key_level != gpio_get_level(gpio_num)) {
            continue;
        }
		// 当按钮处于松开且最后一次按下的时间小于当前时间时
        if (key_level == 1 && last_press_timetamp < esp_timer_get_time() / 1000) {
        	// 从按下到松开的时间小于按钮按钮绑定时间时发送开关灯指令
        	// 按钮按下的时间大于绑定时间小于解绑时间时发送绑定指令
        	// 按钮按下的时间大于解绑的时间时发送解绑指令
            if (esp_timer_get_time() / 1000 - last_press_timetamp < BOOT_KEY_PRESS_TIMEOUT_BIND) {
                ESP_LOGI(TAG, "espnow_ctrl_initiator_send, status: %d", status);
                // 第一个参数时发送者属性(按钮1),第二个时响应者的属性,第三个是数据
                espnow_ctrl_initiator_send(ESPNOW_ATTRIBUTE_KEY_1, ESPNOW_ATTRIBUTE_POWER, status);
                status = !status;
            } else if (esp_timer_get_time() / 1000 - last_press_timetamp < BOOT_KEY_PRESS_TIMEOUT_UNBIND) {
                ESP_LOGI(TAG, "espnow_ctrl_initiator_bind");
                espnow_ctrl_initiator_bind(ESPNOW_ATTRIBUTE_KEY_1, true);
            } else {
                ESP_LOGI(TAG, "espnow_ctrl_initiator_unbind");
                espnow_ctrl_initiator_bind(ESPNOW_ATTRIBUTE_KEY_1, false);
            }
        }
		// 当按钮按下的时候记录时间,否则设置成最大时间(最大相当于不记录)
        last_press_timetamp = (key_level == 0) ? esp_timer_get_time() / 1000 : UINT32_MAX;
    }

    ESP_LOGW(TAG, "Initiator key task is exit");// 能到这里说明代码跑飞了

    vTaskDelete(NULL); 	// 删除自己的线程
}

/**
 * @brief 灯响应任务
 * 
 */
static void responder_light_task(void *arg)
{
	// 配置默认的rmt,用于驱动ws2812
    rmt_config_t config = RMT_DEFAULT_CONFIG_TX(CONFIG_LED_STRIP_GPIO, RMT_CHANNEL_0);
    // 将计数器时钟设置为40M
    config.clk_div = 2;
	
	// 导入rmt配置,初始化rmt
    ESP_ERROR_CHECK(rmt_config(&config));
    // 安装rmt驱动
    ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, 0));

    // 配置默认的ws2812
    led_strip_config_t strip_config = LED_STRIP_DEFAULT_CONFIG(1, (led_strip_dev_t)config.channel);
    // 导入配置,初始化ws2812
    g_strip_handle = led_strip_new_rmt_ws2812(&strip_config);

    // 清除灯带颜色 (设置成白色)
    g_strip_handle->set_pixel(g_strip_handle, 0, 255, 255, 255);
    // 刷新灯带状态
    ESP_ERROR_CHECK(g_strip_handle->refresh(g_strip_handle, 100));
	
	// 用于接收发送者信息
    espnow_attribute_t initiator_attribute;
    // 用于接收响应信息
    espnow_attribute_t responder_attribute;
    // 用于接收响应值
    uint32_t responder_value;

    bool status   = true;
    uint8_t red   = 255;
    uint8_t green = 255;
    uint8_t blue  = 255;
	
	// 设置接收响应绑定参数,第一个是响应超时,第二个是响应最低信号强度,第三个是回调
    espnow_ctrl_responder_bind(30 * 1000, -55, NULL);
	
	// 循环接收信号
    while (espnow_ctrl_responder_recv(&initiator_attribute, &responder_attribute, &responder_value) == ESP_OK) {
        ESP_LOGI(TAG, "espnow_ctrl_responder_recv, initiator_attribute: %d, responder_attribute: %d, value: %d",
                 initiator_attribute, responder_attribute, responder_value);
		// 解析接收信号
        switch (responder_attribute) {
        	// 这个用来开关led
            case ESPNOW_ATTRIBUTE_POWER:
                status = responder_value;
                break;
			// 这个信号用来设置led r的值
            case ESPNOW_ATTRIBUTE_RED:
                red = responder_value;
                break;
			// 这个信号用来设置led g的值
            case ESPNOW_ATTRIBUTE_BLUE:
                blue = responder_value;
                break;
			// 这个信号用来设置led b的值
            case ESPNOW_ATTRIBUTE_GREEN:
                green = responder_value;
                break;

            default:
                break;
        }
		// 实现接收信号
        if (status) {
            g_strip_handle->set_pixel(g_strip_handle, 0, red, blue, green);
            g_strip_handle->refresh(g_strip_handle, 100);
        } else {
            g_strip_handle->clear(g_strip_handle, 100);
        }
    }

    ESP_LOGW(TAG, "Responder light task is exit"); // 除了跑飞,不可能跑到这里
    
    vTaskDelete(NULL); // 删除此线程
}

/**
 * @brief 这里是事件回调函数,还记得之前有设置一个默认的事件循环函数吧,后面有绑定事件,这里用来处理
 * 	这里的事件都是系统产生的,不用我们自己写,只要做处理就ok
 */
static void espnow_event_handler(void* handler_args, esp_event_base_t base, int32_t id, void* event_data)
{
	// 如果不是espnow的事件,就不做处理
    if(base != ESP_EVENT_ESPNOW) {
        return ;
    }
	
	// 处理相应事件
    switch (id) {
    	// 绑定的事件处理
        case ESP_EVENT_ESPNOW_CTRL_BIND: {
            espnow_ctrl_bind_info_t *info = (espnow_ctrl_bind_info_t *)event_data;
            ESP_LOGI(TAG, "band, uuid: " MACSTR ", initiator_type: %d", MAC2STR(info->mac), info->initiator_attribute);
            
            g_strip_handle->set_pixel(g_strip_handle, 0, 0x0, 255, 0x0); //亮绿灯
            ESP_ERROR_CHECK(g_strip_handle->refresh(g_strip_handle, 100));
            break;
        }
		// 解绑的事件处理
        case ESP_EVENT_ESPNOW_CTRL_UNBIND: {
            espnow_ctrl_bind_info_t *info = (espnow_ctrl_bind_info_t *)event_data;
            ESP_LOGI(TAG, "unband, uuid: " MACSTR ", initiator_type: %d", MAC2STR(info->mac), info->initiator_attribute);
            
            g_strip_handle->set_pixel(g_strip_handle, 0, 255, 0x0, 0x00);//亮红灯
            ESP_ERROR_CHECK(g_strip_handle->refresh(g_strip_handle, 100));
            break;
        }

        default:
        break;
    }
}

void app_main(void)
{
    esp_log_level_set("*", ESP_LOG_INFO); // 为给定标签设置日志级别
    esp_storage_init(); // 打开nvs,不然wifi会报错

    wifi_init(); // wifi 初始化

    espnow_config_t espnow_config = ESPNOW_INIT_CONFIG_DEFAULT(); // 默认参数配置espnow
    espnow_init(&espnow_config); // 加载参数并初始化

    esp_event_handler_register(ESP_EVENT_ESPNOW, ESP_EVENT_ANY_ID, espnow_event_handler, NULL); // 绑定回调函数到事件处理
	
	// 开始按钮任务
    xTaskCreate(initiator_key_task, "initiator_key_task", 4 * 1024, NULL, tskIDLE_PRIORITY + 1, NULL);
    // 开始rgb led灯任务
    xTaskCreate(responder_light_task, "responder_light_task", 4 * 1024, NULL, tskIDLE_PRIORITY + 1, NULL);
}

这里做了简单的解析,其主要功能是通过下载好的两个设备可以通过按钮绑定并且可以通过按钮控制绑定好的led亮灭。如何使用可以参照官方的readme:https://github.com/espressif/esp-now/blob/master/examples/control/README.md

吐槽一点

本篇代码其实乍一看有个bug,用来保存按下的时间是使用uint32_t去声明,其最大值也是UINT32_MAX,但是esp_timer_get_time()的时间是启动后的时间(以微秒为单位)是int64_t 类型。这就有可能在一定时间后很长一段时间内last_press_timetamp < esp_timer_get_time() / 1000变为恒等的式子且last_press_timetamp = (key_level == 0) ? esp_timer_get_time() / 1000 : UINT32_MAX;这里的结果会溢出。不知道他们怎么想的。。。

总结

总结一下esp_now在这里所出现的用法

	// esp-now是基于wifi的所以wifi得开启
	esp_storage_init();
	wifi_init();
	
	// 配置与初始化esp_now的相关参数
	espnow_config_t espnow_config = ESPNOW_INIT_CONFIG_DEFAULT();
    espnow_init(&espnow_config);
	
	// 绑定回调用于相关的事件处理
	esp_event_handler_register(ESP_EVENT_ESPNOW, ESP_EVENT_ANY_ID, espnow_event_handler, NULL);
	
	// 绑定
	espnow_ctrl_initiator_bind(ESPNOW_ATTRIBUTE_KEY_1, true);

	// 解绑
	espnow_ctrl_initiator_bind(ESPNOW_ATTRIBUTE_KEY_1, false);

	// 设置接收参数
	espnow_ctrl_responder_bind(30 * 1000, -55, NULL);
	
	// 发送信息
	espnow_ctrl_initiator_send(ESPNOW_ATTRIBUTE_KEY_1, ESPNOW_ATTRIBUTE_POWER, status);
	
	// 接收espnow信息
	espnow_ctrl_responder_recv(&initiator_attribute, &responder_attribute, &responder_value)

这些是基于他们写好的一些库来实现的。如果想要自定义传输内容,就有必要分析一下components里面的内容,这里咱们放到下篇再说。

下面是一个ESP32将摄像头拍摄的图片存储到SD卡中的示例程序,它将图片转换为JPG格式并保存到SD卡中: ``` #include "esp_camera.h" #include "esp_timer.h" #include "img_converters.h" #include "sdmmc_cmd.h" #include "driver/sdmmc_host.h" #include "driver/sdspi_host.h" #include "driver/gpio.h" #include "esp_vfs_fat.h" #include "esp_log.h" #define PIN_NUM_MISO 19 #define PIN_NUM_MOSI 23 #define PIN_NUM_CLK 18 #define PIN_NUM_CS 4 static const char* TAG = "example"; // 初始化 SD 卡 static void sd_card_init() { ESP_LOGI(TAG, "Initializing SD card"); sdmmc_host_t host = SDSPI_HOST_DEFAULT(); sdspi_slot_config_t slot_config = SDSPI_SLOT_CONFIG_DEFAULT(); slot_config.gpio_miso = PIN_NUM_MISO; slot_config.gpio_mosi = PIN_NUM_MOSI; slot_config.gpio_sck = PIN_NUM_CLK; slot_config.gpio_cs = PIN_NUM_CS; esp_vfs_fat_sdmmc_mount_config_t mount_config = { .format_if_mount_failed = true, .max_files = 5, .allocation_unit_size = 16 * 1024 }; sdmmc_card_t* card; esp_err_t ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card); if (ret != ESP_OK) { if (ret == ESP_FAIL) { ESP_LOGE(TAG, "Failed to mount filesystem. If you want the card to be formatted, set format_if_mount_failed = true."); } else { ESP_LOGE(TAG, "Failed to initialize the card (%s). Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret)); } return; } sdmmc_card_print_info(stdout, card); } // 将图片转换为 JPG 格式并保存到 SD 卡中 void save_jpeg_to_sdcard(camera_fb_t * fb) { char *filename = "/sdcard/picture.jpg"; // 以二进制写方式打开文件 FILE *file = fopen(filename, "wb"); if (file == NULL) { ESP_LOGE(TAG, "Failed to open file for writing"); return; } esp_jpeg_encoder_t *encoder = malloc(sizeof(esp_jpeg_encoder_t)); if (!encoder) { ESP_LOGE(TAG, "JPEG encoder init error"); fclose(file); return; } ESP_LOGI(TAG, "Encoding JPEG image"); // 初始化 JPEG 编码器 esp_err_t err = esp_jpeg_encoder_init(encoder); if (err != ESP_OK) { ESP_LOGE(TAG, "JPEG encoder init error"); fclose(file); free(encoder); return; } // 将图片转换为 RGB888 格式 uint8_t *rgb_buf = malloc(fb->width * fb->height * 3); if (!rgb_buf) { ESP_LOGE(TAG, "RGB buffer allocation failed"); fclose(file); free(encoder); esp_jpeg_encoder_deinit(encoder); return; } size_t out_len; err = fmt2rgb888(fb->buf, fb->len, fb->format, rgb_buf); if (err != ESP_OK) { ESP_LOGE(TAG, "RGB conversion error"); fclose(file); free(encoder); esp_jpeg_encoder_deinit(encoder); free(rgb_buf); return; } // 将 RGB888 格式的图片编码为 JPEG 格式 err = esp_jpeg_encode(encoder, rgb_buf, fb->width, fb->height, PIXFORMAT_RGB888, 95, file, &out_len); if (err != ESP_OK) { ESP_LOGE(TAG, "JPEG encoding error"); fclose(file); free(encoder); esp_jpeg_encoder_deinit(encoder); free(rgb_buf); return; } ESP_LOGI(TAG, "JPEG image saved to SD card"); fclose(file); free(encoder); esp_jpeg_encoder_deinit(encoder); free(rgb_buf); } void app_main() { // 初始化摄像头 camera_config_t config = { .pin_pwdn = -1, .pin_reset = -1, .pin_xclk = 32, .pin_sscb_sda = 26, .pin_sscb_scl = 27, .pin_d7 = 35, .pin_d6 = 34, .pin_d5 = 39, .pin_d4 = 36, .pin_d3 = 21, .pin_d2 = 19, .pin_d1 = 18, .pin_d0 = 5, .pin_vsync = 25, .pin_href = 23, .pin_pclk = 22, .xclk_freq_hz = 10000000, .ledc_timer = LEDC_TIMER_0, .ledc_channel = LEDC_CHANNEL_0, .pixel_format = PIXFORMAT_JPEG, .frame_size = FRAMESIZE_SVGA, .jpeg_quality = 12, .fb_count = 2 }; esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { ESP_LOGE(TAG, "Camera init failed with error 0x%x", err); return; } // 初始化 SD 卡 sd_card_init(); // 拍摄图片 camera_fb_t * fb = esp_camera_fb_get(); if (!fb) { ESP_LOGE(TAG, "Camera capture failed"); return; } // 将图片保存到 SD 卡中 save_jpeg_to_sdcard(fb); // 释放摄像头缓冲区 esp_camera_fb_return(fb); ESP_LOGI(TAG, "Done"); } ``` 这个示例代码需要使用开发板上的 SD 卡槽和一个 SD 卡。在这个示例中,我们首先初始化 SD 卡,然后拍摄一张图片,将其转换为 RGB888 格式,然后编码为 JPEG 格式并保存到 SD 卡中。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值