diy机械键盘,类似HHKB的布局,修改fn的位置,增加功能组合键定义,并加入鼠标功能。可实现动态led图案。
说明地址:https://www.cnblogs.com/yangzifb/p/11485313.html
设计**:
- 优先使用大拇指完成任务,实现组合键
- 减轻小拇指负担,将Ctrl移动到caplock处,取消caplock
- 方向键使用vim风格,home end pageup pagedown也使用hjkl实现
- 使用键盘模拟鼠标,用于连贯动作,减少键鼠切换,主要鼠标操作仍通过实体鼠标实现
- 缩小键盘尺寸,降低空间占用,减少手的移动范围
- 使用组合键并通过按键时长实现光标鼠标移动以及滚屏的连续控制,所以需要无段落感的轴
- 使用力量小的轴体以减缓手部疲劳。所以主要使用红轴
- 使用消音胶垫,进一步让感觉变软、变轻,降低噪音
实现参考: http://tieba.baidu.com/p/4592747695
后期可以考虑加入的功能: - 录制宏
- 通过统计按键风格做一些智能动作,例如操作者识别,操作者工作状态、工作类别统计
使用stm32f103vbt6,驱动键盘扫描、led和USB通信,没有使用固件库。
仿HHKB的设计,使用59键,通过fn键复用按键。将fn位置放在左手大拇指处
app键也作为fn使用,方便单手操作。只有APP键单独按下或无有效组合时用作APP键。
复用按键:(失去所有复用按键的组合功能)
- F1~F12: 通过第一排按键实现
- del: 通过backspace和~实现,其中fn+~为shift+del
- 方向键: 通过vim风格的hjkl键实现
- home、end、pgup、pgdn:fn+a+hjkl(失去a+上下左右功能)
- home、end:fn+< > 方便单手操作
- insert、prtscren:fn+i,fn+p
- 上一级(alt+up): fn+u
- 鼠标上下左右滚轮:fn+s+hjklnm(失去s+上下左右功能)
- 鼠标大幅上下左右:fn+a+s+hjklnm
- 鼠标按键:fn+dfgnm:左键中键右键,滚轮
- 鼠标拖动:fn+dfg+hjklnm
- 鼠标大幅拖动:fn+a+dfg+hjklnm
使用键位编辑网站Keyboard-layout-editor编辑键盘的布局:http://www.keyboard-layout-editor.com/
使用定位板生成网站http://builder.swillkb.com/ 生成dxf
将生成的文件导入Altium designer后,作为机械层辅助布局
为了实现全键任意组合,单片机的每一个引脚连接一个按键,单独进行扫描。
通过传感引脚控制led的亮度,led设在按键上拉电阻后,成为上拉回路中的一部分。按键按下时,led最大亮度。按键松开时,单片机控制引脚为输出模式,低电平亮灯;设置为输入上拉模式时,灯灭。通过占空比实现亮度调节。
扫描和灯的控制共用0.5ms的时隙,10ms为一组,每个时间组首先扫描按键,扫描完成后按照灯的亮度设定设置引脚是否需要输出。10ms后重新设置为输入模式扫描按键。所以按键扫描频率为100Hz,led的PWM频率为100Hz,占空比为20档。
USB需申请500mA电流,使用100mA时,键盘容易掉。
led使用压降2~3V的高亮led,限流电阻1K,由MCU引脚直接驱动,驱动电流约1ma,全亮时MCU电流在可承载范围内,且1ma的驱动电流已经足够亮了。
usb预留了miniUSB接口,但实际焊接时,由于线长吃力,容易掉。所以改成通过焊接USB线,或焊接miniUSB转接板的方式出线。
外壳使用GH60塑料外壳;
卫星轴使用PCB卫星轴,3个X2的;
键帽为一套标准键帽,其中右shift键安装在空格位置(可购买无刻2.75X R1),ctrl键购买无刻1.75X R2,反斜杠键购买无刻1X R4,退格键购买无刻1.5X R3
|~f103vbt6/ 单片机程序 | |~app/ 应用代码 | | |-key.c 按键扫描和定义文件 | | |-key.h | | |-led.c led与图形定义 | | `-led.h | |+obj/ 编译中间文件 | |~src/ 单片机基础源文件 | | |-cmd.c | | |-cmd.h | | |-main.c | | |-main.h | | |-queue.c | | `-queue.h | |~sys/ 单片机系统源文件 | | |-core_cm3.h | | |-core_cmFunc.h | | |-core_cmInstr.h | | |-startup_stm32f10x_hd.s | | |-stm32f10x.h [RO] | | |-stm32f1_sys.c | | `-stm32f1_sys.h | |~hal/ 硬件驱动 | | |-f1_uart.c | | |-f1_uart.h 串口驱动文件 | | |-f1_usb.c | | |-f1_usb.h usb寄存器访问工具 | | |-usb_core.c | | |-usb_core.h usb枚举流程 | | |-usb_kb_m.c | | `-usb_kb_m.h usb键盘、鼠标描述符和协议实现 | |-JLinkSettings.ini | |-keilkill.bat* | |-test.uvoptx | `-test.uvprojx |+参考/ 本项目的参考资料 |+pictures/ 本项目过程照片 |+电路/ 电路设计 |+电路库/ 电路库文件 |-.gitignore |-keyboard-layout.json 键盘的布局设计 |-readme.md 说明文件 |-效果.JPG 设计效果图 `-生成的CAD图.dxf 生成的键盘布局图,用于PCB布局
不使用stm32固件库,而是将MCU相关的宏定义、必须的函数集中在sys/stm32f1_sys.c .h文件对中,引用stm32f10x.h、core_cm3.h等少数文件作为单片机系统配置。
在stm32f1_sys模块中定义MGPIO_TypeDef,代替原来固件库中的GPIO定义。在MGPIO_TypeDef中使用位段的方式定义控制寄存器,代替原来的通过函数配置GPIO。
使用事件驱动方式实现并发,定义SYS_task变量用于存储系统中待执行的任务,其中每一bit代表一个任务需要执行。定义了START_TASK宏,用于启动一个任务(将代表此任务的bit置一)
程序入口为main.c文件,main函数中首先配置系统时钟、周期中断、调试串口、调试指令模块,然后初始化USB和键盘模块,最后进入主循环。
系统周期中断频率为2kHz,调试串口115200bps,系统主频72MHz。
系统中定义了SYS_time变量作为系统时钟计数器,可以通过对此变量的求余,实现分频。
在主循环中,不断扫描是否有任务需要执行,若有任务,则首先清除任务标志位,然后再执行任务。若先执行任务再清标志位,会导致在任务执行过程中产生的任务无法执行。
在主循环中通过SYS_idle变量统计CPU占用,原理是每隔100ms就在定时中断中更新SYS_idle_dis变量,并清零SYS_idle变量。也就是说,SYS_idle在100ms内被主循环一直累加,100ms后被周期读取,得到的计数值是主循环的循环次数。在0负荷状态下,100ms内主循环运行约162000次,而系统带负载时,用于轮询主循环的时间线性降低,以比例计算出CPU占用率。所以在主循环中写了for(i=0;i<1620;i++),让SYS_idle变量的值在100ms时大约为100,则带负载时输出SYS_idle变量的值大约就是CPU空闲百分比。
使用USart1作为调试串口,与SWD调试线使用同一接口引出。用此串口实现一个简易的命令行(shell),实现代码为src/cmd.c .h文件对。cmd模块中,接收到的串口数据判断是否为回车换行,若是,则将缓冲中的文本作为指令处理,若不是,则给串口回显,并压入缓冲备用。这样,就可以通过超级终端等串口调试助手对单片机进行命令行控制。
cmd模块的命令通过cmd_reg((CMD_LIST*)&bl_sys_cmd_list);函数实现注册,定义时将命令描述放在bl_sys_cmd_list数组中:
"gk","display keybuf",cmdfun_getkeybuf,//显示发送缓冲
第一列为命令名称,第二列为命令的帮助信息,第三列为命令的执行函数。命令接受参数,以空格分割。命令执行函数的定义:
void cmdfun_ledtest(char *para) //设置led亮度
输入para为参数,通过sscanf函数读取
串口通过统一的收发函数进行操作,收发均使用中断方式,均使用队列解耦。接收时接收中断和过载中断同时响应。
按键扫描的入口函数为key_poll,由主循环调用,频率2kHz。函数首先区分当前周期是扫描按键的周期还是led控制的周期,分别执行不同的功能。
为了方便引脚的访问,提高运行效率,定义了一个数组:BUTTON_STATE keys[232]
keys数组是以按键号为索引的数组,存储了按键的GPIO的port、pin,状态、时间和亮度信息。由于键盘的按键号有很多保留,所以数组中有很多0,规定以port为0表示数组中的一个位置为保留。
定义keys_index数组,用于高效的访问非零的按键数据。
当进行按键扫描时,首先遍历所有非零按键位,将引脚配置为输入上拉模式。然后调用key_scan函数进行按键扫描,在此函数中调用check_press函数判断按键是否按下,并以一定的间隔时间防抖。key_scan函数在扫描完成后开始判断发送值,首先为特殊按键赋值,然后判断Fn组合键,然后判断正常按键。
发送指令时,keyboard_send和mouse_send函数判断是否有变化,若无变化且鼠标速度为0,则不再发送。若以查询式一直发送键鼠数据,则在开机BIOS中无法通过键盘选择菜单。
led模块定义了led数组const u8 ascii_index[256],此数组作为索引,将按键的ascii值转换为按键扫描模块keys数组的索引(按键码)。
led模块以led_poll函数为入口,调用具体图案函数执行,在图案函数中,设置图案数组,调用led_set_pattern函数进行led亮度赋值。图案数组为S_LED_LIGHT结构的数组,第一个域为按键的ascii表示,对于特殊按键的表示见ascii_index数组的注释,若为1,则为一个图形的结束。第二个域为亮度。若第一个域为1,则第二个域表示这个图形的持续时间。led_set_pattern函数只返回持续时间,延时由图案函数执行。延时的单位为led_poll的周期,100Hz。
usb部分分3层:
- 硬件设备访问(f1_usb.c)负责寄存器访问抽象,依赖usb逻辑层
- usb逻辑层(usb_core.c)负责usb描述符等管理逻辑实现,不依赖其他层,具有通用性
- 应用层(usb_kb_m.c)负责具体到本键盘应用的usb描述符实现,usb通信收发,依赖上述两层 usb通信的基本流程是host主动,从机纯被动,所以发送都是事先准备好数据到缓冲区,等host来取。每次传输完成后端点会自动设置为NAK状态,并给中断,让应用程序准备好下次传输。 每个端点有两个分组缓冲,一个收一个发。可位于缓冲区的任意位置,由缓冲区表描述收发缓存的地址和长度,缓冲区表也在缓冲区中。
所以usb的512字节缓冲区如何用是需要应用来设计的,一般从0开始是缓冲区表,依次描述各端点的缓冲区地址和长度。然后为每个缓存分配空间。
usb逻辑层实现描述符等初始化逻辑,由于端点0的通信需要发送的数据长度大于一包长度,所以在usb_core中做了状态机分包发送
通过keyboard_send和mouse_send函数发送数据,只是将数据放在发送缓冲区中,等host来取。并没有实现批量数据的发送功能,如果需要实现任意数据的收发,需要在此做队列缓存