软件工程师为了解决每天频繁“奔走”于键盘各块件之间,创建了“终极黑客键盘”。该键盘分为左右两部分,拥有LED灯,而其中所涉及到的具体技术和过程,你可以在本文中找到答案。
以下为译文:
早在2007年8月的某一天工作中,我不禁意识到普通的PC键盘无法尽可能多的满足于我,每天,我的手指不得不在键盘各块件成百上千次的“奔走”,我想必须要有一个很好的解决办法。
当我想创造一个完美的黑客键盘并去实现它时,我才意识到,作为一名软件开发者,我对硬件是一无所知。(PS:这是多么痛的领悟)
我设法去学习一个全新的技能,同时说服我一位杰出的机械工程师朋友András V?lgyi加入到制作键盘的项目中,并投入足够的时间创建工作原型。如今,“终极黑客键盘”已经成为现实。
对于没有任何电子知识的软件背景,设计和构建一个强大的硬件设备是一个有趣和迷人的经验,在本文中我将描述这个电子杰作是如何工作的。对电子线路图有基本了解的你可以更容易的理解。
如何去构建一个键盘
第一步:没有键的键盘
首先让我们做一个发出x字符(基于每秒一次)的USB键盘,Arduino Micro开发板是实现该目标的一个不错的选择,因为它的功能ATmega32U4microcontroller(一种AVR microcrontroller)和相同的处理器是UHK的大脑。
说到USB-capable AVR microcontrollers,用于AVR(LUFA)的轻量级USB框架是库的选择。它使得这些处理器成为打印机、MIDI设备、键盘或任何其他USB设备类型的“大脑”。
当设备插入USB端口时,设备会传递一些被称为USB描述符的特殊数据结构。这些描述符会告知主机所连接设备的类型和性质,并由一个树结构表示。一个设备可以实现的不止一个函数,而是多个。这让事情更加复杂。让我们看看UHK描述符的结构:
大多数键盘只暴露单一的键盘接口描述符,这是有道理的。然而UHK也会暴露鼠标接口描述符,因为用户可以指令键盘的任意键来控制鼠标指针,所以键盘可以当作鼠标使用。GenericHID接口服务相当于一个为所有特性键盘交换配置信息的通信通道。(你可以在这里看到在LUFA中完整的UHK实施和配置描述符:点此进入)
现在我们已经创建了描述符,下面代码则演示了其每秒一次发送x字符:
-
uint8_t isSecondElapsed = 0;
- int main(void)
- {
- while (1) {
- _delay_us(1000);
- isSecondElapsed = 1;
- }
- }
- bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo,
- uint8_t* const ReportID,
- const uint8_t ReportType,
- void* ReportData,
- uint16_t* const ReportSize)
- {
- USB_KeyboardReport_Data_t* KeyboardReport = (USB_KeyboardReport_Data_t*)ReportData;
- if (isSecondElapsed) {
- KeyboardReport->KeyCode[0] = HID_KEYBOARD_SC_X;
- isSecondElapsed = 0;
- }
- *ReportSize = sizeof(USB_KeyboardReport_Data_t);
- return false;
- }
USB是一种查询协议,这意味着主机定期间隔(通常是125次/s)查询设备,以此来找出是否有任何新的数据发送。与此相关的回调是CALLBACK_HID_Device_CreateHIDReport()函数,在isSecondElapsed变量为1情况下向主机发送x字符的扫描码,isSecondElapsed在每秒的基础上循环的设置为1,回调时设置为0。
第二步:四个键的键盘
在这一点上我们的键盘不是非常有用的,如果我们在这上面作出实际类别,那样会很好。为此我们需要一些键,这些键需被置入一个键盘矩阵。一个全尺寸的104键键盘可以有18列6行,而我们必须把它简化为2x2的键盘矩阵,这是示意图:
它在开发板上是这样呈现的:
假设ROW1连接PINA0、ROW2连接PINA1、COL1连接PORTB0以及COL2连接PORTB1,那么扫描代码会是这样:
- /* A single pin of the microcontroller to which a row or column is connected. */
- typedef struct {
- volatile uint8_t *Direction;
- volatile uint8_t *Name;
- uint8_t Number;
- } Pin_t;
- /* This part of the key matrix is stored in the Flash to save SRAM space. */
- typedef struct {
- const uint8_t ColNum;
- const uint8_t RowNum;
- const Pin_t *ColPorts;
- const Pin_t *RowPins;
- } KeyMatrixInfo_t;
- /* This Part of the key matrix is stored in the SRAM. */
- typedef struct {
- const __flash KeyMatrixInfo_t *Info;
- uint8_t *Matrix;
- } KeyMatrix_t;
- const __flash KeyMatrixInfo_t KeyMatrix = {
- .ColNum = 2,
- .RowNum = 2,
- .RowPins = (Pin_t[]) {
- { .Direction=&DDRA, .Name=&PINA, .Number=PINA0 },
- { .Direction=&DDRA, .Name=&PINA, .Number=PINA1 }
- },
- .ColPorts = (Pin_t[]) {
- { .Direction=&DDRB, .Name=&PORTB, .Number=PORTB0 },
- { .Direction=&DDRB, .Name=&PORTB, .Number=PORTB1 },
- }
- };
- void KeyMatrix_Scan(KeyMatrix_t *KeyMatrix)
- {
- for (uint8_t Col=0; Col
Info->ColNum; Col++) { - const Pin_t *ColPort = KeyMatrix->Info->ColPorts + Col;
- for (uint8_t Row=0; Row
Info->RowNum; Row++) { - const Pin_t *RowPin = KeyMatrix->Info->RowPins + Row;
- uint8_t IsKeyPressed = *RowPin->Name & 1<
Number; - KeyMatrix_SetElement(KeyMatrix, Row, Col, IsKeyPressed);
- }
- }
- }
代码一次扫描一列,并读取个人键开关的状态,然后将这种状态保存到一个数组中,通过我们前文所说的CALLBACK_HID_Device_CreateHIDReport()函数,相关的扫描代码将发送这些基于数组的状态。
第三步:一个键盘两个部分
到目前为止,我们已经构建了一个普通键盘的开端。但是我们的目标是先进的人体工程学,鉴于人都有两只手,我们最好添加另一半键盘。
另一半的键盘将具有另一个键盘矩阵,重复之前的步骤。而其中令人兴奋的是两部分键盘之间的通信,这里有三个最受欢迎的电子设备互连协议:SPI、I2C和UART。在实际当中,我们会在这种情况下用到UART:
UART需要同行使用相同的波特率、数据位和停止位。现在左键盘通过UART将一个字节的信息发送到右键盘,以此代表按下键或释放键,右键盘对这些信息进行半加工并在相应的内存中对这些全键盘矩阵数组的状态进行操作。
左键盘发送信息示例:
- USART_SendByte(IsKeyPressed<<7 | Row*COLS_NUM + Col);
右键盘接受信息如下:
- void KeyboardRxCallback(void)
- {
- uint8_t Event = USART_ReceiveByte();
- if (!MessageBuffer_IsFull(&KeyStateBuffer)) {
- MessageBuffer_Insert(&KeyStateBuffer, Event);
- }
- }
KeyboardRxCallback()中断处理程序会在一个字节通过UART被接收时触发,考虑到中断处理程序应该尽快执行,所以收到的信息会放到一个环形的缓冲区留待后面处理。环形缓冲区最终会被主循环处理,键盘矩阵也会基于信息而被更新。
上面所说的是实现该点的最简单的方法,但是最终的协议要更加复杂。你需要考虑多字节信息的处理,而且个人信息也要通过CRC-CCITT校验来检查其完整性。
在这一点上,我们的实验板模型或许会让你印象深刻:
第四步:满足LED显示屏
这是为了让用户能够定义多个特定于应用的键映射来提高生产效率。用户需要意识到一些正被用于键映射的方式,一个集成的LED显示屏被构建于键盘内,下图展示了这种模型:
LED显示是由一个8x6矩阵实现的:
每两行红色LED符号代表14-segment LED显示的分段,白色LED符号则代表了额外的三个状态指标。
通过LED驱动电流并使其亮起来,相应的列设为高电压,相应的行设为低电压。该系统一个有趣的结果是,在任何给定的时刻,只有一列是可以被启用的,而其他列是被禁用的。有人可能会认为这套系统不能工作于整个LED,但是在现实中,列和行更新的太快以至于凭人的肉眼无法看到明显的闪烁。
LED矩阵由两个集成电路(IC)驱动,一个驱动行,一个驱动列,驱动列的源IC是PCA9634 I2C LED驱动:
驱动行的为TPIC6C595:
让我们看下相关代码:
- uint8_t LedStates[LED_MATRIX_ROWS_NUM];
- void LedMatrix_UpdateNextRow(bool IsKeyboardColEnabled)
- {
- TPIC6C595_Transmit(LedStates[ActiveLedMatrixRow]);
- PCA9634_Transmit(1 << ActiveLedMatrixRow);
- if (++ActiveLedMatrixRow == LED_MATRIX_ROWS_NUM) {
- ActiveLedMatrixRow = 0;
- }
- }
LedMatrix_UpdateNextRow()将以每毫秒的速度被调用,更新LED矩阵的一行,LedMatrix数组存储单个LED灯的状态,状态信息源于通过UART的按/释键事件。
整体情况
到这里我们已经逐步为自己的键盘建立了所有的必需组件。现在需要从全局出发,键盘的内部就像一个微型计算机网络:大量节点相互连接。所不同的是测量节点之间的距离不是米或公里,而是厘米,并且节点不是成熟的计算机,而是微型的集成电路。
但是到目前为止说的大多都是键盘装置方面的细节,关于UHK代理提的不多。UHK代理是配置器应用,通过键盘来满足用户的自定义需求。可以见于下段代码:
-
var enumerationModes = {
- 'keyboard' : 0,
- 'bootloader-right' : 1,
- 'bootloader-left' : 2
- };
- function sendReenumerateCommand(enumerationMode, callback)
- {
- var AGENT_COMMAND_REENUMERATE = 0;
- sendAgentCommand(AGENT_COMMAND_REENUMERATE, enumerationMode, callback);
- }
- function sendAgentCommand(command, arg, callback)
- {
- setReport(new Buffer([command, arg]), callback);
- }
- function setReport(message, callback)
- {
- device.controlTransfer(
- 0x21, // bmRequestType (constant for this control request)
- 0x09, // bmRequest (constant for this control request)
- 0, // wValue (MSB is report type, LSB is report number)
- interfaceNumber, // wIndex (interface number)
- message, // message to be sent
- callback
- );
- }
每一个命令都有一个8字节标识符和一组command-specific参数。目前,只有re-enumerate命令被实现,sendReenumerateCommand()使设备作为左/右引导装载程序重新装置,以此来升级固件或变为一个键盘设备。
创建原型
一切制造之前都需要有一个CAD设计:
3D打印的键盘会是这个样子:
基于机械设计和原理、印刷电路板设计。右半部分的PCB在KiCad中会是这样:
PCB表面贴装组件必须手工焊接:
最后经过3D印刷、抛光、组装等,我们会得到这样一个原型:
结论
这是一个很广泛的话题,作为IT人员,键盘可以说是我们最亲密的“朋友”之一。不知道您看完之后是否会有自己的想法呢?是否会有做一个的冲动?如果有空闲的话,试着去尝试一些,来一场“说做就做的任性”!(编译/陈明)
原文来自:DZone