ESP32 实战:用 UART + select() 做个能听你指令的小控制器

在这里插入图片描述

网罗开发 (小红书、快手、视频号同名)

  大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。

图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG

我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。

展菲:您的前沿技术领航员
👋 大家好,我是展菲!
📱 全网搜索“展菲”,即可纵览我在各大平台的知识足迹。
📣 公众号“Swift社区”,每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友“fzhanfei”,与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
📅 最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!


摘要

在嵌入式开发中,串口(UART)通信几乎是最常见的数据交互方式之一。本文基于 ESP-IDF 提供的 UART + select() 示例代码,构建一个实际功能:通过串口输入字符控制板载 LED 的开关。通过这个例子,你不仅能理解 select() 如何监听串口事件,还能将它用在日常开发中,比如调试工具、控制终端或串口菜单等。

场景描述

想象一下,有时候我们手头没有显示屏,也没有网络连接,想控制一下开发板的行为怎么办?比如控制一个继电器开关、切换系统模式、输出状态信息。这个时候,最简单的方式就是——用串口发送一条指令,让 ESP32 响应

比如我发个 1,灯亮;发个 0,灯灭。是不是比去找按键省事多了?

代码核心

我们基于 UART0,通过 /dev/uart/0 文件描述符用 select() 来监听串口输入,收到字符后做出响应。这里我们扩展一下逻辑——接收到字符 '1' 时点亮 LED,接收到 '0' 时熄灭 LED。

来看下主要修改的部分代码:

#include "driver/gpio.h"

// 定义 LED 使用的 GPIO(这里假设是 GPIO2)
#define LED_GPIO GPIO_NUM_2

static void uart_select_task(void *arg)
{
    // 安装 UART 驱动
    if (uart_driver_install(UART_NUM_0, 2 * 1024, 0, 0, NULL, 0) != ESP_OK) {
        ESP_LOGE(TAG, "Driver installation failed");
        vTaskDelete(NULL);
    }

    // 配置 UART 参数
    uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity    = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .source_clk = UART_SCLK_DEFAULT,
    };
    uart_param_config(UART_NUM_0, &uart_config);

    // 初始化 LED GPIO
    gpio_reset_pin(LED_GPIO);
    gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);

    while (1) {
        int fd;

        if ((fd = open("/dev/uart/0", O_RDWR)) == -1) {
            ESP_LOGE(TAG, "Cannot open UART");
            vTaskDelay(5000 / portTICK_PERIOD_MS);
            continue;
        }

        uart_vfs_dev_use_driver(UART_NUM_0);

        while (1) {
            fd_set rfds;
            FD_ZERO(&rfds);
            FD_SET(fd, &rfds);

            struct timeval tv = {
                .tv_sec = 5,
                .tv_usec = 0,
            };

            int s = select(fd + 1, &rfds, NULL, NULL, &tv);

            if (s > 0 && FD_ISSET(fd, &rfds)) {
                char buf;
                if (read(fd, &buf, 1) > 0) {
                    ESP_LOGI(TAG, "Received: %c", buf);
                    if (buf == '1') {
                        gpio_set_level(LED_GPIO, 1);
                        ESP_LOGI(TAG, "LED ON");
                    } else if (buf == '0') {
                        gpio_set_level(LED_GPIO, 0);
                        ESP_LOGI(TAG, "LED OFF");
                    } else {
                        ESP_LOGW(TAG, "Unknown command: %c", buf);
                    }
                }
            }
        }

        close(fd);
    }

    vTaskDelete(NULL);
}

内容解析

这段代码里,关键点主要有这几个:

  • uart_driver_install()uart_param_config() 是基础配置,安装 UART 驱动,设置波特率、数据位等。
  • open("/dev/uart/0", O_RDWR) 使用文件系统的方式访问 UART,这点对习惯 Linux 编程的同学会很友好。
  • select() 是重点:它在设定时间内等待数据到来,防止程序阻塞或浪费 CPU。
  • 接收到数据后通过 read() 读一个字符,根据字符判断控制 GPIO 输出。
  • 最终实现用串口字符 "1" 控制 LED 亮,字符 "0" 控制 LED 灭。

下面我来把这个 UART + select() 控制 LED 的核心代码,按模块分段讲解,适合初级嵌入式开发者理解。每一段我都会解释它是干嘛的、怎么用、注意点是什么,尽量用接地气的方式讲。

模块 1:初始化 UART 驱动

if (uart_driver_install(UART_NUM_0, 2 * 1024, 0, 0, NULL, 0) != ESP_OK) {
    ESP_LOGE(TAG, "Driver installation failed");
    vTaskDelete(NULL);
}

作用:

安装 UART0 的驱动,让系统知道我们要用这个串口来收发数据。

参数解释:

  • UART_NUM_0:表示我们用的是 UART0;
  • 2 * 1024:接收缓冲区大小是 2KB;
  • 0:发送缓冲区不用(写数据不会缓存);
  • 0, NULL, 0:这些是事件队列相关的,这里我们用不到,直接设为 0 和 NULL 就行。

注意:

这个函数是“开工之前”的准备工作,不装驱动后面的读写都没法用。

模块 2:配置 UART 参数

uart_config_t uart_config = {
    .baud_rate = 115200,
    .data_bits = UART_DATA_8_BITS,
    .parity    = UART_PARITY_DISABLE,
    .stop_bits = UART_STOP_BITS_1,
    .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
    .source_clk = UART_SCLK_DEFAULT,
};

uart_param_config(UART_NUM_0, &uart_config);

作用:

设置串口的基本参数,比如波特率、数据位这些。

参数说明:

  • 115200:波特率,一般默认用这个;
  • 8_BITS:数据位是 8 个;
  • parity disable:不校验;
  • stop_bits = 1:停止位 1 位;
  • flow_ctrl:不开硬件流控;
  • source_clk:使用默认时钟。

注意:

串口两端的参数必须一致,否则通信会出错。

模块 3:初始化 GPIO 作为 LED 控制引脚

gpio_reset_pin(GPIO_NUM_2);
gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);

作用:

把 GPIO2 设为输出,用来控制 LED。

注意:

  • GPIO_NUM_2 是板载 LED 常用的引脚,开发板不同可能不一样;
  • 一定要配置成 OUTPUT,不然后面控制电平无效。

模块 4:打开 UART 文件,准备监听

int fd;
if ((fd = open("/dev/uart/0", O_RDWR)) == -1) {
    ESP_LOGE(TAG, "Cannot open UART");
    vTaskDelay(5000 / portTICK_PERIOD_MS);
    continue;
}
uart_vfs_dev_use_driver(UART_NUM_0);

作用:

打开 /dev/uart/0,相当于告诉系统“我要读写这个串口”。

注意:

  • 这是 ESP-IDF 的 VFS 接口,串口像一个文件一样用;
  • 要用 uart_vfs_dev_use_driver() 绑定 driver,否则你虽然“打开了文件”,但系统不知道怎么读写它。

模块 5:用 select() 监听串口有没有数据

FD_ZERO(&rfds);
FD_SET(fd, &rfds);

struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };

int s = select(fd + 1, &rfds, NULL, NULL, &tv);

作用:

监听串口有没有数据可读,如果 5 秒内没有,就超时返回。

注意:

  • FD_ZERO / FD_SET:设置监听目标;
  • select() 是“等数据来了再处理”,节省 CPU 资源;
  • 如果你不用 select(),就只能一直 read(),那样效率低。

模块 6:读取数据并判断操作

if (FD_ISSET(fd, &rfds)) {
    char buf;
    if (read(fd, &buf, 1) > 0) {
        ESP_LOGI(TAG, "Received: %c", buf);
        if (buf == '1') {
            gpio_set_level(GPIO_NUM_2, 1);  // LED ON
        } else if (buf == '0') {
            gpio_set_level(GPIO_NUM_2, 0);  // LED OFF
        } else {
            ESP_LOGW(TAG, "Unknown command: %c", buf);
        }
    }
}

作用:

  • 如果串口有数据,就读一个字符出来;
  • 判断是 '1' 还是 '0',然后控制 LED 的亮灭。

注意:

这里只读了一个字节,如果你要处理多字符命令(比如 "LED ON"),就得改成读多个字符、解析字符串了。

模块 7:主程序入口 app_main

xTaskCreate(uart_select_task, "uart_select_task", 4 * 1024, NULL, 5, NULL);

作用:

创建一个任务,运行我们的 uart_select_task 函数。

参数说明:

  • "uart_select_task":任务名称;
  • 4 * 1024:栈大小(4KB);
  • 5:任务优先级,5 是中等优先;
  • NULL:没传参数,也不需要任务句柄。

实际效果测试

  1. 烧录程序到 ESP32;
  2. 用串口工具(如 CoolTerm / PuTTY / minicom)连接设备;
  3. 输入字符 1,板载 LED 亮;
  4. 输入字符 0,LED 灭;
  5. 输入其他字符,比如 a,日志输出“Unknown command”。

时间复杂度

在这个案例中,主要处理的逻辑都是常量时间 O(1) 的操作,没有复杂的数据结构或算法迭代。

空间复杂度

空间上几乎没有额外占用,主要内存用于 UART 缓冲区和一个字符变量,空间复杂度也是 O(1)。

总结

这个小例子展示了一个非常实用的 UART 通信控制方法。在很多没法连接网络、没有 GUI 的嵌入式场景里,串口就是我们和设备交流的“唯一窗口”。用 select() 来监听串口,既不会让程序一直卡着,又能实时响应输入,非常适合用于开发调试工具、串口控制终端等。

下一步你还可以扩展一下,比如:

  • 接收完整指令(如 LED ON / LED OFF);
  • 通过 JSON 命令控制多个 GPIO;
  • 实现串口菜单功能,提供不同的系统操作项。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

网罗开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值