1. 项目概述与核心价值
在嵌入式系统开发的早期,尤其是在上世纪90年代,PC的普及为许多外围设备提供了一个现成且稳定的交互平台。其中,IBM AT键盘接口(后来演变为PS/2接口)因其简单、可靠且直接集成在每台PC主板上的特性,成为了一个极具吸引力的“现成”通信与供电通道。它不仅仅是一个键盘插槽,更是一个内置了5V电源、地线以及一套成熟同步串行协议的微型数据总线。对于资源受限的嵌入式设备,如果能“借用”这个接口,就意味着省去了设计独立电源、电平转换甚至部分通信协议栈的麻烦,可以直接与主机进行双向对话。
Motorola(现为NXP的一部分)在1997年发布的这份应用笔记(AN1723)正是瞄准了这一痛点。它详细阐述了如何利用MC68HC05系列单片机,与IBM AT兼容机的键盘接口进行对接。核心目标很明确:让单片机设备能够通过键盘接口从PC获取电源,并利用其数据链路进行通信,同时还要保证原有的键盘功能不受影响。这听起来像是一个“寄生”或“旁路”设计,但其技术内涵相当丰富,涉及到精确的时序模拟、信号仲裁、协议解析以及硬件隔离。
我之所以对这个老项目感兴趣,是因为它体现了嵌入式开发中一种经典的“资源复用”思想。在物联网和USB统治一切的今天,这种“钻空子”式的精巧设计似乎过时了,但其底层逻辑——如何在不干扰现有系统的情况下,安全、可靠地接入并共享资源——在今天的嵌入式系统集成、硬件安全测试甚至一些特殊的工业控制场景中,依然有很高的参考价值。接下来,我将带你深入拆解这份文档,不仅还原其设计精髓,还会补充大量当时文档可能一笔带过、但对于实际复现至关重要的工程细节和避坑经验。
2. IBM AT键盘子系统深度解析
要成功“劫持”一个接口,首先必须彻底理解它的运行机制。IBM AT键盘子系统是一个典型的主从式、双向半双工通信系统。
2.1 硬件构成与信号定义
整个子系统由两部分构成:键盘和键盘接口控制器(通常集成在主板芯片组或一个独立的8042/8742微控制器中)。它们通过一根5芯屏蔽电缆连接,核心是两根信号线:
- 时钟线(Clock):由键盘主导生成,用于同步数据传输。这是一根开集电极(Open-Collector)信号线,意味着无论是主机还是键盘,都只能将其拉低(驱动到0),释放后由上拉电阻拉高(到5V)。这种设计是实现总线仲裁(谁有权说话)的基础。
- 数据线(Data):用于传输实际的数据位。同样是一根开集电极信号线。
此外,还有**+5V电源和地线(GND),以及一个屏蔽地**。这里有一个关键细节:接口提供的5V电源电流能力是有限的,并且很多主板设计会为这条电源线串联一个保险丝。这意味着你的外设功耗必须非常低,通常不能超过100-200mA,否则可能触发保护或导致电压跌落。在设计任何从该接口取电的设备时,第一步必须是实测目标主机的带载能力。
2.2 通信协议:两个角色,两套时序
协议的精妙之处在于,它根据数据传输方向,定义了两套略有不同的时序流程,但共享相同的数据帧格式:1个起始位(0) + 8个数据位(LSB先发) + 1个奇校验位 + 1个停止位(1),总共11位。
2.2.1 主机到键盘(Host-to-Keyboard)协议
当PC想要向键盘发送命令(如复位、设置LED、设置扫描码集)时,使用此协议。流程如下:
- 主机发起:主机首先将时钟线拉低并保持至少100μs,然后在时钟线为低期间,将数据线拉低。这个“时钟低 -> 数据低”的序列是一个明确的“请求发送”信号。
- 键盘接管时钟:键盘检测到这个序列后,会释放时钟线(让其被上拉至高),并准备接收数据。大约1ms后,键盘开始生成时钟信号。
- 数据传送:键盘产生一个频率大约在10-20kHz(周期60-100μs)的时钟。主机在时钟的低电平期间改变数据线的状态,键盘在时钟的上升沿后约5-25μs采样数据线。这样一位一位地传送11位数据帧。
- 键盘应答:如果键盘正确接收到停止位(高电平),它会在紧接着的时钟低电平期间将数据线拉低,作为应答信号(ACK)。如果发生奇偶校验错误或命令无效,键盘会回复
0xFE要求重发。
关键理解:在这个方向上,虽然主机发起了传输,但时钟的生成权完全交给了键盘。主机只是在键盘规定的节奏下“摆放”数据。这要求我们的单片机在模拟主机行为时,必须能精确检测键盘时钟的边沿并快速响应。
2.2.2 键盘到主机(Keyboard-to-Host)协议
当键盘发送扫描码或命令应答时,使用此协议。流程如下:
- 键盘发起:键盘首先检查时钟线和数据线是否都为高(总线空闲)。如果是,键盘将数据线拉低,随后将时钟线拉低。这个“数据低 -> 时钟低”的序列作为起始位。
- 键盘主导传送:键盘生成时钟,并在时钟的高电平期间改变数据线的状态,主机在时钟的下降沿采样数据线。同样传送11位数据帧。
- 主机忙信号:主机如果暂时无法处理数据(例如缓冲区满),可以在收到停止位后的0-50μs内将时钟线拉低,通知键盘“暂停发送”。键盘会进入等待状态。
核心机制——主机优先权:这是整个设计中最需要小心处理的部分。在键盘到主机的传输过程中,主机随时可以夺取总线控制权。夺取的方式有两种:在数据线为高时将其拉低,或在时钟线为高时将其拉低。键盘必须在输出每个高电平数据位时采样数据线,并在每个时钟上升沿后采样时钟线,一旦发现被拉低,必须立即释放总线(停止驱动时钟和数据),转为接收模式。 这意味着,如果你的单片机设备正在模拟键盘向主机发送数据,必须时刻准备被主机“打断”。如果处理不当,就会导致总线冲突、数据损坏。
2.3 键盘接口的编程模型
PC端软件与键盘接口交互,是通过读写两个I/O端口实现的:
- 端口 0x60(数据端口):
- 写操作:数据被写入键盘控制器的输入缓冲区,随后控制器会将其作为命令发送给键盘。
- 读操作:从键盘控制器的输出缓冲区读取数据,这可能是键盘的应答,也可能是扫描码。
- 端口 0x64(命令/状态端口):
- 写操作:向键盘控制器写入控制命令(如复位键盘接口、设置扫描码转换等)。
- 读操作:读取键盘控制器的状态寄存器。
状态寄存器(图7)的每一位都至关重要,特别是:
- Bit 0: Output Buffer Full (OBF):当该位为1时,表示输出缓冲区(0x60)有数据可读(来自键盘)。这是PC端软件轮询(Polling)方式获取键盘数据的关键标志位。
- Bit 1: Input Buffer Full (IBF):当该位为1时,表示输入缓冲区(0x60)已满,控制器正忙,主机不应写入新命令。
- Bit 2: System Flag:系统上电自检后置位,表示键盘控制器已就绪。
- Bit 3: Command/Data:区分写入0x60端口的是命令(1)还是数据(0)。
- Bit 4: Keyboard Locked:键盘是否被锁定(禁止)。
- Bit 5: Auxiliary Device Output Buffer Full:PS/2鼠标数据就绪标志。
- Bit 6: Timeout Error和Bit 7: Parity Error:通信错误标志。
在实际编程中,向键盘发送命令的标准流程是:
- 读取状态寄存器(0x64),等待IBF位为0(输入缓冲区空)。
- 向数据端口(0x60)写入命令字节。
- 读取状态寄存器(0x64),等待OBF位为1(输出缓冲区满,即键盘已应答)。
- 从数据端口(0x60)读取应答字节。
3. 将键盘接口作为嵌入式资源的设计挑战与方案
原文档的核心创新点在于,它不满足于仅仅理解协议,而是提出了一个更大胆的想法:让一个非键盘的嵌入式设备(如温度计)共享这个接口。这带来了三个核心挑战:
3.1 挑战一:总线冲突与信号隔离
键盘和设备都连接在同一对时钟/数据线上。如果设备试图与主机通信,键盘会“听到”所有信号。如果主机发送的命令恰好是键盘能识别的(如0xED设置LED),键盘就会尝试应答,导致设备和键盘同时驱动总线,造成短路或数据混乱。
解决方案:物理隔离与“通行证”机制文档提出的方案非常巧妙:
- 默认状态(旁路模式):设备作为“透明桥接器”,将键盘的信号直连到主机。此时设备只被动监听总线上的通信。
- 激活信号(Activation Sequence):PC端程序发送一个特殊的序列来“召唤”设备。这个序列必须满足两个条件:(a) 对键盘状态无任何影响;(b) 在正常操作中极不可能偶然出现。文档选择了连续两个“回显(Echo)命令-应答”序列。Echo命令(
0xEE)的作用是键盘原样返回0xEE,不改变键盘任何设置,是完美的“心跳检测”命令。连续两次则构成了一个独特的激活指纹。 - 设备接管:设备在监听中识别到这个独特的“双Echo”序列后,立即通过模拟开关(如4066)物理断开键盘与总线的连接,同时将键盘的时钟线拉低。根据协议,时钟线被拉低意味着键盘认为主机正忙,它会停止发送并缓冲后续的击键。至此,设备独占了总线。
- 通信与恢复:设备完成与主机的数据交换后,重新连接键盘,释放其时钟线,键盘恢复正常工作。
3.2 挑战二:MCU I/O与开集电极要求的矛盾
MC68HC05的大部分I/O引脚是推挽输出(主动驱动高/低电平),而键盘接口要求开集电极输出(只能拉低,靠上拉电阻拉高)。直接连接推挽输出到总线上,当MCU输出高电平而总线被另一方拉低时,会产生电流冲突。
解决方案:开集电极缓冲器文档使用7407六路开集电极缓冲器作为接口芯片。MCU的推挽输出连接到7407的输入,7407的输出(开集电极)连接到总线,并外接一个4.7kΩ的上拉电阻至5V。这样,MCU输出高电平时,7407内部晶体管截止,输出为高阻态,总线由上拉电阻拉高;MCU输出低电平时,7407导通,将总线拉低。完美实现了开集电极行为。 对于需要双向通信的数据线,需要两个引脚和两个缓冲器:一个配置为输出(经缓冲器驱动总线),一个配置为输入(直接或经缓冲器读取总线状态)。
3.3 挑战三:协议模拟的时序精度
无论是模拟主机发送还是模拟键盘发送,都需要严格满足协议规定的时序要求,特别是μs级别的延迟和边沿检测。
解决方案:精确延时与中断驱动对于MC68HC05这类没有硬件串行外设(UART)支持此特定协议的单片机,必须用软件“位碰撞(Bit Banging)”来实现。
- 延时:必须根据CPU时钟频率精确计算指令周期,编写微秒级延时函数。通常使用汇编语言内嵌或高度优化的C代码循环。
- 边沿检测:模拟键盘发送时,需要在时钟高电平期间设置数据,并确保在下降沿前稳定。模拟主机发送时,需要检测键盘产生的时钟上升沿。这要求I/O口的读取速度足够快。强烈建议将时钟线连接到具有外部中断功能的引脚上,利用中断来响应边沿,而不是纯粹的轮询,以提高可靠性和降低CPU负载。
- 超时处理:任何通信都必须有超时机制。例如,等待键盘时钟变高、等待主机应答等,如果超过预定时间(如2ms),应判定为通信失败并执行错误恢复流程。
4. 数字温度计实例的硬件设计与实现细节
原文档以基于MC68HC(7)05J1A和DS1820温度传感器的数字温度计为例。我们深入其硬件设计。
4.1 系统框图与电源管理
整个设备夹在PC键盘接口和AT键盘之间。
PC键盘接口 (6-pin DIN) <---> [温度计设备] <---> AT键盘 (MC68HC05J1A + DS1820)设备从PC键盘接口的+5V和GND引脚取电。务必在设备的电源入口处放置一个大容量(如100μF)的电解电容和一个104(0.1μF)的陶瓷去耦电容,以平滑可能存在的电源噪声和瞬间电流需求。考虑到接口的供电能力有限,整个系统(MCU、DS1820、7407、4066)应选择低功耗器件,并在MCU空闲时进入睡眠模式。
4.2 关键电路:信号切换与驱动
这是整个设计的核心电路,如图8所示,但我们需要更具体的连接:
MCU侧信号定义:
PA0:配置为输入,用于读取主机侧的时钟线状态。PA1:配置为输出,经7407缓冲后,用于驱动时钟线到主机(当设备模拟键盘时)。PA3:配置为输入,用于读取主机侧的数据线状态。PA4:配置为输出,经7407缓冲后,用于驱动数据线到主机(当设备模拟键盘时)。- 另外需要两个I/O口控制4066模拟开关,用于切换键盘的连接。
模拟开关(4066)连接:
- 一片4066包含4个独立的双向模拟开关。
- 开关A:串联在键盘时钟线与公共时钟总线(连接主机和设备)之间。控制端由MCU的一个I/O口(如
PA2)控制。 - 开关B:串联在键盘数据线与公共数据总线之间。控制端由MCU的另一个I/O口(如
PA5)控制。 - 当
PA2和PA5输出高电平时,开关导通,键盘连接到总线(旁路模式)。 - 当
PA2和PA5输出低电平时,开关断开,键盘与总线隔离(设备独占模式)。此时,必须通过一个电阻(如10kΩ)将键盘的时钟线拉低到地,使其进入“主机忙”的等待状态。
上拉电阻:
- 公共时钟总线和公共数据总线各需要一个4.7kΩ的上拉电阻至5V。这个电阻必不可少,它为开集电极信号提供了确定的高电平。
- 键盘侧的时钟线和数据线在内部已有上拉,但为了在断开连接时稳定状态,也可以在设备板上为它们各添加一个上拉电阻(如10kΩ)。
4.3 软件流程与关键代码逻辑
设备的固件是一个状态机,主要包含以下状态:
监听模式:
- 模拟开关闭合,键盘直连。
- MCU持续监控总线上的通信。它需要解析每一个数据帧(无论是主机到键盘还是键盘到主机),这需要实现完整的协议解析器。
- 一旦检测到连续的
0xEE命令和0xEE应答(共两对),立即触发状态转换。
隔离与接管模式:
PA2和PA5置低,断开4066开关。- 将连接键盘时钟线的I/O口(需额外定义)置低,拉低键盘时钟。
- 短暂延时(如10ms),确保键盘完全进入等待状态。
数据采集与发送模式:
- 通过单总线协议读取DS1820的温度值。
- 将温度值(如“23.5C”)转换为AT扫描码序列。这里有一个重要技巧:发送的必须是扫描码(Scan Code),而不是ASCII码。例如,字符‘2’的按下扫描码是
0x03,释放码是0xF0+0x03。你需要一个扫描码查找表。 - 调用“模拟键盘发送”函数,将扫描码序列一位一位发送给主机。发送函数必须严格遵守“键盘到主机”协议,并且在每个时钟高电平期间,都要检查数据线是否被主机拉低(被中断)。如果被中断,必须立即停止发送,释放总线,并可能需要进行错误恢复。
恢复模式:
- 发送完成后,将驱动时钟和数据的MCU引脚(
PA1,PA4)设置为高阻态(或输出高电平,经7407表现为高阻)。 PA2和PA5置高,闭合4066开关,重新连接键盘。- 释放拉低键盘时钟的I/O口。
- 返回监听模式。
- 发送完成后,将驱动时钟和数据的MCU引脚(
5. 实操要点、常见问题与调试心得
基于这类项目的实际经验,以下是一些至关重要的注意事项和排查技巧:
5.1 电源与接地
- 问题:设备工作不稳定,时而复位,通信错误率高。
- 排查:首先用示波器测量设备端的5V电源纹波。键盘接口的电源线较细,阻抗大。设备瞬间电流(如MCU启动、7407切换)可能造成电压骤降。
- 解决:
- 确保电源去耦电容(大电解+小陶瓷)尽可能靠近MCU的VCC和GND引脚。
- 考虑在设备端增加一个低压差线性稳压器(LDO),如3.3V输出,为MCU和逻辑芯片供电。这样设备端的电流变化对键盘接口电源的影响更小。
- 共地至关重要!确保PC、设备和键盘三者地线连接良好。
5.2 时序精度与信号完整性
- 问题:通信时好时坏,主机收不到数据或收到错误数据。
- 排查:使用示波器或逻辑分析仪同时捕捉时钟线和数据线。
- 检查起始位:在键盘到主机传输中,起始位是否是“数据先低,随后时钟低”?
- 检查数据建立/保持时间:在主机到键盘方向,数据在时钟上升沿前是否稳定了足够时间(>5μs)?在键盘到主机方向,数据在时钟下降沿前是否稳定?
- 检查信号边沿:由于总线电容和上拉电阻,上升沿可能过缓。过缓的边沿可能导致采样错误。
- 解决:
- 优化软件延时循环。使用CPU空指令(NOP)或硬件定时器来产生精确延时。
- 适当减小上拉电阻。4.7kΩ是典型值,但如果线缆较长或负载电容大,可以尝试减小到2.2kΩ以加快上升时间。但注意这会增加总线拉低时的电流。
- 确保7407缓冲器的驱动能力足够。
5.3 总线冲突与仲裁失败
- 问题:设备发送数据时,系统死锁或键盘失灵。
- 排查:检查在设备发送期间,模拟开关是否确实完全断开了键盘?用万用表测量开关两端的通断。
- 解决:
- 确保控制模拟开关的MCU引脚在断开时输出稳定的低电平,导通时输出稳定的高电平。
- 在设备发送数据的函数中,严格实现主机中断检测。每次准备将数据位驱动为高电平时,先读取数据线状态(通过输入引脚
PA3),如果发现为低(被主机拉低),必须立即中止发送,释放总线(将输出引脚设为高阻),并切换到接收模式。 - 增加“看门狗”定时器。如果通信卡死,看门狗能复位MCU,恢复旁路模式,避免整个系统锁死。
5.4 PC端软件(THERMO.EXE)的编写要点
原文档提到了用Borland C++ 3.1编写DOS程序。在今天,我们可以用现代语言(如C/C++、Python)通过直接端口读写(在Linux下用ioperm/iopl,在Windows下需要内核驱动或使用inpout32等库)来实现。 关键流程如下:
- 通过
0x64端口读取状态,等待输入缓冲区空(IBF=0)。 - 向
0x60端口写入0xEE(Echo命令)。 - 等待输出缓冲区满(OBF=1),从
0x60读取应答,应为0xEE。 - 立即重复步骤1-3,发送第二个Echo序列。这就构成了激活信号。
- 发送后,持续轮询状态寄存器(OBF),等待设备回复的扫描码。需要设置超时(如2秒)。
- 收到扫描码后,需要将其转换为ASCII码进行显示。DOS/BIOS可能已经完成了这个转换,但在底层直接读取时,你需要自己实现扫描码到字符的映射。
5.5 现代环境下的替代与思考
今天,PS/2接口已不常见,但这个项目的思想并未过时。
- 接口演变:USB键盘同样有复杂的协议(HID),但“共享接口”的思想可以借鉴。例如,一些USB设备可以伪装成复合设备(如键盘+鼠标+自定义设备)。
- MCU选择:现代低功耗ARM Cortex-M系列单片机(如STM32G0系列)性能远超MC68HC05,且有更丰富的外设。你可以使用通用定时器精确生成和捕捉波形,甚至用DMA来辅助,大大降低CPU负担。
- 应用场景:这种设计适合于需要与PC进行简单、可靠、免驱动(被识别为键盘)通信的小型嵌入式设备。例如,硬件密钥、简单的数据采集器、特殊的输入设备等。它的优势在于兼容性极广,几乎任何有键盘接口的电脑都能用,且不需要安装任何驱动程序。
这个项目更像是一个经典的嵌入式系统“练习题”,它综合了硬件接口设计、低速串行协议模拟、资源冲突解决和系统集成等多个方面的知识。虽然具体的芯片和接口已经老旧,但其中蕴含的“理解协议、解决冲突、安全共享”的设计哲学,对于任何从事嵌入式系统或硬件交互开发的工程师来说,都是一笔宝贵的财富。