老机械键盘改造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哦

  1. void USB_IRQHandler(void)

  2. {

  3. if(USB->ISTR & USB_ISTR_CTR)

  4. {

  5. if((USB->ISTR & 0x0f)==0)   // EP_ID==0

    >>>请点击阅读原文查看完整代码<<<

  6. if(USB->ISTR & USB_ISTR_SOF)

  7. {

  8. USB->ISTR = ~USB_ISTR_SOF;  // write 0 to clear

  9. }

  10. }

因为只有两个EP需要管,数据量也很小,STM32F0的PMA(Packet Memory Area)固定分配好就不动了,读写数据都直接在PMA上读写。在USB Reset中断的时候,把PMA和EP都重新初始化。

主程序中用一个无限循环,每次中断过后处理一下USB请求,以及来自键盘扫描的检测。

  1. while(1)

  2. {

  3. static char row=0;

  4. __WFI();

  5. if(ep0_state & 0x80)    // request data processing

  6. {

  7. if(ep0_state==0x80) // SETUP phase

  8. >>>请点击阅读原文查看完整代码<<<

  9. test>>=1;

  10. }

  11. }

  12. row=scan_row;

  13. }

  14. }

EP0的控制传输,把用到的请求处理一下

  1. char setup_packet_service(void)

  2. {

  3. if(ep0_std_req->bmRequestType & 0x20)   // class-specific

  4. {

  5. switch(ep0_std_req->bRequest)

  6. {

  7. case REQ_GET_REPORT: break;

  8. >>>请点击阅读原文查看完整代码<<<

  9. USB->EP0R = USB_EP_TYPE_CONTROL|USB_EP_STAT_TX0;

  10. USB_PMA[1]=0;   // Zero DATA

  11. ep0_state=4;    // No DATA phase

  12. return 1;

  13. default: return 0;

  14. }

  15. }

  16. }

各种描述符,是USB开发首先要处理的

  1. char descriptor_service(void)

  2. {

  3. switch((ep0_std_req->wValue)>>8)

  4. {

  5. case DESC_TYPE_DEVICE:

  6. return ep0_preparedata(&DevDesc, sizeof(DevDesc));

  7. >>>请点击阅读原文查看完整代码<<<

  8. return ep0_preparedata(&HidReportDesc, sizeof(HidReportDesc));

  9. default:

  10. return 0;

  11. }

  12. }

下面说下我对键盘的处理。因为有14+8条线,其中8条一组我称为列,用PA0~7读取;另外14条我称为行,在一个时刻只有1条为低电平(输出),其它13条为高阻。在列扫描线上加上上拉,因此没有键按下的时候,PA0~7都是高电平的。若某键被按下,当在所在的行被扫描时,对应的列就会变成低。我用了Timer6中断,每0.5ms切换一次扫描线,这样扫描一遍矩阵键盘用7ms.

  1. void TIM6_DAC_IRQHandler(void)

  2. {

  3. __IO uint8_t *PA_IDR = (uint8_t *)&(GPIOA->IDR);

  4. TIM6->SR &= ~TIM_SR_UIF;

(0)

相关推荐