大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括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
:没传参数,也不需要任务句柄。
实际效果测试
- 烧录程序到 ESP32;
- 用串口工具(如 CoolTerm / PuTTY / minicom)连接设备;
- 输入字符
1
,板载 LED 亮; - 输入字符
0
,LED 灭; - 输入其他字符,比如
a
,日志输出“Unknown command”。
时间复杂度
在这个案例中,主要处理的逻辑都是常量时间 O(1) 的操作,没有复杂的数据结构或算法迭代。
空间复杂度
空间上几乎没有额外占用,主要内存用于 UART 缓冲区和一个字符变量,空间复杂度也是 O(1)。
总结
这个小例子展示了一个非常实用的 UART 通信控制方法。在很多没法连接网络、没有 GUI 的嵌入式场景里,串口就是我们和设备交流的“唯一窗口”。用 select()
来监听串口,既不会让程序一直卡着,又能实时响应输入,非常适合用于开发调试工具、串口控制终端等。
下一步你还可以扩展一下,比如:
- 接收完整指令(如
LED ON
/LED OFF
); - 通过 JSON 命令控制多个 GPIO;
- 实现串口菜单功能,提供不同的系统操作项。