metro94
metro94
# 按键扫描与读取 这部分的设计与硬件直接相关,需要考虑如何与BL70x高效配合。 ## 电路设计 对于键盘类应用,每个按键分配一个输入引脚一般是不现实的(按键实在是太多了)。通用的方法是引入键盘矩阵,理论上`n`个引脚最多支持`n**2/4`个按键,代价是引入了比较复杂的扫描机制。 关于键盘扫描的机制和“鬼键”的处理(二极管的应用),这里不再赘述,有兴趣的同学可以参考[How a Keyboard Matrix Works](https://github.com/qmk/qmk_firmware/blob/master/docs/how_a_matrix_works.md#how-a-keyboard-matrix-works)。 根据前面的讨论结果,电路设计最终被确定为7x11的矩阵,理论上支持77个按键,实际上使用68个,并且占用了18个引脚。根据BL70x的KeyScan支持的特性,我们定义:列(Column)为11、行(Row)为7,驱动信号由*列引脚*发出并且*低电平*有效。因此,二极管的电流方向应该是从*行引脚*到*列引脚*。 ## 扫描周期和消抖 按键本身并不是理想器件,在每次按下和弹起前都会经历一定的抖动,这可能会导致主控读取到的状态不正确;另外还有一些轴体可能会受到环境的影响而产生错误的脉冲等。为此,我们需要考虑消除抖动的方法,而这与扫描周期有关。 关于抖动的原理和常见的消抖方法,这里不再赘述,有兴趣的同学可以参考[Contact bounce / contact chatter](https://github.com/qmk/qmk_firmware/blob/master/docs/feature_debounce_type.md#contact-bounce--contact-chatter)。 考虑到扫描周期的稳定性(扫描发生的时间是规律的),个人倾向于使用最通用的方法(也是QMK的默认方法):每次读取一整个键盘的状态,在键盘所有按键的状态保持稳定到一定时间后,才认为当前状态是有效的。这种方法消耗的资源最少,并且对噪声的容忍度也比较好(当然这种方法不大适合按键本身非常不稳定的情况,会导致找不到稳态,不过对于机械轴来说应该不会发生)。 至于扫描周期的设置,很明显应该小于上述稳定时间,这样才能通过多次采样排除掉不稳定的情况。原则上,整个键盘的扫描周期应当不高于键盘输出到USB或者BLE的时间,也就是1ms;但是,键盘扫描的周期也不应该太高,否则会影响其它代码的正常执行。 ## KeyScan应用 KeyScan是BL70x自带的一个功能模块,看名字就知道是用于键盘扫描模块,支持功能如下: * 最大支持20x8的键盘,其中20为驱动列引脚,8为读取行引脚 * 最多可以存储4个按键 *...
# 键位映射 如果说按键扫描主要是硬件设计的功劳,那么键位映射就纯粹是软件代码的艺术了。特别是对于小配列的键盘来说,一个合理的键位映射可以起到事半功倍的效果,而支撑起这一功能的必然是一套非常灵活而功能丰富的软件框架。 对于键位映射,我认为可以分解成不同层次(hierarchy)的问题: * 在最低的层次,我们只关心键盘上按键的当前状态,也就是“按下”与“释放”。通过上面提到的键盘扫描和消抖(包括软件和硬件消抖),我们可以在一系列时间点上获取键盘所有按键的情况,但尚未建立起不同时间点的状态转移关系。 * 在稍高的层次,我们将前面提到的按键状态以时间为单位联系在一起,此时可以分析出更多信息,例如按键被按下的时刻、按键是轻触还是长按等,这有助于我们实现更加复杂的功能(例如在同一个按键区分轻触和长按对应的键值,或是在按下其中一个按键后另一个按键的键值变化等)。 * 在更高的层次,我们会引入空间上的按键状态,这里问题会变得更加复杂。通过引入层(layer)的概念,我们可以在不同的状态下部署不同的键位和键值(注意到键值实际上是和当前键盘的状态相关的,而非只和按键位置有关),从而扩展出键盘可以接受的所有按键组合(包括时间和空间)。 * 最后,我们会将所有的按键组合打包成为一个配列,这个配列对应了一套完整的键位映射配置。根据需要,我们可以在线(例如macOS和Windows的键位切换)或者离线(例如通过刷机支持新的配列)对配列进行修改,从而在不同的应用场景使用不同的配置方案。 对于键位映射的详细介绍超出了本文的范围。如读者对相关内容感兴趣,可以阅读以下文档(主要来自于QMK和ZMK): * QMK * [Keymap Overview](https://github.com/qmk/qmk_firmware/blob/master/docs/keymap.md#keymap-overview) * [Keycodes Overview](https://github.com/qmk/qmk_firmware/blob/master/docs/keycodes.md#keycodes-overview) * [Modifier Keys](https://github.com/qmk/qmk_firmware/blob/master/docs/feature_advanced_keycodes.md) * [Layers](https://github.com/qmk/qmk_firmware/blob/master/docs/feature_layers.md#layers-idlayers) * [Layouts: Using a...
# QMK键盘部分代码分析 在QMK中,关于键盘部分的代码主要位于`tmk_core/common/keyboard.c`文件的`keyboard_task()`函数中,这一函数涵盖了与键盘相关的各种函数。我们这里主要关心键盘扫描和键位映射这两部分。 考虑到这里主要是研究QMK的软件架构,这里选择了最简单的配置(关闭各种花里胡哨的功能)来进行介绍。 `keyboard_task()`的主要调用关系如下(函数名字都很清楚,想必不用介绍具体功能了): * `keyboard_task()` * `matrix_scan()` * **`read_rows_on_col()`** * `select_col()` / `unselect_col()` * `matrix_output_select_delay()` / `matrix_output_unselect_delay()` * `readPin()` * **`debounce()`** * `action_exec()` * `process_record()` * `process_record_handler()` *...
# 软件架构设计 根据前面的代码分析,我们发现键盘设计离不开对于按键扫描和键位映射的处理。由于键盘的很多功能就是围绕按键来进行的(不论是基本的按键输入还是LED灯效等进阶特性),因此我们考虑将软件架构设计为以按键为核心、多个模块同时运行的机制,这就需要引入RTOS来实现各种代码的有机结合。 在下面的软件架构设计中,我们把键盘分成最基本的三个部分,每个部分都对应一个RTOS中的任务: * 按键扫描(KeyScan) * 键位映射(KeyMap) * 事件路由(EventRouter) 首先我们说说“事件”这个概念。对于键盘类操作,我们将一些由各个模块生成、并可能需要某些特定模块响应的动作称为“事件”(即event)。在我看来,一个事件可以包括以下部分: * 事件类型:记录事件的具体类型,方便我们对事件进行路由。不同模块通常对不同的事件感兴趣,通过对事件进行标记和订阅,我们可以将事件从发送者推送到感兴趣的接收者,在这其中事件会被自动复制和分发,不需要发送者或接收者进行更多操作。 * 数据:记录事件本身携带的数据。我们将数据长度定为32位,这是因为我们可以在32位MCU上将完整的地址放在数据对应的变量中,从而可以携带更多有价值的信息。 * 时间戳:记录事件发生的时间,也就是tick的值。对于我们的任务,时间戳通常具有重要的意义,它指示了一个事件在哪个时间点发生,我们可以利用这个值来维护任务之间的数据同步。这个时间戳通常由RTOS来提供。 可以看到,在引入了事件的概念后,可以将不同任务之间的信息交互转化成对事件的操作,这有助于我们从更高的抽象层次考虑问题,从而有助于设计出更加简洁清晰的架构和代码。 其次,我们将KeyScan与KeyMap分成两个任务,这是考虑到了以下因素: * KeyScan和KeyMap本就是两个意义明确的不同任务,并且相互之间的耦合性并不强(KeyScan只需要记录并传递“按钮按下或释放”这样的事件并传输给KeyMap),也就是说具备分开操作的基础。 * KeyScan和KeyMap与软硬件交互的特性并不相同,分开考虑能够更好地解决问题。 * KeyScan与硬件是深度绑定的,每个实体键盘都有一套相对固定的硬件参数(可插拔之类的暂时不考虑),也就是说可以在编译阶段就把硬件参数固化到固件中,在运行时不会进行修改或重配置。 * KeyMap则通常被看作是可以在软件端重配置的,因此需要有较强的灵活性。很多KeyMap相关的功能(如特定按键切换键位映射等)需要软件配合进行修改和计算,这决定了相关的逻辑不适合写死到代码中。 * KeyScan和KeyMap对事件的处理也并不统一,有时候将两者分开考虑能够更加便于软件设计。 *...
# 键盘扫描相关文档 键盘扫描(包括按键扫描和键位映射)是机械键盘的核心部分,不仅是机械键盘固件中不可或缺的成分,同时也是客制化中的一大亮点。 这个文档记录了键盘扫描的一些基本概念,以及当前关于键盘扫描部分的实现方式和相关API。 ## 基本概念 此处约定了机械键盘的相关定义,并且简要描述了键盘扫描中各个部分是如何运作的。 ### 矩阵和布局 在机械键盘中,我们将矩阵(matrix)和布局(layout)的概念区分开来,其中: * *矩阵*指的是机械键盘在电信号连接方面的组成方式。在机械键盘中,通常需要用较少的引脚数来处理大量按键的状态读取,因此使用GPIO读取单个按键的状态在大部分情况下是不现实的。为此,我们使用矩阵扫描的方式,最多可以实现在`n`个引脚上读取`n ** 2 / 4`个按键的状态。 * *布局*指的是机械键盘在外观排列上的组成方式。对于用户而言,一个键盘的按键数量是容易感知的,例如标准的65%配列键盘一共有5行、15列按键。布局和矩阵通常是一一映射的关系,但是两者之间的映射并不是确定的,对于用户而言通常只关心布局部分。 考虑到矩阵和布局的不同概念,我们倾向于分别在以下情景中使用它们: * 矩阵:矩阵对于硬件来说是唯一且固定不变的,因此一般是直接硬编码到固件中。对于键盘扫描等更加底层的处理过程,我们以*矩阵*为单位进行操作,这样的处理更加自然,效率也更高。 * 布局:布局是可以随着软件改变的,开发者和用户都能(在不同程度上)自定义键盘的布局。对于键位设计等更加贴近用户层面的处理过程,我们以*布局*为单位进行操作,这样可以方便后期进行直观的展示和修改。 当然,我们会在代码中完成从布局到矩阵的转换,这部分通常也是硬编码到固件的。 ### 软件框架 在软件中,键盘扫描对应两个部分,分别是: * 按键扫描:从键盘矩阵中读取按键状态,经过消抖处理后,最终转化成按键位置相关的事件。 *...
# TapEngine设计与使用 为了使得键盘支持Tap-Hold及相关功能,这里我们引入了TapEngine的相关设计,并给出了基本的使用方法。 ## Tap-Hold简介 Tap-Hold(短按-长按)是机械键盘键位映射的核心功能之一,基本思路是根据按键时间的长度来判断当前按键的使用状态,从而赋予单个按键以更多的功能。 Tap-Hold有以下几种基本应用: * 键盘上的Fn键(常见于笔记本电脑和小配列机械键盘):通常Fn键的功能是,连续短按两次长期切换新的键盘布局(包括F区按键或多媒体键等),长按则临时切换;如果当前已经是新的键盘布局,则按一次Fn键可以返回正常模式。 * 粘滞键:在多次短按同一个按键后,可以临时将某个键位保持为按下状态,直到再次短按取消。这个功能对需要多次按下某个Modifier Key的情况有帮助(例如Ctrl-C/Ctrl-V)。 对于Tap-Hold来说,以下几个参数可以决定相关的特性: * `TAPPING_TERM`:短按时间上限。当按键时间少于一定长度,则会被判定成是短按(Tap),否则为长按(Hold)。这个值在QMK固件中默认取200 ms。 * `TAPPING_INTERVAL`:连续短按间隔长度上限。连续短按功能要求相邻的短按时间间隔少于一定值,如果超过则清空短按次数的计数器。 * `TAPPING_COUNT`:连续短按触发次数。为了避免误操作,可以将连续短按触发次数调整到比较高的值(例如5次),这对于粘滞键的应用来说很有帮助。 当然,与Tap-Hold相关的特性还有很多(典型的是与其它按键的交互,或者与Layer之间的相互影响)。这里我们主要集中于Fn键的相关应用,其它应用留待后续开发者自由发挥。 ## 设计思路 一般来说,Tapping相关的特性与键值有关,因此我们将其放在KeyMap的相关部分;不过,由于TapEngine的设计相对复杂,并且不影响其它功能的设计,我们将其作为KeyMap的独立子模块进行设计较为合适。 对于已有的KeyMap代码,我们还需要添加不同的结构体和函数。 ### `smk_keyboard_tapengine_type` 这个结构体以键值为单位来存储使能了Tapping功能的相应按键。结构体的声明如下: ``` c...