老式机械键盘改造:老机械键盘改造USB
老式机械键盘改造:老机械键盘改造USB到2012年下半年,我在淘宝发现了有“机械键盘”这东东,认识了Cherry MX轴。然后到2013年农历年后,我花一百多一点买了一块老旧的国产青轴机械键盘,虽然很陈旧状态也差了,敲了一会儿我就发现:这就是我要的手感啊,一比起来用了多年的薄膜键盘简直太委屈手指了。我后来花了更多的钱买了新的轴(就是机械键盘的开关)来更换修复,使之成为上班工作用。我是经常要写代码的人,对键盘要求比较高,一定要顺手。从1998年拥有电脑开始,第一块键盘用了5年,实在是塑料结构磨损严重了才换了。第二块键盘用了大概也有5年,第三块是淘宝买到的和第二块同样的。除了手感,我对键盘还有个挑剔是要大回车键(老键盘惯出来的)。到了用上笔记本电脑,键盘问题只能忍忍了。我最后买的一块Benq的”轻指飞扬"绝版键盘因为是USB,作为笔记本键盘替补一直保留到现在。其实在电传动打字机问世之后,打字键盘的键位布局就可以自由了。但是QWER
这个DIY项目的想法已经有很久了,如今终于达到了的设计的初衷。要体现“任性”的特点,先介绍背景吧。
在看这个帖子的诸位一定都在用计算机键盘吧。键盘上的数字键是1到9从左至右排列,或者是右小键盘区那样三个一排有序排列,反正规律很明显。但是字母键却不是A B C...到Z这么按字母序有规律地排下来的。我刚接触电脑(其实还是学习机)的时候,没在意这个问题,觉得是要盲打嘛,反正对两手的手指头来说,按字母序排列并没有什么好处。
于是用多了这些排列也就记住了,从来不管它为什么要这样。其实,PC的键盘键位排布上是延用了打字机的键盘,这是设备演变过程中很自然的一个延续。打字机的历史就要早很多了,我没有亲见过打字机长什么样,而且,咱们汉字是铅字打上去的,和英文打字机方式完全不同。
为什么得到这样一个字母排列?在当时的确经过了多次的优化改进,因为打字机是机械的动作,要尽量避免连续的击键引起冲突。结果是因为商业上的成功,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;
-
>>>请点击阅读原文查看完整代码<<<
-
if(scan_row<13)
-
scan_row ;
-
else
-
scan_row=0;
-
}
扫描的状态被Timer 6 ISR记录在 key_state[] 数组当中,上一次的状态保存为 prev_key_state[]. 这样在主程序中只要比较这两个数组,就知道是否键盘的状态有变化了。有变化(按下或者抬起)的时候,再调用 update_key_matrix() 函数去生成USB HID报告。
要从键盘的扫描行、列坐标得到按键的编码(此处不是ASCII,也不是PS/2的键盘扫描码,而是USB HID定义的键盘Usage ID),需要用到查找表。我需要切换Dvorak和QWERTY两个布局,因此需要准备两个查找表:
-
const char hid_keymap_qwerty[14][8]={
-
{HK_RShift HK_NONE HK_NONE HK_A HK_R HK_7 HK_F9 HK_Esc}
-
>>>请点击阅读原文查看完整代码<<<
-
{HK_Pause HK_Tab HK_PrtScr HKR_2 HKR_3 HKR_1 HKR_0 HK_MODE}
-
};
上面的宏定义另写在 translate.h 头文件中。HK_NONE是没有实际按键的位置,HK_MODE是我另外加的一个键,用来切换两个键盘布局。这个“一键切换”的键加装,可以用原来键盘上的AT/XT开关,也可以用我预留的User I/O,做到后来发现用键盘矩阵的空闲位置更方便。
HID报告的8个字节,第一个是8个特殊键的状态(Shift Alt Ctrl GUI),第二个保留,后6个每个非0值是一个按下的键的Usage ID. 产生HID报告的子程序:
-
void update_key_matrix(char row char col char onoff)
-
{
-
static uint16_t hid_report[4]={0 0 0 0};
-
static char (*hid_keymap)[8]=hid_keymap_dvorak;
-
>>>请点击阅读原文查看完整代码<<<
-
E_INTERRUPT|USB_EP_STAT_TX0|1;
-
ep1_wait=1;
-
}
-
}
完整的程序在附件中。我这个程序没有使用任何USB的库函数,完全是从底层操作,这样对USB的工作细节可以了解得比较清楚,当然,也费了很多工夫啦。
关注EEWORLD微信,回复“键盘”可看完整代码并与作者直接沟通。
以上图文内容均是EEWORLD论坛网友:cruelfox 原创,在此感谢。
关注EEWORLDEEWORLD(电子工程世界)回复“投稿”我们帮你上头条!
欢迎微博@EEWORLD
与更多行业内网友进行交流请登陆EEWORLD论坛。