背景
前段时间入手了一把HHKB,感觉键位设计真的不错,尤其是方向键和Home、End,可以在手指不离开打字键位的同时轻松按到,让手完全留在主键盘区。但生活也有几点不那么完美的:
- 用惯了之后,再用原来的键盘真的超级难受,一个是Ctrl,另一个是backspace,老是按错。
- 用背包来回带很麻烦,进公司要查包,坐地铁要安检。
- 手感……是个玄学,但是用了一段HHKB之后再用我的茶轴IKBC,感觉IKBC手感真的好,可能我更喜欢机械键盘那种更干脆的手感。
有了HHKB之后,打算把一直吃灰的RK61卖掉,感觉再也不需要它了。但是不小心把它外壳摔裂破相了,估计价格要大打折扣了。于是我想,干脆把RK61改成HHKB的键位,来解决上面的问题吧!
软硬件环境介绍
电脑:联想G410笔记本
操作系统:Debian 9/Linux Mint
调试工具:JLink V9仿真器
软件开发环境:STM32CubeIDE
开发板:YL-8开发板
电路板软件:KiCAD
3D模型制作:Blender/FreeCAD
关于RK61
RK61是我的第一把60%键盘,这把键盘有点坑,Fn的功能很有限,没有办法按出Home、End等键,用起来很不方便,买了之后用了一周就吃灰了。看官网现在的版本好像支持更多的组合键了,但是老键盘并不提供固件升级。
先拆开它看看。主要有两个芯片
- SH68F83
这个芯片是一个支持USB协议的单片机,有线模式时的USB协议应该是这个芯片实现的。 - BCM20730模块
基于博通的BCM20730芯片,这个芯片主要是为无线游戏控制器、键盘、3D眼镜等设计的。其中键盘相关功能我们可以重点关注一下。它支持最多8×20的硬件矩阵扫描,用来做键盘还是很方便的。
一开始考虑的是把蓝牙模块的软件换掉,硬件不动。但是这个模块的资料很少,没找到足够的开发资料,冒然开搞后面卡壳失败风险太大。最终选择了比较稳妥的方案:直接把主控换掉。这样我们只要搞懂按键的电路就可以了。同时我对无线的需求不大,决定先搞成一个USB有线键盘算了。
虽然没有使用这个蓝牙模块,但是对它的研究还是很有用的。从BCM20730的数据手册上我们了解到,硬件矩阵键盘扫描使用的引脚是固定的。
- P0-P7 row0-row7 (KSI0-KSI7)
- P8-P27 column0-column19 (KSO0-KSO19)
有了这个信息,再看看蓝牙模块上这些引脚都是接出来的,我们就能通过蓝牙模块的引脚,比较方便地确定键盘的矩阵电路。
RK61一共61个键,有了前面的蓝牙模块对应的引脚,然后用万用表量一量确认一下,就能知道这些键分为8行8列,最后一行5个按键,其他行每行8个。具体分布如下:
这个矩阵键盘的电路的结构和GH60的很像,只是行列的排布有一点区别。大多数键盘的应该都差不多。 大家可以去下载GH60的电路看一看。
矩阵按键电路跟单片机的小矩阵键盘有点区别,每个按键串联了一个二极管。因为矩阵键盘在扫描时,会拉低一个扫描线,同时保持其他扫描线为高电平。但是当多个按键同时按下时,可能会形成回路拉低条其它扫描线,出现多条扫描线同时为低,如下图(1)。这个二极管可以阻止这个回路的形成,如下图(2)。
我们只要把矩阵键盘的行列都引出来,接到单片机的IO上就可以了。剩下的问题基本上就是软件实现USB设备、键盘扫描了。
STM32流程打通
软件环境
之前其实没用过STM32单片机,大部分都是用的51单片机。读一些资料后,开始搭建环境,经过尝试,觉得最省事的开发环境就是STM32CubeIDE了。基于Eclipse,集成了STM32CubeMX,装好后装了个VIM插件就直接开用了。因为不需要写太多代码,所以找个最省心的环境就可以了。
使用STM32CubeMX + Vscode + platformIO的同学,需要注意一下platformIO对CubeMx生成的USB库的支持不是太好,生成代码后不能直接编译通过,可能需要配置一番。
不得不说现在STM32的软件做得真方便,直接在图形界面里配置好了之后,按一下生成代码,所有初始化代码就全部生成了,很省事。
开发板hello world
用的开发板是很多年前买的YL-8开发板。还好在网盘里找到的它的电路图。买的开发板、模块什么的,一般都会把资料放到网盘里,防止哪天要用的时候,发现只剩板子,啥资料也没有了,真个好习惯,哈哈。
按照小开发板的电路图,在CubeIDE配置好引脚后,在主循环里加了两行代码,就实现了一个检测按键状态控制LED亮灭的小Demo。
int i = HAL_GPIO_ReadPin(button1_GPIO_Port, button1_Pin);
HAL_GPIO_WritePin(GPIOC, led1_Pin, i);
使用STM32的USB接口
为了实现USB键盘,先要会用STM32的USB模块,这块感觉挺麻烦的,好在网上有很多现成的例子,相关教程不再赘述了,大家需要的网上找一找就有了。
我这里遇到了一个棘手的问题:按照网上的例子配置完成后,电脑竟然不识别USB设备! 被这个问题困扰了好久,还买来一本《圈圈教你玩USB》来学习USB协议,但是还是不行。调试USB初始化代码,也没有报错。找来网上的例程烧进去,也没有反应。直到一天看书时注意到,USB主设备会根据从设备的上拉电阻来检测从设备,再看看YL-8开发板,USB接口并没有上拉电阻,而网上找的其它开发板的电路,则是带上拉电阻的。
难道是STM32F103RC里面不带上拉电阻? 于是我接上了一个电阻试一了试,果然立马识别出USB设备了!
原来只是一个电阻的问题!后来又在网上看了不少开发板,很多mini开发板USB接口电路都没有接好。这种开发板一般也没有带USB相关的例程,使用时需要注意检查一下电路。正常情况下,在CubeIDE里配置好了USB设备,烧进单片机之后,USB设备就能正常识别的。如果不能正常识别,很可能就是电路有问题。
不过经过这个问题,倒是学到了不少USB相关的知识,推荐《圈圈教你玩USB》这本书,学到了不少东西。
键盘HID设备实现
为了实现键盘的功能,需要把USB设备配置为HID键盘。CubeIDE生成的默认设备是一个Joystick的,我们主要需要改两个地方,一个是配置描述符,把端点从鼠标改成键盘;另一个是要新建一个键盘的报告描述符,这里偷懒直接把圈圈的报告描述符拷过来了。这些改动大部分都在usbd_hid.c里。具体改动可以看在Github上找到:https://github.com/thelxz/xzkb
我们后面发报告要按报告描述符描述的格式来发,所以只要弄明白报告描述符描述的格式就可以了。因为报告描述符用的圈圈的,所以报告格式和他的也是一样的,一共有八个字节:
第一个字节是特殊按键的状态,第二个字节是固定的0,之后的六个字节分别是同时按下的除特殊按键外的六个按键的UID。每个键对应的UID可以通过《USB HID Usage Tables》这个文档的"keyboard/keypad page"找到。
我一开始有点怀疑,只记录六个键够不够?后来试了下,因为功能键有单独的bit控制,除了功能键,我几乎不可能同时按下六个按键,所以如果不玩对按键要求比较高的游戏的话,这里一般就够用了。这点好像也是普通的USB键盘不能实现全键无冲的原因,有一些特殊的方法可以实现全键无冲,但是我们这里就不考虑了。
按这个格式生成报告了之后,使用下面的接口发送Report,没有意外的话,就电脑就可以收到单片机发送的按键了!
USBD_HID_SendReport(&hUsbDeviceFS, reportBuf, reportLen);
连接键盘和开发板
这一步比较简单,就是连线,我把行列分别连到了GPIOA和GPIOB上。唯一需要注意的地址是,最好找几个连续的引脚,这样后面编程会方便一些。
添加软件逻辑
其实本来是想直接用TMK的代码的,但是发现TMK对STM32的支持好像并不是太好,而且还使用了ChibiOS这个BIOS,比较复杂,想看懂需要花些时间。于是决定自己写,更简单可控。
大家可以直接看代码,主要逻辑都在keyboard.h和keyboard.c里。
使用了两个数组作为字典来表示两个层,默认情况下,每个按键对应的UID通过keyUID[0]翻译,当Fn按下时,对应的按键通过keyUID[1]来翻译。这个逻辑是很简单的,代码可以在这里找到。但这样做其实有一个小问题,比如使用F2(组合键Fn+2)重命名文档时,如果先松开了Fn,2这个键就会发出去,结果就把文档重命名成“2”了。不过这个问题不常出现,我们先不管它,后面再修。
完成这一步之后,我们的键盘其实就能用了!
但是很明显,这样有点丑,而且插的线偶尔会接触不良或脱落,导致一些按键失灵!看来为了把电路板塞到键盘壳子里,需要一个小一点的主控板了。
制作主控板
KiCAD介绍
正如之前提到的,网上好多比较小的开发板,有很多USB电路都不是直接连好的,就想着干脆自己做一个吧。发现现在PCB比以前便宜多了,10×10的才30来块钱,也好久没做过了,就试一下吧。
电路图软件试了试Eagle和KiCAD。很久以前用Eagle挺好用的,命令很方便,但是现在发现Eagle被Autodesk收购了,功能也有变化,比如显示3D库会直接跳到Autodesk网站,感觉Eagle已经变了,只好放弃它了。
反观KiCAD,最近发展好像很迅猛,装上之后发现功能比几年前强大了许多,文档也很丰富了,建议大家试一试。如果是第一次用,建议先把中文文档浏览一遍。
电路图:
PCB和3D图:
PCB画完之后,可以生成gerber发现商家制作了。本来还担心KiCAD国内用的不多,PCB制作会有问题,结果却很顺利,文件发过去直接就做出来了。
软件的各种操作在官方文档里说的很全了,有一些觉得有用的点分享给大家:
- 布线模式设置为推挤(shove),可以在步线时把之前的线推走,默认没有打开
-
3D模型的制作
方法1:对Blender比较熟悉的话,可以使用Blender制作3D模型, 导出OBJ之后FreeCAD选择材质,再导出wrl文件,KiCAD就可以导入了。Blender建模型比较快,但是导入KiCAD之后还需要重新调整大小,而且Blender的材质不能导出后就丢了。
方法2:直接使用FreeCAD制作模型,导出STEP文件,这样的话,直接导入KiCAD里面就会带材质,而且大小也不需要调整。后面尝试了一下,这个流程是可以的,但是我FreeCAD用的不熟,建模很慢。
前面PCB的3D模型里,缺了一些元件的模型,用Blender做了几个:
焊接组装
PCB回来发现做工一般,便宜了质量果然有些下降,但也还好,没什么问题。因为想让它当个小开发板的,所以没有做太小,还加了一些按键和LED。
开工焊元件吧!结果发现自己动手能力已经严重退化了,镊子夹着元件抖得不行。还发现自己电路画错了,Reset键不灵,只好搞了个飞线,看一下最后效果,简直惨不忍睹…
上面黑乎乎的是松香,记得之前酒精是可以洗掉松香的,现在怎么洗了半天洗不掉,不知道是酒精不行还是松香不好。算了,就这样吧,反正放到壳子里也看不到。
为了降高度,一些不用的元件去掉了,把SWD调试口接了一个MicroUSB连到外面去了,方便下载程序。这样处理是偷懒了,后面有心情时再研究一下怎么通过USB升级程序或者把USB接口复用一下,只留一个接口。
装上壳子,外面看起来还不错吧!
细节处理
安装支脚、防滑垫
RK61是没有支脚的,不是太舒服,而且原来的防滑垫也老化了,掉了两个,都需要处理一下。
支脚网上有卖的,挺好看的,但是大都需要运费,就自己打了个孔,装了个六角铜柱,套上了一个小橡胶帽来防滑就可以了,橡胶帽是原来吃鸡手柄带的电容屏按键帽,防滑效果还是挺好的。
下面加了一个原来买键盘托带的橡胶垫,用螺丝刀的六角刀头在螺丝处打一个孔。
Fn功能键优化
现在来处理Fn键松开后,已经按下的键功能发生变化的问题。为了解决这个问题,不能只考虑现在按键的状态,还必须考虑到上次按键的状态。代码的变化主要有:
- 按键的level不是全局的,而是每个按下的键都有一个自己的level。
- 在按键扫描时,记录上次扫描状态,如果这个键上次扫描就已经按下了,它的level状态就不受现在Fn状态的影响,而是直接复制上次的level信息。
功耗优化
现在键盘的功能基本上搞完了,但是心里一直有一个疑惑:我写的程序一直循环检查按键状态,就算等待的时间,也是在原地循环,这样电路的功耗会不会很大?为了方便的搞清这个问题,采购了一个测量USB电流的小设备。这个工具主要是用来测量充电器的功率的,但是精确到了mA级,测量键盘的功率应该也是可以的。测量结果如下:
我们做的键盘功耗是有一点大,比达尔优的104键还大一点。但是再看HHKB Pro2,好家伙,功耗竟然是我的三倍多!难道是内部集成了USB HUB的原因?不知道现在新的HHKB功耗降下去没有。
优化功耗可以从两方面入手:
- 把延时程序的循环查询改成Sleep
- 降低MCU的频率
我们先看一看HDL_Delay() 这个延时函数:
__weak void HAL_Delay(uint32_t Delay) //注意 __weak
{
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay;
/* Add a freq to guarantee minimum wait */
if (wait < HAL_MAX_DELAY)
{
wait += (uint32_t)(uwTickFreq);
}
while ((HAL_GetTick() - tickstart) < wait)
{ //这里是用循环在等时间到达
}
}
发现这个函数是个弱函数,我们只要按照它重新实现一个就可以,不需要修改HAL库。我们把它的循环等待里加一行sleep函数,让它在等待时进入低功耗状态,等systick来了的时候,会从sleep状态唤醒它,检查时间是否到达。代码如下:
void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay;
/* Add a freq to guarantee minimum wait */
if (wait < HAL_MAX_DELAY)
{
wait += (uint32_t)(uwTickFreq);
}
while ((HAL_GetTick() - tickstart) < wait)
{
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
}
}
其中的HAL_PWR_EnterSLEEPMode()函数可以调用WFI,进入低功耗状态。看一下修改完成后的功率:
效果还是很明显的,功耗降低了约50%。下面再试试降低时钟频率,把AHB总线的时钟4分频了一下,再看键盘的功耗:
又降了40%!虽然只有两点比较简单的改进,但是效果还是相当不错的,现在键盘的功率已经降到了60mW,试了试比我手上所有的USB设备都省电,就先这样吧!
完工
我们的键盘基本上完工了,总结一下情况吧:
主控:STM32F103RET6
代码量:< 500 行(不含自动生成代码)
接口:MiniUSB,SWD接口
按键:61键,国产红轴,Fn组合键类似HHKB
源码:https://github.com/thelxz/xzkb
电路PCB:https://github.com/thelxz/xzkb_pcb
因为只有周日有时间,改装键盘的过程前前后后经历了好几周,但是收获还是很多的,了解了USB协议,学会了KiCAD,抢救了一下我的动手能力,也完成了我们最初的目标:HHKB再也不用来回带了。
现在这个键盘已经成为在家的主力键盘了。