老机械键盘改造USB、QWERTY/Dvorak一键切换
这个DIY项目的想法已经有很久了,如今终于达到了的设计的初衷。要体现“任性”的特点,先介绍背景吧。
在看这个帖子的诸位一定都在用计算机键盘吧。键盘上的数字键是1到9从左至右排列,或者是右小键盘区那样三个一排有序排列,反正规律很明显。但是字母键却不是A,B,C...到Z这么按字母序有规律地排下来的。我刚接触电脑(其实还是学习机)的时候,没在意这个问题,觉得是要盲打嘛,反正对两手的手指头来说,按字母序排列并没有什么好处。
于是用多了这些排列也就记住了,从来不管它为什么要这样。其实,PC的键盘键位排布上是延用了打字机的键盘,这是设备演变过程中很自然的一个延续。打字机的历史就要早很多了,我没有亲见过打字机长什么样,而且,咱们汉字是铅字打上去的,和英文打字机方式完全不同。
上面这个照片(来自wikipedia),是"Sholes and Glidden Type-Writer.",第一个获得成功的商用打字机(1873)。请注意它的键盘字母键排列。
为什么得到这样一个字母排列?在当时的确经过了多次的优化改进,因为打字机是机械的动作,要尽量避免连续的击键引起冲突。结果是因为商业上的成功,QWERTY这个布局也跟着被越来越多的制造商吸收采用。在非英语语言的键盘上,个别键位可能不同,属大同小异了。最早的IBM PC键盘:
其实在电传动打字机问世之后,打字键盘的键位布局就可以自由了。但是QWERTY的流行没有被改变——习惯的力量是强大的。虽然是大众所接受,QWERTY也有被人诟病的地方,比如说左右手分配不平衡,在英语里面单独用左手能打出来的单词远比单独用右手的多。那么,除了QWERTY还能用啥?在ANSI标准里面还有另外一个键盘布局,叫做DVORAK.
Dvorak(德沃夏克)布局,是以其发明人之一: August Dvorak 的姓命名的。在20世纪30年代,Dvorak 和 Dealey 在他们多年的研究工作基础上发明了Dvorak布局,目标是减少打字出错几率、提高速度和减少手的疲劳。最初发明的布局是这个样子:
Dvorak布局的最明显特征是让使用频率最高的键安排在中间的一排(Home row),这样手指不用移动就触得到。当然还有左右手均衡的设计等等。尽管不是所有人都同意Dvorak布局能够比QWERTY布局提高键盘输入的效率,最快打字速度的记录的确是在Dvorak键盘上创造的。
我是经常要写代码的人,对键盘要求比较高,一定要顺手。从1998年拥有电脑开始,第一块键盘用了5年,实在是塑料结构磨损严重了才换了。第二块键盘用了大概也有5年,第三块是淘宝买到的和第二块同样的。除了手感,我对键盘还有个挑剔是要大回车键(老键盘惯出来的)。到了用上笔记本电脑,键盘问题只能忍忍了。我最后买的一块Benq的”轻指飞扬"绝版键盘因为是USB,作为笔记本键盘替补一直保留到现在。
到2012年下半年,我在淘宝发现了有“机械键盘”这东东,认识了Cherry MX轴。然后到2013年农历年后,我花一百多一点买了一块老旧的国产青轴机械键盘,虽然很陈旧状态也差了,敲了一会儿我就发现:这就是我要的手感啊,一比起来用了多年的薄膜键盘简直太委屈手指了。我后来花了更多的钱买了新的轴(就是机械键盘的开关)来更换修复,使之成为上班工作用。
机械键盘用着爽,后来我发现手指别扭的地方了,跟QWERTY键盘布局有关系。了解了Dvorak布局之后,我下定决心,换用Dvorak. 这个过程很漫长,大约是一年以后才抛开了QWERTY根深蒂固的影响。到如今两年多,我也没有肯定我的输入速度是否达到自己曾经QWERTY时候最快的水平,不过可以肯定的是换了Dvorak,手指头是舒服了。借个图说明两种布局的差别:
从QWERTY换到Dvorak,除了决心以及过程中的痛苦外,还有额外的成本。一是操作系统的支持,虽然DOS, Windows, Linux都支持Dvorak,但需要加载keymap,或者设置键盘布局,且每台机器,每个用的系统都要改。在Windows上,Dvorak和默认的En-US是平级的,但中文输入法只能用En-US也就是压根儿没考虑Dvorak. 于是我将en_us.dll直接替换掉了,但也不是完美的解决,比如Sogou拼音会从更底层调用读键盘,还是没法用(于是我一直用智能ABC咯)。二是用别人电脑的时候,比如同事要请帮忙,又不能SSH过去,我就只好盯着键盘来“一指禅”了;以及电脑安装系统的时候,应急启动时候,类似的困难。三是我的电脑夫人也就没法用,同样的道理。四是虽然内部变成Dvorak,键盘上印的还是QWERTY那样的,必须盲打,必须双手干活,不能一只手拿着食物啦。这时候我多希望它还是QWERTY,可以用用一指禅。
综上,在操作系统软件层次上修改键盘布局来使用Dvorak,问题还是多多。那么我在键盘上面改,硬件直接搞定好了。附带的好处是可以随时切换键盘布局,键盘也可以共享给夫人用。国产老机械键盘里面主控是8049 MCU,虽然不能对它编程,我换掉它还是可以的。于是就有了这次的“任性"DIY。
先是改造的对象,主角: 这已经是拆解出机械键盘中的PCB板+钢板,并且拆掉了全部的键轴之后的样子。这块键盘买来时的成色相当差,很脏,惟有键帽还不错,但原本的轴已进灰,状态差。
轴全部拆下来之后才能将钢板和PCB分离,不然是被卡住的。原来键盘里面的灰比照片上还多得多。注意到这块DIP40的芯片,就是键盘的主控。
特写,80C49
LED部分,使用了一片D触发器锁存指示灯状态.
暴力破坏,将80C49拆掉。
拆掉原来的键盘主控,我用什么顶替呢?没有引脚全兼容的单片机了,而且我要制作USB键盘,所以……STM32F072,做块一样大小的PCB. 因为主要是使用原有的键盘扫描矩阵,有些引脚是不需要连的。
焊好元件后的板子,准备替换80C49
用剪下的电阻腿作连接吧,对好位把引脚都焊上。STM32F0的SWD接口务必要留出来下载程序的。
这是在软件开发当中调试的场景。USB线需要飞线,因为原来的键盘PCB上就没有USB.
开始安装钢板,主键区焊上全新的Cherry MX茶轴(2.5 RMB一颗)。F区暂且空着,因为使用频率不高,换新轴就显得浪费了,等下再把部分旧轴清洗一下装回去。
我设计的MCU PCB要在键盘PCB和钢板之间。除了SWD的引脚,把UART飞线出来供调试的不时之需。
主键区键帽就位
编辑键区也安装好,确认这里替换后不会有冲突。调试用的线和针脚以后是要拆掉的。
最后的组装,USB线,以及切换键盘布局的附加按钮。部分键轴还没有装,低优先级的。
DIY过程直播完了,下面说硬件的设计。
80C49是块MCU,貌似也就在PS/2键盘上面用。搜到其datasheet对引脚的定义:
其实最关心的还是键盘矩阵怎么接的,这个我就靠人肉了,在的PCB背面寻着每条扫描行或列线找,记录在草稿纸上。最终整理出来的结果是这样的:(最上边和最右边铅笔写的数字是引脚编号)
扫描矩阵是8x14的,最多可以支持112个按键,实际上只有101个键,空出了一些。对照上面那个引脚定义,可以把用到的I/O口确定了。除了电源引脚,剩下还有几个引脚使用到:PS/2的CLK和DATA占用2个,状态指示LED的电路占用1个,AT/XT开关使用了一个。我用STM32F072C8,有48个引脚刚好是够的,富余的I/O就飞线引出了。
这是我设计的电路图:
PCB Layout:
不从80C49引脚上走的信号包括: SWD接口,USB D+/D-,USART TX/RX,额外两个可用I/O.
软件上的工作比硬件多得多。因为想改造成USB键盘,不得不把USB HID的实现稍微看懂一下。PS/2模式硬件上也是保留的,暂时我还没去写软件。
总结一下,USB HID键盘需要使用两种HID报告:一是从设备到主机的,按键状态的报告,8字节;二是主机到设备的,指示灯状态的报告,1字节。第一个报告我使用EP1(Endpoint 1, 端点1)来发送,中断传输;第二个报告就使用默认的EP0,控制传输。USB的描述符,可以从现有的USB键盘上修改而来。下面是用USBTreeView这个工具查看到的的我的这个键盘的描述符:
其中USB报告描述符我没完全看懂,copy了现成的(这个也没必要自己重新写嘛)
USB的中断ISR,bare metal哦
void USB_IRQHandler(void)
{
if(USB->ISTR & USB_ISTR_CTR)
{
if((USB->ISTR & 0x0f)==0) // EP_ID==0
>>>请点击阅读原文查看完整代码<<<
if(USB->ISTR & USB_ISTR_SOF)
{
USB->ISTR = ~USB_ISTR_SOF; // write 0 to clear
}
}
因为只有两个EP需要管,数据量也很小,STM32F0的PMA(Packet Memory Area)固定分配好就不动了,读写数据都直接在PMA上读写。在USB Reset中断的时候,把PMA和EP都重新初始化。
主程序中用一个无限循环,每次中断过后处理一下USB请求,以及来自键盘扫描的检测。
while(1)
{
static char row=0;
__WFI();
if(ep0_state & 0x80) // request data processing
{
if(ep0_state==0x80) // SETUP phase
>>>请点击阅读原文查看完整代码<<<
test>>=1;
}
}
row=scan_row;
}
}
EP0的控制传输,把用到的请求处理一下
char setup_packet_service(void)
{
if(ep0_std_req->bmRequestType & 0x20) // class-specific
{
switch(ep0_std_req->bRequest)
{
case REQ_GET_REPORT: break;
>>>请点击阅读原文查看完整代码<<<
USB->EP0R = USB_EP_TYPE_CONTROL|USB_EP_STAT_TX0;
USB_PMA[1]=0; // Zero DATA
ep0_state=4; // No DATA phase
return 1;
default: return 0;
}
}
}
各种描述符,是USB开发首先要处理的
char descriptor_service(void)
{
switch((ep0_std_req->wValue)>>8)
{
case DESC_TYPE_DEVICE:
return ep0_preparedata(&DevDesc, sizeof(DevDesc));
>>>请点击阅读原文查看完整代码<<<
return ep0_preparedata(&HidReportDesc, sizeof(HidReportDesc));
default:
return 0;
}
}
下面说下我对键盘的处理。因为有14+8条线,其中8条一组我称为列,用PA0~7读取;另外14条我称为行,在一个时刻只有1条为低电平(输出),其它13条为高阻。在列扫描线上加上上拉,因此没有键按下的时候,PA0~7都是高电平的。若某键被按下,当在所在的行被扫描时,对应的列就会变成低。我用了Timer6中断,每0.5ms切换一次扫描线,这样扫描一遍矩阵键盘用7ms.
void TIM6_DAC_IRQHandler(void)
{
__IO uint8_t *PA_IDR = (uint8_t *)&(GPIOA->IDR);
TIM6->SR &= ~TIM_SR_UIF;