基于ESP32与红外通信的TV-B-Gone项目实践:从原理到实现
1. 项目概述与核心思路
几年前,我在一个创客社区里偶然看到了一个叫“TV-B-Gone”的老项目,它的想法简单又带点恶作剧的趣味:一个能发射几乎所有常见电视关机红外码的小设备,让你能在公共场合“悄悄”关掉那些吵人的电视。这个由Mitch Altman发起的开源项目,其硬件版本已经迭代了很多年。当我手头拿到一块Lilygo T-Watch 2020时,我立刻意识到,把这块搭载了ESP32、带触摸屏的智能手表开发板,变成一个可穿戴的、更现代的TV-B-Gone,会是一件非常酷的事。这不仅仅是复刻,更是将一种经典的硬件黑客精神,移植到当下更普及、功能更强的物联网平台上。
这个项目的核心目标很明确:利用Lilygo T-Watch 2020内置的硬件资源,编写一个Arduino程序,使其能够循环发射一系列预存的红外关机信号,从而实现对兼容红外遥控的老式电视的“万能”关闭功能。它非常适合那些对嵌入式开发、物联网设备改装感兴趣的爱好者,无论是想学习ESP32与Arduino的结合使用,还是想深入了解红外通信协议,亦或是单纯想制作一个有趣的极客玩具,这个项目都能提供一条清晰的实践路径。你需要准备的硬件非常简单,主要就是一块Lilygo T-Watch 2020 V3(注意版本),软件上则需要Arduino IDE和相应的开发板支持库。
2. 硬件平台与开发环境搭建
2.1 认识Lilygo T-Watch 2020 V3
在开始敲代码之前,我们必须先了解手中的“武器”。Lilygo T-Watch 2020 V3并不是一个消费级智能手表,而是一个面向开发者的开源硬件平台。它的核心是一颗双核Xtensa® 32-bit LX6的ESP32芯片,主频高达240MHz,性能足以应对复杂的逻辑处理。更重要的是,它集成了我们项目所需的关键外设:
- 红外发射管(IR LED):通常位于手表顶部或侧面。这是我们的“信号枪”,负责将电信号转换为940nm左右的红外光脉冲。你需要确认你的手表版本是否焊接了此元件,早期有些版本可能需要自行焊接。
- 1.54英寸电容触摸屏:采用ST7789V驱动IC。这为我们提供了图形化交互界面,我们可以设计一个按钮来触发关机码发送,而不是依赖物理按键。
- 振动马达:用于提供触觉反馈。我们可以在代码发送红外信号时,让手表震动一下,作为操作确认。
- 电源管理:内置AXP202电源管理芯片,能高效管理电池充放电,确保长时间运行。
注意:市面上有T-Watch 2020的多个变种(如V1, V2, V3)。本项目主要基于V3版本进行开发,其引脚定义和库支持最为完善。如果你的版本不同,可能在引脚配置或库函数上需要微调。
2.2 Arduino IDE环境配置
Arduino IDE以其简单易用著称,是快速上手ESP32开发的绝佳选择。配置环境看似步骤繁多,但一步步来非常清晰:
安装Arduino IDE:从Arduino官网下载并安装最新稳定版(1.8.x或2.0.x均可)。
添加ESP32开发板支持:
- 打开Arduino IDE,进入
文件 -> 首选项。 - 在“附加开发板管理器网址”中,填入以下URL(如果已有其他,用逗号分隔):
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - 点击“好”保存。
- 打开Arduino IDE,进入
安装ESP32开发板包:
- 进入
工具 -> 开发板 -> 开发板管理器。 - 在搜索框中输入“esp32”。
- 找到由“Espressif Systems”发布的“esp32”平台,点击安装。这个过程会下载必要的编译工具链和核心库,耗时较长,请耐心等待。
- 进入
安装T-Watch专用库:
- 我们需要
LilyGo-T-Watch库来驱动屏幕、触摸和振动等外设。最方便的方式是通过Arduino的库管理器安装。 - 进入
项目 -> 加载库 -> 管理库...。 - 搜索“LilyGo T-Watch”,通常能找到由“LilyGo”发布的库,选择并安装。
- 这个库会依赖安装其他库如
LVGL(图形库)、TFT_eSPI等,一并同意安装即可。
- 我们需要
选择开发板与端口:
- 用USB-C数据线连接T-Watch和电脑。手表需处于开机状态。
- 在Arduino IDE中,
工具 -> 开发板选择 “ESP32 Arduino” 下的 “TTGO T-Watch”。 工具 -> Upload Speed设置为 “921600” 以获得更快的上传速度。工具 -> Port选择识别到的串口(在Windows上是COMx,在Mac/Linux上是/dev/cu.usbmodemxxx)。
实操心得:环境配置中最常见的坑是端口选择错误或驱动问题。如果连接后无法识别端口,可以尝试:
- 更换数据线(必须使用支持数据传输的线)。
- 在设备管理器中检查是否有未知设备,可能需要手动安装CP210x或CH340等USB转串口芯片的驱动(这些驱动通常在你安装的ESP32包中已包含,或可从芯片厂商官网下载)。
- 按住手表上的“复位”键再插入USB线,有时能强制进入下载模式。
3. 代码结构与核心逻辑解析
拿到一个开源项目,直接烧录固然快,但理解其代码结构才能举一反三。本项目的代码主要由四个文件构成,它们各司其职。
3.1 核心文件职责分解
TV-B-Gone.ino:这是项目的主程序文件,Arduino工程的入口。它包含了setup()和loop()两个核心函数,负责硬件初始化、图形界面创建和主业务逻辑调度。WORLD_IR_CODES.h:这是项目的“弹药库”。它以一个巨大的二维数组形式,存储了数百个针对不同品牌、不同型号电视的原始红外关机码(主要是北美NA和欧洲EU制式)。每个码由一系列的开/关脉冲时长(以微秒为单位)构成。config.h与main.h:这两个头文件是“配置中心”。config.h通常来自T-Watch的示例库,定义了手表硬件相关的引脚映射、屏幕参数、背光控制等。main.h则来自TV-B-Gone的ESP32移植库,定义了红外发射的引脚、频率(通常为38kHz)、以及一些全局变量和函数声明。
3.2 红外信号发射原理与编码
为什么一串数字能控制电视?这需要理解红外遥控的基本原理。它并非直接发送“关机”这个指令,而是发送一段特定的**脉冲位置调制(PPM)或脉冲宽度调制(PWM)**波形。
- 载波:为了抗干扰和提高发射效率,红外信号会调制在一个固定频率(通常是38kHz)的载波上。你可以把它理解为用38kHz这个“哨音”来传递信息,接收器只“听”这个频率的声音。
- 编码协议:常见的协议有NEC、RC-5、Sony SIRC等。每种协议都定义了自己的“语法”,比如:一个完整的信号以多长的引导码开始,“0”和“1”分别用什么脉宽组合表示。
- 数据:在协议框架内,包含设备地址码和命令码。例如,“松下电视的地址是0x04,关机命令是0x08”。
本项目的工作方式:WORLD_IR_CODES.h中存储的,并不是原始的协议帧数据,而是已经调制好的、最终的红外LED亮灭时间序列。代码的工作就是严格按照这个时间序列,以极高的时间精度(微秒级)控制连接到红外LED的GPIO引脚输出高电平和低电平。输出高电平时,红外LED以38kHz频率闪烁发光(产生载波);输出低电平时,LED熄灭。电视的红外接收头接收到这段光脉冲序列后,进行解调和解码,如果匹配到自身的关机指令,就执行关机动作。
重要提示:由于这些红外码采集于2009年左右,它们主要对应当时及更早的电视型号。现代许多智能电视可能使用了不同的红外协议、蓝牙或Wi-Fi进行控制,因此这个项目对新型电视很可能无效。这是其技术局限性,也是其“复古”趣味的一部分。
3.3 主程序逻辑流程剖析
让我们深入TV-B-Gone.ino,看它是如何运转起来的:
初始化阶段(
setup()函数):- 初始化串口调试,便于打印日志。
- 调用
watch.begin()初始化T-Watch硬件(屏幕、触摸、电源、振动等)。 - 设置屏幕亮度,加载LVGL图形库。
- 关键一步:将指定的GPIO引脚(例如
IR_PIN,在main.h中定义为4)设置为输出模式,用于控制红外LED。 - 创建一个全屏的按钮控件,并为其绑定一个回调函数(例如
btn_event_cb)。当用户在屏幕上点击这个按钮时,就会触发这个函数。
事件驱动与主循环(
loop()与 LVGL任务处理器):loop()函数的核心通常是调用lv_task_handler(),用于处理LVGL的界面刷新和触摸事件。Arduino的主循环在这里退居二线,主要任务交给了图形系统。- 当用户点击屏幕按钮时,LVGL会检测到触摸事件,并调用我们预先绑定的
btn_event_cb回调函数。
红外发射触发(回调函数内):
- 在回调函数中,首先触发一次短震动,给用户触觉反馈。
- 然后,调用核心的
sendAllCodes()或类似函数。 - 这个函数会遍历
WORLD_IR_CODES.h中的某个数组(例如NApowerCodes北美码表)。 - 对于数组中的每一组红外码,它通过一个精密计时循环(使用
micros()函数获取微秒级时间),按照码值交替控制IR_PIN输出高电平和低电平,从而生成精确的红外信号波形。 - 发送完一组码后,会延迟一小段时间(如100毫秒),再发送下一组,避免信号拥塞。
区域代码选择:在代码开头,通常通过宏定义来选择发送北美(NA)还是欧洲(EU)的码集。你需要根据所在地区的电视主流制式来修改:
// 对于北美用户 #define NA 1 #define EU 0 // 对于欧洲用户 // #define NA 0 // #define EU 1
实操心得:调试红外发射时,肉眼不可见。一个极其有用的技巧是用手机摄像头辅助调试。大多数手机摄像头的CMOS传感器对红外光敏感。在暗处,用手机摄像头对准手表上的红外发射管,当你触发发射时,你应该能在手机屏幕上看到发射管发出白色的闪烁光点。这是快速验证硬件连接和代码是否成功驱动了红外LED的最简单方法。
4. 完整项目实现与代码集成
理解了原理,现在我们将分散的代码整合成一个可编译、可上传的完整Arduino项目。
4.1 创建工程与文件管理
- 在Arduino IDE中,点击
文件 -> 新建,创建一个新的工程。 - 点击
文件 -> 保存,将其保存到一个独立的文件夹中,例如TWatch_TV_B_Gone。 - 在这个文件夹内,你需要创建或放置四个关键文件:
TWatch_TV_B_Gone.ino(主文件,可将下载的TV-B-Gone.ino内容复制过来)config.hmain.hWORLD_IR_CODES.h关键点:.ino文件必须与文件夹同名。头文件(.h)放在同一目录下即可,Arduino编译时会自动查找。
4.2 核心代码实现与定制化修改
以下是一个高度整合和注释后的主程序框架示例,展示了如何将各个部分串联起来:
// TV-B-Gone.ino #include "config.h" // T-Watch硬件配置 #include "main.h" // TV-B-Gone全局配置和函数声明 #include "WORLD_IR_CODES.h" // 红外码库 // 定义区域:1为启用,0为禁用 #define NA 1 // 北美码 #define EU 0 // 欧洲码 TWatchClass *watch = nullptr; // T-Watch对象 lv_obj_t *btn = nullptr; // LVGL按钮对象 // 屏幕按钮事件回调函数 static void btn_event_cb(lv_obj_t *obj, lv_event_t event) { if (event == LV_EVENT_CLICKED) { Serial.println("Button clicked, sending IR codes..."); // 1. 触觉反馈:振动1秒 watch->motor_shake(2, 50); // 参数可能因库版本而异,需调整 // 2. 发送红外关机码 sendAllCodes(); // 3. 可选:在屏幕上显示发送完成提示 lv_label_set_text(lv_obj_get_child(btn, NULL), "Sent!"); delay(1000); lv_label_set_text(lv_obj_get_child(btn, NULL), "TV-B-Gone\nClick Me"); } } void setup() { Serial.begin(115200); Serial.println("\n\nT-Watch 2020 TV-B-Gone Starting..."); // 初始化手表硬件 watch = TWatchClass::getWatch(); watch->begin(); watch->motor_shake(1, 20); // 开机短震 // 关闭蓝牙和Wi-Fi以省电(本项目不需要) watch->bluetooth_off(); watch->wifi_off(); // 设置屏幕亮度并初始化LVGL watch->backlight_set_value(128); // 亮度值0-255 watch->lvgl_begin(); // 设置红外发射引脚 pinMode(IR_PIN, OUTPUT); digitalWrite(IR_PIN, LOW); // 初始状态为低,关闭红外 // 创建用户界面 lv_obj_t *scr = lv_scr_act(); lv_scr_load(scr); lv_obj_set_style_local_bg_color(scr, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); btn = lv_btn_create(scr, NULL); lv_obj_set_size(btn, 200, 200); lv_obj_align(btn, NULL, LV_ALIGN_CENTER, 0, 0); lv_obj_set_event_cb(btn, btn_event_cb); lv_obj_t *label = lv_label_create(btn, NULL); lv_label_set_text(label, "TV-B-Gone\nClick Me"); lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, 0); Serial.println("Setup complete. Ready to zap TVs!"); } void loop() { lv_task_handler(); // 处理LVGL界面任务 delay(5); // 短暂延迟,释放CPU控制权 } // 发送所有红外码的核心函数 void sendAllCodes() { Serial.println("Starting to blast IR codes..."); #if NA int numCodes = sizeof(NApowerCodes) / sizeof(NApowerCodes[0]); Serial.printf("Sending %d NA codes.\n", numCodes); for (int i = 0; i < numCodes; i++) { sendCode(NApowerCodes[i]); delay(INTERVAL_BETWEEN_CODES); // 发送间隔,例如100ms } #endif #if EU int numCodes = sizeof(EUpowerCodes) / sizeof(EUpowerCodes[0]); Serial.printf("Sending %d EU codes.\n", numCodes); for (int i = 0; i < numCodes; i++) { sendCode(EUpowerCodes[i]); delay(INTERVAL_BETWEEN_CODES); } #endif Serial.println("All codes sent."); } // 发送单组红外码的函数 void sendCode(const uint16_t *code) { int index = 0; uint32_t codeLength = code[index++]; // 第一个元素是码的长度 uint32_t freq = code[index++]; // 第二个元素是频率(通常为38000) for (uint32_t i = 0; i < codeLength; i++) { uint32_t duration = code[index++]; if (i & 1) { // 奇数索引是“空间”(低电平,LED灭) digitalWrite(IR_PIN, LOW); } else { // 偶数索引是“脉冲”(高电平,LED以载波频率闪烁) // 这里需要生成38kHz载波。一个简单但占用CPU的方法是: uint32_t halfPeriod = 1000000L / freq / 2; // 计算半周期微秒数 for (uint32_t j = 0; j < (duration / (halfPeriod * 2)); j++) { digitalWrite(IR_PIN, HIGH); delayMicroseconds(halfPeriod); digitalWrite(IR_PIN, LOW); delayMicroseconds(halfPeriod); } } delayMicroseconds(duration); // 等待这个脉冲/空间的总时长 } digitalWrite(IR_PIN, LOW); // 发送完毕,确保LED关闭 }关键修改点:
- 振动函数:
watch->motor_shake()的参数需要根据你实际安装的LilyGo-T-Watch库的API进行调整。查阅库的示例代码是最好方法。 - 红外发射引脚:确认
IR_PIN在main.h或你的代码中的定义与T-Watch V3的实际连接引脚一致。常见的是GPIO4。 - 载波生成:上面的
sendCode函数中的载波生成循环是一个简化的、阻塞式的实现。在发送长码时可能会影响系统响应。更高效的方法是使用ESP32的LEDC(LED PWM控制器)硬件外设来生成38kHz的PWM波,然后只需控制其输出使能即可。这属于进阶优化。
4.3 编译与上传
- 确保所有文件已正确放置。
- 在Arduino IDE中,选择正确的开发板和端口。
- 点击“验证”(对勾图标)编译代码。首次编译会较慢,需要下载核心库。
- 编译无误后,点击“上传”(右箭头图标)。上传过程中,手表屏幕可能会闪烁或变黑,这是正常现象。
- 上传成功后,Arduino IDE会显示“上传完毕”。手表可能会自动重启。
5. 测试、优化与问题排查
5.1 功能测试与验证
上传完成后,手表屏幕应显示一个大的按钮。点击它,你会感觉到手表震动一下。
- 基础测试(手机摄像头法):在光线较暗的环境下,打开手机的相机应用,将手表顶部的红外发射管对准手机摄像头。点击手表屏幕按钮。你应该能在手机屏幕上清晰地看到红外发射管发出快速、明亮的白色闪烁光。这说明GPIO控制正常,红外LED在工作。
- 实际设备测试:找一台老式的CRT电视或早期液晶电视(2000-2010年代的产品成功率较高)。将手表红外发射头对准电视的红外接收窗(通常在正面下方或侧面),距离在2-5米内,中间无遮挡。点击发送按钮。如果电视在几秒到十几秒内关机,恭喜你,成功了!多尝试几个不同品牌的老电视。
5.2 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 编译错误 | 1. 库未安装或版本不兼容。 2. 头文件路径错误或内容缺失。 3. 开发板未正确选择。 | 1. 检查“管理库”中LilyGo-T-Watch和lvgl等库是否已安装。尝试更新到最新版。2. 确保四个源文件都在同一文件夹内,且 .ino文件与文件夹同名。检查#include语句的文件名是否正确。3. 确认开发板选择为“TTGO T-Watch”, ESP32核心版本建议使用较稳定的1.0.6或2.0.x。 |
| 上传失败 | 1. 端口被占用或选择错误。 2. 手表未进入下载模式。 3. 驱动问题。 | 1. 关闭其他可能占用串口的软件(如串口监视器)。重新拔插USB线,重新选择端口。 2. 尝试按住手表侧的“复位”(RST)按钮不放,再按一下“电源”(PWR)按钮,然后松开RST按钮。此时电脑应识别到新的下载端口。 3. 安装正确的CP210x或CH340 USB转串口驱动。 |
| 手机摄像头看不到红外光 | 1. 红外LED损坏或未焊接。 2. GPIO引脚定义错误。 3. 代码未正确控制引脚。 | 1. 检查硬件。用万用表二极管档测量红外LED是否正常。 2. 核对原理图,确认 IR_PIN的宏定义(如GPIO4)是否与实物连接一致。3. 在 sendCode函数开头添加Serial.println(“Sending code...”);,通过串口监视器查看程序是否执行到发射部分。 |
| 能看到红外光,但电视无反应 | 1. 电视太新,不支持旧码库。 2. 发射功率不足或距离太远。 3. 载波频率偏差大。 4. 码库区域错误。 | 1. 这是预期之内,项目主要针对老电视。 2. 确保红外发射头正对电视接收窗,距离拉近到1米内。可尝试将手表靠近电视不同角度。 3. 38kHz载波是关键。简易循环生成的频率可能不准。考虑改用ESP32的LEDC硬件PWM生成精确的38kHz方波。 4. 检查代码开头的 #define NA 1和#define EU 0,根据你的地理位置切换。 |
| 点击按钮无振动/屏幕卡死 | 1. 振动电机驱动代码错误。 2. LVGL任务阻塞。 | 1. 注释掉振动代码,先测试其他功能。参考库示例中的正确振动函数用法。 2. sendAllCodes()函数发送码时是阻塞的,期间lv_task_handler()无法执行,导致界面无响应。这是此简化设计的一个缺点。优化方向是使用FreeRTOS任务或将发送过程分段非阻塞化。 |
| 功耗过高,手表发热快 | 1. 屏幕背光常亮。 2. Wi-Fi/蓝牙未关闭。 | 1. 在setup()中可适当调低背光值,或添加代码在空闲时自动降低背光、进入睡眠。2. 确保 watch->wifi_off();和watch->bluetooth_off();被调用。红外发射时电流较大,属于正常现象。 |
5.3 进阶优化与扩展思路
一个基础功能实现后,便是极客精神发挥的时刻:
- 硬件优化:增强发射功率:手表内置的红外LED功率有限。你可以外接一个NPN三极管(如8050)驱动一个更大功率的红外发射管,并将它放在表带上,这样发射距离和角度都能大幅提升。
- 软件优化:非阻塞式发送与UI响应:当前的
sendAllCodes()会阻塞主循环。你可以利用ESP32的双核特性,创建一个独立的FreeRTOS任务来负责红外发送,这样UI在发送时依然可以响应。或者,使用状态机在loop()中分段发送,每次只发送一个码的一部分,用millis()进行非阻塞延时。 - 功能扩展:学习与自定义遥控:让项目不止于“关机”。可以增加一个“学习模式”,用手表接收其他遥控器的信号并存储下来。然后你可以创建多个按钮,分别对应“开机”、“音量+”、“频道-”等,把它变成一个真正的万能学习型遥控手表。
- 界面美化:利用LVGL库,设计更炫酷的界面,比如模拟一个复古的电视关机动画,或者添加发射进度条。
这个基于Lilygo T-Watch 2020的TV-B-Gone项目,从看到想法到亲手实现,整个过程充满了硬件交互的乐趣。它不仅仅是一个恶作剧工具,更是一个深入了解红外通信协议、ESP32编程和LVGUI设计的绝佳切入点。当你拿着自己改装的手表,成功让一台老电视悄然熄灭时,那种成就感是纯粹的。硬件项目的魅力就在于此,代码和电路从虚拟变为现实,并产生了物理世界的影响。
