用 ESP32-S3 打造你的智能桌面触控副屏:不只是“小屏幕”,而是生产力外挂 💡
你有没有过这样的时刻?
正在写代码,突然要切到终端运行命令;做设计时频繁在 Photoshop 的图层和工具栏之间来回跳;直播时手忙脚乱地切换场景、调音量、开弹幕……手指在键盘鼠标间飞舞,脑子却跟不上节奏。
我们每天都在和电脑“搏斗”——不是因为它不好用,而是它缺少一个 真正懂你工作流的助手 。
而今天,我想告诉你:这个助手,完全可以自己动手做一个。
一块小小的彩色触摸屏,几个按钮,一点无线通信能力,再配上一颗 ESP32-S3 芯片,就能变成你桌面上最顺手的那个“第三只手”。
别误会,这不是什么极客炫技项目。它是实实在在能提升效率的东西 —— 就像当年程序员发现 Vim 快捷键一样,一旦用了就回不去了 🚀
为什么是 ESP32-S3?这颗芯片有点“猛”
说到嵌入式开发板,大家第一反应可能是 Arduino 或者树莓派 Pico。但如果你真想做个带彩屏、能联网、还能流畅响应触摸的设备,那些家伙就有点力不从心了。
而 ESP32-S3 ,简直就是为这类任务量身定做的“六边形战士”。
它来自乐鑫(Espressif),也就是搞出 ESP8266 和 ESP32 的那家公司。但这回他们玩得更狠了:
- 双核 Xtensa 32 位处理器,主频飙到 240MHz
- 支持 Wi-Fi 4 + Bluetooth 5 (LE),还能走 USB OTG
- 内建 LCD 接口(SPI/I80/RGB),直接驱动 TFT 屏不用额外控制器
- 原生支持 14 个电容式触摸 GPIO ,连触摸 IC 都可以省掉
- 开发环境成熟:ESP-IDF、Arduino、MicroPython 全都支持
最关键的是——价格便宜!一片 ESP32-S3-WROOM 模组,淘宝上也就十几二十块钱 💸
这意味着什么?
意味着你可以花不到 ¥80 的成本,做出一个功能堪比 Elgato Stream Deck 的设备,而且还能无限定制、OTA 升级、跨平台使用。
“那不就是个宏键盘?”
不完全是。
宏键盘只能按预设顺序发按键,而我们的目标是一个 有图形界面、能反馈状态、会主动交互的智能前端 。
怎么让它显示画面?LVGL + SPI TFT 是黄金组合
光有芯片不行,得让东西“看得见”。这时候就得搬出两个关键角色: TFT LCD 屏幕 和 LVGL 图形库 。
先说硬件:选块合适的屏幕
市面上常见的小尺寸 TFT 多是基于 ST7789、ILI9341 或 ILI9806 驱动的 SPI 接口屏,分辨率从 128×128 到 320×480 不等。我推荐起步用 2.4” 或 2.8” 的 240×240 屏 ,够大又不至于太占桌面空间。
这些屏幕通过 SPI 总线连接 ESP32-S3,典型接线如下:
| 屏幕引脚 | ESP32-S3 引脚 |
|---|---|
| SCL | GPIO18 |
| SDA | GPIO19 |
| CS | GPIO5 |
| DC | GPIO2 |
| RST | GPIO4 |
| BLK/VCC/GND | 对应供电 |
虽然 SPI 看似“慢”,但只要配置得当,刷帧率完全够用。实测下,ST7789 在 40MHz 主频下,全屏刷新一次大约 20ms,也就是理论 50fps —— 对 GUI 来说绰绰有余。
再看软件:LVGL 让一切变得简单
LVGL(Light and Versatile Graphics Library)是个开源轻量级 GUI 框架,专为嵌入式系统设计。它支持按钮、滑块、标签、图表、动画,甚至 CSS 风格布局,关键是内存占用低,跑在几百 KB RAM 上也没问题。
在 ESP-IDF 中集成 LVGL 几乎是一键完成的事。官方已经提供了完整的 porting 示例,你只需要告诉它:“我的屏幕多大?怎么初始化 SPI?”剩下的渲染、事件处理、内存管理,LVGL 自己搞定。
下面这段代码,就是点亮屏幕的核心骨架 👇
#include "lvgl.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
static void lv_tick_task(void *arg) {
lv_tick_inc(1); // 每毫秒调一次,维持动画计时
}
void app_main(void) {
// 初始化 SPI 总线
spi_bus_config_t buscfg = {
.sclk_io_num = 18,
.mosi_io_num = 19,
.miso_io_num = -1,
.max_transfer_sz = 32768,
};
spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
// 配置 LCD IO 控制
esp_lcd_panel_io_handle_t io_handle;
esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = 2,
.cs_gpio_num = 5,
.spi_mode = 0,
.pclk_hz = 40 * 1000 * 1000,
};
esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &io_handle);
// 创建面板实例(以 ST7789 为例)
esp_lcd_panel_handle_t panel_handle;
esp_lcd_panel_dev_st7789_config_t panel_cfg = {
.reset_gpio_num = 4,
.bus = io_handle,
.width = 240,
.height = 240,
};
esp_lcd_new_panel_st7789(&panel_cfg, &panel_handle);
// 启动面板
esp_lcd_panel_reset(panel_handle);
esp_lcd_panel_init(panel_handle);
// 初始化 LVGL
lv_init();
lv_port_disp_init(panel_handle); // 自定义移植函数
// 添加定时器,每 1ms 触发 tick
const esp_timer_create_args_t timer_args = {
.callback = lv_tick_task,
.name = "lv_tick"
};
esp_timer_handle_t lv_timer;
esp_timer_create(&timer_args, &lv_timer);
esp_timer_start_periodic(lv_timer, 1000); // 1ms
// 创建主界面
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_pos(btn, 80, 80);
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "Hello!");
// 主循环处理 GUI 事件
while (1) {
lv_task_handler(); // 核心!处理所有 GUI 动画与输入
vTaskDelay(pdMS_TO_TICKS(10));
}
}
看到没?就这么几十行代码,就已经能在屏幕上画出一个按钮了。
重点在于最后那个 lv_task_handler() —— 它就像 LVGL 的“心跳”,必须定期调用,否则按钮不会响应、动画也会卡住。所以我们通常把它放在一个独立任务里跑,优先级高于其他非实时任务。
触摸是怎么实现的?两种方案,各有千秋
有了画面,下一步当然是“点它”!
目前主流的实现方式有两种: 外接 I2C 触摸 IC 和 原生触摸 GPIO 检测 。它们适用场景不同,选哪个要看你要做什么。
方案一:I2C 触摸 IC(适合全屏操作)
这是最常见的做法。你在买的 TFT 模块背面会发现一块黑色小芯片,比如 FT6236、GT911、SSD2808 —— 这些都是专用的电容触摸控制器。
它们的工作原理很简单:
- 芯片持续扫描整个触摸面板;
- 当手指靠近时,局部电容发生变化;
- 芯片计算出 X/Y 坐标,并存入寄存器;
- ESP32-S3 通过 I2C 定期读取这些数据。
优点非常明显:
- 支持多点触控(最多 5 点)
- 分辨率高(可达 1024×1024),定位精准
- 抗干扰能力强,稳定性好
缺点嘛……就是多了个芯片,稍微增加一点 BOM 成本。
下面是读取 FT6236 的示例代码:
#define TOUCH_ADDR 0x38
#define REG_STATUS 0x02
#define REG_XH 0x03
#define REG_YH 0x05
static bool touch_read(lv_indev_drv_t *drv, lv_indev_data_t *data) {
uint8_t buf[4];
i2c_master_write_read_device(I2C_NUM_0, TOUCH_ADDR, ®_STATUS, 1, buf, 4, portMAX_DELAY);
uint8_t stat = buf[0] & 0x0F;
if (stat == 0) {
data->state = LV_INDEV_STATE_REL;
return false;
}
int x = ((buf[0] & 0x0F) << 8) | buf[1];
int y = ((buf[2] & 0x0F) << 8) | buf[3];
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PR;
return false; // 没有更多点
}
// 在初始化时注册
lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touch_read;
lv_indev_drv_register(&indev_drv);
注意这里的 read_cb 回调函数 —— LVGL 会在每次需要获取触摸状态时自动调用它。你只需返回当前坐标和按下/释放状态即可。
这种模式下,你可以轻松实现滑动菜单、长按弹窗、双击切换等功能,体验接近手机。
方案二:原生触摸 GPIO(适合按钮式控制)
如果你不需要精确坐标,只是想做几个“虚拟按键”,那其实根本不用外接触摸芯片。
ESP32-S3 自带 14 个电容式触摸引脚(T0~T13) ,可以直接检测导体表面的电容变化。
怎么做?
很简单:把一根杜邦线焊到 T0 引脚,另一端贴一小块铜箔或导电胶布,就成了一个“触摸按钮”。当你手指靠近时,引脚充放电时间变长,芯片就能感知到“有人碰了”。
代码也极其简洁:
#include "driver/touch_pad.h"
#define BUTTON_PIN TOUCH_PAD_NUM9
void touch_button_init() {
touch_pad_init();
touch_pad_config(BUTTON_PIN, 0); // 阈值由系统自动校准
}
bool is_touched() {
uint16_t val;
touch_pad_read(BUTTON_PIN, &val);
return val < 50; // 实际阈值需测试调整
}
然后在一个任务中轮询检测:
void button_task(void *arg) {
while (1) {
if (is_touched()) {
ESP_LOGI("BTN", "Button pressed!");
// 发送指令 or 模拟按键
vTaskDelay(pdMS_TO_TICKS(200)); // 简单防抖
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
这种方式的优势在于:
- 零成本 :不需要任何额外芯片
- 低功耗 :触摸外设可独立工作,CPU 可休眠
- 高度灵活 :按钮位置完全自定义,甚至可以藏在木桌下当隐藏开关 😎
当然也有局限:
- 只能判断“是否被触”,不能精确定位
- 不支持滑动、手势等复杂交互
- 每个引脚对应一个区域,扩展性有限
所以更适合做固定功能的快捷键面板,比如“播放/暂停”、“下一曲”、“一键 mute”。
如何跟电脑通信?三种方式任你挑
屏幕能看,按钮能点,接下来最关键的问题来了: 怎么让 PC 知道你点了啥?
这里有三条路可走,各有利弊,你可以根据需求自由选择。
路径一:USB CDC 虚拟串口(稳定可靠,调试神器)
这是最简单的方案。利用 ESP32-S3 的 USB OTG 功能,模拟成一个串口设备插进电脑。PC 端用 Python/C#/Node.js 监听串口,收到字符串指令后执行相应动作。
例如,点击“静音”按钮,ESP 发送:
{"action":"toggle_mute"}
PC 端解析 JSON,调用 PowerShell 执行:
$vol = new-object -com wscript.shell; $vol.SendKeys('^{M}')
优点:
- 即插即用,无需配对
- 数据传输稳定,延迟低
- 适合开发阶段频繁调试
缺点:
- 必须插线,不够“无线自由”
- 需要在 PC 上运行监听程序
不过对于固定工位用户来说,这反而是优点 —— 线缆反而保证了连接可靠性。
路径二:Wi-Fi TCP Socket(远程控制,动态配置)
如果希望摆脱线缆束缚,Wi-Fi 是首选。
ESP32-S3 连上局域网后,作为 TCP Client 主动连接 PC 上的一个服务端口(比如 8888)。每次点击按钮,就发送一条 JSON 指令。
PC 端可以用 Python 写个简单服务器:
import socket
import json
import subprocess
def handle_client(conn):
data = conn.recv(1024).decode()
cmd = json.loads(data)
if cmd['action'] == 'mute':
subprocess.run(['powershell', '-c', '(New-Object -ComObject WScript.Shell).SendKeys("^%{M}")'])
# 主循环监听
sock = socket.socket()
sock.bind(('', 8888))
sock.listen(1)
while True:
conn, addr = sock.accept()
handle_client(conn)
conn.close()
好处显而易见:
- 完全无线,移动摆放不受限
- 支持 OTA 固件更新、远程重配界面
- 可接入家庭网络,实现跨房间控制
但也需要注意:
- 要处理断连重连机制
- 建议加简单认证,防止局域网内被恶意操控
- IP 地址变动可能导致连接失败(可用 mDNS 解决)
路径三:BLE HID 模拟键盘(即插即用,免驱)
这是最“无感”的方式 —— 让 ESP32-S3 变成一个蓝牙键盘。
当用户点击按钮时,设备直接发送 HID 报文,告诉电脑“我按下了 F13”或者“Ctrl+Shift+S”。
PC 根本不知道这是个触屏面板,只觉得多了一个神奇的快捷键设备 ✨
实现也不难,ESP-IDF 提供了 BT/BLE HID Device 示例。你只需定义一组按键映射:
uint8_t key_report[8] = {0};
key_report[2] = HID_USAGE_CONSUMER_MUTE; // 静音
esp_hidd_send_keyboard_report(hid_dev, 0, key_report, 8);
优点炸裂:
- 无需安装任何驱动或客户端软件
- 支持 Windows/macOS/Linux 全平台
- 插电即用,用户体验极佳
缺点也很明确:
- 只能发按键,无法接收反馈(比如“当前是否已静音”)
- 无法传递复杂指令(如参数化命令)
- 某些系统对 BLE HID 设备有安全限制
所以适合做通用型快捷键设备,不适合需要双向通信的高级场景。
实战案例:给程序员做的“IDE 快捷中心”
说了这么多技术细节,不如来点实际的。
这是我给自己做的一个真实项目:一块 2.4” 触屏,固定在显示器边框上,专门服务于 VS Code 工作流。
界面上有这几个按钮:
- 🔁 Build & Run(编译并运行当前项目)
- 🐞 Start Debug(启动调试会话)
- 📂 Open Terminal(打开集成终端)
- 🔄 Format Code(格式化代码)
- 📤 Push Git(提交并推送)
每个按钮背后,ESP32-S3 通过 Wi-Fi 向本地 Node.js 服务发送指令,后者调用 VS Code 的 CLI 工具或 D-Bus API 实现控制。
比如点击“Build & Run”,流程是这样的:
- ESP 发送:
{"cmd": "run_task", "task": "build_and_run"} - Node.js 收到后执行:
bash code --reuse-window --goto . code --wait && npm run build:run - 执行完成后返回结果,ESP 屏幕显示绿色勾号 ✔️
整个过程不到 1 秒,比手动 Ctrl+Shift+P 再选任务快得多。
更酷的是,我还加入了“上下文感知”逻辑:
- 如果当前文件是
.py,则“Run”按钮执行python main.py - 如果是
.js,则运行node index.js - 如果 git 有未提交更改,“Push”按钮变红提醒
这才是真正的“智能辅助”——不只是快捷键,而是理解你在干什么。
设计经验谈:这些坑我都踩过了 ⚠️
别看现在说得轻松,刚开始我也被各种问题搞得焦头烂额。
分享几点血泪教训,帮你少走弯路:
1. 电源噪声导致屏幕闪屏?
一定是供电不稳!
TFT 屏幕对电源质量很敏感,尤其是背光开启瞬间电流突增。建议:
- 使用 AMS1117-3.3 或 MP1584 等稳压模块单独供电
- 在 VCC 和 GND 之间并联 100μF 电解电容 + 0.1μF 陶瓷电容滤波
- 避免与电机、继电器共用电源
2. 触摸不准、漂移严重?
先检查接地。
很多初学者把触摸屏的 GND 和 MCU 的 GND 分开接,结果形成地环路,干扰极大。
正确做法:
- 所有模块共地,走粗线
- 触摸 IC 的 INT 引脚加上拉电阻(通常 4.7kΩ)
- 在软件中加入滑动平均滤波:
c x_filtered = x_filtered * 0.7 + new_x * 0.3;
3. LVGL 卡顿、掉帧?
优化策略很重要:
- 开启双缓冲:
disp_buf.size = screenWidth * 10; - 启用部分刷新(partial update),只重绘变化区域
- 把耗时操作(如网络请求)放到后台任务,避免阻塞 GUI 线程
- 图片尽量用压缩格式(如 LZ4),减少 Flash 读取压力
4. Wi-Fi 断连怎么办?
一定要加心跳保活机制!
// 每 30 秒 ping 一次 PC
void keepalive_task(void *arg) {
while (1) {
if (tcp_connected) {
send_packet("{\"ping\":1}");
}
vTaskDelay(pdMS_TO_TICKS(30000));
}
}
同时设置自动重连逻辑,确保网络恢复后能快速回归。
成本 vs 商品:自己做真的划算吗?
很多人问:“买个 Stream Deck Mini 才两百多,干嘛费劲自己搞?”
我们来算笔账 💰
| 项目 | 自制方案 | 商用产品(Stream Deck Mini) |
|---|---|---|
| 硬件成本 | ~¥75(ESP32-S3 + 屏 + 外壳) | ¥200+ |
| 按钮数量 | 不限(可软件定义) | 固定 6 个 |
| 显示内容 | 完全自定义,动态刷新 | 固定图标,需配套软件 |
| 功能扩展 | 支持 OTA、语音反馈、传感器联动 | 闭源,功能受限 |
| 跨平台 | 支持 Win/macOS/Linux | 主要优化 Windows/macOS |
| 个性化程度 | 极高,可深度集成工作流 | 中等 |
看出区别了吗?
商用产品赢在“开箱即用”,但长期来看, 可编程性才是王道 。
更重要的是,自制的过程本身就是一种创造。你会更了解自己的设备,也能随时根据新需求迭代升级。
比如我现在已经在第二代产品中加入了:
- 温湿度传感器(实时显示环境数据)
- RGB 氛围灯(根据系统负载变色)
- 语音提示(TTS 播报构建结果)
- LittleFS 文件系统(存储用户配置)
这些东西,没有哪个成品设备能给你。
写在最后:技术的意义,是让人活得更轻松
做这个项目的初衷,其实很简单:我不想再为了调音量而去摸耳机线,也不想在写代码时分心去找快捷键。
我希望有一个安静的伙伴,懂我的习惯,默默帮我完成那些重复的小事。
而现在,它就在我的桌面上,亮着柔和的光,随时待命。
有时候我会想,科技发展了这么多年,我们拥有了超算、AI、元宇宙……但真正改变生活的,往往是最朴素的那一部分。
一块小屏幕,几个按钮,一段代码。
但它让你每天节省 10 分钟,一年就是 60 小时 —— 相当于多出一周假期。
这,就是 DIY 的魅力。
你不需要成为专家才能开始。
只要你愿意花一个周末,跟着教程焊几根线、烧录一段代码,就能拥有属于自己的“数字外脑”。
而当你第一次点击自己做的按钮,看到电脑乖乖执行命令的时候……
那种感觉,真的很爽 😎
📌 附:快速上手资源清单
- 开发框架 : ESP-IDF
- 图形库 : LVGL
- 屏幕驱动库 : esp_lcd
- 触摸示例 :
peripherals/touch_pad示例工程 - 完整项目参考 :
- https://github.com/lvgl/lv_port_esp32
- https://github.com/Aircoookie/WLED (学习 UI 架构)
🔧 工具推荐:
- 使用 ESP32-S3-DevKitC-1 开发板(自带 USB-JTAG)
- 屏幕选带 I2C 触摸的 2.4” ST7789 模块(淘宝搜“ESP32 触摸屏”)
- 编程用 VS Code + ESP-IDF 插件,体验丝滑
现在,就去点亮你的第一块像素吧。✨

2325


被折叠的 条评论
为什么被折叠?



