从单文件点灯到 BSP:用 idf.py 创建 DNESP32S3 工程

2026-09-09 00:00    #ESP32-S3   #ESP-IDF   #GPIO   #LED   #BSP   #Ubuntu  

上一篇介绍了 ESP-IDF、EIM 与 VS Code 的安装,以及用 hello_world 验证编译的方法。这一篇跟着《DNESP32S3 使用指南(IDF 版)》第 10 章,控制正点原子 DNESP32S3 的红色用户 LED。我们会做两遍同一件事:

两节各有一个可独立构建的示例工程;建议先完成 2.1,再读 2.2。

验证范围

2.1 单文件工程2.2 BSP 工程都已在 macOS 的 ESP-IDF v5.5.5 下执行 idf.py set-target esp32s3idf.py build,编译成功。Ubuntu 26.04 实机及开发板当前未接入;串口号、烧录和实际闪灯仍须在目标机器上验证。

先读原理图:GPIO1 控制哪盏灯

下面两幅是 ATK_DNESP32S3_V1.2.pdf 的局部图(图源:正点原子课程资料)。第一幅找芯片引脚,第二幅顺着同名网络找到 LED 电路。

ESP32-S3-WROOM-1 模组物理 39 脚为 IO1,连接 LED 网络

图 1:模组的物理 39 脚标为 IO1,网络名为 LED。程序里用的是 GPIO1,不是 GPIO39。

3.3 V 经 R4 与红色 LED 接到 IO1;蓝色 PWR 灯直接接地

图 2:VCC3.3 → R4(510 Ω)→ 红色 LED → LED/IO1。下方蓝色 PWR 灯接电源与 GND,是电源指示灯,不由 GPIO1 控制。

因此,GPIO1 输出低电平时,电流经 R4 和红色 LED 流向 GPIO1,红灯点亮;输出高电平时红灯熄灭。教材第 10 章和官方 01_led 工程也使用 GPIO1。

要核对的量本篇取值
目标芯片ESP32-S3:esp32s3
红色用户 LEDGPIO1(模组物理 39 脚)
有效电平低电平亮,高电平灭
预期现象亮 500 毫秒、灭 500 毫秒,循环

2.1 一个 main.c 完成点灯

idf.py 创建空工程

上一篇激活 ESP-IDF。下方脚本名是本机示例;在 Ubuntu 上先运行 find "$HOME/.espressif/tools" -maxdepth 1 -name 'activate_idf_*.sh' -print,换成实际安装版本。项目路径不要包含空格。

1source "$HOME/.espressif/tools/activate_idf_v5.5.5.sh"
2idf.py --version
3mkdir -p "$HOME/esp"
4cd "$HOME/esp"
5idf.py create-project 02-01-main-led
6cd 02-01-main-led
7mv main/02-01-main-led.c main/main.c

create-project 生成根目录和 main/ 的 CMake 文件,以及一个空的 app_main()。它没有自动选择 ESP32-S3。根目录 CMakeLists.txt 保持生成的 project(02-01-main-led);我们只需要把 main/CMakeLists.txt 改成:

1idf_component_register(SRCS "main.c"
2                    INCLUDE_DIRS "."
3                    PRIV_REQUIRES esp_driver_gpio)

这里显式写出 main 的 GPIO 驱动依赖;driver/gpio.h 只在 main.c 中使用,所以采用 PRIV_REQUIRES。再在根目录新建 sdkconfig.defaults,记录目标芯片和这块板的 16 MB Flash:

1CONFIG_IDF_TARGET="esp32s3"
2CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y

此时还没有 BSP。main.c唯一承载应用逻辑的 C 文件;CMake 和 sdkconfig.defaults 是构建配置,不是另一套点灯代码。

main.c 直接控制 GPIO

main/main.c 完整替换为下面的代码:

 1#include <stdbool.h>
 2#include "driver/gpio.h"
 3#include "esp_check.h"
 4#include "esp_log.h"
 5#include "freertos/FreeRTOS.h"
 6#include "freertos/task.h"
 7
 8#define LED_GPIO GPIO_NUM_1
 9
10static const char *TAG = "led_demo";
11
12static esp_err_t led_set(bool on)
13{
14    return gpio_set_level(LED_GPIO, on ? 0 : 1);
15}
16
17static esp_err_t led_init(void)
18{
19    const gpio_config_t config = {
20        .pin_bit_mask = 1ULL << LED_GPIO,
21        .mode = GPIO_MODE_OUTPUT,
22        .pull_up_en = GPIO_PULLUP_DISABLE,
23        .pull_down_en = GPIO_PULLDOWN_DISABLE,
24        .intr_type = GPIO_INTR_DISABLE,
25    };
26
27    esp_err_t err = gpio_config(&config);
28    if (err != ESP_OK) {
29        return err;
30    }
31    return led_set(false);
32}
33
34void app_main(void)
35{
36    ESP_ERROR_CHECK(led_init());
37
38    while (1) {
39        ESP_ERROR_CHECK(led_set(true));
40        ESP_LOGI(TAG, "LED on");
41        vTaskDelay(pdMS_TO_TICKS(500));
42
43        ESP_ERROR_CHECK(led_set(false));
44        ESP_LOGI(TAG, "LED off");
45        vTaskDelay(pdMS_TO_TICKS(500));
46    }
47}

看这段程序时抓住三件事:gpio_config() 将 GPIO1 配成输出;led_set(true) 输出 0 才是点亮红灯;pdMS_TO_TICKS(500) 把 500 毫秒换成 FreeRTOS tick 数。ESP_ERROR_CHECK 会在 GPIO 调用失败时报告错误,日志则帮助我们把程序状态和肉眼看到的灯对起来。

编译与实板验收

02-01-main-led 根目录运行:

1idf.py set-target esp32s3
2idf.py build
3grep -E 'CONFIG_IDF_TARGET=|CONFIG_ESPTOOLPY_FLASHSIZE=' sdkconfig

后两项配置应显示 esp32s316MBProject build complete 且命令以 0 退出,表示固件编译成功;它还不能证明 LED 闪烁。以后只改普通 C 代码,直接重新 idf.py build 即可,不必每次 set-target

用支持数据传输的 USB 线连接开发板,插拔一次并比较设备列表,找出新出现的串口。Ubuntu 常见 /dev/ttyUSB*/dev/ttyACM*,macOS 常见 /dev/cu.*。把下方占位符换成实际端口:

1idf.py -p <实际串口> flash monitor

验收要同时满足两项:Monitor 交替打印 LED onLED off;板上的红色用户 LED亮约 0.5 秒、灭约 0.5 秒并循环。蓝色 PWR 灯持续亮属于正常供电现象。Monitor 用 Ctrl+] 退出;若 Linux 报串口权限不足,按上一篇的串口权限步骤处理。

2.2 把同一程序整理成 BSP

BSP 到底是什么

**BSP(Board Support Package,板级支持包)**是把“这块板子怎么接线、怎么初始化和控制外设”的代码集中起来的一种组织方式。这里 components/BSP 是我们给 ESP-IDF 组件取的名字;ESP-IDF 负责按组件的 CMake 声明编译和链接,BSP 这个目录名并不会自动赋予特殊功能。

在 2.1,main.c 同时知道“每 500 毫秒亮灭一次”和“LED 接 GPIO1、低电平亮”。现在保留前者,把后者移到 BSP。为便于重做和对照,2.2 工程是独立快照;两个工程运行后的灯和日志应该完全一样。

从 2.1 工程开始重构

~/esp 创建第二个空工程,复制 2.1 的主程序和配置,再新建 BSP 目录:

1cd "$HOME/esp"
2idf.py create-project 02-02-bsp-led
3cd 02-02-bsp-led
4mv main/02-02-bsp-led.c main/main.c
5cp ../02-01-main-led/main/main.c main/main.c
6cp ../02-01-main-led/sdkconfig.defaults sdkconfig.defaults
7mkdir -p components/BSP/LED

components/BSP/LED/led.h 声明 BSP 对应用层提供的两个动作:

1#pragma once
2
3#include <stdbool.h>
4#include "esp_err.h"
5
6esp_err_t led_init(void);
7esp_err_t led_set(bool on);

components/BSP/LED/led.c 放入原来 main.c 里的 GPIO 细节。GPIO1 和低电平有效只在这里出现:

 1#include "led.h"
 2#include "driver/gpio.h"
 3
 4#define LED_GPIO GPIO_NUM_1
 5
 6esp_err_t led_init(void)
 7{
 8    const gpio_config_t config = {
 9        .pin_bit_mask = 1ULL << LED_GPIO,
10        .mode = GPIO_MODE_OUTPUT,
11        .pull_up_en = GPIO_PULLUP_DISABLE,
12        .pull_down_en = GPIO_PULLDOWN_DISABLE,
13        .intr_type = GPIO_INTR_DISABLE,
14    };
15
16    esp_err_t err = gpio_config(&config);
17    if (err != ESP_OK) {
18        return err;
19    }
20    return led_set(false);
21}
22
23esp_err_t led_set(bool on)
24{
25    return gpio_set_level(LED_GPIO, on ? 0 : 1);
26}

components/BSP/CMakeLists.txt 注册 BSP 组件:

1idf_component_register(SRCS "LED/led.c"
2                    INCLUDE_DIRS "LED"
3                    PRIV_REQUIRES esp_driver_gpio)

main/CMakeLists.txt 不再直接依赖 GPIO 驱动,改为依赖 BSP:

1idf_component_register(SRCS "main.c"
2                    INCLUDE_DIRS "."
3                    PRIV_REQUIRES BSP)

最后,把 main/main.c 中的 led_init()led_set() 实现移走,留下调用:

 1#include "esp_check.h"
 2#include "esp_log.h"
 3#include "freertos/FreeRTOS.h"
 4#include "freertos/task.h"
 5#include "led.h"
 6
 7static const char *TAG = "led_demo";
 8
 9void app_main(void)
10{
11    ESP_ERROR_CHECK(led_init());
12
13    while (1) {
14        ESP_ERROR_CHECK(led_set(true));
15        ESP_LOGI(TAG, "LED on");
16        vTaskDelay(pdMS_TO_TICKS(500));
17
18        ESP_ERROR_CHECK(led_set(false));
19        ESP_LOGI(TAG, "LED off");
20        vTaskDelay(pdMS_TO_TICKS(500));
21    }
22}

根目录 CMakeLists.txt 仍由 create-project 生成,项目名应为 project(02-02-bsp-led)。此时目录结构是:

 102-02-bsp-led/
 2├── CMakeLists.txt
 3├── sdkconfig.defaults
 4├── main/
 5│   ├── CMakeLists.txt
 6│   └── main.c
 7└── components/
 8    └── BSP/
 9        ├── CMakeLists.txt
10        └── LED/
11            ├── led.h
12            └── led.c

02-02-bsp-led 根目录重新选择目标并编译:

1idf.py set-target esp32s3
2idf.py build

接板后用同一个实际串口执行 idf.py -p <实际串口> flash monitor,验收标准与 2.1 完全相同。还可以在两个工程里分别搜索 GPIO_NUM_1:2.1 只应在 main/main.c 找到,2.2 只应在 components/BSP/LED/led.c 找到。这是板级细节从应用层移出的直接证据。

这次分层到底得到了什么

比较2.1 单文件2.2 BSP
main.c 管什么闪烁时序、GPIO1、有效电平闪烁时序与日志
GPIO1 和低电平有效在哪main/main.ccomponents/BSP/LED/led.c
依赖关系main → esp_driver_gpiomain → BSP → esp_driver_gpio
改板卡接线时修改应用文件集中修改 BSP 实现
额外成本文件少多了头文件、实现文件和组件 CMake

**BSP 的收益是隔离板级细节,不是让这一盏 LED 闪得更好。**如果程序只有一个灯,2.1 更直接;当按键、蜂鸣器等外设陆续加入时,统一的板级接口能防止应用流程里到处散落引脚和有效电平。led_set(bool) 也没有让程序自动兼容其他板子:换板仍要核对原理图并修改或替换 BSP 实现。这里两节都固定使用 GPIO1;引脚做成 Kconfig.projbuild 可配置项留待后续多板实践。

和教材官方工程对答案

教材第 10 章的官方 01_led 工程同样是 GPIO1 低电平点亮。本篇两种实现有意采用同一控制逻辑,方便只观察文件组织的变化;它们与官方实现的差异是:

项目官方 01_led本篇两个工程原因
引脚模式GPIO_MODE_INPUT_OUTPUT,使能上拉GPIO_MODE_OUTPUT,不使能内部上下拉本篇不通过 gpio_get_level() 读回状态
切换方式LED_TOGGLE() 读回电平后翻转led_set(true/false) 显式设置代码里直接看出亮灭顺序
延时vTaskDelay(500)vTaskDelay(pdMS_TO_TICKS(500))明确表达 500 毫秒
错误处理GPIO 返回值未检查ESP_ERROR_CHECK 检查调用失败时报告位置

官方工程的 CONFIG_FREERTOS_HZ=1000,因此它的 500 tick 在该工程里等于 500 毫秒;本篇工程默认 tick 配置可能不同,不能直接照抄数值。单纯 GPIO 点灯不需要 NVS 初始化,所以本篇没有加入教材其他片段里的 NVS 代码。

参考资料