尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

ESP32复古哑终端DIY:用VGA与PS/2键盘打造纯正CRT时代交互体验

ESP32复古哑终端DIY:用VGA与PS/2键盘打造纯正CRT时代交互体验 最近在整理房间时翻出一台老戴尔的VGA显示器突然特别想做一台上世纪七八十年代那种象牙白色的哑终端。所谓Retro Dumb Terminal核心理念就一个字哑。它自己不跑程序、不装系统只负责把键盘输入发出去、把主机返回的字符显示在屏幕上开机等两秒就能进终端。比起现在动不动就装个桌面环境的“伪复古”这种纯粹到极致的设备反而更有味道。这个项目做出来之后能干嘛对我来说主要是三件事一是作为我随手写代码的专注终端连上一台Linux服务器当全屏编辑器用二是摆在书桌上当装饰品那种绿色荧光字符滚动起来本身就是赛博朋克风三是纯粹满足折腾欲从焊电路到写固件再到做外壳全流程都能玩一遍。适合想深入了解串口通信、单片机视频输出、终端仿真原理的人也适合喜欢复古硬件但又不想直接买一台原厂VT100的玩家毕竟那玩意的价格实在不太友好。我最后选择的主方案是ESP32 PS/2键盘 自制VGA输出电路 UART/网络双通道连接宿主机外壳用一台老式收音机的木质框改装。下面我把整个项目的思路、选型、实操过程、踩坑记录全部拆开讲你可以直接照着复现。1. 项目缘起与整体思路拆解1.1 什么是“哑终端”它凭什么这么“哑”哑终端的英文叫Dumb Terminal是相对于智能终端而言的。智能终端自己能跑程序比如现在你手机上的终端App其实是把键盘输入编码后发给远程服务器本地还要负责绘制界面、处理字体哑终端则更彻底本地几乎没有任何计算能力甚至很多型号连字体库都没有所有字符图案都是直接从主机接收的像素数据。如果拿生活中的东西类比它有点像老式的固定电话座机本身不具备任何“智能”只是把你说话的声音编码后传到交换机对方的声音再原样解码播放。哑终端也是一样键盘按键被编码成ASCII码或转义序列通过串口线或网线发给主机主机的Shell程序运行后把输出返回来终端就把这些输出渲染成肉眼可见的字符。正因为本地不做任何加工哑终端反而有一个今天智能设备很难给到的体验确定性。你按下回车屏幕上下一秒一定出现命令输出结果没有输入法弹窗没有焦点抢占没有任何后台进程偷偷跑。这种极简交互在现代反而显得奢侈。做这个项目最核心的目标就是把这个“确定性”重新拉回来。1.2 方案选型为什么我选了ESP32而不是树莓派动手之前我先在纸上列了几条路每条都仔细盘过才最终定下来。方案A是树莓派Zero 2 W加一块VGA HAT再接老式CRT。优点是开发快性能强劲跑个完整的Linux终端模拟器毫无压力字体渲染也漂亮。缺点有两个一是成本偏贵树莓派加扩展板快三百了二是它本质上一台Linux电脑不“哑”只是披着终端外壳的PC连错了反而少了那种复古极客感。方案B是STM32加串口屏或者干脆买一块现成的工业串口屏改装。这个上手非常简单串口屏本身就是为工业HMI设计的接上就能显示。但串口屏的图形界面是现代风格很难做出那种扫描线、磷光质感的CRT效果调整起来也处处受限于厂商的协议。方案C就是最终采用的ESP32方案。选择它有四个原因第一ESP32价格不到二十块板载Wi-Fi和蓝牙省去额外的通信模块第二它带有DAC和I2S外设可以输出RGB视频信号虽然分辨率只能做到320×240左右但做字符终端完全够用还能加扫描线特效第三Arduino生态成熟PS2KeyAdvanced库和VGA库都是现成的不用从底层驱动一个一个啃第四也是最打动我的ESP32的方案里每行代码、每个信号都可以自己控制这才是“做一台终端”而不是“装一台终端”。1.3 预期实现效果与最终交互形态我给自己定的目标是做一个支持VT100基本控制序列的串口网络双模哑终端本地支持80列×25行的标准终端尺寸带绿色磷光效果和扫描线模拟键盘用原汁原味的PS/2机械键盘。通过串口线直连宿主机或者通过Wi-Fi用TCP连接宿主机上启动的终端服务都能获得可用的Shell。交互形态上我参考了DEC VT100的经典操作感受开机后屏幕上只有光标闪烁按一个键字符立刻显示在光标位置主机回显的字符也会逐个出现。本地不做任何历史记录不做拷贝粘贴虽然现代终端离了复制粘贴就没法活但为了复古我决定先做成纯单向输入输出。这个目标看似朴素走到后面才发现牵扯到视频时序、键盘扫描、串口中断、TCP重连、控制序列解析等问题每一个都不算难但合在一起足够让人折腾一个周末。所以如果你是第一次做这类项目我建议拆成两步先实现串口回显再去做网络和特效。2. 核心硬件选型与材料准备2.1 显示方案怎么选CRT、LCD还是电子墨水显示部分是复古感的主要来源不同选择带来的效果差异非常大。我实际调研后大致分了三种路线。如果你真的想接一台老CRT显示器那需要生成模拟VGA信号至少是部分兼容的VGA时序。ESP32可以通过两个GPIO输出行同步和场同步信号再用一组电阻网络把三路RGB数字信号变成模拟电平或者更大胆一点用ESP32内置DAC直接输出模拟R/G/B。这种方案效果最正开机时那种“嗡”一声的高压启动声就是复古味道的保证。缺点是CRT往往体积大、功耗高还要注意高压元件不适合放书桌上长期亮着。如果你像我一样用老式VGA液晶屏比如书架角落吃灰的那台情况会简单很多。液晶显示器接受标准VGA模拟信号但要求的时序比CRT宽容很多esp32-vga库默认的640×350或320×240输出都能点亮。这种方案兼顾了视觉上的复古扫描线和人类可接受的体积是我最终选择的路线。电子墨水屏也有人用过显示静态文本特别有质感像一本不断翻页的电子书。但问题太明显刷新率太低光标闪烁和滚动输出根本没法用除非你只需要它当一块“状态牌”。我不建议优先考虑普通SPI接口的数码屏比如常见的2.4寸TFT。它能显示彩色、驱动简单但现代屏幕观感太重很难营造出老终端的质感另外小尺寸屏幕写代码也实在不方便。2.2 主控与视频输出核心电路主控选择ESP32-WROOM-32开发板即可我买的是十块钱左右的DevKitC。注意要选带外部天线版本的同时尽量买板载USB转串口芯片的型号调试时直接Micro-USB连电脑能省很多事。ESP32的GPIO输出能力比较强但驱动VGA电阻网络时需要加一个74HC245缓冲器更稳否则在高分辨率下波形沿会出现明显的过冲和振铃。视频输出电路我采用的方案是所谓的“电阻DAC 同步分离”这是ESP32做复古显示最常见的接法。信号ESP32 GPIO接法说明红色GPIO21470Ω电阻 → VGA引脚1等效电平约0.7V时偏暗可换330Ω绿色GPIO22470Ω电阻 → VGA引脚2文本色通常用绿色可实验微调蓝色GPIO23470Ω电阻 → VGA引脚3蓝通道如果不用可悬空行同步GPIO25用100Ω电阻 → VGA引脚13拉低有效场同步GPIO26用100Ω电阻 → VGA引脚14拉低有效地线GND直连VGA引脚5、6、7、8、10注意共地这个电路并不复杂面包板就能完成初期验证但如果要做固定线材建议直接焊到洞洞板上并用热缩管套住。电阻DAC的原理是通过不同的并联电阻产生不同灰度级但ESP32的GPIO只有数字高/低两种状态所以单个引脚只能输出纯色或黑色。我最终是让每个像素点要么绿色要么黑色也就是所谓的1-bit绿显配合扫描线后效果反而最像老式单色终端。如果你想要更丰富的“半亮”字符可以把两个GPIO配不同电阻合成一个通道就能得到两档灰度但这会让代码复杂度上升不少。2.3 键盘输入和复古外壳怎么处理键盘部分有两个选择USB键盘和PS/2键盘。USB键盘需要ESP32外接USB Host Shield这方案接线复杂而且库也比较老旧所以我直接选了PS/2键盘。PS/2是多年前PC的标准接口很多老式机械键盘仍然只支持它某二手平台几十块就能买到成色很好的老键盘。这种键盘的特色是段落感清晰咔嗒声在深夜里写代码非常有仪式感。PS/2接口定义很简单总共4根有效线5V电源、GND、时钟(CLK)和数据(DATA)。这两根信号线需要接2.2kΩ上拉电阻到5V但ESP32的GPIO不是全5V容忍尤其开发板的一些引脚只能承受3.3V。解决方法是加一个电平转换模块或者用两个MOS管做的双向电平转换电路。我在项目里用的是最便宜的TXS0108E模块8路转换只用了两路剩下的备用。外壳的复古感是整个项目外观的关键。我的方案是去旧货市场淘了一台已经不能响的老式收音机把内脏掏空只保留木壳前脸。然后在原来调谐指示窗的位置安装显示屏幕在机身下方开一个矩形孔安装键盘主机板藏到机箱底部。如果不太想折腾木工也可以用3D打印做一个仿CRT的外壳网上能找到很多终端机箱的STL文件按比例缩放后在顶部加一个小的“屏幕罩”效果就很棒。3. 系统搭建与核心配置3.1 串口链路怎么建从物理层到会话层哑终端最重要的通信通道是串口。物理层上ESP32的UART0默认用于烧录和调试所以我改用UART1连接外部主机。如果直连主机的话需要一个USB转TTL模块常用的是CH340或者CP2102。这里有个极易踩坑的地方USB转TTL模块的TX和RX必须交叉连接ESP32的RX和TX很多新手就是在这卡了半天。波特率的选择上我最终稳定用1152008位数据位无校验1位停止位。为什么是这个配置因为终端主要传输的是ASCII字符和转义序列8位数据足够覆盖扩展ASCII无校验是因为VGA渲染本身没做数据校验串口帧校验会增加系统负担但对显示错误没有明显帮助1位停止位是标准配置。如果追求极致的稳定性可以降到57600或38400但如果你的程序里有大量的全屏刷新操作115200能明显减少等待时间。连接串口后你还需要在主机上启动一个登录会话。最简单的方式是安装一个串口终端服务比如在Linux上运行systemd-gettyttyUSB0或者用ser2net把串口映射成TCP端口。我的做法是直接用getty启动登录会话这样对着哑终端敲回车屏幕上就会显示login:提示符。3.2 终端仿真与控制序列解析最小的VT100子集终端光能显示字符是不够的主机输出中通常会包含光标移动、清屏、颜色切换等控制序列。这些序列以ESC字符0x1B开头后面跟一串参数。比如ESC[2J是清屏ESC[10;5H是把光标移动到第10行第5列。一个完整的VT100实现规范很长但做项目时只需要实现其中一小部分就能让Bash和Vim正常工作。我实现的子集包含五个核心功能显示可打印字符0x20~0x7E并自动换行遇到行尾则滚屏清屏ESC[2J和 清行ESC[K光标定位ESC[row;colH光标上下左右移动ESC[AESC[BESC[CESC[D显示属性如ESC[31m直接忽略并丢弃对很多控制序列可以直接“吞掉”。但注意丢的时候要完整地解析完整个序列否则后续的字符会被误认为是序列的一部分导致屏幕乱码。这是个很隐蔽的bug我在Vim里上下翻页时经常出现半个转义序列泄漏到屏幕上排查了半天才发现是解析状态机少了一个终止条件。代码结构上我建议用状态机来解析输入流而不是简单用阻塞式的串口读取。因为串口数据不是按帧到达的一个控制序列可能分两次收到导致你读半个序列就开始处理。所以更好的做法是维护一个字节流环形缓冲区再一个字节一个字节喂给解析状态机。3.3 Wi-Fi网络通道让终端“长距离无线化”串口适合在家里调试但终端如果只能在USB线范围内用那做成一个设备的意义就小了很多。所以我额外加了Wi-Fi通道让终端通过网络连接同一局域网内的任意主机。网络层的方案最经典的是telnet因为telnet协议就是为终端会话设计的而且实现极其简单。ESP32上我们不需要跑一个完整的telnet客户端协议栈只需要建立一个TCP连接然后在应用层把终端数据透传过去就行因为Linux自带的telnetd服务会处理终端协商。但如果你偷懒也可以直接让ESP32连接一个TCP端口主机上跑utelnetd或ser2net来做协议转换。实际使用中我发现Wi-Fi通道的延迟比串口略大大约在10到20毫秒这个延迟在交互式命令行中几乎感知不到但如果你在Vim里快速连续按方向键屏幕滚动会有一点点粘滞感。解决办法是ESP32侧加一个128字节的环形发送缓冲区键盘事件先入队然后由后台任务批量发送避免每个按键都触发一次TCP发送。4. 实操过程从零到一发制作一台可用的哑终端4.1 接线与供电的每一步细节开始焊接之前我建议先用面包板完整验证把基础功能跑通避免一上来就焊接到最终机箱里导致排查困难。需要准备的接线如下ESP32的GPIO21/22/23连接VGA的R/G/B通道GPIO25/26连接行场同步GND连接所有地线。PS/2键盘的CLK和数据线通过电平转换模块后分别接到GPIO32和GPIO33注意PS/2的电源必须用5V不能用3.3V。供电方面是整个项目里最容易被忽略的地方。ESP32的高频工作电流能达到500mAPS/2键盘也需要200mA左右再加上电阻网络整机峰值电流可能超过1A。如果用电脑USB口供电很可能会被限流导致高频重启。我最终是给ESP32单独配了一个5V/2A的电源适配器同时在电源入口并联一个470µF电解电容和0.1µF陶瓷电容给数字电路做去耦。这个电容组合几乎是所有单片机项目的安全牌能在很大程度上避免画面的高频噪声条纹。接线固定后先用万用表逐一确认没有短路。特别注意VGA头的金属外壳和GND之间不能有电压差否则插上显示器的瞬间可能烧掉信号线。很多工控板的问题是电源地和视频地之间因为多路供电引入地弹所以我在木壳里特意把所有GND都汇聚到一个公共接地点。4.2 固件核心逻辑解析显示、键盘、通信三线程配合固件我用的是Arduino框架原因是你不用自己写启动代码串口库、WiFi库都是现成的。整个系统的核心是“三个循环配合”显示循环、键盘扫描循环、收发循环。显示循环由VGA库的垂直同步中断驱动每隔16.7ms刷新一帧。这一帧中CPU要完成256个像素宽、240行高的扫描线绘制每个字符是8×16像素图形所以一屏80×25个字符需要渲染2000个字符块。如果字符位图从Flash读取速度勉强够为了提高性能我把字符位图放在RAM里A-Z、0-9和常见符号每个字符8字节总共几百字节完全不占空间。键盘扫描循环每秒检查一次PS/2时钟把收到的扫描码转换为ASCII码。这里有个经典细节PS/2键盘按下和松开时会分别发送不同的扫描码松开时前面多一个0xE0或0xF0前缀。如果不判断松开事件就会导致按键重复输入甚至是一个键按住后不停输出。我的代码里维护了一个简单的去重逻辑只响应按下事件。通信循环既管串口也管TCP核心逻辑是把接收到的数据推给解析状态机然后更新屏幕缓冲区。发送数据时则从一个全局队列取键盘事件优先发送到串口再发送到TCP连接。因为这两个通道可能同时被使用我用了一个互斥锁保护共享的缓冲区避免数据竞争导致花屏。下面是最核心的一段伪代码/缩写示意展示这三个循环如何咬合// 在VGA垂直同步中绘制字符 void vgaCallback() { for (int y 0; y 25; y) { for (int x 0; x 80; x) { drawChar(vga, screenBuf[y][x], x * 8, y * 16); } } } // 键盘事件 void onKeyPress(uint8_t ascii) { // 本地回显按键时立即在屏幕上显示字符 screenBuf[cursorRow][cursorCol] ascii; ... // 同时发往宿主机 if (serialEnabled) Serial1.write(ascii); if (tcpConnected) client.write(ascii); } // 从主机接收数据 void onHostData(uint8_t byte) { vt100Parser.feed(byte); // 解析状态机 if (vt100Parser.isPrintable()) { screenBuf[cursorRow][cursorCol] byte; advanceCursor(); } }4.3 外壳改造与屏幕安装外壳改造是整个项目里最需要耐心的部分。我淘来的收音机外壳是木质的表面有一层清漆年份久了有些裂纹。我先把内部电子元件拆干净用木工钳剪掉所有金属支架再用砂纸把内壁磨平。为了让屏幕正好嵌在原来调谐窗的位置我用卡尺量好尺寸用电磨开了一个略小于屏幕边框的方孔。开孔时要留2mm左右的余量方便后面用3D打印的屏幕压框卡住。屏幕固定后我在面板原来旋钮的位置加装了复古风格的拨动开关一个控制整机电源一个控制串口/网络模式切换。按键和旋钮都保留了原来收音机上的金属质感部件装在黑檀色木壳上显得历史感和科技感并存。键盘外壳因为原来的收音机是卧式的所以我把键盘放在机身前方做成一个略微倾斜的斜面。3D打印一个键盘托架用螺丝固定在木壳底部即可。键盘托架上方可以贴一层黑色植绒布既防止键盘划伤木板又有复古电子设备的内部特征。5. 常见问题与排查技巧实录5.1 屏幕乱码和花屏先从波特率和电气连接下手做这个项目的第一个周末我遇到最频繁的问题是屏幕出现随机乱码比如敲几个字母屏幕却显示成“üµÛ”之类。第一反应是波特率不匹配。串口通信双方波特率不相同每个字节的位采样会出现偏差就会产生这种随机字符。我检查了主机侧stty -F /dev/ttyUSB0确认是115200ESP32侧代码里也写了Serial1.begin(115200)但问题依然存在。后来发现是USB转TTL模块质量问题CH340的TXD引脚电平转换速度不够快高频率下产生毛刺导致ESP32误判。换成CP2102模块后立刻恢复正常。如果换模块不方便也可以在串口线上串联一个100Ω的小电阻给信号做个低通滤波能吸收一部分高频噪声。5.2 键盘完全无响应或按键连发PS/2键盘插上后完全没反应这是我第二头疼的bug。排查步骤是先用逻辑分析仪看键盘的CLK线是否有脉冲如果没有说明键盘没通电或者键盘本身坏。实测中发现是电平转换模块的VCCA引脚必须接3.3V、VCCB接5V如果两个电源接反TXS0108E的输出电平会被拉到0.6VESP32的GPIO根本识别不了。按键连发的问题则主要出在松开扫描码的处理上。如果你发现每次按一个键终端里出现两个甚至三个相同字符去查一下你的代码是否只处理了“按下”事件而没有屏蔽“重复按下”事件。PS/2协议里按住同一个键超过约500ms后会自动重复发送按键码所以如果你不在乎长按重复那就要在“按下”事件里判断一个键盘状态标志只有在状态变化时才发送。5.3 网络通道偶发掉线重连逻辑怎么做TCP连接不够健壮时Wi-Fi通道会出现掉线后不再连上的现象。最简单的处理是在主循环里定时检查client.connected()断线后关闭客户端等待2秒再重连。但如果宿主机重启或路由器重新分配IP这种简单的重连可能失败因为IP地址变了。我的方案是用固定IP地址绑定的方式在路由器上给终端分一个静态IP并在ESP32设置里用固定的服务器IP和端口。另外建议把终端软件设计成“串口为主、网络为辅”的降级模式一旦TCP掉线就通过串口发出提示“Network dropped, continue on serial”这样至少不会完全失去与主机的连接。5.4 电源噪声导致画面横纹和闪烁最后聊聊电源。ESP32在Wi-Fi发射瞬间电流会从几十毫安跳到三四百毫安这个冲击如果电源去耦不足会直接在VGA信号的参考地上面产生压降导致画面出现周期性横纹或亮度抖动。我解决这个问题分了三步第一在ESP32的5V供电入口加一组470µF0.1µF电容第二将VGA输出电路中的GND单独连到电源地的同一极点避免经过ESP32开发板内部走线第三把电源适配器换成带屏蔽的“2.4A快速充电器”那种电磁干扰明显小得多。经过这几步后画面就稳定了。写在最后的一些个人体会做完这台哑终端之后我最大的感受是现代操作系统复杂到你已经很难再找到一个“打开就是命令行、按了就有回显”的环境。而这台设备却从头到尾只有一层薄薄的固件在运行几乎是在裸机上与世界对话。如果你也想复刻这个项目我建议从串口版本着手先把“键盘输入→ESP32→串口→主机→回显→ESP32→屏幕”这条链路跑通再考虑加Wi-Fi和外壳。这条链路看似简单实际上通杀了很多基础知识点我调试过程中学到的东西远比最终成品本身更有价值。一个小技巧是在做外壳之前先用面包板和裸屏完整“怼”上一个周末确认没有莫名其妙的花屏、掉线、按键连发问题后再动手把硬件焊接到机箱里。否则信号线被机壳遮挡后排查一个简单的断线都会让你想砸东西。这个项目后续还有很多扩展空间比如把绿色荧光改成琥珀色或者加一个3.5英寸软驱用来保存“终端截图”——那些玩法先留着等我把这一版的扫描线特效调得更“古早”再做。
返回列表