ESP-IDF

技术栈
嵌入式 SDK
esp32esp-idf乐鑫WiFi蓝牙RTOS

概览

ESP-IDF 技术栈概览

ESP-IDF(Espressif IoT Development Framework)是乐鑫科技为 ESP32 / ESP32-S / ESP32-C / ESP32-H 系列芯片 打造的官方物联网开发框架。基于 FreeRTOS 实时操作系统,提供完整的 WiFi/BLE/Thread 协议栈、外设驱动和安全方案。

解决什么问题

  • IoT 全栈需求:WiFi + BLE + 双核 MCU 一体化,无需额外通信模块
  • 功耗敏感场景:内置多种低功耗模式(Light-sleep、Deep-sleep、ULP 协处理器)
  • 安全合规:支持安全启动、Flash 加密、WPA3、TLS/SSL 等企业级安全
  • 商业级可靠性:乐鑫芯片出货数亿颗,ESP-IDF 经过大规模量产验证

关键特性

  • 基于 FreeRTOS:原生多任务、队列、信号量,实时性强
  • 组件化架构:模块化设计,每个功能独立为 component 可复用
  • 丰富的协议支持:WiFi STA/AP/STA+AP、BLE Mesh、Thread、Zigbee、Matter
  • Python 工具链:idf.py 命令行工具,CMake 构建系统
  • 官方生态:ESP-ADF(音频)、ESP-MDF(Mesh)、ESP-BLE-MESH 等扩展框架

安装

ESP-IDF 安装指南

1. 环境准备

平台 要求
Windows Windows 10/11 (64-bit),PowerShell 或 Git Bash
macOS macOS 10.15 Catalina 及以上
Linux Ubuntu 20.04+ / Debian 11+ / Fedora 36+

依赖项

  • Python ≥ 3.8
  • Git ≥ 2.x
  • CMake ≥ 3.16
  • Ninja build
  • 交叉编译链(安装脚本自动处理)

2. 安装步骤

方式一:官方安装脚本(全平台推荐)

# 1. 克隆 ESP-IDF(推荐 v5.x LTS)
git clone --recursive https://github.com/espressif/esp-idf.git -b v5.3

# 2. 运行安装脚本
cd esp-idf
./install.sh all        # Linux/macOS(安装所有目标芯片工具链)
# 或指定目标:
./install.sh esp32,esp32s3

# Windows PowerShell:
# .\install.ps1 all

# 3. 导出环境变量
. ./export.sh           # Linux/macOS(注意前面的 . 和空格)
# Windows:
# .\export.ps1

方式二:VSCode ESP-IDF 插件

  1. 安装 VSCode → 扩展 → 搜索 ESP-IDF
  2. 按向导自动下载 ESP-IDF + 工具链
  3. 完成后 F1ESP-IDF: Configure 可切换版本

方式三:IDE 集成

  • CLion:支持 ESP-IDF 项目的 CMake 导入
  • Eclipse:安装 Espressif IDF Eclipse Plugin

验证安装

idf.py --version
idf.py set-target esp32
idf.py menuconfig      # 图形化配置界面

3. 常见安装问题

问题 解决方案
git clone 慢 / 子模块拉取失败 使用国内镜像 git clone https://gitee.com/EspressifSystems/esp-idf.git;子模块用 git submodule update --init --recursive 配合代理
python 版本不对 使用 pyenv 安装 Python 3.8-3.12 版本
CMake 版本过低 Ubuntu 用 pip install cmake --upgrade 或 snap 安装新版
Windows 路径过长 将 ESP-IDF 放在 C:\esp-idf 等短路径,启用长路径支持
export.sh 报错 确保 Python 在 PATH 且已安装 pip install -r requirements.txt
串口权限 (Linux) sudo usermod -a -G dialout $USER
macOS Python 冲突 不使用系统 Python,用 brew install python@3.12 创建 venv

示例

ESP-IDF 双核任务:传感器采集 + OLED 显示

目标

利用 ESP32 双核架构:Core 0 专用于 OLED 显示刷新,Core 1 处理传感器采集和 WiFi 上报。通过 FreeRTOS 队列实现核心间通信。

完整代码

/* dual_core_demo.c — ESP32 双核任务示例
 * Core 0: OLED 显示任务(刷新周期 50ms)
 * Core 1: 传感器采集 + WiFi 上报(采集周期 1000ms)
 * 核间通信:FreeRTOS 队列
 */

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "driver/gpio.h"
#include "nvs_flash.h"

static const char *TAG = "dual_core";

/* ── 传感器数据结构(队列元素) ── */
typedef struct {
    int64_t timestamp_us;
    float   temperature;
    float   humidity;
    float   pressure;
    uint32_t sequence;
} sensor_data_t;

/* ── 队列句柄 ── */
static QueueHandle_t sensor_queue;

/* ═══════════════════════════════════════════
 * Core 0 任务:OLED 显示
 * ═══════════════════════════════════════════ */
void display_task(void *pvParameters) {
    sensor_data_t data;
    TickType_t last_wake = xTaskGetTickCount();

    ESP_LOGI(TAG, "[Core %d] 显示任务启动", xPortGetCoreID());

    while (1) {
        /* 非阻塞接收队列(50ms 超时) */
        if (xQueueReceive(sensor_queue, &data, pdMS_TO_TICKS(50)) == pdTRUE) {
            /* 实际项目:将这些数据写入 OLED */
            ESP_LOGI(TAG, "[Core %d] 🖥️ 显示更新 | "
                          "T=%.1f°C H=%.1f%% P=%.1fhPa | 序列=%lu",
                          xPortGetCoreID(),
                          data.temperature, data.humidity,
                          data.pressure, data.sequence);
        } else {
            /* 无新数据——刷新静态屏保 */
            ESP_LOGD(TAG, "[Core %d] 屏保刷新", xPortGetCoreID());
        }

        /* 精确 50ms 周期 */
        vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(50));
    }
}

/* ═══════════════════════════════════════════
 * Core 1 任务:传感器采集与 WiFi
 * ═══════════════════════════════════════════ */
void sensor_task(void *pvParameters) {
    sensor_data_t data;
    uint32_t seq = 0;

    ESP_LOGI(TAG, "[Core %d] 传感器任务启动", xPortGetCoreID());

    while (1) {
        /* 模拟温度:25 + 正弦漂移 + 噪声 */
        float base_temp = 25.0f;
        float drift = 4.0f * sinf((float)seq * 0.3f);
        float noise = ((float)(esp_random() % 100) - 50.0f) / 100.0f;

        data.timestamp_us = esp_timer_get_time();
        data.temperature  = base_temp + drift + noise;
        data.humidity     = 55.0f + 10.0f * cosf((float)seq * 0.2f);
        data.pressure     = 1013.25f + noise * 2.0f;
        data.sequence     = seq++;

        /* 发送到 Core 0 的显示队列 */
        if (xQueueSend(sensor_queue, &data, pdMS_TO_TICKS(100)) != pdTRUE) {
            ESP_LOGW(TAG, "队列满!丢弃数据 seq=%lu", data.sequence);
        } else {
            ESP_LOGI(TAG, "[Core %d] 📡 采集 seq=%lu | T=%.1f°C H=%.1f%%",
                     xPortGetCoreID(), data.sequence,
                     data.temperature, data.humidity);
        }

        /* WiFi 上报逻辑(实际工程) */
        // send_to_mqtt(&data);

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

/* ═══════════════════════════════════════════
 * 监控任务:统计 CPU 使用率
 * ═══════════════════════════════════════════ */
void monitor_task(void *pvParameters) {
    char buf[512];
    while (1) {
        vTaskList(buf);
        ESP_LOGI(TAG, "\n=== 任务列表 ===\n%s", buf);

        ESP_LOGI(TAG, "队列消息数: %d / 队列空余: %d",
                 uxQueueMessagesWaiting(sensor_queue),
                 uxQueueSpacesAvailable(sensor_queue));

        vTaskDelay(pdMS_TO_TICKS(10000));
    }
}

/* ═══════════════════════════════════════════
 * app_main
 * ═══════════════════════════════════════════ */
void app_main(void) {
    ESP_LOGI(TAG, "ESP32 双核任务演示");

    /* 创建队列(容量 16 条) */
    sensor_queue = xQueueCreate(16, sizeof(sensor_data_t));
    assert(sensor_queue != NULL);

    /* ── 任务创建(显式指定核心) ── */
    BaseType_t ret;

    /* Core 1: 传感器任务(优先级 3) */
    ret = xTaskCreatePinnedToCore(
        sensor_task,     // 函数
        "sensor",        // 名称
        4096,            // 栈大小(字)
        NULL,            // 参数
        3,               // 优先级
        NULL,            // 任务句柄(不需要)
        1                // 绑定 Core 1
    );
    assert(ret == pdPASS);

    /* Core 0: 显示任务(优先级 2,PRO 协议栈使用 Core 0) */
    ret = xTaskCreatePinnedToCore(
        display_task, "display", 4096, NULL, 2, NULL, 0);
    assert(ret == pdPASS);

    /* 任意核心:监控任务 */
    ret = xTaskCreate(
        monitor_task, "monitor", 4096, NULL, 1, NULL);
    assert(ret == pdPASS);

    ESP_LOGI(TAG, "所有任务已创建完成!");
    /* app_main 返回后被自动删除,不占用 Stack */
}

运行步骤

. ~/esp/esp-idf/export.sh
idf.py set-target esp32
idf.py build flash monitor

预期输出

I (xxx) dual_core: ESP32 双核任务演示
I (xxx) dual_core: [Core 0] 显示任务启动
I (xxx) dual_core: [Core 1] 传感器任务启动
I (xxx) dual_core: 所有任务已创建完成!
I (xxx) dual_core: [Core 1] 📡 采集 seq=0 | T=25.0°C H=54.5%
I (xxx) dual_core: [Core 0] 🖥️ 显示更新 | T=25.0°C H=54.5% P=1013.2hPa | 序列=0
...
I (xxx) dual_core:
=== 任务列表 ===
sensor           R   3   4096    xxx   1
display          B   2   4096    xxx   0
monitor          B   1   4096    xxx   1

关键点

  • xTaskCreatePinnedToCore 的最后一个参数 0/1 决定在哪个核心上运行
  • Core 0 默认运行 PRO CPU(WiFi/BT 协议栈),显示任务与其共享
  • Core 1 运行 APP CPU,适合计算密集型传感器处理
  • FreeRTOS 队列是核间通信的首选方式(线程安全、无锁)
  • 固定周期任务用 vTaskDelayUntil 避免累积误差

ESP-IDF Hello World:WiFi 扫描仪

目标

在 ESP32 上运行 ESP-IDF FreeRTOS 任务,扫描周围 WiFi 网络并输出信号强度。

完整代码

/* main.c — ESP32 WiFi Scanner
 * 硬件:任意 ESP32/ESP32-S3 开发板
 * 依赖:ESP-IDF v5.x
 */

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

static const char *TAG = "wifi_scan";

/* WiFi 扫描任务 */
void wifi_scan_task(void *pvParameters) {
    // 初始化 WiFi(STA 模式)
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());

    while (1) {
        // 触发扫描
        ESP_ERROR_CHECK(esp_wifi_scan_start(NULL, true));
        ESP_LOGI(TAG, "=== WiFi 扫描完成 ===");

        // 获取扫描结果
        uint16_t ap_count = 0;
        ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_count));
        ESP_LOGI(TAG, "发现 %d 个 WiFi 网络", ap_count);

        if (ap_count > 0) {
            wifi_ap_record_t *ap_records = calloc(ap_count, sizeof(wifi_ap_record_t));
            ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_count, ap_records));

            // 打印前 10 个最强信号
            int show = ap_count > 10 ? 10 : ap_count;
            for (int i = 0; i < show; i++) {
                ESP_LOGI(TAG,
                    "SSID: %-20s | RSSI: %4d dBm | CH: %2d | %s",
                    ap_records[i].ssid,
                    ap_records[i].rssi,
                    ap_records[i].primary,
                    ap_records[i].authmode == WIFI_AUTH_OPEN ? "开放" : "加密");
            }
            free(ap_records);
        }

        // 清理扫描结果
        esp_wifi_clear_ap_list();

        ESP_LOGI(TAG, "5 秒后重新扫描...\n");
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

void app_main(void) {
    // 初始化 NVS(WiFi 库需要)
    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);

    ESP_LOGI(TAG, "ESP32 WiFi 扫描仪启动");

    // 创建扫描任务
    xTaskCreate(wifi_scan_task, "wifi_scan", 4096, NULL, 5, NULL);
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(wifi_scanner)

运行步骤

# 1. 激活 ESP-IDF 环境
. ~/esp/esp-idf/export.sh

# 2. 设置目标芯片
idf.py set-target esp32

# 3. 配置(可选 — 修改分区表等)
idf.py menuconfig

# 4. 编译 & 烧录 & 监控
idf.py build flash monitor
# 退出监控:Ctrl+]

预期输出

I (456) wifi_scan: ESP32 WiFi 扫描仪启动
I (1234) wifi_scan: === WiFi 扫描完成 ===
I (1235) wifi_scan: 发现 12 个 WiFi 网络
I (1236) wifi_scan: SSID: MyHome-5G           | RSSI:  -42 dBm | CH: 11 | 加密
I (1237) wifi_scan: SSID: Starbucks WiFi       | RSSI:  -58 dBm | CH:  6 | 开放
...
I (1238) wifi_scan: 5 秒后重新扫描...

关键点

  • app_main 是 ESP-IDF 的入口函数(类似 main
  • 必须先初始化 NVS,WiFi 库依赖它存储配置
  • FreeRTOS 任务栈大小 4096 是典型值,复杂任务需增大
  • esp_wifi_scan_start(NULL, true)true 表示阻塞等待扫描完成

教程

ESP-IDF 开发:从环境搭建到 WiFi 应用

本章目标

搭建 ESP-IDF 开发环境,理解项目结构和组件化架构,开发第一个 WiFi + FreeRTOS 应用。


1. ESP-IDF 与 Arduino ESP32 的区别

维度 Arduino ESP32 ESP-IDF
底层 封装 ESP-IDF 直接 FreeRTOS
控制粒度 粗(简单 API) 细(完整控制)
双核利用 有限 手动指定核心
功耗优化 基本 deep-sleep + ULP
量产 不建议 官方推荐

2. 项目结构

my_app/
├── CMakeLists.txt           ← 顶层 CMake
├── sdkconfig                ← idf.py menuconfig 生成
├── main/
│   ├── CMakeLists.txt       ← idf_component_register
│   └── main.c
├── components/
│   └── my_driver/
│       ├── CMakeLists.txt   ← idf_component_register(SRCS ... REQUIRES ...)
│       ├── my_driver.c
│       └── include/my_driver.h
└── partitions.csv           ← 分区表

3. 第一个 FreeRTOS 任务

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

void my_task(void *pvParameters) {
    int *param = (int *)pvParameters;
    while (1) {
        printf("Task %d running on Core %d\n", *param, xPortGetCoreID());
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void) {
    static int id1 = 1, id2 = 2;
    xTaskCreate(my_task, "task1", 2048, &id1, 2, NULL);
    xTaskCreate(my_task, "task2", 2048, &id2, 2, NULL);
    // app_main 返回后,其栈空间被回收
}

栈大小经验值

任务类型 推荐栈 (words)
简单打印 2048
WiFi 通信 4096-6144
HTTP 客户端 8192
JSON 解析 6144+

4. WiFi 连接(事件驱动)

#include "esp_wifi.h"
#include "esp_event.h"

static void wifi_event_handler(void *arg, esp_event_base_t base,
                                int32_t id, void *event_data) {
    if (id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (id == WIFI_EVENT_STA_DISCONNECTED) {
        esp_wifi_connect();  // 自动重连
    }
}

static void ip_event_handler(void *arg, esp_event_base_t base,
                              int32_t id, void *event_data) {
    if (id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t *ev = (ip_event_got_ip_t *)event_data;
        ESP_LOGI("WiFi", "Got IP: " IPSTR, IP2STR(&ev->ip_info.ip));
    }
}

void wifi_init_sta(const char *ssid, const char *pass) {
    // 初始化网络栈 + 事件循环
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    // 注册事件
    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,
                               &ip_event_handler, NULL);

    // 配置并启动 WiFi
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    wifi_config_t wifi_cfg = {0};
    strcpy((char *)wifi_cfg.sta.ssid, ssid);
    strcpy((char *)wifi_cfg.sta.password, pass);

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg));
    ESP_ERROR_CHECK(esp_wifi_start());
}

5. 组件化开发

# components/my_sensor/CMakeLists.txt
idf_component_register(
    SRCS "my_sensor.c"
    INCLUDE_DIRS "include"
    REQUIRES driver freertos esp_timer
)
// components/my_sensor/include/my_sensor.h
#pragma once
#include "esp_err.h"

typedef struct {
    float temperature;
    float humidity;
} sensor_reading_t;

esp_err_t sensor_init(void);
esp_err_t sensor_read(sensor_reading_t *out);

组件之间通过 REQUIRES 声明依赖,构建系统自动处理 include 路径和链接顺序。


6. 功耗优化

// 进入 Deep Sleep(仅 RTC + ULP 运行,~5µA)
esp_sleep_enable_timer_wakeup(60 * 1000000);  // 60 秒后唤醒
esp_deep_sleep_start();

// 唤醒后从 app_main 重新执行(类似复位)

思考题

  1. 为什么 ESP-IDF 使用事件驱动而非阻塞等待 WiFi 连接?
  2. 组件化架构相比把所有代码放 main/ 有什么优势?
  3. vTaskDelay vs vTaskDelayUntil 的区别是什么?