从头开始:我如何构建开发人员的梦想键盘

已发表: 2022-03-11

2007 年 8 月的一天工作,我不禁意识到,我的常规 PC 键盘并没有为我提供尽可能多的服务。 我不得不过度地在键盘的各个块之间移动我的手,每天数百甚至数千次,而且我的手彼此靠近令人不舒服。 一定有更好的办法,我想。

当我想到为开发人员定制最好的键盘时,我感到无比兴奋——后来,我意识到,作为一名自由嵌入式软件开发人员,我对硬件一无所知。

当时,我忙于其他项目,但没有一天不考虑构建黑客键盘。 很快我就开始把空闲时间投入到这个项目上。 我设法学习了一套全新的技能,说服了我的一个朋友,杰出的机械工程师 Andras Volgyi 加入这个项目,召集了一些关键人物,并投入了足够的时间来创建工作原型。 如今,终极黑客键盘已成为现实。 我们每天都在进步,众筹活动的启动触手可及。

我开始考虑如何更改键盘布局,并完成了这个!

从对电子产品一无所知的软件背景,到设计和构建功能强大、适销对路的硬件设备,是一种有趣而引人入胜的体验。 在本文中,我将介绍这款电子杰作的设计原理。 对电子电路图有基本的了解可以帮助您跟进。

你是怎么做键盘的?

在我为这个话题投入了数千小时的生命之后,要给出一个简短的答案对我来说是一个巨大的挑战,但有一种有趣的方式可以回答这个问题。 如果我们从简单的东西开始,比如 Arduino 板,然后逐渐将其打造为终极黑客键盘,会怎样? 它不仅应该更容易消化,而且应该极具教育意义。 因此,让我们的键盘教程之旅开始吧!

第一步:没有键的键盘

首先,让我们制作一个每秒发出一次x字符的 USB 键盘。 Arduino Micro 开发板是实现此目的的理想选择,因为它具有 ATmega32U4 微控制器 - AVR 微控制器和与 UHK 大脑相同的处理器。

Arduino Micro 板是为开发人员构建键盘的基础。

对于支持 USB 的 AVR 微控制器,适用于 AVR 的轻量级 USB 框架 (LUFA) 是首选库。 它使这些处理器成为打印机、MIDI 设备、键盘或几乎任何其他类型的 USB 设备的大脑。

将设备插入 USB 端口时,设备必须传输一些称为 USB 描述符的特殊数据结构。 这些描述符告诉主机正在连接的设备的类型和属性,并由树结构表示。 更复杂的是,一个设备不仅可以实现一种功能,而且可以实现多种功能。 让我们看看 UHK 的描述符结构:

  • 设备描述符
    • 配置描述符
      • 接口描述符 0:GenericHID
        • 端点描述符
      • 接口描述符一:键盘
        • 端点描述符
      • 接口描述符2:鼠标
        • 端点描述符

大多数标准键盘只公开一个键盘接口描述符,这是有道理的。 但是,作为自定义编程键盘,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 次)查询设备,以确定是否有任何新数据要发送。 相关的回调是CALLBACK_HID_Device_CreateHIDReport()函数,在这种情况下,只要isSecondElapsed变量包含1 ,它就会将x字符的扫描码发送到主机。 isSecondElapsed从主循环设置为1 ,并从回调设置为0

第二步:四键键盘

在这一点上,我们的键盘并不是非常有用。 如果我们真的能在上面打字就好了。 但是为此我们需要按键,并且按键必须排列成键盘矩阵。 一个全尺寸的 104 键键盘可以有 18 行和 6 列,但我们只需一个简陋的 2x2 键盘矩阵即可启动。 这是示意图:

要自定义黑客键盘,您必须仔细考虑按键矩阵。

这就是它在面包板上的样子:

配置面包板是为开发人员构建键盘的关键步骤。

假设ROW1连接到PINA0ROW2连接到PINA1COL1连接到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<KeyMatrix->Info->ColNum; Col++) { const Pin_t *ColPort = KeyMatrix->Info->ColPorts + Col; for (uint8_t Row=0; Row<KeyMatrix->Info->RowNum; Row++) { const Pin_t *RowPin = KeyMatrix->Info->RowPins + Row; uint8_t IsKeyPressed = *RowPin->Name & 1<<RowPin->Number; KeyMatrix_SetElement(KeyMatrix, Row, Col, IsKeyPressed); } } }

代码一次扫描一列,并在该列中读取各个按键开关的状态。 然后按键开关的状态被保存到一个数组中。 在我们之前的CALLBACK_HID_Device_CreateHIDReport()函数中,将根据该数组的状态发送相关的扫描代码。

第三步:一个有两半的键盘

到目前为止,我们已经创建了普通键盘的开端。 但在本键盘教程中,我们的目标是先进的人体工程学设计,鉴于人们有两只手,我们最好在组合中添加另一半键盘。

另一半将具有另一个键盘矩阵,其工作方式与前一个相同。 令人兴奋的新事物是键盘两半之间的通信。 互连电子设备的三种最流行的协议是 SPI、I 2 C 和 UART。 出于实际目的,我们将在这种情况下使用 UART。

要成为一个好的编程键盘,两半之间必须有良好的沟通。

根据上图,双向通信向右流过 RX,向左流过 TX。 VCC 和 GND 是传输功率所必需的。 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); } }

只要通过 UART 接收到一个字节,就会触发KeyboardRxCallback()中断处理程序。 鉴于中断处理程序应尽快执行,接收到的消息将被放入环形缓冲区以供以后处理。 环形缓冲区最终会在主循环中得到处理,键盘矩阵将根据消息进行更新。

以上是实现这一点的最简单方法,但最终协议会稍微复杂一些。 必须处理多字节消息,并且必须使用 CRC-CCITT 校验和检查各个消息的完整性。

在这一点上,我们的面包板原型看起来非常令人印象深刻:

面包板原型开始呈现为开发人员定制键盘的形状。

第四步:认识LED显示屏

我们与 UHK 的目标之一是让用户能够定义多个特定于应用程序的键盘映射,以进一步提高生产力。 用户需要通过某种方式来了解正在使用的实际键盘映射,因此键盘中内置了一个集成的 LED 显示屏。 这是一个所有 LED 都亮起的原型显示器:

在本教程中,LED 显示屏是为开发人员构建最佳键盘的核心。

LED 显示屏由 8x6 LED 矩阵实现:

如果没有 8x6 LED 矩阵,黑客键盘将是不完整的。

每两行红色 LED 符号代表 14 段 LED 显示屏之一的段。 白色 LED 符号代表另外三个状态指示灯。

为了驱动电流通过 LED 并将其点亮,对应的列设置为高电压,对应的行设置为低电压。 该系统的一个有趣结果是,在任何给定时刻,只能启用一列(该列上应点亮的所有 LED 都将其相应的行设置为低电压),而其余列被禁用. 有人可能会认为该系统无法使用全套 LED,但实际上列和行更新得如此之快,以至于人眼看不到闪烁。

LED 矩阵由两个集成电路 (IC) 驱动,一个驱动其行,另一个驱动其列。 驱动列的源 IC 是 PCA9634 I2C LED 驱动器:

两个集成电路驱动 Ultimate Hacker 键盘上的 LED 矩阵。

驱动行的 LED 矩阵接收器 IC 是 TPIC6C595 功率移位寄存器:

驱动 LED 行的 IC 看起来像这样。

我们来看看相关代码:

 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 矩阵。 LedStates数组存储各个 LED 的状态,通过 UART 根据来自右半键盘的消息进行更新,与按键/按键释放事件的方式几乎相同。

大局

到目前为止,我们已经逐渐为我们的自定义黑客键盘构建了所有必要的组件,是时候看看大局了。 键盘的内部就像一个迷你计算机网络:许多节点相互连接。 不同之处在于节点之间的距离不是以米或公里为单位,而是以厘米为单位,并且节点不是成熟的计算机,而是微型集成电路。

我们教程键盘的内部由互连的节点组成。

到目前为止,关于开发者键盘的设备端细节已经说了很多,但关于主机端软件 UHK Agent 的说法并不多。 原因是,与硬件和固件不同,Agent 在这一点上非常初级。 但是,代理的高级架构已经确定,我想分享一下。

UHK Agent 是一个配置器应用程序,通过它可以定制键盘以满足用户的需求。 尽管是一个富客户端,但 Agent 使用 Web 技术并在 node-webkit 平台之上运行。

代理通过发送特殊的、特定于设备的 USB 控制请求并处理其结果,使用 node-usb 库与键盘进行通信。 它使用 Express.js 公开 REST API 以供第三方应用程序使用。 它还使用 Angular.js 来提供简洁的用户界面。

 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 位标识符和一组特定于命令的参数。 目前,只实现了重新枚举命令。 sendReenumerateCommand()使设备重新枚举为左引导加载程序或右引导加载程序,用于升级固件或作为键盘设备。

有人可能不知道这款软件可以实现的高级功能,所以我仅举几例:代理将能够可视化各个按键的磨损情况并通知用户他们的预期寿命,因此用户可以为即将进行的维修购买几个新的钥匙开关。 代理还将提供一个用户界面,用于配置黑客键盘的各种键盘映射和图层。 还可以设置鼠标指针的速度和加速度,以及许多其他超级功能。 天空是极限。

创建原型

创建定制的键盘原型需要做很多工作。 首先,机械设计必须最终确定,这本身就相当复杂,涉及定制设计的塑料部件、激光切割的不锈钢板、精密铣削的钢导轨和将两个键盘半部固定在一起的钕磁铁。 在制造开始之前,一切都在 CAD 中设计。

CAD 绘图有助于构建适合开发人员的键盘。

这是 3D 打印键盘盒的外观:

我们从 3D 打印编程键盘外壳开始。

根据机械设计和原理图,必须设计印刷电路板。 KiCad 中右侧的 PCB 如下所示:

对键盘进行编程从设计印刷电路板开始。

然后 PCB 被制造出来,表面贴装的元件必须手工焊接:

焊接自定义键盘组件可确保它在放入机箱后正常工作。

最后,在制作完所有部件之后,包括 3D 打印、抛光和涂漆塑料部件以及组装一切,我们最终得到了一个可以工作的黑客键盘原型,如下所示:

结论

我喜欢将开发人员的键盘与音乐家的乐器进行比较。 如果您考虑一下,键盘是相当亲密的对象。 毕竟,我们整天都在使用它们来逐个字符地制作明天的软件。

可能是因为上述原因,我认为开发 Ultimate Hacking Keyboard 是一种特权,尽管困难重重,但它往往是一次非常激动人心的旅程和令人难以置信的紧张学习体验。

这是一个广泛的话题,我只能在这里触及表面。 我希望这篇文章很有趣,并且充满了有趣的材料。 如果您有任何问题,请在评论中告诉我。

最后,欢迎您访问 https://ultimatehackingkeyboard.com 了解更多信息,并在那里订阅我们的活动启动通知。