本文概述
2007年8月的一天, 我忍不住意识到普通的PC键盘无法为我提供尽可能多的服务。我不得不在键盘的各个块之间过度动手, 每天要动动数百次(即使不是数千次), 而且彼此的手也不舒服。我想一定有更好的方法。
当我想到为开发人员定制最好的键盘时, 这种感觉随之而来, 给人以压倒性的兴奋感;后来, 我意识到, 作为一名自由嵌入式软件开发人员, 我对硬件毫无头绪。
当时, 我非常忙于其他项目, 但是一天没想到不打算构建黑客键盘。不久, 我开始将自己的空闲时间投入到该项目中。我设法学习了一套全新的技能, 并说服了我的一个朋友机械工程师ExtrarásVölgyi加入该项目, 召集了一些关键人员, 并花了足够的时间来创建工作原型。如今, 终极黑客键盘已成为现实。我们每天都在进步, 而且众筹活动即将启动。
从软件背景到对电子学一无所知, 再到设计和构建功能强大的适销对路的硬件设备, 是一种有趣而有趣的经历。在本文中, 我将描述该电子杰作的设计。对电子电路图的基本了解可以帮助你继续学习。
你如何制作键盘?
在将我的一生投入数千小时之后, 简短地回答这个问题对我来说是一个巨大的挑战, 但是有一种有趣的方式可以回答这个问题。如果我们从诸如Arduino板之类的简单事物开始, 逐步将其构建成为Ultimate Hacking Keyboard键盘, 该怎么办?它不仅应该更易于消化, 而且应该具有很高的教育意义。因此, 让我们的键盘教程之旅开始吧!
第一步:没有按键的键盘
首先, 让我们制作一个USB键盘, 该键盘每秒发出一次x字符。 Arduino Micro开发板是实现此目的的理想选择, 因为它具有ATmega32U4微控制器-AVR微控制器和UHK的大脑相同的处理器。
对于支持USB的AVR微控制器, 轻便的AVR USB框架(LUFA)是首选的库。它使这些处理器成为打印机, MIDI设备, 键盘或几乎任何其他类型的USB设备的大脑。
将设备插入USB端口时, 设备必须传输一些称为USB描述符的特殊数据结构。这些描述符告诉主机计算机所连接设备的类型和属性, 并由树结构表示。使事情变得更加复杂的是, 设备不仅可以实现一个功能, 而且可以实现多种功能。我们来看看UHK的描述符结构:
- 设备描述符
- 配置描述符
- 接口描述符0:GenericHID
- 端点描述符
- 接口描述符1:键盘
- 端点描述符
- 接口描述符2:鼠标
- 端点描述符
- 接口描述符0:GenericHID
- 配置描述符
大多数标准键盘只公开一个键盘接口描述符, 这很有意义。但是, 作为自定义编程键盘, 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字符的扫描码发送给主机。从回调到0。
第二步:四个键的键盘
在这一点上, 我们的键盘还不是很有用。如果我们可以实际输入, 那就太好了。但是为此, 我们需要按键, 并且按键必须排列在键盘矩阵中。完整的104键键盘可以包含18行和6列, 但我们仅需一个不起眼的2×2键盘矩阵即可启动。这是原理图:
这是在面包板上的外观:
假设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<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, I2C和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显示由一个8×6 LED矩阵实现:
每两行红色LED符号代表14段LED显示屏之一的段。白色LED符号代表另外三个状态指示灯。
为了驱动电流通过LED并点亮它, 相应的列设置为高电压, 相应的列设置为低电压。该系统的一个有趣结果是, 在任何给定时刻, 只能启用一列(该列上所有应点亮的LED均将其对应的行设置为低电压), 而其余列均被禁用。可能有人认为该系统无法使用全套LED, 但实际上列和行的更新速度如此之快, 以至于肉眼看不到闪烁。
LED矩阵由两个集成电路(IC)驱动, 一个驱动其行, 另一个驱动其列。驱动列的源IC是PCA9634 I2C LED驱动器:
驱动行的LED矩阵接收器IC是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矩阵。 LedStates阵列存储各个LED的状态, 并根据来自右键盘一半的消息通过UART更新, 这几乎与按键/释放事件的方式相同。
整体情况
到现在为止, 我们已经逐步为自定义黑客键盘构建了所有必需的组件, 是时候看到大图了。键盘的内部就像一个小型计算机网络:许多节点相互连接。不同之处在于, 节点之间的距离不是以米或公里为单位, 而是以厘米为单位, 并且节点不是成熟的计算机, 而是微型集成电路。
到目前为止, 关于开发人员键盘的设备端详细信息已经说了很多, 但是关于主机端软件UHK Agent却说得很少。原因是, 与硬件和固件不同, 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位标识符和一组特定于命令的参数。当前, 仅实现了re-enumerate命令。 sendReenumerateCommand()使设备重新枚举为左引导加载程序或右引导加载程序, 以升级固件或键盘设备。
人们可能不知道此软件可以实现的高级功能, 因此我仅举几例:代理将能够可视化各个按键的磨损并通知用户其预期寿命, 因此用户可以购买了几个新的钥匙开关, 以备即将维修。代理还将提供一个用户界面, 用于配置黑客键盘的各种按键映射和层。还可以设置鼠标指针的速度和加速度, 以及其他超级功能的负载。天空是极限。
创建原型
创建定制的键盘原型需要大量工作。首先, 必须完成机械设计, 该机械设计本身非常复杂, 涉及定制设计的塑料部件, 激光切割的不锈钢板, 精密铣削的钢制导板和将两个半键盘固定在一起的钕磁铁。在制造开始之前, 一切都以CAD设计。
这是3D打印键盘盒的外观:
基于机械设计和原理图, 必须设计印刷电路板。在KiCad中, 右侧PCB如下所示:
然后制作PCB, 并且必须用手焊接表面安装的组件:
最后, 在制造完所有零件, 包括3D打印, 抛光和上漆塑料零件并组装好所有零件之后, 我们最终得到了一个可行的黑客键盘原型, 如下所示:
总结
我喜欢将开发人员的键盘与音乐家的乐器进行比较。如果考虑一下, 键盘是相当私密的对象。毕竟, 我们一整天都在使用它们来逐字逐句地制作明天的软件。
可能由于上述原因, 我认为开发Ultimate Hacking Keyboard是一种特权, 尽管有许多困难, 但总的来说, 这是一段非常激动人心的旅程, 并且获得了难以置信的强烈学习经验。
这是一个广泛的话题, 我只能在这里进行介绍。我希望这篇文章很有趣, 并且充满了有趣的材料。如有任何疑问, 请在评论中让我知道。
最后, 欢迎你访问https://ultimatehackingkeyboard.com了解更多信息, 并订阅该网站, 以通知我们广告系列的启动。
评论前必须登录!
注册