ESP32开发入门

发布于:2025-06-17 ⋅ 阅读:(12) ⋅ 点赞:(0)

写在前面:

1 Linux下开发环境搭建

1.1 安装虚拟机和esp-idf

1.1.1 Ubuntu下载

清华大学Ubuntu镜像,里面自动从Ubuntu官方源切换到了清华镜像源。网站:清华镜像

1.1.2 环境搭建

1、安装各种必要的工具
sudo apt-get install git wget flex bison gperf python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 net-tools

2、新建esp32目录
mkdir esp32
cd esp32

3、拉取gitee工具
git clone https://gitee.com/EspressifSystems/esp-gitee-tools.git

4、执行gitee工具切换镜像脚本
cd esp-gitee-tools
./jihu-mirror.sh set

5、拉取esp-idf源码
cd ..
git clone --recursive https://github.com/espressif/esp-idf.git

6、切换esp-idf版本分支到v5.2
cd esp-idf
git checkout v5.2
git submodule update --init --recursive
如果提示失败或有错误试下这句:../esp-gitee-tools/submodule-update.sh

7、更换pip源
pip config set global.index-url http://mirrors.aliyun.com/pypi/simple
pip config set global.trusted-host mirrors.aliyun.com

8、安装编译工具
../esp-gitee-tools/install.sh

9、设置环境变量并将环境变量放到.bashrc中
source export.sh
echo "source ~/esp32/esp-idf/export.sh" >> ~/.bashrc

10、编译
cd esp32-board/helloworld
idf.py build

11、设置USB串口权限
sudo usermod -aG dialout usrname  usrname需要换成你的用户名

12、重启


1.1.3 esp-idf目录分析

components:组件代码,esp-idf 的核心, 包含通用的功能部件, 比如 freeRTOS、 wifi、 GPIO驱动、 PWM 驱动等等。
examples:官方例程, 大部分的应用都可以在这里找到相关例子
docs:一些介绍文档
tools: 一些工具, 如编译、 烧录

1.2 vscode配置

1.2.1 安装如下插件

  • Remote - SSH
  • ESP-IDF
  • C/C++

1.2.2 头文件爆红

1.删除.vscode文件
2.vscode快捷键ctrl + shirt + p,输入如下

select a Kit
# 根据不同的esp32芯片,选择不同的工具包

在这里插入图片描述

3.vscode快捷键ctrl + shirt + p,输入如下

esp-idf:add vs code configuration folder

4.idf.py build编译一下

1.3 配置Remote-SSH免密登录

1.打开git bash
2.生成SSH密钥对

ssh-keygen -t ed25519 -C "youxiang@youxiang.com"

3.使用ssh-copy-id命令将公钥复制到远程服务器

# username:Linux登录用户名
# password:LinuxIP
ssh-copy-id username@remote_host

如果ssh-copy-id命令不可用,可以手动复制公钥。把C:\Users\你的用户名\.ssh\id_ed25519.pub中的内容复制,添加到Linux的~/.ssh/authorized_keys

4.测试连通性

# username:Linux登录用户名
# password:Linux登录密码
ssh username@remote_host

5.vscode配置ssh文件
在这里插入图片描述

# Read more about SSH config files: https://linux.die.net/man/5/ssh_config
Host ubuntuqinghua
    HostName 192.168.5.11
    Port 22
    User username
    IdentityFile "C:\Users\31727\.ssh\id_ed25519"

1.4 esp-idf常用脚本命令

# 设置芯片型号
idf.py set-target esp32s3
# menuconfig
idf.py menuconfig
# 编译
idf.py build
# 清除编译
idf.py fullclean
# 烧录
idf.py flash
# 查看串口运行日志
idf.py monitor
# 新建工程
idf.py create-project projectName

# 组合指令,编译,烧录,查看日志
idf.py build flash monitor

# 添加组件,以LVGL为例
idf.py add-dependency "lvgl/lvgl^8.3.10"

2.GPIO

include "driver/gpio.h"  // GPIO头文件
#define     LED_PIN     48

// 方法一
gpio_config_t led_config = {
        .pin_bit_mask = (1ULL<<LED_PIN),          //配置引脚
        .mode =GPIO_MODE_OUTPUT,                  //输出模式
        .pull_up_en = GPIO_PULLUP_DISABLE,        //不使能上拉
        .pull_down_en = GPIO_PULLDOWN_DISABLE,    //不使能下拉
        .intr_type = GPIO_INTR_DISABLE            //不使能引脚中断
};
 gpio_config(&led_config);
 
方法二
gpio_reset_pin(LED_PIN);                          //初始化LED_PIN引脚
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);    //配置引脚为输出模式

// 设置GPIO电平
esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level)
gpio_set_level(LED_PIN, 0);


// 获取GPIO电平
int gpio_get_level(gpio_num_t gpio_num);
if( gpio_get_level(KEY_PIN) == 0 )
{}

在这里插入图片描述

3.外部中断

//配置按键
gpio_config_t io_conf = {};
//设置为 下降沿中断
io_conf.intr_type = GPIO_INTR_NEGEDGE;
//设置 GPIO 引脚
io_conf.pin_bit_mask = ( 1 << KEY_PIN);
//设置为 输入模式
io_conf.mode = GPIO_MODE_INPUT;
//设置为 使能上拉电阻
io_conf.pull_up_en = 1;
//设置为 禁止下拉电阻
io_conf.pull_down_en = 0;
//将以上配置设置到引脚上
gpio_config(&io_conf);
//注册中断服务
gpio_install_isr_service(ESP_INTR_FLAG_EDGE);
//设置GPIO的中断服务函数
gpio_isr_handler_add(KEY_PIN, gpio_isr_handler, (void*)NULL);
//使能GPIO模块中断信号
gpio_intr_enable(KEY_PIN);

//中断服务函数
static void IRAM_ATTR gpio_isr_handler(void* arg)
{}

4.ADC

ADC 的基本参数通常包括以下几个:

  1. 分辨率:指 ADC 的数字输出位数,也称为量化位数。例如,12 位 ADC 具有 4096 个离散的数字输出。
  2. 采样速率:指 ADC 可以进行采样的最大速率。对于 ESP32-S3,最大采样速率为 2.5 MS/s。
  3. 输入范围:指 ADC 可以测量输入信号的电压范围。对于 ESP32-S3,输入范围为 0-3.3V。
  4. 噪声:指 ADC 在采集时分辨率的误差和干扰的影响。噪声越小,ADC 测量结果越精准。
  5. 稳定性:指 ADC 输出的稳定性。稳定性好的 ADC 输出变化小,测量结果更加准确。

#include <string.h>
#include <stdio.h>
#include "sdkconfig.h"
#include <esp_log.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_adc_cal.h"     // adc
#include "driver/adc.h"      // adc

#define DEFAULT_VREF    1100        //默认参考电压,单位mV

static esp_adc_cal_characteristics_t *adc_chars;

#define channel     ADC_CHANNEL_0               // ADC测量通道
#define width       ADC_WIDTH_BIT_12            // ADC分辨率
#define atten       ADC_ATTEN_DB_11             // ADC衰减
#define unit        ADC_UNIT_1                  // ADC1

void app_main(void)
{
    int read_raw_1=0, read_raw_2=0, read_raw_3=0;
    uint32_t voltage =0;
    float voltage_f = 0;
    adc1_config_width(width);// 12位分辨率
    
    //ADC_ATTEN_DB_0:表示参考电压为1.1V
    //ADC_ATTEN_DB_2_5:表示参考电压为1.5V
    //ADC_ATTEN_DB_6:表示参考电压为2.2V
    //ADC_ATTEN_DB_11:表示参考电压为3.3V
    //adc1_config_channel_atten( channel,atten);// 设置通道0和3.3V参考电压

    // 分配内存
    adc_chars = calloc(1, sizeof(esp_adc_cal_characteristics_t));
    // 对 ADC 特性进行初始化,使其能够正确地计算转换结果和补偿因素
    esp_adc_cal_characterize(unit, atten, width, DEFAULT_VREF, adc_chars);

    while(1)
    {
        //采集三个通道的ADC值
        read_raw_1 = adc1_get_raw(ADC1_CHANNEL_0);  //GPIO1
        read_raw_2 = adc1_get_raw(ADC1_CHANNEL_1);  //gpio2
        read_raw_3 = adc1_get_raw(ADC1_CHANNEL_2);  //gpio3
        //将ADC1的通道0(gpio1)的结果转换成电压,单位mV
        voltage = esp_adc_cal_raw_to_voltage(read_raw_1, adc_chars);

        //输出ADC值 与 ADC1通道0(GPIO1)实际电压值
        printf("read_raw_1 = %d\tread_raw_2 = %d\tread_raw_3 = %d\tvoltage: %f\n",read_raw_1,read_raw_2,read_raw_3,voltage/1000.0);
        
        vTaskDelay(100 / portTICK_PERIOD_MS);
    }
}

上面例子中,输出如下,电压 = read_raw_1 / 4095 * 3.3 = 1775/4095*3.3=1.43
在这里插入图片描述

5.定时器和PWM(LEDC)

5.1 定时器

ESP32S3芯片具有两个通用定时器组,每个定时器组包含两个通用定时器,例如 Timer0、Timer1 等,每个定时器都包含多个通道。可以通过指定定时器号和通道号来选择具体使用的定时器和通道
在这里插入图片描述

以下是一些基本概念和定时器的共有属性:

  • 计时器(Counter): 定时器的核心组件,负责持续计数。
  • 定时器溢出(Overflow): 当计数器达到其最大值然后归零时发生。
  • 预置值(Preset Value): 计数器达到该值时会产生中断或其它事件。
  • 分频器(Prescaler): 用于减小计数器接收的时钟信号频率,以延长定时器的最大计时范围。
  • 中断(Interrupt): 当定时器达到预置值时,可以配置它来产生一个中断,中断处理程序将执行一些任务。
// 这个例子启动了一个定时器,定时器定时1S,向队列里面放个数字,while队列有数字就拿

#include "driver/gptimer.h"			// 定时器
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "freertos/queue.h"
/**
 * @函数说明        定时器回调函数
 * @传入参数            
 * @函数返回        
 */
static bool IRAM_ATTR TimerCallback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data)
{
	// 这里的&high_task_awoken作为第三个参数传递给xQueueSendFromISR,该函数内
	// 部可能会根据具体情况将high_task_awoken设置为pdTRUE,如果它认为有必要进行
	// 任务切换的话。当ISR结束后,通过检查high_task_awoken的值并据此决定是否返回
	// pdTRUE,可以让FreeRTOS知道是否需要在退出ISR后立即执行一次上下文切换,以运
	// 行刚刚变为就绪状态的更高优先级的任务。
	// 这种机制保证了只有在确实有需要的情况下才会进行任务切换,
	// 有助于维持系统的高效性和响应性
    BaseType_t high_task_awoken = pdFALSE;
    //将传进来的队列保存
    QueueHandle_t queue = (QueueHandle_t)user_data;
    
    static int time = 0;
    time++;
    
    //从中断服务程序(ISR)中发送数据到队列
    xQueueSendFromISR(queue, &time, &high_task_awoken);
   
    return (high_task_awoken == pdTRUE);
}

/**
 * @函数说明        定时器初始化配置
 * @传入参数        resolution_hz=定时器的分辨率  alarm_count=触发警报事件的目标计数值
 * @函数返回        创建的定时器回调队列
 */
QueueHandle_t timerInitConfig(uint32_t resolution_hz, uint64_t alarm_count)
{
    //定义一个通用定时器
    gptimer_handle_t gptimer = NULL;

    //创建一个队列
    QueueHandle_t queue = xQueueCreate(10, sizeof(10));
    //如果创建不成功
    if (!queue) {
        ESP_LOGE("queue", "Creating queue failed");
        return NULL;
    }

    //配置定时器参数
    gptimer_config_t timer_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT, //定时器时钟来源 选择APB作为默认选项
        .direction = GPTIMER_COUNT_UP,      //向上计数
        //计数器分辨率(工作频率)以Hz为单位,因此,每个计数滴答的步长等于(1 / resolution_hz)秒
        //假设 resolution_hz = 1000 000
        //1 / resolution_hz = 1 / 1000000 = 0.000001(秒) = 1(微秒) ( 1 tick= 1us )
        .resolution_hz = resolution_hz, 
    };
    //将配置设置到定时器,这个是随机分配定时器
    gptimer_new_timer(&timer_config, &gptimer);
    
    //绑定一个回调函数
    gptimer_event_callbacks_t cbs = {
        .on_alarm = TimerCallback,
    };
    //设置定时器gptimer的 回调函数为cbs  传入的参数为NULL
    gptimer_register_event_callbacks(gptimer, &cbs, queue);

    //使能定时器
    gptimer_enable(gptimer);
    
    //通用定时器的报警值设置 
    gptimer_alarm_config_t alarm_config = {
        .reload_count = 0,  //重载计数值为0 
        .alarm_count = alarm_count, // 报警目标计数值 1000000 = 1s
        .flags.auto_reload_on_alarm = true, //开启重加载
    };
    //设置触发报警动作
    gptimer_set_alarm_action(gptimer, &alarm_config);
    //开始定时器开始工作
    gptimer_start(gptimer);

    return queue;
}
void app_main(void)
{
    int number = 0;
    QueueHandle_t queue = 0;

    // 初始化定时器 1秒进入回调函数一次
    queue = timerInitConfig(1000000,1000000);
    
    while(1)
    {
        //从队列中接收一个数据,不能在中断服务函数使用
        if (xQueueReceive(queue, &number, pdMS_TO_TICKS(2000))) 
        {
            ESP_LOGI(TAG, "Timer stopped, count=%d", number);
        } else {
            ESP_LOGW(TAG, "Missed one count event");
        }
    }
}

5.2 LEDC实现PWM

// 设置占空比
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, LEDC_DUTY);
// 更新通道占空比
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
// 宏定义相关例子下面代码中有
// 设置了事件标志组,渐变为最亮或者熄灭,设置事件标志组,检测到事件标志组,开启下一轮渐变
#include <string.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <driver/rmt_rx.h>
#include <driver/rmt_tx.h>
#include <soc/rmt_reg.h>
#include "driver/gpio.h" 
#include <esp_log.h>
#include <freertos/queue.h>
#include <freertos/event_groups.h>
#include "esp32/rom/ets_sys.h"
#include "driver/ledc.h"

//定义LED的GPIO口
#define LED_GPIO  GPIO_NUM_27

#define TAG     "LEDC"

#define LEDC_TIMER              LEDC_TIMER_0            //定时器0
#define LEDC_MODE               LEDC_LOW_SPEED_MODE     //低速模式
#define LEDC_OUTPUT_IO          (LED_GPIO)              //选择GPIO端口
#define LEDC_CHANNEL            LEDC_CHANNEL_0          //PWM通道
#define LEDC_DUTY_RES           LEDC_TIMER_13_BIT       //分辨率
#define LEDC_DUTY               (4095)                  //最大占空比值,这里是2^13-1
#define LEDC_FREQUENCY          (5000)                  //PWM周期

//用于通知渐变完成
static EventGroupHandle_t   s_ledc_ev = NULL;

//关灯完成事件标志
#define LEDC_OFF_EV  (1<<0)

//开灯完成事件标志
#define LEDC_ON_EV   (1<<1)

//渐变完成回调函数
bool IRAM_ATTR ledc_finish_cb(const ledc_cb_param_t *param, void *user_arg)
{
    BaseType_t xHigherPriorityTaskWoken;
    if(param->duty)
    {
        xEventGroupSetBitsFromISR(s_ledc_ev,LEDC_ON_EV,&xHigherPriorityTaskWoken);
    }
    else
    {
        xEventGroupSetBitsFromISR(s_ledc_ev,LEDC_OFF_EV,&xHigherPriorityTaskWoken);
    }
    return xHigherPriorityTaskWoken;
}

//ledc 渐变任务
void ledc_breath_task(void* param)
{
    EventBits_t ev;
    while(1)
    {
        ev = xEventGroupWaitBits(s_ledc_ev,LEDC_ON_EV|LEDC_OFF_EV,pdTRUE,pdFALSE,pdMS_TO_TICKS(5000));
        if(ev)
        {
            //设置LEDC开灯渐变
            if(ev & LEDC_OFF_EV)
            {
                ledc_set_fade_with_time(LEDC_MODE,LEDC_CHANNEL,LEDC_DUTY,2000);
                ledc_fade_start(LEDC_MODE,LEDC_CHANNEL,LEDC_FADE_NO_WAIT);
            }
            else if(ev & LEDC_ON_EV)    //设置LEDC关灯渐变
            {
                ledc_set_fade_with_time(LEDC_MODE,LEDC_CHANNEL,0,2000);
                ledc_fade_start(LEDC_MODE,LEDC_CHANNEL,LEDC_FADE_NO_WAIT);
            }
            //再次设置回调函数
            ledc_cbs_t cbs = {.fade_cb=ledc_finish_cb,};
            ledc_cb_register(LEDC_MODE,LEDC_CHANNEL,&cbs,NULL);
        }
    }
}

//LED呼吸灯初始化
void led_breath_init(void)
{
    //初始化一个定时器
    ledc_timer_config_t ledc_timer = {
        .speed_mode       = LEDC_MODE,      //低速模式
        .timer_num        = LEDC_TIMER,     //定时器ID
        .duty_resolution  = LEDC_DUTY_RES,  //占空比分辨率,这里是13位,2^13-1
        .freq_hz          = LEDC_FREQUENCY,  // PWM频率,这里是5KHZ
        .clk_cfg          = LEDC_AUTO_CLK    // 时钟
    };
    ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

    //ledc通道初始化
    ledc_channel_config_t ledc_channel = {
        .speed_mode     = LEDC_MODE,        //低速模式
        .channel        = LEDC_CHANNEL,     //PWM 通道0-7
        .timer_sel      = LEDC_TIMER,       //关联定时器,也就是上面初始化好的那个定时器
        .intr_type      = LEDC_INTR_DISABLE,//不使能中断
        .gpio_num       = LEDC_OUTPUT_IO,   //设置输出PWM方波的GPIO管脚
        .duty           = 0, // 设置默认占空比为0
        .hpoint         = 0
    };
    ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));

    //开启硬件PWM
    ledc_fade_func_install(0);

    //创建一个事件组,用于通知任务渐变完成
    s_ledc_ev = xEventGroupCreate();

    //配置LEDC渐变
    ledc_set_fade_with_time(LEDC_MODE,LEDC_CHANNEL,LEDC_DUTY,2000);

    //启动渐变
    ledc_fade_start(LEDC_MODE,LEDC_CHANNEL,LEDC_FADE_NO_WAIT);

    //设置渐变完成回调函数
    ledc_cbs_t cbs = {.fade_cb=ledc_finish_cb,};
    ledc_cb_register(LEDC_MODE,LEDC_CHANNEL,&cbs,NULL);

    xTaskCreatePinnedToCore(ledc_breath_task,"ledc",2048,NULL,3,NULL,1);
}


// 主函数
void app_main(void)
{
    led_breath_init();      //呼吸灯
}

6.SDIO读写SD卡

在这里插入图片描述


#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "driver/sdmmc_host.h"

#define EXAMPLE_MAX_CHAR_SIZE    64

static const char *TAG = "example";

#define MOUNT_POINT "/sdcard"   //挂载点名称

// 写文件
static esp_err_t s_example_write_file(const char *path, char *data)
{
    ESP_LOGI(TAG, "Opening file %s", path);
    FILE *f = fopen(path, "w");
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for writing");
        return ESP_FAIL;
    }
    fprintf(f, data);
    fclose(f);
    ESP_LOGI(TAG, "File written");

    return ESP_OK;
}
// 读文件
static esp_err_t s_example_read_file(const char *path)
{
    ESP_LOGI(TAG, "Reading file %s", path);
    FILE *f = fopen(path, "r");
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for reading");
        return ESP_FAIL;
    }
    char line[EXAMPLE_MAX_CHAR_SIZE];
    fgets(line, sizeof(line), f);
    fclose(f);

    // strip newline
    char *pos = strchr(line, '\n');
    if (pos) {
        *pos = '\0';
    }
    ESP_LOGI(TAG, "Read from file: '%s'", line);

    return ESP_OK;
}
void app_main(void)
{
    esp_err_t ret;
    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;
    const char mount_point[] = MOUNT_POINT;
    ESP_LOGI(TAG, "Initializing SD card");

    ESP_LOGI(TAG, "Using SDMMC peripheral");

    //默认配置,速度20MHz,使用卡槽1
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();

    //默认的IO管脚配置,
    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();

    //4位数据
    slot_config.width = 4;

    //不适用通过IO矩阵进行映射的管脚,只使用默认支持的SDMMC管脚,可以获得最大性能
    #if 0
    slot_config.clk = CONFIG_EXAMPLE_PIN_CLK;
    slot_config.cmd = CONFIG_EXAMPLE_PIN_CMD;
    slot_config.d0 = CONFIG_EXAMPLE_PIN_D0;
    slot_config.d1 = CONFIG_EXAMPLE_PIN_D1;
    slot_config.d2 = CONFIG_EXAMPLE_PIN_D2;
    slot_config.d3 = CONFIG_EXAMPLE_PIN_D3;
    #endif
    // Enable internal pullups on enabled pins. The internal pullups
    // are insufficient however, please make sure 10k external pullups are
    // connected on the bus. This is for debug / example purpose only.
    //管脚启用内部上拉
    slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;

    ESP_LOGI(TAG, "Mounting filesystem");
    ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card);

    if (ret != ESP_OK) {
        if (ret == ESP_FAIL) {
            ESP_LOGE(TAG, "Failed to mount filesystem. ");
        } 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;
    }
    ESP_LOGI(TAG, "Filesystem mounted");

    // Card has been initialized, print its properties
    sdmmc_card_print_info(stdout, card);

    // Use POSIX and C standard library functions to work with files:

    // First create a file.
    const char *file_hello = MOUNT_POINT"/hello.txt";
    char data[EXAMPLE_MAX_CHAR_SIZE];
    snprintf(data, EXAMPLE_MAX_CHAR_SIZE, "%s %s!\n", "Hello", card->cid.name);
    ret = s_example_write_file(file_hello, data);
    if (ret != ESP_OK) {
        return;
    }

    const char *file_foo = MOUNT_POINT"/foo.txt";
    // Check if destination file exists before renaming
    struct stat st;
    if (stat(file_foo, &st) == 0) {
        // Delete it if it exists
        unlink(file_foo);
    }

    // Rename original file
    ESP_LOGI(TAG, "Renaming file %s to %s", file_hello, file_foo);
    if (rename(file_hello, file_foo) != 0) {
        ESP_LOGE(TAG, "Rename failed");
        return;
    }

    ret = s_example_read_file(file_foo);
    if (ret != ESP_OK) {
        return;
    }

    // Format FATFS
    #if 0
    ret = esp_vfs_fat_sdcard_format(mount_point, card);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to format FATFS (%s)", esp_err_to_name(ret));
        return;
    }

    if (stat(file_foo, &st) == 0) {
        ESP_LOGI(TAG, "file still exists");
        return;
    } else {
        ESP_LOGI(TAG, "file doesnt exist, format done");
    }
    #endif
    const char *file_nihao = MOUNT_POINT"/nihao.txt";
    memset(data, 0, EXAMPLE_MAX_CHAR_SIZE);
    snprintf(data, EXAMPLE_MAX_CHAR_SIZE, "%s %s!\n", "Nihao", card->cid.name);
    ret = s_example_write_file(file_nihao, data);
    if (ret != ESP_OK) {
        return;
    }

    //Open file for reading
    ret = s_example_read_file(file_nihao);
    if (ret != ESP_OK) {
        return;
    }

    // All done, unmount partition and disable SDMMC peripheral
    esp_vfs_fat_sdcard_unmount(mount_point, card);
    ESP_LOGI(TAG, "Card unmounted");
}

8.串口

// 串口2自己发,自己读
#include "driver/uart.h"
#include "driver/gpio.h"
#include <string.h>

/**
 * @函数说明        uart_init_config
 * @传入参数        uart_port = 串口号 
 *                      可选参数:UART_NUM_0、UART_NUM_1、UART_NUM_2
 *                 baud_rate = 串口波特率
 *                 tx_pin = 发送引脚
 *                 rx_pin = 接收引脚     
 * @函数返回        无
 */
void uart_init_config(uart_port_t uart_port, uint32_t baud_rate, int tx_pin, int rx_pin)
{

    //定义 串口配置结构体,必须赋初值,否则无法实现
    uart_config_t uart_config={0};

    uart_config.baud_rate = baud_rate;                  //配置波特率
    uart_config.data_bits = UART_DATA_8_BITS;           //配置数据位为8位
    uart_config.parity = UART_PARITY_DISABLE;           //配置校验位为不需要校验
    uart_config.stop_bits = UART_STOP_BITS_1;           //配置停止位为 一位
    uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;   //禁用硬件流控制

    //将以上参数加载到串口1的寄存器
    uart_param_config(uart_port, &uart_config);

    //绑定引脚  TX=tx_pin RX=rx_pin RTS=不使用 CTS=不使用
    uart_set_pin(uart_port, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    //安装 串口 驱动程序
    uart_driver_install(uart_port, 200, 200, 0, NULL, 0);
}

/**
 * @函数说明        串口2接收任务
 * @传入参数        无
 * @函数返回        无
 * @函数说明        请通过xTaskCreate创建该任务
 */
void uart2_rx_task(void)
{
    uint8_t rx_data[200]={0};
    uint8_t temp[50]={0};
    while(1)
    {
        //接收串口数据收到的数据长度
        int rx_bytes = uart_read_bytes(UART_NUM_2, rx_data, 200, 10 / portTICK_PERIOD_MS);
        if( rx_bytes > 0 )//数据长度大于0,说明接收到数据
        {
            rx_data[rx_bytes] = 0;//将串口数据的最后一个设置为0,形成字符串

            //输出接收数据的长度
            sprintf((const char*)temp,"uart2 string length : %d\r\n", rx_bytes);
            uart_write_bytes(UART_NUM_2, (const char*)temp, strlen((const char*)temp));
            //通过串口2输出接受到的数据
            uart_write_bytes(UART_NUM_2, (const char*)"uart2 received : ", strlen("uart2 received : "));
            uart_write_bytes(UART_NUM_2, (const char*)rx_data, strlen((const char*)rx_data));
            //UART环缓冲区刷新。丢弃UART RX缓冲区中的所有数据,准备下次接收
            uart_flush(UART_NUM_2);
        }
    }
}


void app_main(void)
{
    //初始化串口2 TX=GPIO10 RX=GPIO9
    uart_init_config(UART_NUM_2, 115200, 10, 9);

    //创建串口2接收任务
    xTaskCreate(uart2_rx_task, "uart2_rx_task", 1024*2, NULL, configMAX_PRIORITIES, NULL);

    //通过串口2发送字符串 start uart demo
    uart_write_bytes(UART_NUM_2, (const char*)"start uart demo", strlen("start uart demo"));

    while(1)
    {
        //串口2发送数据
        uart_write_bytes(UART_NUM_2, (const char*)"Task running : main", strlen("Task running : main"));    
        //ESP32S3的日志输出数据
        ESP_LOGI("main", "Task running : main\r\n");
        vTaskDelay(1000 / portTICK_PERIOD_MS);       
    }
}

9.SPI

ESP32-S3芯片集成了四个 SPI 控制器:
• SPI0
• SPI1
• 通用 SPI2,即 GP-SPI2
• 和通用 SPI3,即 GP-SPI3
SPI0 和 SPI1 控制器主要供内部使用以访问外部 flash 及 PSRAM。我们只能使用SPI2和SPI3。

/**********************************************************
 * 函 数 名 称:w25q64_init_config
 * 函 数 功 能:w25q64初始化
 * 传 入 参 数:无
 * 函 数 返 回:无
 * 备       注:无
**********************************************************/
esp_err_t w25q64_init_config(spi_device_handle_t* handle)
{   
    //00 定义错误标志
    esp_err_t e;

    //01 配置总线初始化结构体
    static spi_bus_config_t bus_cfg;    //总线配置结构体

    bus_cfg.miso_io_num     = Flash_SPI_MISO;                //miso
    bus_cfg.mosi_io_num     = Flash_SPI_MOSI;                //mosi
    bus_cfg.sclk_io_num     = Flash_SPI_SCLK;                //sclk
    bus_cfg.quadhd_io_num   = Flash_SPI_HD;                 // HD
    bus_cfg.quadwp_io_num   = Flash_SPI_WP;                 // WP
    bus_cfg.max_transfer_sz = 4092;                         //非DMA最大64bytes,DMA最大4092bytes
    //bus_cfg.intr_flags = 0;                               //这个用于设置中断优先级的,0是默认
    bus_cfg.flags = SPICOMMON_BUSFLAG_MASTER;
    //这个用于设置初始化的时候要检测哪些选项。比如这里设置的是spi初始化为主机模式是否成功。检测结果通过spi_bus_initialize函数的
    //返回值进行返回。如果初始化为主机模式成功,就会返回esp_ok

    //02 初始化总线配置结构体
    e = spi_bus_initialize(Flash_SPI, &bus_cfg, SPI_DMA_CH_AUTO);

    if (e != ESP_OK)
    {
        printf("bus initialize failed!\n");
        return e;
    }

    //03 配置设备结构体
    static spi_device_interface_config_t interface_cfg; //设备配置结构体

   interface_cfg.address_bits = Flash_Address_Bits;   //配置地址位长度
   //(1)如果设置为0,在通讯的时候就不会发送地址位。
   //(2)如果设置了非零值,就会在spi通讯的地址发送阶段发送指定长度的address数据。
   //如果设置了非零值并且在后面数据发送结构体中没有定义addr的值,会默认发送指定长度0值
   //(3)我们后面发送数据会使用到spi_transaction_t结构体,这个结构体会使用spi_device_interface_config_t中定义好address、command和dummy的长度
   //如果想使用非固定长度,就要使用spi_transaction_ext_t结构体了。这个结构体包括了四个部分,包含了一个spi_transaction_t和address、command、dummy的长度。
   //我们要做的就是在spi_transaction_ext_t.base.flags中设置SPI_TRANS_VARIABLE_ADDR/CMD/DUMMY
   //然后定义好这三部分数据的长度,然后用spi_transaction_ext_t.base的指针代替spi_transaction_t的指针即可

    interface_cfg.command_bits = Flash_Command_Bits;  //配置命令位长度
    //与address_bits是一样的

    interface_cfg.dummy_bits = Flash_Dummy_Bits;  //配置dummy长度
    //这里的配置方法与address_bits是一样的。但是要着重说一下这个配置的意义,后面会再说一遍
    //(1)dummy_bits是用来用来补偿输入延迟。
    //(2)在read phase开始阶段之前被插入进去。在dummy_bits的时钟下,并不进行数据读取的工作
    //相当于这段时间发送的clock都是虚拟的时钟,并没有功能。在输入延迟最大允许时间不够的时候,可以通过这种方法进行配置,从而
    //能够使得系统工作在更高的时钟频率下。
    //(3)如果主机设备只进行write操作,可以在flags中设置SPI_DEVICE_NO_DUMMY,关闭dummy bits的发送。只有写操作的话,即使使用了gpio交换矩阵,时钟周期也可以工作在80MHZ

    //interface_cfg.input_delay_ns = 0;  //配置输入延时的允许范围
    //时钟发出信号到miso进行输入直接会有延迟,这个参数就是配置这个允许的最大延迟时间。
    //如果主机接收到从机时钟,但是超过这个时间没有收到miso发来的输入信号,就会返回通讯失败。
    //这个时间即使设置为0,也能正常工作,但是最好通过手册或逻辑分析仪进行估算。能够实现更好的通讯。
    //超过8M的通讯都应该认真设置这个数字

    interface_cfg.clock_speed_hz = Flash_CLK_SPEED;  //配置时钟频率
    //配置通讯的时钟频率。
    //这个频率受到io_mux和input_delay_ns限制。
    //如果是io直连的,时钟上限是80MHZ,如果是gpio交换矩阵连接进来的,时钟上限是40MHZ。
    //如果是全双工,时钟上限是26MHZ。并且还要考虑输入延时。在相同输入延时的条件下,使用gpio交换矩阵会比使用io mux最大允许的时钟频率小。可以通过
    //spi_get_freq_limit()来计算能够允许的最大时钟频率是多少
    //有关SPI通讯时钟极限和配置的问题,后面会详细说一下。

    interface_cfg.mode = 0; //设置SPI通讯的相位特性和采样边沿。包括了mode0-3四种。要看从设备能够使用哪种模式

    interface_cfg.spics_io_num = Flash_SPI_CS; //配置片选线

    interface_cfg.duty_cycle_pos = 0;  //配置占空比
    //设置时钟的占空比,比例是 pos*1/256,默认为0,也就是50%占空比

    //interface_cfg.cs_ena_pretrans; //在传输之前,片选线应该保持激活状态多少个时钟,只有全双工的时候才需要配置
    //interface_cfg.cs_ena_posttrans; //在传输之后,片选线应该保持激活状态多少个时钟,只有全双工的时候才需要配置

    interface_cfg.queue_size = 6; //传输队列的长度,表示可以在通讯的时候挂起多少个spi通讯。在中断通讯模式的时候会把当前spi通讯进程挂起到队列中

    //interface_cfg.flags; //配置与从机有关的一些参数,比如MSB还是LSB,使不使用三线SPI

    //interface_cfg.pre_cb; 
    //配置通讯前中断。比如不在这里配置cs片选线,把片选线作为自行控制的线,把片选线拉低放在通讯前中断中

    //interface_cfg.post_cb;
    //配置通讯后中断。比如不在这里配置cs片选线,把片选线作为自行控制的线,把片选线拉高放在通讯前中断中

    //04 设备初始化
    e = spi_bus_add_device(Flash_SPI, &interface_cfg, handle);
    if (e != ESP_OK)
    {
        printf("device config error\n");
        return e;
    }
    return ESP_OK;
}

10.IIC

#include "driver/gpio.h"
#include "driver/i2c.h"

#define ACK_CHECK_EN          0x1      /*!< I2C 主机将【理会】  应答*/
#define ACK_CHECK_DIS         0x0      /*!< I2C 主机将【不理会】应答 */

#define I2C_MASTER_TX_BUF_DISABLE   0   /*!< I2C 主机不需要发送缓存区 */
#define I2C_MASTER_RX_BUF_DISABLE   0   /*!< I2C 主机不需要接收缓存区 */
/******************************************************************
 * 函 数 名 称:DS1307_GPIO_Init
 * 函 数 说 明:对IIC引脚初始化
 * 函 数 形 参:无
 * 函 数 返 回:无
 * 作       者:LC
 * 备       注:无
******************************************************************/
void DS1307_GPIO_Init(void)
{                                
    int i2c_master_port = I2C_NUM_1;//I2C_NUMBER(CONFIG_I2C_SLAVE_PORT_NUM);
    i2c_config_t conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = GPIO_SDA,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_io_num = GPIO_SCL,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = 100000,
        // .clk_flags = 0,          /*!< Optional, you can use I2C_SCLK_SRC_FLAG_* flags to choose i2c source clock here. */
    };
    esp_err_t err = i2c_param_config(i2c_master_port, &conf);

    //注册I2C服务即使能
    i2c_driver_install(I2C_NUM_1, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0); 
}

/******************************************************************
 * 函 数 名 称:Write1307
 * 函 数 说 明:向DS1307的add地址写入dat数据
 * 函 数 形 参:add写入寄存器地址    dat写入数据
 * 函 数 返 回:0写入成功    1写入器件地址无应答      2写入寄存器地址无应答
 * 作       者:LC
 * 备       注:器件地址=0xD0   
******************************************************************/
unsigned char Write1307(unsigned char add,unsigned char dat)
{
    unsigned char temp;
        
    /* 10进制转BCD码 */
    temp=dat/10; 
    temp<<=4;
    temp=dat%10+temp;
        
    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
    int err=0;
    i2c_master_start(cmd);

    err = i2c_master_write_byte(cmd, 0xD0, ACK_CHECK_EN);     

    i2c_master_write_byte(cmd, add, ACK_CHECK_EN);

    i2c_master_write_byte(cmd, temp, ACK_CHECK_EN);    
     
    i2c_master_stop(cmd);
    i2c_master_cmd_begin(I2C_NUM_1, cmd, 1000 / portTICK_PERIOD_MS);
    i2c_cmd_link_delete(cmd);  

    return (0);
}

/******************************************************************
 * 函 数 名 称:Read1307
 * 函 数 说 明:读取DS1307的时间数据
 * 函 数 形 参:add读取的寄存器地址
 * 函 数 返 回:255-读取失败  其他-读取成功
 * 作       者:LC
 * 备       注:无
******************************************************************/
unsigned char Read1307(unsigned char add)
{
    int i =0;
    unsigned char temp;
    unsigned char dat;

    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
    i2c_master_start(cmd);
    i2c_master_write_byte(cmd, 0xD0, ACK_CHECK_EN);
    i2c_master_write_byte(cmd, add, ACK_CHECK_EN);
 
    i2c_master_start(cmd);
    i2c_master_write_byte(cmd, 0xD1, ACK_CHECK_EN);

    i2c_master_read_byte(cmd, &dat, ACK_CHECK_EN);

    i2c_master_stop(cmd);
    esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_1, cmd, 1000 / portTICK_PERIOD_MS);
    i2c_cmd_link_delete(cmd);

    /* BCD码转19进制 */
    temp=dat/16;
    dat=dat%16;
    dat=dat+temp*10;

    return(dat);
}

11.LVGL

11.1 组件管理方式

一、下载,有以下两种方式:
1.自行下载
工程根目录,添加components文件夹,然后clone

git clone --recursive https://github.com/lvgl/lvgl

在根目录的CMakeLists.txt中,添加

set(EXTRA_COMPONENT_DIRS ./components)

2.乐鑫组件管理器idf.py下载,从乐鑫的组件管理服务器上面下载组件并加入到工程中

idf.py add-dependency "lvgl/lvgl^8.3.10"

二、组件目录下添加CMakeLists.txt
这里以添加cst816t和st7789目录为例
在这里插入图片描述
在组件目录下放一个CMakeLists.txt文件,内容如下

idf_component_register(
    SRCS "st7789_driver.c" 
    "cst816t_driver.c" 
    "led.c"
    "dht11.c"
    "led_ws2812.c"
    INCLUDE_DIRS "."
    REQUIRES esp_lcd nvs_flash # 表示依赖esp的这两个组件
)

11.2 移植LVGL

(1)下载源代码
工程根目录,添加components文件夹,在这个文件夹内,clone LVGL 的仓库,并且切换到想要的版本

git clone --recursive https://github.com/lvgl/lvgl
cd lvgl
git checkout v8.3.10

(2)添加组件
在根目录的CMakeLists.txt中,添加

set(EXTRA_COMPONENT_DIRS ./components)

(3)menuconfig配置LVGL

idf.py menuconfig
Component config -> lvgl configuration
# 配置颜色格式
color settings  -> swap the 2bytes of 
# 使用malloc/free代替LVGL的alloc,malloc会默认有psram优先使用psram
memory settings ->  if true use custom malloc/free,oterwise use the built-in 'lv_mem_alloc()'and'lv_mem_free()'
# 启用测试demo
Demo -> show some widget
# 设置字体
Component config -> LVGL configuration -> Font usage -> Enable built-in fonts

(4)初始化LVGL

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "lvgl.h"
#include "esp_log.h"
#include "st7789_driver.h"
#include "cst816t_driver.h"
#include "esp_timer.h"
#define TAG "lv_port"

/*
    1.初始化和注册LVGL显示驱动
    2.初始化和注册LVGL触摸驱动
    3.初始化ST7789硬件接口
    4.初始化CST816T硬件接口
    5.提供一个定时器给LVGL使用
*/

#define LCD_WIDTH 240
#define LCD_HEIGHT 280

static lv_disp_drv_t disp_drv;  // 定义一个全局显示驱动变量

void lv_flush_done_cb(void *param)
{
    lv_disp_flush_ready(&disp_drv);
}


void disp_flush(struct _lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    st7789_flush(area->x1,area->x2+1,area->y1+20,area->y2+1+20,color_p);
}

void IRAM_ATTR indev_read(struct _lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
    int16_t x,y;
    int state;
    // cst816t_read(data->point.x,data->point.y,&data->state);
    cst816t_read(&x,&y,&state);
    data->point.x = x;
    data->point.y = y;
    data->state = state;
}

void lv_disp_init(void)
{
    static lv_disp_draw_buf_t disp_buf; 
    const size_t disp_buf_size = LCD_WIDTH * (LCD_HEIGHT / 7);
    lv_color_t *disp1 = heap_caps_malloc(disp_buf_size * sizeof(lv_color_t),MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); // 内存大小,内存类型
    lv_color_t *disp2 = heap_caps_malloc(disp_buf_size * sizeof(lv_color_t),MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); // 内存大小,内存类型
    if(!disp1 || !disp2)
    {
        ESP_LOGE(TAG,"disp buff malloc fail!");
        return;
    }
    lv_disp_draw_buf_init(&disp_buf,disp1,disp2,disp_buf_size);

    lv_disp_drv_init(&disp_drv);

    disp_drv.hor_res = LCD_WIDTH;
    disp_drv.ver_res = LCD_HEIGHT;
    disp_drv.draw_buf = &disp_buf;
    disp_drv.flush_cb = disp_flush; // 写入数据到显示芯片

    lv_disp_drv_register(&disp_drv);
}

void lv_indev_init(void)
{
    static lv_indev_drv_t indev_drv;
    lv_indev_drv_init(&indev_drv);
    indev_drv.type = LV_INDEV_TYPE_POINTER;
    indev_drv.read_cb = indev_read;

    lv_indev_drv_register(&indev_drv);

}

void st7789_hw_init(void)
{
    st7789_cfg_t st7789_config;
    st7789_config.bl = GPIO_NUM_26;
    st7789_config.clk = GPIO_NUM_18;
    st7789_config.cs = GPIO_NUM_5;
    st7789_config.dc = GPIO_NUM_17;
    st7789_config.mosi = GPIO_NUM_19;
    st7789_config.rst = GPIO_NUM_21;
    st7789_config.spi_fre = 40*1000*1000; // 40MHZ
    st7789_config.height = LCD_HEIGHT;
    st7789_config.width = LCD_WIDTH;
    st7789_config.spin = 0;
    st7789_config.done_cb = lv_flush_done_cb;  // lvgl写入完成之后,不收到写入完成通知,是不会再次写入的
    st7789_config.cb_param = &disp_drv;

    st7789_driver_hw_init(&st7789_config);
}

void cst816t_hw_init(void)
{
    cst816t_cfg_t cst816t_config;
    cst816t_config.scl = GPIO_NUM_22;
    cst816t_config.sda = GPIO_NUM_23;
    cst816t_config.fre = 300*1000; // 300K
    cst816t_config.x_limit = LCD_WIDTH;
    cst816t_config.y_limit = LCD_HEIGHT;
    cst816t_init(&cst816t_config);
}

void lv_timer_cb(void *arg)
{
    uint32_t tick_interval = *((uint32_t*)arg);
    lv_tick_inc(tick_interval);
}

void lv_tick_init(void)
{
    static uint32_t tick_interval = 5;
    const esp_timer_create_args_t timer_create_arg = {
        .arg = &tick_interval,
        .callback = lv_timer_cb,
        .name = "",
        .dispatch_method = ESP_TIMER_TASK,
        .skip_unhandled_events = true     // 被阻塞的回调函数,是否跳过执
    };

    esp_timer_handle_t timer_handle;
    esp_timer_create(&timer_create_arg,&timer_handle);
    esp_timer_start_periodic(timer_handle,tick_interval*1000);

}

void lv_port_init(void)
{
    lv_init();  // lvgl库的初始化
    st7789_hw_init();
    cst816t_hw_init();
    lv_disp_init();
    lv_indev_init();
    lv_tick_init();  
}

12.分区表

12.1 分区表介绍

*********************************************
# ESP-IDF Partition Table
# Name,   Type,  SubType, Offset, Size, Flags
  nvs,    data,  nvs,     0x9000, 24K,
phy_init, data,  phy,     0xf000, 4K,
factory,  app,   factory, 0x10000,1M,
*********************************************
类型 分区属性 值类型
Name 分区名称 不重要
Type 类型 app、data、0x40-0xFE(自定义)
SubType 子类型 如果Type == app,可选factory、ota_0 - ota_15
如果Type == data,可选ota、phy、nvs等
Offset 偏移地址 分区在Flash中的起始地址
Size 分区大小 分区占用的空间
Flags 标志 可选encrypted和readonly

NVS: 非易失性存储器,用于存储每台设备的PHY校准数据,WIFI数据,和一些用户自定义的应用程序数据。存储键值对,命名空间隔离,不同的命名空间可以有相同的键值,默认最长15个字节
phy: 存储PHY初始化数据。

注意NVS存储的是校准数据,phy存储的是初始化数据

factory: 默认的app分区,上电后从这里启动,如果存在subtype=oat的分区,bootloader会检查ota分区里面的内容,再决定用哪个app分区里面的程序启动。

注意:app 分 区 的 偏 移 地 址 必 须 与 0x10000 (64 KB) 对 齐

ota_0-ota_15: 自定义

12.2 分区表实验

分区表可以在menuconfig中更改,路径如下
类型选择Partition Table ->Partition Table (Custom partition table CSV)
在这里插入图片描述
上面的文件类型中,第一个对应的是partitions_singleapp.csv,第二个队友的是partitions_singleapp.csvAPP分区更大,第三个对应的是partitions_two_ota.csv第四个是自定义分区,如果选择了第四个,需要修改文件名称(partitions_user.csv) Custom partition CSV file
在ESP-IDF中,分区表在以下路径
/esp-idf/components/partition_table,默认使用的是partitions_singleapp.csv

下面,新建一个分区表,做个实验
(1)新建工程,添加分区表文件,文件名partitions_user.csv,里面自定义了分区,分区名称,类型,子类型全部自定义

# Name,   Type, SubType, Offset,  Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs,      data, nvs,     ,        0x6000,
phy_init, data, phy,     ,        0x1000,
factory,  app,  factory, ,        1M,
user1,    0x40,0x01,     ,        0x1000,

(2)分区内写数据,再读数据

#include <stdio.h>
#include "esp_partition.h"
#include "esp_log.h"
#include <string.h>

static const char *TAG = "partition";

#define USER_PARTITION_TYPE 0x40
#define USER_PARTITION_SUBTYPE 0x01

static const esp_partition_t *partition_ptr = NULL;    //用户分区指针


void app_main(void)
{
    ESP_LOGI(TAG,"partition start");
    partition_ptr = esp_partition_find_first(USER_PARTITION_TYPE,USER_PARTITION_SUBTYPE,NULL);
    if(partition_ptr == NULL)
    {
        ESP_LOGE(TAG,"partition not found");
        return;
    }
    esp_partition_erase_range(partition_ptr,0,0x1000);       // 擦除最小4KB
    const char *data = "hello world";
    esp_partition_write(partition_ptr,0,data,strlen(data));
    char *read_data = (char *)malloc(strlen(data));
    esp_partition_read(partition_ptr,0,read_data,strlen(data));
    ESP_LOGI(TAG,"read data: %s",read_data);
    free(read_data);
}

13.NVS

13.1 NVS介绍

NVS 即 Non-volatile storage, 意思是非易失存储, 也就是掉电后能依然能持久化保存数据。 在我们应用 NVS 时, 一般用于存储一些配置数据、 状态数据等, 一般不会用来存储存放大量的数据量。
NVS存储类型: NVS存储的是键值对
NVS存储特点: 命名空间隔离,不同的命名空间可以有相同的键值,默认最长15个字节

13.2 NVS实验

#include <stdio.h>
#include "nvs_flash.h"
#include "esp_err.h"
#include "esp_log.h"
#include "string.h"

#define NAME_SPACE_WIFI1 "wifi1"
#define NAME_SPACE_WIFI2 "wifi2"

#define NVS_WIFI_SSID "ssid"
#define NVS_WIFI_PASS "password"


void nvs_blob_read(const char *name_space,const char *key,void *value,size_t maxlen)
{
    nvs_handle_t nvs_handle;
    size_t len = 0;
    nvs_open(name_space,NVS_READONLY,&nvs_handle);
    nvs_get_blob(nvs_handle,key,NULL,&len);
    if(len > 0 && len < maxlen)
    {
        nvs_get_blob(nvs_handle,key,value,&len);
    }
    nvs_close(nvs_handle);
}


void app_main(void)
{
    nvs_handle_t nvs_handle1;
    nvs_handle_t nvs_handle2;

    esp_err_t err = nvs_flash_init();
    if(err != ESP_OK)
    {
        nvs_flash_erase();
        ESP_ERROR_CHECK(nvs_flash_init());
    }
    /************************写入命名空间1************************/
    nvs_open(NAME_SPACE_WIFI1,NVS_READWRITE,&nvs_handle1);
    nvs_set_blob(nvs_handle1,NVS_WIFI_SSID,"wifi1_esp32",strlen("wifi1_esp32"));
    nvs_set_blob(nvs_handle1,NVS_WIFI_PASS,"1234567890",strlen("1234567890"));
    nvs_commit(nvs_handle1);  // 立即写入,而不是等到nvs_close时写入
    nvs_close(nvs_handle1);

    /************************写入命名空间2************************/
    nvs_open(NAME_SPACE_WIFI2,NVS_READWRITE,&nvs_handle2);

    nvs_set_blob(nvs_handle2,NVS_WIFI_SSID,"wifi2_esp32",strlen("wifi2_esp32"));
    nvs_set_blob(nvs_handle2,NVS_WIFI_PASS,"0987654321",strlen("0987654321"));
    nvs_commit(nvs_handle2);  // 立即写入,而不是等到nvs_close时写入
    nvs_close(nvs_handle2);

    /************************读取命名空间************************/
    char buf[100] = {0};
    nvs_blob_read(NAME_SPACE_WIFI1,NVS_WIFI_SSID,buf,sizeof(buf));
    ESP_LOGI("NAME_SPACE_WIFI1","ssid: %s",buf);
    nvs_blob_read(NAME_SPACE_WIFI1,NVS_WIFI_PASS,buf,sizeof(buf));
    ESP_LOGI("NAME_SPACE_WIFI1","password: %s",buf);

    nvs_blob_read(NAME_SPACE_WIFI2,NVS_WIFI_SSID,buf,sizeof(buf));
    ESP_LOGI("NAME_SPACE_WIFI2","ssid: %s",buf);
    nvs_blob_read(NAME_SPACE_WIFI2,NVS_WIFI_PASS,buf,sizeof(buf));
    ESP_LOGI("NAME_SPACE_WIFI2","password: %s",buf);
}

14.SPIFFS文件系统

14.1 SPIFFS介绍

区别于FATFS,SPIFFS是专为NOR FLASH设计的,磨损均衡来延长闪存的使用寿命,更轻量级,代码体积小,运行时占用的内存也较少。

SPIFFS 尚不支持目录, 但可以生成扁平结构,例如" /spiffs/tmp/myfile.txt",是文件名直接是这个,而不是"/spiffs/tmp/"目录下的"myfile.txt"文件。

虚拟文件系统VFS组件为驱动程序提供一个统一接口,可以操作类文件对象,这类驱动程序可以是FAT,SPIFFS等真实文件系统,也可以是提供文件类接口的设备驱动程序。这样就可以使用标准C语言接口来操作了。

14.2 SPIFFS实验

(1)创建分区表,并且更改menuconfig为自定义分区表

# Name,   Type, SubType, Offset,  Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs,      data, nvs,     ,        0x6000,
phy_init, data, phy,     ,        0x1000,
factory,  app,  factory, ,        1M,
storage  ,data,spiffs ,  ,        512K,

(2)编写代码

#include <stdio.h>
#include "esp_spiffs.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>

void app_main(void)
{
    // 配置SPIFFS文件系统
    esp_vfs_spiffs_conf_t conf = {
        .base_path = "/spiffs",
        .partition_label = "storage",
        .max_files = 5,
        .format_if_mount_failed = true
    };

    // 注册 SPIFFS 文件系统
    esp_err_t ret = esp_vfs_spiffs_register(&conf);
    if (ret != ESP_OK) {
        ESP_LOGE("SPIFFS", "Failed to mount SPIFFS");
        return;
    }

    // 检查SPIFFS文件系统是否正常
    ret = esp_spiffs_check(conf.partition_label);
    if(ret != ESP_OK)
    {
        ESP_LOGE("SPIFFS", "Failed to check SPIFFS");
        return;
    }

    // 获取SPIFFS文件系统信息,包括总大小和已使用大小
    size_t total_size, used_size;
    ret = esp_spiffs_info(conf.partition_label, &total_size, &used_size);
    if(ret != ESP_OK)
    {
        ESP_LOGE("SPIFFS", "Failed to get SPIFFS info");
        return;
    }
    ESP_LOGI("SPIFFS", "Total size: %d, Used size: %d", total_size, used_size);

    // 如果已使用大小大于总大小,则进行检查
    if(used_size > total_size)
    {
        ret = esp_spiffs_check(conf.partition_label);
        if(ret != ESP_OK)
        {
            ESP_LOGE("SPIFFS", "Failed to check SPIFFS");
            return;
        }
    }

    // 创建文件
    FILE *f = fopen("/spiffs/test.txt", "w");
    if(f == NULL)
    {
        ESP_LOGE("SPIFFS", "Failed to open file");
        return;
    }
    fprintf(f, "Hello, SPIFFS!\n");
    fclose(f);

    vTaskDelay(pdMS_TO_TICKS(1000));

    // 读取文件
    f = fopen("/spiffs/test.txt", "r");
    if(f == NULL)
    {
        ESP_LOGE("SPIFFS", "Failed to open file");
        return;
    }
    char buffer[100];
    fgets(buffer, sizeof(buffer), f);
    fclose(f);

    char *p = strchr(buffer, '\n');
    if(p != NULL)
    {
        *p = '\0';
    }
    ESP_LOGI("SPIFFS", "Read from file: %s", buffer);

    // 卸载SPIFFS文件系统
    esp_vfs_spiffs_unregister(conf.partition_label);
    ESP_LOGI("SPIFFS", "SPIFFS unmounted");
}

15.WIFI连接

ESP32 的 WIFI, 有三种工作模式:
STA 模式: 这种模式是 ESP32 最常用的模式, ESP32 可以连接到任何已经存在的 WiFi 网络: 从而允许 ESP32 与网络上的其他设备进行通信, 类似于一台普通的 WiFi 客户端设备。
AP 模式: 这种模式下 ESP32 创建自己的 WiFi 网络, 成为一个小型 WiFi 路由器, 接受其它 WiFi 终端设备连接, 这种模式多用于设备配网。
STA+AP 模式: 在这种模式下, ESP32 同时工作在 STA 和 AP 两种模式下, 既可以连接到已有的 WiFi 网络, 也可以提供 WiFi 热点, 这种高级功能使 ESP32 能够连接到现有的WiFi 网络, 同时创建自己的网络, 充当桥接器或中继器, 通俗的讲就是连接到一个热点后, ESP32 自己又创建了一个网络, 允许其它设备接入, 这些设备以 ESP32 为中继节点, 可以访问互联网。

15.1 STA模式

#include <stdio.h>
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "string.h"

#define WIFI_SSID "nikuailian"
#define WIFI_PASSWORD "2002123456"

void wifi_event_handler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
    if(event_base == WIFI_EVENT)
    {
        switch (event_id)
        {
            case WIFI_EVENT_STA_START:
                esp_wifi_connect();
                break;
            case WIFI_EVENT_STA_CONNECTED:
                ESP_LOGI("STA","WiFi connected");
                break;
            case WIFI_EVENT_STA_DISCONNECTED:
                esp_wifi_connect(); // 实际使用的时候,可以延时多久重连,连多少次连不上就不连了
                ESP_LOGI("STA","WiFi disconnected");
                break;
            default:
                break;
        }
    }else if(event_base == IP_EVENT)
    {
        switch (event_id)
        {
            case IP_EVENT_STA_GOT_IP:  // 获取到了无线路由器分配到的IP地址
                ESP_LOGI("STA","WiFi got IP");
                break;
        }
    }
}

void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());  // 初始化TCP/IP协议栈,ESP-IDF默认使用LWIP
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,wifi_event_handler,NULL);
    esp_event_handler_register(IP_EVENT,IP_EVENT_STA_GOT_IP,wifi_event_handler,NULL);

    wifi_config_t wifi_config = {
        .sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK,
        .sta.pmf_cfg.capable = true,
        .sta.pmf_cfg.required = true,
    };
    memset(&wifi_config,0,sizeof(wifi_config));
    memcpy(wifi_config.sta.ssid,WIFI_SSID,strlen(WIFI_SSID));
    memcpy(wifi_config.sta.password,WIFI_PASSWORD,strlen(WIFI_PASSWORD));

    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_set_config(WIFI_IF_STA,&wifi_config);
    esp_wifi_start();

    ESP_LOGI("STA","WiFi init");
}

15.2 AP模式服务端

/*  WiFi softAP Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"

#include "esp_system.h"
#include "esp_event_loop.h"
 #include "esp_err.h"

// #include "lwip/err.h"
// #include "lwip/sys.h"
#include "lwip/sockets.h"

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

/* The examples use WiFi configuration that you can set via project configuration menu.

   If you'd rather not, just change the below entries to strings with
   the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid"
*/
#define EXAMPLE_ESP_WIFI_SSID      "LCKFB-ESP32"
#define EXAMPLE_ESP_WIFI_PASS      "12345678"
//#define EXAMPLE_ESP_WIFI_CHANNEL   CONFIG_ESP_WIFI_CHANNEL
#define EXAMPLE_MAX_STA_CONN       5    //最大连接数
#define TCP_PORT                9527                // 监听客户端端口
#define WIFI_CONNECTED_BIT  BIT0

static const char *TAG = "wifi softAP";

//socket
static int server_socket = 0;                       // 服务器socket
static struct sockaddr_in server_addr;              // server地址
static struct sockaddr_in client_addr;              // client地址
static unsigned int socklen = sizeof(client_addr);  // 地址长度
static int connect_socket = 0;                      // 连接socket
bool g_rxtx_need_restart = false;                   // 异常后,重新连接标记

EventGroupHandle_t tcp_event_group;// wifi建立成功信号量

//wifi 事件
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                                     int32_t event_id, void* event_data)
{
    switch (event_id){
    case WIFI_EVENT_AP_STACONNECTED:  //AP模式-有STA连接成功
        // 作为ap,有sta连接
//      ESP_LOGI(TAG, "station:" MACSTR " join,AID=%d\n",MAC2STR(event->event_info.sta_connected.mac),event->event_info.sta_connected.aid);
        //设置事件位
        xEventGroupSetBits(tcp_event_group, WIFI_CONNECTED_BIT);
        break;
    case WIFI_EVENT_AP_STADISCONNECTED://AP模式-有STA断线
//      ESP_LOGI(TAG, "station:" MACSTR "leave,AID=%d\n",MAC2STR(event->event_info.sta_disconnected.mac),event->event_info.sta_disconnected.aid);
        //重新建立server
        g_rxtx_need_restart = true;
        xEventGroupClearBits(tcp_event_group, WIFI_CONNECTED_BIT);
        break;
    default:
        break;
    }
}

void wifi_init_softap(void)
{
    tcp_event_group = xEventGroupCreate();
    
    //tcpip_adapter_init();   
    

    //初始化TCP/IP底层栈
    ESP_ERROR_CHECK(esp_netif_init());
    //创建默认事件循环
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    //创建默认WIFI AP。在任何初始化错误的情况下,此API中止。
    esp_netif_create_default_wifi_ap();
    
    

    //WiFi栈配置参数传递给esp_wifi_init调用。
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();

    //为WIFI任务分配资源
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    //将事件处理程序的实例注册到默认循环。
    //这个函数的功能与esp_event_handler_instance_register_with相同,
    //只是它将处理程序注册到默认的事件循环。
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));

    wifi_config_t wifi_config = {
        .ap = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,  //WIFI账号
            .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),//WIFI账号长度
            //.channel = EXAMPLE_ESP_WIFI_CHANNEL,
            .password = EXAMPLE_ESP_WIFI_PASS,//WIFI密码
            .max_connection = EXAMPLE_MAX_STA_CONN,//最大客户端接入数
#ifdef CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT
            .authmode = WIFI_AUTH_WPA3_PSK,
            .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
#else /* CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT */
            .authmode = WIFI_AUTH_WPA2_PSK,
#endif
            .pmf_cfg = {
                    .required = true,
            },
        },
    };
    if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    }

    //设置WIFI工作模式为AP模式
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    //设置AP模式的配置
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
    //开启WIFI
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s ",
             EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);

}
// 获取socket错误代码
int get_socket_error_code(int socket)
{
    int result;         
    u32_t optlen = sizeof(int);
    int err = getsockopt(socket, SOL_SOCKET, SO_ERROR, &result, &optlen);
    if (err == -1){
        //WSAGetLastError();
        ESP_LOGE(TAG, "socket error code:%d", err);
        ESP_LOGE(TAG, "socket error code:%s", strerror(err));
        return -1;
    }
    return result;
}
// 获取socket错误原因
int show_socket_error_reason(const char *str, int socket)
{
    int err = get_socket_error_code(socket);
    if (err != 0){
        ESP_LOGW(TAG, "%s socket error reason %d %s", str, err, strerror(err));
    }
    return err;
}
// 关闭socket
void close_socket()
{
    close(connect_socket);
    close(server_socket);
}
// 接收数据任务
void recv_data(void *pvParameters)
{
    int len = 0;
    char databuff[1024];
    while (1){
        memset(databuff, 0x00, sizeof(databuff));//清空缓存
        len = recv(connect_socket, databuff, sizeof(databuff), 0);//读取接收数据
        g_rxtx_need_restart = false;
        if (len > 0){
            ESP_LOGI(TAG, "recvData: %s", databuff);//打印接收到的数组
            send(connect_socket, databuff, strlen(databuff), 0);//接收数据回发
            //sendto(connect_socket, databuff , sizeof(databuff), 0, (struct sockaddr *) &remote_addr,sizeof(remote_addr));
        }else{
        show_socket_error_reason("recv_data", connect_socket);//打印错误信息
            g_rxtx_need_restart = true;//服务器故障,标记重连
            vTaskDelete(NULL);
        }
    }
    close_socket();
    g_rxtx_need_restart = true;//标记重连
    vTaskDelete(NULL);
}

// 建立tcp server
esp_err_t create_tcp_server(bool isCreatServer)
{
    //首次建立server
    if (isCreatServer){
        ESP_LOGI(TAG, "server socket....,port=%d", TCP_PORT);
        server_socket = socket(AF_INET, SOCK_STREAM, 0);//新建socket
        if (server_socket < 0){
            show_socket_error_reason("create_server", server_socket);
            close(server_socket);//新建失败后,关闭新建的socket,等待下次新建
            return ESP_FAIL;
        }
        //配置新建server socket参数
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(TCP_PORT);
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        //bind:地址的绑定
        if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){
            show_socket_error_reason("bind_server", server_socket);
            close(server_socket);//bind失败后,关闭新建的socket,等待下次新建
            return ESP_FAIL;
        }
    }
    //listen,下次时,直接监听
    if (listen(server_socket, 5) < 0){
        show_socket_error_reason("listen_server", server_socket);
        close(server_socket);//listen失败后,关闭新建的socket,等待下次新建
        return ESP_FAIL;
    }
    //accept,搜寻全连接队列
    connect_socket = accept(server_socket, (struct sockaddr *)&client_addr, &socklen);
    if (connect_socket < 0){
        show_socket_error_reason("accept_server", connect_socket);
        close(server_socket);//accept失败后,关闭新建的socket,等待下次新建
        return ESP_FAIL;
    }
    ESP_LOGI(TAG, "tcp connection established!");
    return ESP_OK;
}
// 建立TCP连接并从TCP接收数据
static void tcp_connect(void *pvParameters)
{
    while (1){
        g_rxtx_need_restart = false;
        // 等待WIFI连接信号量,死等
        //阻塞等待一个或多个位在先前创建的事件组中被设置。
        xEventGroupWaitBits(tcp_event_group, WIFI_CONNECTED_BIT, false, true, portMAX_DELAY);
        
        //启动TCP连接
        ESP_LOGI(TAG, "start tcp connected");
        TaskHandle_t tx_rx_task = NULL;
        vTaskDelay(3000 / portTICK_PERIOD_MS);// 延时3S准备建立server

        ESP_LOGI(TAG, "create tcp server");
        int socket_ret = create_tcp_server(true);// 建立server
        if (socket_ret == ESP_FAIL){// 建立失败
            ESP_LOGI(TAG, "create tcp socket error,stop...");
            continue;
        }else{// 建立成功
            ESP_LOGI(TAG, "create tcp socket succeed...");
                //建立tcp接收数据任务
            if (pdPASS != xTaskCreate(&recv_data, "recv_data", 4096, NULL, 4, &tx_rx_task)){
                ESP_LOGI(TAG, "Recv task create fail!");
            }else{
                ESP_LOGI(TAG, "Recv task create succeed!");
            }
        }
        while (1){
            vTaskDelay(3000 / portTICK_PERIOD_MS);
            if (g_rxtx_need_restart){// 重新建立server,流程和上面一样
                ESP_LOGI(TAG, "tcp server error,some client leave,restart...");
                // 重新建立server
                if (ESP_FAIL != create_tcp_server(false)){
                    if (pdPASS != xTaskCreate(&recv_data, "recv_data", 4096, NULL, 4, &tx_rx_task)){
                        ESP_LOGE(TAG, "tcp server Recv task create fail!");
                    }else{
                        ESP_LOGI(TAG, "tcp server Recv task create succeed!");
                        g_rxtx_need_restart = false;//重新建立完成,清除标记
                    }
                }
            }
        }
    }
    vTaskDelete(NULL);
}
void app_main(void)
{
    //初始化FLASH
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    //建立一个AP
    ESP_LOGI(TAG, "ESP_WIFI_MODE_AP");
    wifi_init_softap();

    //新建一个tcp连接任务
    xTaskCreate(&tcp_connect, "tcp_connect", 4096, NULL, 5, NULL);
   

  while(1)
  {
    vTaskDelay(1000/portTICK_PERIOD_MS);
  }
}

15.3 AP模式客户端

/*  WiFi softAP Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"

#include "esp_system.h"
#include "esp_event_loop.h"
 #include "esp_err.h"

// #include "lwip/err.h"
// #include "lwip/sys.h"
#include "lwip/sockets.h"

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

#include "driver/gpio.h"
/* The examples use WiFi configuration that you can set via project configuration menu.

   If you'd rather not, just change the below entries to strings with
   the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid"
*/
#define EXAMPLE_ESP_WIFI_SSID      "LCKFB-ESP32"
#define EXAMPLE_ESP_WIFI_PASS      "12345678"
//#define EXAMPLE_ESP_WIFI_CHANNEL   CONFIG_ESP_WIFI_CHANNEL
#define EXAMPLE_MAX_STA_CONN       5    //最大连接数
#define TCP_PORT                9527                // 监听客户端端口
#define WIFI_CONNECTED_BIT  BIT0

static const char *TAG = "wifi softAP";

#define TCP_SERVER_ADRESS       "192.168.4.2"
//socket
static int server_socket = 0;                       // 服务器socket
static struct sockaddr_in server_addr;              // server地址
static struct sockaddr_in client_addr;              // client地址
static unsigned int socklen = sizeof(client_addr);  // 地址长度
static int connect_socket = 0;                      // 连接socket
bool g_rxtx_need_restart = false;                   // 异常后,重新连接标记

EventGroupHandle_t tcp_event_group;// wifi建立成功信号量

//wifi 事件
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                                     int32_t event_id, void* event_data)
{
    switch (event_id){
    case WIFI_EVENT_AP_STACONNECTED:  //AP模式-有STA连接成功
        // 作为ap,有sta连接
//      ESP_LOGI(TAG, "station:" MACSTR " join,AID=%d\n",MAC2STR(event->event_info.sta_connected.mac),event->event_info.sta_connected.aid);
        //设置事件位
        xEventGroupSetBits(tcp_event_group, WIFI_CONNECTED_BIT);
        break;
    case WIFI_EVENT_AP_STADISCONNECTED://AP模式-有STA断线
//      ESP_LOGI(TAG, "station:" MACSTR "leave,AID=%d\n",MAC2STR(event->event_info.sta_disconnected.mac),event->event_info.sta_disconnected.aid);
        //重新建立server
        g_rxtx_need_restart = true;
        xEventGroupClearBits(tcp_event_group, WIFI_CONNECTED_BIT);
        break;
    default:
        break;
    }
}

void wifi_init_softap(void)
{
    tcp_event_group = xEventGroupCreate();
    
    //tcpip_adapter_init();   
    

    //初始化TCP/IP底层栈
    ESP_ERROR_CHECK(esp_netif_init());
    //创建默认事件循环
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    //创建默认WIFI AP。在任何初始化错误的情况下,此API中止。
    esp_netif_create_default_wifi_ap();
    
    

    //WiFi栈配置参数传递给esp_wifi_init调用。
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();

    //为WIFI任务分配资源
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    //将事件处理程序的实例注册到默认循环。
    //这个函数的功能与esp_event_handler_instance_register_with相同,
    //只是它将处理程序注册到默认的事件循环。
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));

    wifi_config_t wifi_config = {
        .ap = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,  //WIFI账号
            .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),//WIFI账号长度
            //.channel = EXAMPLE_ESP_WIFI_CHANNEL,
            .password = EXAMPLE_ESP_WIFI_PASS,//WIFI密码
            .max_connection = EXAMPLE_MAX_STA_CONN,//最大客户端接入数
#ifdef CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT
            .authmode = WIFI_AUTH_WPA3_PSK,
            .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
#else /* CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT */
            .authmode = WIFI_AUTH_WPA2_PSK,
#endif
            .pmf_cfg = {
                    .required = true,
            },
        },
    };
    if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    }

    //设置WIFI工作模式为AP模式
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    //设置AP模式的配置
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
    //开启WIFI
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s",
             EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);

}
// 获取socket错误代码
int get_socket_error_code(int socket)
{
    int result;         
    u32_t optlen = sizeof(int);
    int err = getsockopt(socket, SOL_SOCKET, SO_ERROR, &result, &optlen);
    if (err == -1){
        //WSAGetLastError();
        ESP_LOGE(TAG, "socket error code:%d", err);
        ESP_LOGE(TAG, "socket error code:%s", strerror(err));
        return -1;
    }
    return result;
}
// 获取socket错误原因
int show_socket_error_reason(const char *str, int socket)
{
    int err = get_socket_error_code(socket);
    if (err != 0){
        ESP_LOGW(TAG, "%s socket error reason %d %s", str, err, strerror(err));
    }
    return err;
}
// 关闭socket
void close_socket()
{
    close(connect_socket);
    close(server_socket);
}
// 接收数据任务
void recv_data(void *pvParameters)
{
    int len = 0;
    char databuff[1024];
    while (1){
        memset(databuff, 0x00, sizeof(databuff));//清空缓存
        len = recv(connect_socket, databuff, sizeof(databuff), 0);//读取接收数据
        g_rxtx_need_restart = false;
        if (len > 0){
            ESP_LOGI(TAG, "recvData: %s", databuff);//打印接收到的数组
            send(connect_socket, databuff, strlen(databuff), 0);//接收数据回发
            //sendto(connect_socket, databuff , sizeof(databuff), 0, (struct sockaddr *) &remote_addr,sizeof(remote_addr));
        }else{
        show_socket_error_reason("recv_data", connect_socket);//打印错误信息
            g_rxtx_need_restart = true;//服务器故障,标记重连
            vTaskDelete(NULL);
        }
    }
    close_socket();
    g_rxtx_need_restart = true;//标记重连
    vTaskDelete(NULL);
}

// 建立tcp client
esp_err_t create_tcp_client(void)
{
    ESP_LOGI(TAG, "will connect gateway ssid : %s port:%d",TCP_SERVER_ADRESS, TCP_PORT);
    //新建socket
    connect_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (connect_socket < 0){
        show_socket_error_reason("create client", connect_socket);//打印报错信息
        close(connect_socket);//新建失败后,关闭新建的socket,等待下次新建
        return ESP_FAIL;
    }
    //配置连接服务器信息
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(TCP_PORT);
    server_addr.sin_addr.s_addr = inet_addr(TCP_SERVER_ADRESS);
    ESP_LOGI(TAG, "connectting server...");
    //连接服务器
    if (connect(connect_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){
        show_socket_error_reason("client connect", connect_socket);//打印报错信息
        ESP_LOGE(TAG, "connect failed!");
        //连接失败后,关闭之前新建的socket,等待下次新建
        close(connect_socket);
        return ESP_FAIL;
    }
    ESP_LOGI(TAG, "connect success!");
    return ESP_OK;

    /*
    //首次建立server
    if (isCreatServer){
        ESP_LOGI(TAG, "server socket....,port=%d", TCP_PORT);
        server_socket = socket(AF_INET, SOCK_STREAM, 0);//新建socket
        if (server_socket < 0){
            show_socket_error_reason("create_server", server_socket);
            close(server_socket);//新建失败后,关闭新建的socket,等待下次新建
            return ESP_FAIL;
        }
        //配置新建server socket参数
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(TCP_PORT);
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        //bind:地址的绑定
        if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){
            show_socket_error_reason("bind_server", server_socket);
            close(server_socket);//bind失败后,关闭新建的socket,等待下次新建
            return ESP_FAIL;
        }
    }
    //listen,下次时,直接监听
    if (listen(server_socket, 5) < 0){
        show_socket_error_reason("listen_server", server_socket);
        close(server_socket);//listen失败后,关闭新建的socket,等待下次新建
        return ESP_FAIL;
    }
    //accept,搜寻全连接队列
    connect_socket = accept(server_socket, (struct sockaddr *)&client_addr, &socklen);
    if (connect_socket < 0){
        show_socket_error_reason("accept_server", connect_socket);
        close(server_socket);//accept失败后,关闭新建的socket,等待下次新建
        return ESP_FAIL;
    }
    ESP_LOGI(TAG, "tcp connection established!");
    return ESP_OK;
    */
}
// 建立TCP连接并从TCP接收数据
static void tcp_connect(void *pvParameters)
{
    while (1){
        g_rxtx_need_restart = false;
        // 等待WIFI连接信号量,死等
        //阻塞等待一个或多个位在先前创建的事件组中被设置。
        xEventGroupWaitBits(tcp_event_group, WIFI_CONNECTED_BIT, false, true, portMAX_DELAY);
        
        //启动TCP连接
        ESP_LOGI(TAG, "start tcp connected");
        TaskHandle_t tx_rx_task = NULL;
        vTaskDelay(3000 / portTICK_PERIOD_MS);// 延时3S准备建立server

        ESP_LOGI(TAG, "create tcp server");
        //建立client
        int socket_ret = create_tcp_client();
        if (socket_ret == ESP_FAIL){// 建立失败
            ESP_LOGI(TAG, "create tcp socket error,stop...");
            continue;
        }else{// 建立成功
            ESP_LOGI(TAG, "create tcp socket succeed...");
                //建立tcp接收数据任务
            if (pdPASS != xTaskCreate(&recv_data, "recv_data", 4096, NULL, 4, &tx_rx_task)){
                ESP_LOGI(TAG, "Recv task create fail!");
            }else{
                ESP_LOGI(TAG, "Recv task create succeed!");
            }
        }
        while (1){
            vTaskDelay(3000 / portTICK_PERIOD_MS);
            if (g_rxtx_need_restart){// 重新建立server,流程和上面一样
                ESP_LOGI(TAG, "tcp server error,some client leave,restart...");
                // 重新建立server
                if (ESP_FAIL != create_tcp_client()){
                    if (pdPASS != xTaskCreate(&recv_data, "recv_data", 4096, NULL, 4, &tx_rx_task)){
                        ESP_LOGE(TAG, "tcp client Recv task create fail!");
                    }else{
                        ESP_LOGI(TAG, "tcp client Recv task create succeed!");
                        g_rxtx_need_restart = false;//重新建立完成,清除标记
                    }
                }
            }
        }
    }
    vTaskDelete(NULL);
}
void app_main(void)
{
    //初始化FLASH
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    //建立一个AP
    ESP_LOGI(TAG, "ESP_WIFI_MODE_AP");
    wifi_init_softap();

    //新建一个tcp连接任务
    xTaskCreate(&tcp_connect, "tcp_connect", 4096, NULL, 5, NULL);
   
// 配置GPIO结构体
    gpio_config_t io_conf;
    io_conf.intr_type = GPIO_INTR_ANYEDGE;      // 下降沿和上升沿触发中断
    io_conf.pin_bit_mask = 1 << 0;  // 设置GPIO号
    io_conf.mode = GPIO_MODE_INPUT;             // 模式输入
    io_conf.pull_up_en = GPIO_PULLUP_ENABLE;    // 端口上拉使能
    gpio_config(&io_conf);

  while(1)
  {
    vTaskDelay(500/portTICK_PERIOD_MS);
    if(gpio_get_level(0)==0){
            //新建一个tcp连接任务
            xTaskCreate(&tcp_connect, "tcp_connect", 4096, NULL, 5, NULL);
            break;
        }
  }
}

16.SmartConfig一键配网

流程: 让esp32处于混杂模式下,监听网络中的所有报文,手机APP将SSID和密码编码到UDP报文中,广播发送,esp32接收到UDP报文后解码,得到争取的SSID和密码,然后主动连接到这个网络。

基础知识扫盲:
UDP数据由20字节IPv4头+8字节UDP报文头+UDP内容。
Smart中,有一个概念叫算法常量,这个由APP和ESP32厂商定义,需要一致。
UDP数据长度+算法常量=Smart密文
发送示例:
要发送1234为例:假设算法常量是10,那么还需要发送UDP数据长度1224,再减去UDP数据20字节IPv4头+8字节UDP报文头,所以我们只需要发送1196个字节长度的任意内容。

流程:
设备WIFI开启混杂模式,在所处环境中快速切换各条信道来抓取每个信道中的数据包, 当遇到正在发送前导码数据包的信道时, 锁定该信道并继续接收广播数据, 直到收到足够的数据来解码出其中的 WiFi 密码然后连接 WiFi

假设手机 APP 要发送”test”四个字符, 算法常量为 16, 流程如下:
1) APP 连续发送 3 个 UDP 广播包, 数据为均为前导码。
2) APP 发送 1 个 UDP 广播包, IP 报文数据长度为’t’-16。
3) APP 发送 1 个 UDP 广播包, IP 报文数据长度为’e’-16。
4) APP 发送 1 个 UDP 广播包, IP 报文数据长度为’s’-16。
5) APP 发送 1 个 UDP 广播包, IP 报文数据长度为’t’-16。
6) APP 切换 WIFI 信道重复上述步骤

#include <stdio.h>
#include "esp_smartconfig.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "string.h"

#define TAG "smartconfig"

void wifi_event_handler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
    if(event_base == WIFI_EVENT)
    {
        switch (event_id)
        {
            case WIFI_EVENT_STA_START:
                esp_wifi_connect();
                break;
            case WIFI_EVENT_STA_CONNECTED:
                ESP_LOGI(TAG,"WiFi connected");
                break;
            case WIFI_EVENT_STA_DISCONNECTED:
                esp_wifi_connect(); // 实际使用的时候,可以延时多久重连,连多少次连不上就不连了
                ESP_LOGI(TAG,"WiFi disconnected");
                break;
            default:
                break;
        }
    }else if(event_base == IP_EVENT)
    {
        switch (event_id)
        {
            case IP_EVENT_STA_GOT_IP:  // 获取到了无线路由器分配到的IP地址
                ESP_LOGI(TAG,"WiFi got IP");
                break;
        }
    }else if(event_base == SC_EVENT)
    {
        switch (event_id)
        {
            case SC_EVENT_SCAN_DONE:     // 扫描完成
                ESP_LOGI(TAG,"Scan done");
                break;
            case SC_EVENT_GOT_SSID_PSWD:    // 获取到了SSID和密码
                ESP_LOGI(TAG,"Got SSID and password");
                smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *)event_data;
                wifi_config_t wifi_config;
                memset(&wifi_config,0,sizeof(wifi_config));
                // memcpy(&wifi_config.sta.ssid,evt->ssid,sizeof(wifi_config.sta.ssid));
                // memcpy(&wifi_config.sta.password,evt->password,sizeof(wifi_config.sta.password));
                snprintf((char *)wifi_config.sta.ssid,sizeof(wifi_config.sta.ssid),"%s",evt->ssid);
                snprintf((char *)wifi_config.sta.password,sizeof(wifi_config.sta.password),"%s",evt->password);
                ESP_LOGI(TAG,"SSID: %s, Password: %s",wifi_config.sta.ssid,wifi_config.sta.password);
                wifi_config.sta.bssid_set = evt->bssid_set;
                if(wifi_config.sta.bssid_set)
                {
                    memcpy(wifi_config.sta.bssid,evt->bssid,sizeof(wifi_config.sta.bssid));
                }
                esp_wifi_disconnect();
                esp_wifi_set_config(WIFI_IF_STA,&wifi_config);
                esp_wifi_connect();
                break;
            case SC_EVENT_SEND_ACK_DONE: // 告诉APP,已经收到了SSID和密码
                ESP_LOGI(TAG,"Send ack done");
                esp_smartconfig_stop();
                break;
            default:
            break;
        }
    }
}

void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());  // 初始化TCP/IP协议栈,ESP-IDF默认使用LWIP
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,wifi_event_handler,NULL);
    esp_event_handler_register(IP_EVENT,IP_EVENT_STA_GOT_IP,wifi_event_handler,NULL);
    esp_event_handler_register(SC_EVENT,ESP_EVENT_ANY_ID,wifi_event_handler,NULL);

    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();

    esp_smartconfig_set_type(SC_TYPE_ESPTOUCH);
    smartconfig_start_config_t smartconfig_cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
    esp_smartconfig_start(&smartconfig_cfg);
}

17 MQTT

17.1 MQTT介绍

简介: MQTT( Message Queuing Telemetry Transport, 消息队列遥测传输协议) , 是一种基于发布/订阅(publish/subscribe) 模式的"轻量级"通讯协议, 该协议构建于 TCP/IP 协议上。
优点: 可以以极少的代码和有限的带宽, 为连接远程设备提供实时可靠的消息服务。
应用: 作为一种低开销、 低带宽占用的即时通讯协议, 使其在物联网、 小型设备、 移动应用等方面有较广泛的应用。

MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。工作在网络的应用层,再传输控制协议/互联网协议(TCP/IP)堆栈之上

17.1.1 发布和订阅

MQTT使用的发布/订阅消息模式,它提供了一对多的消息分发机制,从而实现与应用程序的解耦。

这是一种消息传递模式,消息不是直接从发送器发送到接收器(即点对点),而是由MQTT server(或称为 MQTT Broker)分发的。可以理解为有一个代理,用来中转,发送端和接收端都和代理沟通。
在这里插入图片描述

  • 不管是消息的订阅者,还是消息的推送者,都是客户端,MQTT broker是服务端。
  • 订阅者和推送者之间,通过topic来约定,订阅者A订阅了主题“topic1”,那么所有推送者,推送的主题为“topic1”的消息,订阅者A都会收到。

17.1.2 服务质量QoS(Quality of Service levels)

QoS 0: 最多发一次,会丢失。即发送次数<=1;
QoS 1: 至少传递一次,不会丢失,会重复。发送者发布(PUBLISH)之后,会存储消息,直到接收者接收并发送确认(PUBACK),如果一定时间内为收到确认,将会重新发送。
QoS 2: 恰好一次,不会丢失,不会重复。有四个包,发布包(PUBLISH),发布收到包(PUBREC),发布释放包(PUBREL),发布完成包(PUBCOMP)。流程如下:

  • (1)发送者发送publish
  • (2)接收者收到publish包之后,发送pubrec
  • (3)发布者收到pubrec包之后,发送pubrel
  • (4)接收者收到pubrel包之后,发送pubcomp

如此完成一次发布,发布者和接收者可以删除保存的消息副本

问:在QoS2中,假如发送publish包成功,但是接收pubrec失败,是否会重复收到?
答:不会,在QoS2中,双方会保存一个16位的消息ID,每一次发送都携带者这个ID,当接收方已经收到了这个ID的消息,发送方继续发送,接收方不处理。

问:QoS1中,有ID这个机制吗?
答:有,不过QoS1中,发送方附带一个ID,puback也会附带ID,不过QoS1收到ID重复的,还是会接收

17.2 MQTT数据包结构

固定头(Fixed header): 所有MQTT数据包中都存在,表示数据包的类型以及数据包的分组类标识;
可变头(Variable header): 部分MQTT数据包存在,数据包的类型(固定包头)决定了可变头是否存在极其具体内容;
消息体(Payload): 部分MQTT数据包存在,表示客户端收到的具体内容;
在这里插入图片描述

17.2.1 固定头

在这里插入图片描述
(1)消息类型(message type)
位置: byte1,4-7一共4位
作用: 交代这条MQTT消息类型
值: 如下表

名称 流方向 描述
Reserved 0 不可用 保留位
CONNECT 1 客户端到服务器 客户端请求连接到服务器
CONNACK 2 服务器到客户端 连接确认
PUBLISH 3 双向 发布消息
PUBACK 4 双向 发布确认
PUBREC 5 双向 发布收到(保证第1部分到达)
PUBREL 6 双向 发布释放(保证第2部分到达)
PUBCOMP 7 双向 发布完成(保证第3部分到达)
SUBSCRIBE 8 客户端到服务器 客户端请求订阅
SUBACK 9 服务器到客户端 订阅确认
UNSUBSCRIBE 10 客户端到服务器 请求取消订阅
UNSUBACK 11 服务器到客户端 取消订阅确认
PINGREQ 12 客户端到服务器 PING请求
PINGRESP 13 服务器到客户端 PING应答
DISCONNECT 14 客户端到服务器 中断连接
Reserved 15 不可用 保留位

(2) 标志位(DUP)
作用: 标志MQTT消息的一些机制,详见下面。在不使标识位的消息类型中,标识位被作为保留位。如果收到无效的标志时,接收端必须关闭网络连接。
位置: byte1,0-3共4位
值:

名称 标识位 Bit3 Bit2 Bit1 Bit0
CONNECT 保留位 0 0 0 0
CONNACK 保留位 0 0 0 0
PUBLISH MQTT3.1.1使用 DUP1 QoS2 QoS2 RETAIN3
PUBACK 保留位 0 0 0 0
PUBREC 保留位 0 0 0 0
PUBREL 保留位 0 0 0 0
PUBCOMP 保留位 0 0 0 0
SUBSCRIBE 保留位 0 0 0 0
SUBACK 保留位 0 0 0 0
UNSUBSCRIBE 保留位 0 0 0 0
UNSUBACK 保留位 0 0 0 0
PINGREQ 保留位 0 0 0 0
PINGRESP 保留位 0 0 0 0
DISCONNECT 保留位 0 0 0 0

DUP: 发布消息的副本,用来在保证消息的可靠传输,如果设置为1,则在下面的变长中增加MessageId,并且需要回复确认,以保证消息传输完成,但不能用于检测消息重复发送。
DUP,重复分发标志,0第一次发送,1表示是重发报文
QoS: 发布消息的服务质量(前面已经做过介绍),即:保证消息传递的次数
00:最多一次,即:<=1
01:至少一次,即:>=1
10:一次,即:=1
11:预留
RETAIN: 发布保留标识,为1表示服务器要保留这次推送的信息,如果有新的订阅者出现,就把这消息推送给它。为0表示如果设有那么推送至当前订阅者后释放。

(3)剩余长度(remaining length)
位置: byte 1
作用: 固定头的第二字节用来保存变长头部和消息体的总大小的,但不是直接保存的。这一字节是可以扩展,其保存机制,前7位用于保存长度,后一部用做标识。当最高位为 1时,表示长度不足,需要使用二个字节继续保存。
举例:

0b1110 0111 = 0xE7 去掉第 7 位值为 0x67(第 7 位是 1, 说明后面一个字节也表示剩余长度)
0b1000 1101 = 0x8D 去掉第 7 位值为 0x0D(第 7 位是 1, 说明后面一个字节也表示剩余长度)
0b0001 0001 = 0x11 去掉第 7 位值为 0x0D(第 7 位是 0, 表示后面没有长度)
计算方法位: 剩余长度 len = 0x67 + (0x0D<<7) + (0x11<<14)

17.2.2 可变头/Variable header

这个根据不同的包类型,是不一样的,比较常见的应用是作为包标识:

Bit 7-0
byte1 包标签符(MSB)
btye2 包标签付(LSB)

下面这些类型的数据包中都包括一个2字节的数据包标识字段:PUBLISH (QoS > 0)、PUBACK、PUBREC、PUBREL、PUBCOMP、SUBSCRIBE、SUBACK、UNSUBSCRIBE、UNSUBACK

17.2.3 Payload消息体

Payload消息体是MQTT数据包的第三部分,CONNECTSUBSCRIBESUBACKUNSUBSCRIBE四种类型的消息 有消息体:

  • CONNECT:消息体是客户端的ClientID、订阅的Topic、Message以及用户名和密码;
  • SUBSCRIBE:消息体内容是一系列的要订阅的主题以及QoS;
  • SUBACK:消息体内容是服务器对于SUBSCRIBE所申请的主题及QoS进行确认和回复;
  • UNSUBSCRIBE:消息体内容是要订阅的主题。

17.3 MQTT测试客户端

MQTTX:这是一个服务器,你可以用他来测试订阅者或者发布者。
MQTT.fx:这是一个工具,你可以配置服务器(阿里云,华为云等等),服务器连通初期用来调试。
MQTTX下载地址
MQTT.fx下载地址

18 蓝牙

ESP32使用蓝牙,需要menuconfig中选择蓝牙

Component config → Bluetooth :选中Bluetooth

Component config → Bluetooth → Host:选择bluedroid

18.1 介绍

蓝牙分为经典蓝牙和低功耗蓝牙

经典蓝牙(BT):支持音频和数据协议,多用于音响、耳机、汽车电子行业,功耗较高

低功耗蓝牙(BLE):蓝牙4.0协议之后推出的一种新模式,BLE不支持音频协议,速度受限,多用于物联网行业(如穿戴设备)功耗十分低。

双模蓝牙:支持BT也支持BLE的设备

注意BT和BLE之间协议不互通

18.2 BLE分层架构

蓝牙协议栈的分层结构包括:底层硬件模块、中间协议层和高端应用层三大部分

在这里插入图片描述

18.3 ATT和GATT

ATT协议定义了一个叫attribute数据结构。GATT定义了一个框架,将attribute结构组织起来,将attribute的内容分为服务、特征、值或描述。

在这里插入图片描述

一个例子

在这里插入图片描述

特征声明: 这里声明了特征值的UUID是哪一个,特征值的权限是什么。不同的协议是不一样的,这里只是举例。

这是一个心率计的例子,可读代表其他蓝牙可以读取,通知代表有值改变的时候心率计主动通知。

启用指示,代表心率计上报数据后,主机需要回复。

18.4 UUID

UUID用于标志不同的服务(Services)、特征(Characteristics)已经及描述符(Descriptors),这些UUID是的设备之间能够识别并交换特定类型的数据。

16位UUID:0000xxxx-0000-1000-8000-00805F9B34FB

32位UUID:xxxxxxxx-0000-1000-8000-00805F9B34FB

128位UUID:00000000-0000-1000-8000-00805F9B34FB

Bluetooth Gatt 服务UUID 对照表:http://www.corenchip.com/2280.html

18.5 Profiles的组织

在这里插入图片描述

18.6 蓝牙连接和广播

主机(客户端): 发起扫描并主动连接从机设备,比如:手机

从机(服务端): 发起广播,等待主机连接,比如:蓝牙灯,手环

一个BLE设备既可以做为主机,也可以做为从机

信道: 蓝牙一共有40个信道,从2402MHz到 ~ 2480MHz,37、38、39是蓝牙广播信道,剩余的是数据信道。

在这里插入图片描述

广播间隔: 从机每经过一个时间间隔(一般20ms 到 10.24S)发送一次广播数据;

广播事件: 一次广播的时间,每次广播事件会在37,38,39信道上一次广播;

18.7 BLE广播数据格式

BLE广播格式: 一个广播数据包最长37个字节,其中6个字节的mac地址,31个字节的广播数据结构体。

广播数据结构体格式: 每个结构体包含3个东西,长度(length)、类型(type)和内容(content)。

在这里插入图片描述

18.8 BLE连接

连接事件: 在BLE连接中使用跳频方案,两个设备在特定时间、频道上彼此发送和接收数据。这些设备稍后在新的通道上通过约定的时间相遇,这次用于收发数据的相遇称为连接事件。

连接间隔: 两次简介事件之间的时间间隔称为连接间隔,1.25ms为单位,范围从最小值7.5ms到最大值4.0s

从机延迟: 可以跳过的最大事件数。可以节省功耗

监控超时: 两次成功连接事件之间的最长事件。如果在此时间内没有成功的连接事件,设备将终止连接并返回到未连接状态。(100ms - 32 S)

有效连接间隔: 实际有效的交互通信间隔,有效连接间隔 = 连接间隔 + 从机延迟 * 连接间隔

  • 每个“连接事件”之间,间隔“连接间隔”,如果超过了“监控超时”,将断开。

网站公告

今日签到

点亮在社区的每一天
去签到