【SoC】【ESP32】基于VSCode+ESP-IDF插件开发环境的示例工程

一、简介

(1)开发环境

  • VSCode:一款轻量级但功能强大的跨平台代码编辑器,拥有丰富的插件生态系统,方便进行代码编辑、调试等操作。
  • ESP-IDF 插件:由乐鑫公司开发,集成了 ESP32 系列芯片开发所需的工具链、编译环境等,能在 VSCode 中直接进行 ESP32 项目的创建、编译、烧录和调试等工作。

(2)硬件基础

  • ESP32 芯片:这是乐鑫推出的一款低功耗、高性能的 Wi-Fi + Bluetooth 双模无线微控制器,拥有双核处理器(或单核),丰富的外设接口(如 GPIO、SPI、I2C 等),可以满足各种物联网应用场景。
  • LED:发光二极管,通过连接到 ESP32 的 GPIO 引脚,根据芯片输出的高低电平信号来实现点亮和熄灭的效果。

(3)软件核心 ——FreeRTOS

  • 实时操作系统:FreeRTOS 是一个开源的实时操作系统(RTOS),被广泛应用于资源受限的嵌入式系统中。它提供了任务管理、任务调度、信号量、消息队列等功能。
  • 多任务机制:在这个示例工程中,利用 FreeRTOS 的多任务特性,创建了 3 个独立的任务,每个任务负责控制一个 LED 进行闪烁操作。任务之间通过 FreeRTOS 的调度器进行调度,使得系统能够在同一时间内看似并发地执行多个任务 。

二、工程创建

1.新建工程,点击New project

2.配置工程

3.选择模板

  • 插件名称espressif.esp-idf-extension ,版本 1.10.2
  • 模板名称
    • arduino-as-component:可将 Arduino 代码作为组件集成到 ESP-IDF 项目
    • fibonacci-app:斐波那契数列示例,演示 FreeRTOS 多任务
    • template-app:ESP-IDF 基础项目模板,适合快速开始开发
    • unity-app:集成 Unity 单元测试框架,用于组件测试

选择空项目模板

或者freertos下的斐波那契数列程序的示例模板,我选择的是这个

4.首先,由左下角的烧写方式、烧写接口、确认芯片类型

5.进行SDK详细配置

(1)flash

ESP32-S3-N16R8 中, “N16” 代表的是16MB 的 Flash,“R8” 表示8MB 的 PSRAM。所以这款芯片的 Flash 大小是 16MB。

(2)Partition Table

选择自定义分区表,分区表自定义名称:partitions_tabel_16MB.csv

(3)PSRAM

使能外部SRAM,主要调整为八线模式和80MHz。

(4)CPU Frequency

(5)FreeRTOS

一、GDB Stub(调试相关)

GDB Stub 是 FreeRTOS 中用于支持 GDB 调试器的组件,允许通过 GDB 查看任务状态、断点调试等。

Enable listing FreeRTOS tasks through GDB Stub
启用后,GDB 调试器可以列出当前所有 FreeRTOS 任务的信息(如任务名、状态、优先级等),方便调试时监控任务运行状态。
Maximum number of tasks supported by GDB Stub: 32
GDB Stub 最多支持同时显示 32 个任务的信息(超过则无法通过 GDB 完整列出)。

二、FreeRTOS Kernel(内核核心配置)
1. 内核基础

Run the Amazon SMP FreeRTOS kernel instead (FEATURE UNDER DEVELOPMENT)
启用后将使用亚马逊的 SMP(对称多处理)版本 FreeRTOS 内核(实验性功能),支持多核心同时运行任务(适用于 ESP32S3 等双核芯片)。默认不启用时,使用标准单核心调度的 FreeRTOS 内核。
Run FreeRTOS only on first core
限制 FreeRTOS 仅在芯片的第一个核心(Core 0)上运行,第二个核心(若有,如 ESP32S3 的 Core 1)可用于其他裸机程序或独立任务。
configTICK_RATE_HZ: 1000
FreeRTOS 系统时钟节拍频率,单位为 Hz。此处设置为 1000,即每秒产生 1000 个时钟节拍(每个节拍间隔 1ms),用于任务调度、延时(如 vTaskDelay())等时间相关操作。节拍频率越高,时间精度越高,但内核开销略增。

2. 栈溢出检测

configCHECK_FOR_STACK_OVERFLOW: Check using canary bytes (Method 2)
启用栈溢出检测功能,采用「金丝雀字节(canary bytes)」方式(方法 2):在任务栈的末尾放置特殊标记字节(金丝雀值),每次任务切换时检查标记是否被改写,若改写则判定为栈溢出(会触发栈溢出钩子函数)。这是一种常用的内存越界检测机制。

3. 任务与内存相关

configNUM_THREAD_LOCAL_STORAGE_POINTERS: 1
每个任务可分配的「线程本地存储指针(TLS)」数量。TLS 用于存储任务私有数据(如任务专属的变量),此处最多支持 1 个指针。
configMINIMAL_STACK_SIZE (Idle task stack size): 1536
空闲任务(Idle Task)的栈大小(单位:字节)。空闲任务是 FreeRTOS 自动创建的最低优先级任务,用于系统空闲时运行,栈大小需根据系统最小需求配置(此处 1536 字节适用于多数嵌入式场景)。
configMAX_TASK_NAME_LEN: 16
任务名称的最大长度(包含终止符 \0),超过则会被截断。例如,任务名最长为 15 个可见字符 + 1 个终止符。
configENABLE_BACKWARD_COMPATIBILITY
启用后兼容旧版本 FreeRTOS 的 API 或宏定义(如旧版中的函数参数、结构体成员),避免因版本升级导致的兼容性问题。

4. 定时器功能

configUSE_TIMERS
启用 FreeRTOS 软件定时器功能,允许创建周期性或一次性定时器(通过 xTimerCreate() 等 API)。
configTIMER_SERVICE_TASK_NAME: Tmr Svc
定时器服务任务的名称(默认 Tmr Svc),该任务负责处理所有软件定时器的触发逻辑。
configTIMER_SERVICE_TASK_CORE_AFFINITY: No affinity
定时器服务任务的核心绑定(适用于多核芯片),「No affinity」表示不固定核心,由内核自动调度到任意核心。
configTIMER_TASK_PRIORITY: 1
定时器服务任务的优先级(数值越大优先级越高),此处为 1(较低优先级,避免抢占关键任务)。
configTIMER_TASK_STACK_DEPTH: 2048
定时器服务任务的栈大小(2048 字节),需根据定时器回调函数的复杂度调整(若回调函数复杂,需增大栈大小)。
configTIMER_QUEUE_LENGTH: 10
定时器命令队列的长度,用于存储待处理的定时器操作(如启动、停止定时器),超过 10 个则需等待队列空闲。

5. 队列与通知

configQUEUE_REGISTRY_SIZE: 0
队列注册表的大小,用于将队列 / 信号量与名称关联(通过 vQueueAddToRegistry()),方便调试时识别队列。0 表示禁用该功能。
configTASK_NOTIFICATION_ARRAY_ENTRIES: 1
每个任务的「任务通知数组」长度。任务通知是一种轻量级的通信机制(替代信号量 / 队列),此处最多支持 1 个通知条目(每个任务可接收 1 种通知)。

6. 调试与统计

configUSE_TRACE_FACILITY
启用跟踪功能,支持 FreeRTOS 的跟踪宏(如 vTaskList() 列出任务状态、vTaskGetRunTimeStats() 统计任务运行时间),用于系统性能分析。
configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES
启用链表数据完整性检查,在 FreeRTOS 内部链表(如任务就绪链表、阻塞链表)的节点中添加校验字节,检测链表是否被意外篡改(如内存越界导致的链表损坏)。
configGENERATE_RUN_TIME_STATS
启用任务运行时间统计功能,可通过 vTaskGetRunTimeStats() 获取每个任务的运行时间占比(需配合硬件定时器实现)。
configUSE_APPLICATION_TASK_TAG
允许为任务设置「标签(tag)」,通过 vTaskSetApplicationTaskTag() 关联一个整数或指针,用于标识任务(如区分任务类型、归属模块)。

三、Port(端口配置,与硬件相关)

针对特定硬件平台(如 ESP32S3 的 Xtensa 架构)的 FreeRTOS 移植配置。

Wrap task functions
对任务函数进行包装(如添加进入 / 退出钩子),通常用于调试或跟踪任务的创建 / 销毁。
Enable stack overflow debug watchpoint
启用栈溢出调试断点(依赖硬件 watchpoint 功能),当栈指针超出栈范围时触发调试中断,方便定位栈溢出问题。
Enable thread local storage pointers deletion callbacks
启用线程本地存储(TLS)指针的删除回调,当任务销毁时自动调用 TLS 指针的清理函数(释放资源)。
Enable task pre-deletion hook
启用任务删除前的钩子函数(vTaskPreDeleteHook()),在任务被删除前执行自定义逻辑(如释放任务持有的资源)。
Enable static task clean up hook (DEPRECATED)
启用静态任务的清理钩子(已过时),用于静态创建的任务(xTaskCreateStatic())销毁时的资源清理。
Check that mutex semaphore is given by owner task
检查互斥锁(mutex)的释放操作是否由持有锁的任务执行(避免其他任务释放不属于自己的锁,导致死锁)。
ISR stack size: 1536
中断服务程序(ISR)的栈大小(1536 字节),用于处理中断时的临时数据存储(栈不足会导致系统崩溃)。
Enable backtrace from interrupt to task context
启用从中断上下文到任务上下文的回溯功能,当发生中断时,可追踪中断前正在运行的任务调用栈(方便调试中断相关问题)。
Use float in Level 1 ISR
允许在 Level 1 中断服务程序中使用浮点数(需硬件支持浮点运算,且会增加中断处理开销)。
Tick timer source (Xtensa Only): SYSTIMER 0 (level 1)
选择 FreeRTOS 系统节拍(tick)的定时器源(仅 Xtensa 架构),此处使用 SYSTIMER 0(系统定时器 0),且为 Level 1 中断(较高优先级,确保节拍计时准确)。
Place FreeRTOS functions into Flash
将 FreeRTOS 函数存储在 Flash 中(而非 RAM),节省 RAM 空间(适用于 RAM 有限的嵌入式设备)。
*Tests compliance with Vanilla FreeRTOS port _CRITICAL calls
检查与「标准 FreeRTOS 端口」中临界区函数(如 taskENTER_CRITICAL())的兼容性,确保临界区操作符合官方规范。

四、Extra(额外配置)

Allow external memory as an argument to xTaskCreateStatic (READ HELP)
允许将外部内存(如 SPI RAM)作为参数传递给 xTaskCreateStatic()(静态创建任务的 API),即任务的栈和控制块可存储在外部内存中(需确保外部内存的访问速度和稳定性)。

有如下更改:

100对应的节拍周期是10ms,1000对应的节拍周期是1ms

6.保存SDK配置后,ctrl+shift+p打开分区表编辑器

       这是 ESP-IDF Partition Table Editor(ESP-IDF 分区表编辑器) 的界面,用于可视化配置 ESP32/ESP32-S3 等设备的 Flash 分区表。以下逐行解释核心内容:

1. 界面标题与功能说明

ESP-IDF Partition Table Editor
标题:明确这是 ESP-IDF 框架的分区表可视化工具。
Partition Editor can help you to easily edit, build & flash partition table through GUI...
功能说明:无需手动编辑 CSV 文件,可通过图形界面(GUI)轻松编辑、构建和烧录分区表。

2. 操作按钮(右上角)

Select Flash Method
选择烧录方式:指定如何将分区表烧录到设备(如串口、JTAG 等)。
Build
构建:根据当前配置生成分区表二进制文件(.bin)。
Flash
烧录:将生成的分区表二进制文件烧录到设备的 Flash 中。

3. 分区表配置区域

表格中每行对应一个 Flash 分区,包含以下字段:

字段 1:Name(分区名称)
nvs:Non-Volatile Storage(非易失性存储)分区,用于保存设备配置、键值对数据(如 WiFi 密码、自定义参数)。
phy_init:PHY(物理层)初始化数据分区,存储 WiFi / 蓝牙射频的校准参数。
factory:工厂应用分区,存储默认的固件镜像(设备首次启动或恢复出厂设置时运行的程序)。
vfs:Virtual File System(虚拟文件系统)分区,类型为 fat(FAT 文件系统),可用于存储文件(如配置文件、日志)。
storage:自定义存储分区,类型为 spiffs(SPIFFS 文件系统),适合嵌入式设备的轻量级文件存储。
字段 2:Type(分区类型)
data:数据分区(存储配置、文件系统等非程序数据)。
app:应用分区(存储固件程序,可被 Bootloader 加载运行)。
字段 3:Sub Type(子类型)
nvs:对应 nvs 分区,标识该数据分区用于 NVS。
phy:对应 phy_init 分区,标识该数据分区用于 PHY 初始化。
factory:对应 factory 分区,标识该应用分区为工厂固件。
fat:对应 vfs 分区,标识该数据分区使用 FAT 文件系统。
spiffs:对应 storage 分区,标识该数据分区使用 SPIFFS 文件系统。
字段 4:Offset(分区起始偏移)
如 0x9000、0xF000 等,表示该分区在 Flash 中的起始地址(相对于 Flash 起始位置)。
需注意分区之间不能重叠,且需符合 Flash 擦除块(erase block)对齐要求。
字段 5:Size(分区大小)
如 0x6000(24KB)、0x1F0000(2,048KB = 2MB)等,表示该分区占用的 Flash 空间大小。
字段 6:Encrypted(加密)
复选框:启用后,该分区数据会被加密(需配合 ESP-IDF 的安全机制,如 Secure Boot、Flash 加密)。
字段 7:×(删除按钮)
点击可删除对应分区的配置。

4. 关键分区的作用

nvs:必选分区,用于保存设备运行时的配置(掉电不丢失)。
phy_init:必选分区(若使用 WiFi / 蓝牙),存储射频校准参数,确保无线通信稳定。
factory:必选分区,存储默认固件,设备启动时 Bootloader 会优先加载此分区的程序。
vfs(FAT):可选分区,适合存储文件(如文本、二进制数据),支持电脑直接挂载访问。
storage(SPIFFS):可选分区,轻量级文件系统,适合嵌入式设备(无需复杂的文件系统支持)。

5. 实际应用场景

分区表的作用:将 Flash 划分为多个逻辑区域,分别存储程序、配置、文件系统等,让不同功能的数据互不干扰。
修改注意事项:
调整分区大小或偏移时,需确保 Flash 总容量足够(如 ESP32-S3-N16R8 的 Flash 是 16MB)。
若新增文件系统分区(如 vfs、storage),需在代码中初始化对应的文件系统(如 esp_vfs_fat_mount() 或 esp_spiffs_mount())。
烧录新分区表后,若覆盖了原有数据分区(如 nvs),可能导致配置丢失,需谨慎操作。
简单说,这个界面让你可视化管理 ESP32 设备的 Flash 分区,像分硬盘区一样把 Flash 划给固件、配置、文件系统等不同功能使用,避免手动写 CSV 配置的麻烦~

分区如下:

7.添加新的组件,选择第4个选项,组件名bsp_led

三、代码编写

1.编写3个led的初始化和闪烁代码

bsp_led.h

#ifndef BSP_LED_H
#define BSP_LED_H

#define LED1_GPIO_PIN GPIO_NUM_1

#define LED2_GPIO_PIN GPIO_NUM_2

#define LED3_GPIO_PIN GPIO_NUM_3

void bsp_led_init(void);
void bsp_led1_toggle(void);
void bsp_led2_toggle(void);
void bsp_led3_toggle(void);

#endif // BSP_LED_H

bsp_led.c

#include <stdio.h>

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

#include "bsp_led.h"

#include "driver/gpio.h"

void bsp_led_init(void)
{
    //初始化LED1
    gpio_config_t io_conf = {
       .pin_bit_mask = (1ULL << LED1_GPIO_PIN),
       .mode = GPIO_MODE_OUTPUT,
       .pull_up_en = GPIO_PULLUP_ENABLE,
       .pull_down_en = GPIO_PULLDOWN_DISABLE,
       .intr_type = GPIO_INTR_DISABLE  
    };
    gpio_config(&io_conf);
    //gpio_set_direction(LED1_GPIO_PIN, GPIO_MODE_OUTPUT);
    //gpio_set_level(LED1_GPIO_PIN, 1); // 默认关闭LED1

    //初始化LED2
    
    gpio_set_direction(LED2_GPIO_PIN, GPIO_MODE_OUTPUT);
    gpio_set_level(LED2_GPIO_PIN, 1); // 默认关闭LED2

    //初始化LED3
    gpio_set_direction(LED3_GPIO_PIN, GPIO_MODE_OUTPUT);
    gpio_set_level(LED3_GPIO_PIN, 1); // 默认关闭LED3
}

void bsp_led1_toggle(void)
{
    // gpio_set_level(LED1_GPIO_PIN, !gpio_get_level(LED1_GPIO_PIN));//无作用,原因不明
    // vTaskDelay(pdMS_TO_TICKS(100));//CONFIG_FREERTOS_HZ//无作用,原因不明

    gpio_set_level(LED1_GPIO_PIN, 1);
    vTaskDelay(1000);
    gpio_set_level(LED1_GPIO_PIN, 0);
    vTaskDelay(1000);
}

void bsp_led2_toggle(void)
{
    gpio_set_level(LED2_GPIO_PIN, 1);
    vTaskDelay(2000);
    gpio_set_level(LED2_GPIO_PIN, 0);
    vTaskDelay(2000);
}

void bsp_led3_toggle(void)
{
    gpio_set_level(LED3_GPIO_PIN, 1);
    vTaskDelay(3000);
    gpio_set_level(LED3_GPIO_PIN, 0);
    vTaskDelay(3000);
}

CMakeLists.txt

idf_component_register(SRCS "bsp_led.c"
                    INCLUDE_DIRS "include"
                    REQUIRES driver
                    REQUIRES freertos)

2.编写多任务

main.c

#include "sdkconfig.h"
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
//#include "esp_spi_flash.h"//已废弃
#include "spi_flash_mmap.h"

#include "bsp_led.h"

//led1
uint32_t led1_task_stack_size_word = 1024*3;//栈深度,单位:字节
UBaseType_t uxPriority_led1 = 2;//优先级
TaskHandle_t xHandle_led1;//任务句柄

//led2
uint32_t led2_task_stack_size_word = 1024*3;//栈深度,单位:字节
UBaseType_t uxPriority_led2 = 2;//优先级
TaskHandle_t xHandle_led2;//任务句柄

//led3
#define led3_task_stack_size_word 1024*3//栈深度,单位: 字节
UBaseType_t uxPriority_led3 = 2;//优先级
StackType_t puxStackBuffer_led3[led3_task_stack_size_word];//静态分配的栈
StaticTask_t xHandle_led3;//任务句柄

void led1_task(void *pvParameters)
{
    
    while (1) {
        printf("LED1 Task is running\n");
        bsp_led1_toggle();
    }
}

void led2_task(void *pvParameters)
{
    
    while (1) {
        printf("LED2 Task is running\n");
        bsp_led2_toggle();
    }
}

void led3_task(void *pvParameters)
{
    
    while (1) {
        printf("LED3 Task is running\n");
        bsp_led3_toggle();
    }
}

void app_main(void)
{
    // 初始化LED硬件
    bsp_led_init();

    // 动态创建LED1任务
    xTaskCreate(led1_task,       // 任务函数指针(入口函数)
                "led1_task",       // 任务名称(用于调试,长度建议不超过 configMAX_TASK_NAME_LEN)
                led1_task_stack_size_word,     // 任务栈大小(单位:字节)
                NULL,       // 传递给任务函数的参数(可空)
                uxPriority_led1,          // 任务优先级(0 为最低,configMAX_PRIORITIES-1 为最高)
                &xHandle_led1  // 任务句柄(用于后续操作任务,如删除、挂起等)(可空)
                );
    
    // 动态创建LED2任务
    xTaskCreate(led2_task,       // 任务函数指针(入口函数)
                "led2_task",       // 任务名称(用于调试,长度建议不超过 configMAX_TASK_NAME_LEN)
                led2_task_stack_size_word,     // 任务栈大小(单位:字节)
                NULL,       // 传递给任务函数的参数(可空)
                uxPriority_led2,          // 任务优先级(0 为最低,configMAX_PRIORITIES-1 为最高)
                &xHandle_led2  // 任务句柄(用于后续操作任务,如删除、挂起等)(可空)
                );

    // 静态创建LED3任务
    xTaskCreateStatic(led3_task,       // 任务函数指针(入口函数)
                        "led3_task",       // 任务名称(用于调试,长度建议不超过 configMAX_TASK_NAME_LEN)
                        led3_task_stack_size_word,     // 任务栈大小(单位:字节)
                        NULL,       // 传递给任务函数的参数(可空)
                        uxPriority_led3,          // 任务优先级(0 为最低,configMAX_PRIORITIES-1 为最高)
                        puxStackBuffer_led3,  // 任务句柄(用于后续操作任务,如删除、挂起等)(可空)
                        &xHandle_led3); // 静态分配的任务控制块

    // vTaskStartScheduler();//ESP-IDF v5.0 及以上版本不需要调用此函数,FreeRTOS 已经在底层自动启动调度器  
 	// for( ;; );//ESP-IDF v5.0 及以上版本不需要调用此函数,FreeRTOS 已经在底层自动启动调度器 
}

 CMakeLists.txt

idf_component_register(SRCS "main.c"
                    REQUIRES esp_system
                    REQUIRES bsp_led
                    REQUIRES spi_flash
                    INCLUDE_DIRS ".")

 3.关闭看门狗

四、实验效果

示例工程链接:

https://github.com/Molesidy/ESP32.githttps://github.com/Molesidy/ESP32.git

### 配置 VSCode 中的 ESP-IDF 开发环境以支持 ESP32 为了在 Windows 系统上通过 Visual Studio Code (VSCode) 进行 ESP32 的开发,需要完成一系列必要的配置步骤。以下是详细的说明: #### 1. 安装工具链 ESP32 的开发依赖于特定的工具链来编译代码。可以通过以下方法安装: - 下载并解压适用于 Windows 平台的 xtensa-esp32-elf 工具链[^1]。 - 将工具链路径添加到系统的 `PATH` 环境变量中。 #### 2. 安装 CMake 和 Ninja 构建工具 CMake 是一种跨平台的构建系统生成器,而 Ninja 则是一种高效的构建工具。两者对于 ESP-IDF 的项目管理至关重要。 - 可以从官方站点下载最新版本的 CMake 和 Ninja,并按照提示完成安装过程[^3]。 #### 3. 获取 ESP-IDF 框架 ESP-IDFEspressif 提供的一个开源框架,包含了所有的硬件抽象层以及驱动程序接口。 - 访问 GitHub 或者 Espressif 官方网站,克隆或者下载最新的 ESP-IDF 版本至本地目录[^2]。 - 解压缩后,在命令行界面执行初始化脚本来设置所需的子模块和其他资源文件。 #### 4. 配置 VSCode 插件 Visual Studio Code 支持多种扩展插件来增强其功能,针对 ESP-IDF 存在一个专门设计好的插件可以简化工作流程。 - 打开 Extensions Marketplace (`Ctrl+Shift+X`) ,搜索 “ESP-IDF”,找到由 Espressif 维护的那个插件并点击 Install。 - 同时还需要确认 Python 插件已启用,因为部分操作需要用到它作为辅助脚本解释器。 #### 5. 初始化与验证环境 当上述准备工作完成后,就可以测试整个环境是否正常运作了。 - 创建一个新的工程模板(比如 blink),利用 esp-idf.py 脚本引导创建; - 在终端里输入 `idf.py build` 来尝试构建该项目,如果没有任何错误消息,则表明一切准备就绪。 ```bash # 示例:启动新的终端会话前先加载 IDF_PATH export IDF_PATH=/path/to/your/idf_directory source $IDF_PATH/export.sh ``` ```python import os print(os.environ.get('IDF_PATH')) ``` 以上就是基于 VSCode 实现 ESP32 开发所需的主要环节概述。每一步都非常重要,请严格按照顺序逐一实施直至成功部署完毕为止。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值