-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
键盘按键扫描 @metro94
2021-07-28
#4
Comments
按键扫描与读取这部分的设计与硬件直接相关,需要考虑如何与BL70x高效配合。 电路设计对于键盘类应用,每个按键分配一个输入引脚一般是不现实的(按键实在是太多了)。通用的方法是引入键盘矩阵,理论上 关于键盘扫描的机制和“鬼键”的处理(二极管的应用),这里不再赘述,有兴趣的同学可以参考How a Keyboard Matrix Works。 根据前面的讨论结果,电路设计最终被确定为7x11的矩阵,理论上支持77个按键,实际上使用68个,并且占用了18个引脚。根据BL70x的KeyScan支持的特性,我们定义:列(Column)为11、行(Row)为7,驱动信号由列引脚发出并且低电平有效。因此,二极管的电流方向应该是从行引脚到列引脚。 扫描周期和消抖按键本身并不是理想器件,在每次按下和弹起前都会经历一定的抖动,这可能会导致主控读取到的状态不正确;另外还有一些轴体可能会受到环境的影响而产生错误的脉冲等。为此,我们需要考虑消除抖动的方法,而这与扫描周期有关。 关于抖动的原理和常见的消抖方法,这里不再赘述,有兴趣的同学可以参考Contact bounce / contact chatter。 考虑到扫描周期的稳定性(扫描发生的时间是规律的),个人倾向于使用最通用的方法(也是QMK的默认方法):每次读取一整个键盘的状态,在键盘所有按键的状态保持稳定到一定时间后,才认为当前状态是有效的。这种方法消耗的资源最少,并且对噪声的容忍度也比较好(当然这种方法不大适合按键本身非常不稳定的情况,会导致找不到稳态,不过对于机械轴来说应该不会发生)。 至于扫描周期的设置,很明显应该小于上述稳定时间,这样才能通过多次采样排除掉不稳定的情况。原则上,整个键盘的扫描周期应当不高于键盘输出到USB或者BLE的时间,也就是1ms;但是,键盘扫描的周期也不应该太高,否则会影响其它代码的正常执行。 KeyScan应用KeyScan是BL70x自带的一个功能模块,看名字就知道是用于键盘扫描模块,支持功能如下:
由于KeyScan对于按键数量的限制,我们不将其用于工作状态时的键盘读取,但可以将其用作低功耗状态时的唤醒源。具体的使用方法见下文的低功耗优化部分。 根据参考手册和硬件实测,我们发现KeyScan的处理方式如下:
低功耗优化对于键盘类应用(尤其是无线连接),低功耗是非常重要的,一个好的低功耗设计可以显著延长电池的工作时间。显然,键盘扫描是一个主要的唤醒源(大家都希望按下按键就能一键唤醒),需要针对这种常见进行优化。 在我看来,键盘类应用可以设计三级的低功耗状态:
通过查阅参考手册,我们可以发现,KeyScan位于PD_CORE_MISC_DIG的电源域,这个电源域并不在AON的范围内,意味着只能在睡眠模式(PDS)下使用,不能在休眠模式(HBN)下使用。由于休眠模式下只支持少量唤醒源,并且与当前的键盘扫描功能难以配合,因此可能不做考虑(注4)。
另外,对于KeyScan的时钟源,我们尽量选择32.768KHz的时钟源以节省功耗。BL70x支持RC32K(内置晶振)和XTAL32K(外置晶体)作为KeyScan的时钟源(图3),这个可以通过GLB寄存器进行配置。通常而言,内置晶振的准确性比外置晶体差,但是通常具有更低的功耗(不绝对),可以根据其它模块的需要和电路设计进行选择。 还有一个问题,就是关于行读取引脚的上拉电阻。更大的上拉电阻可以降低导通时的功耗,代价是放电时的时间常数更大。BL70x可以配置低功耗状态下的引脚上拉,但不清楚上拉电阻的阻值如何,这个也可以做更多尝试。 |
键位映射如果说按键扫描主要是硬件设计的功劳,那么键位映射就纯粹是软件代码的艺术了。特别是对于小配列的键盘来说,一个合理的键位映射可以起到事半功倍的效果,而支撑起这一功能的必然是一套非常灵活而功能丰富的软件框架。 对于键位映射,我认为可以分解成不同层次(hierarchy)的问题:
对于键位映射的详细介绍超出了本文的范围。如读者对相关内容感兴趣,可以阅读以下文档(主要来自于QMK和ZMK): |
QMK键盘部分代码分析在QMK中,关于键盘部分的代码主要位于 考虑到这里主要是研究QMK的软件架构,这里选择了最简单的配置(关闭各种花里胡哨的功能)来进行介绍。
这里加粗的函数对应键盘部分三个最基本的功能,分别是读取键盘状态、消抖和处理按键动作。下面会简单介绍这三个功能的代码结构。 读取键盘状态读取键盘状态本身倒没啥好说的,上面已经介绍过了。这里看看读取的核心代码(外层是以 static bool read_rows_on_col(matrix_row_t current_matrix[], uint8_t current_col) {
bool matrix_changed = false;
// Select col
select_col(current_col);
matrix_output_select_delay();
// For each row...
for (uint8_t row_index = 0; row_index < MATRIX_ROWS; row_index++) {
// Store last value of row prior to reading
matrix_row_t last_row_value = current_matrix[row_index];
matrix_row_t current_row_value = last_row_value;
// Check row pin state
if (readPin(row_pins[row_index]) == 0) {
// Pin LO, set col bit
current_row_value |= (MATRIX_ROW_SHIFTER << current_col);
} else {
// Pin HI, clear col bit
current_row_value &= ~(MATRIX_ROW_SHIFTER << current_col);
}
// Determine if the matrix changed state
if ((last_row_value != current_row_value)) {
matrix_changed |= true;
current_matrix[row_index] = current_row_value;
}
}
// Unselect col
unselect_col(current_col);
matrix_output_unselect_delay(); // wait for all Row signals to go HIGH
return matrix_changed;
} 这里值得关注的主要是两个 对于两个
消抖消抖的算法有很多,我们举最简单的一个例子,也就是默认的 static uint16_t debouncing_time;
void debounce(matrix_row_t raw[], matrix_row_t cooked[], uint8_t num_rows, bool changed) {
if (changed) {
debouncing = true;
debouncing_time = timer_read();
}
if (debouncing && timer_elapsed(debouncing_time) > DEBOUNCE) {
for (int i = 0; i < num_rows; i++) {
cooked[i] = raw[i];
}
debouncing = false;
}
} 在确认键盘不再发生抖动后,消抖程序会使用新值 处理按键动作这个说起来就复杂了,建议直接看看代码:action.c@process_action。相信在看完后读者会对按键动作的复杂度有一定的认知(腹黑笑)。 |
This comment has been minimized.
This comment has been minimized.
键盘扫描相关文档键盘扫描(包括按键扫描和键位映射)是机械键盘的核心部分,不仅是机械键盘固件中不可或缺的成分,同时也是客制化中的一大亮点。 这个文档记录了键盘扫描的一些基本概念,以及当前关于键盘扫描部分的实现方式和相关API。 基本概念此处约定了机械键盘的相关定义,并且简要描述了键盘扫描中各个部分是如何运作的。 矩阵和布局在机械键盘中,我们将矩阵(matrix)和布局(layout)的概念区分开来,其中:
考虑到矩阵和布局的不同概念,我们倾向于分别在以下情景中使用它们:
当然,我们会在代码中完成从布局到矩阵的转换,这部分通常也是硬编码到固件的。 软件框架在软件中,键盘扫描对应两个部分,分别是:
其中:
键位映射与层结构在65%等非完整配列键盘中,通常需要对标准键盘上的按键进行缩减,这会导致部分非常用键值没有对应的独立实体按键(比如F1到F12)。在这种情况下,我们可以引入组合键的方式来扩展键盘的功能。 在键位映射中,层是一个比较重要的概念,它用简单清晰的抽象方式来快速扩展已有的按键功能。举个例子,我们会使用Fn+第一排数字键的方式在65%键盘中引入F1到F12,那我们可以把F1到F12放在同一层中,并且规定在Fn按下时该层生效,从而一次性改变多个按键的功能。 关于层的各种特性和应用超出了本文的范围,可以参考其它成熟键盘固件(如QMK)的文档以了解相关知识。 代码结构考虑到键盘部分的代码量较大,我们将这部分源代码和头文件单独放在对应的keyboard文件夹中,避免与其它代码混淆。
处理流程按键扫描按键扫描可以进一步分为原始值读取、抖动消除和键位事件提交三个部分。 在按键扫描的过程中,我们需要保存以下信息:
以上信息均以矩阵中的位置(也就是键位)为索引值进行存储。为了节省内存空间,每个键位都对应1个bit,并且均统一为1按下0释放。 由于按键扫描是周期性执行的,因此需要使用RTOS的定时器特性,保证在一段时间内按键扫描的程序会执行一次。 原始值读取这一步比较简单,就是通过扫描键盘矩阵的方式,读取键盘的当前状态。由于这一步还没有进行消抖处理,因此读取的值可能包含了按键的抖动状态,不能直接拿来使用。这一步记录的数值将会存储到 抖动消除抖动消除是可以根据需要进行定制的一套算法,算法的复杂度与实现的方式相关。这里,我们选择最简单的一种方式,对应的是QMK固件中的
当然,后续可以通过修改代码的方式添加新的消抖算法,可以参考QMK或其它键盘固件的文档,这里不再赘述。 在消抖的过程中,先前发生改变的状态将会被记录到 键位事件提交这一步也比较简单,就是根据 另外,为了支持后续其它模块的处理,这里还同时记录了扫描结束的事件(即 键位映射键位映射可以进一步分为键值搜索和键值处理两部分。 键值搜索键值搜索涉及到前面所说的层结构。具体来说,我们需要根据当前的层状态,找到键位事件中实际对应的键值。 键值搜索有以下几个问题需要处理:
键值处理在获取到实际键值后,我们可以对键值进行处理了。前面提到,我们会在键盘中使用组合键值,以实现更加复杂的特性。当前的代码已经实现了这些特性:
当然,我们后续还能实现新的特性,包括层的高级功能和其它辅助功能(例如系统控制等),这些都需要后续工作来完善。 API设计为了方便进行二次开发,这里我们将API的设计一并列出,可以根据需要进行修改和扩展。 键盘配置文件对于每个键盘,都会有一组硬件参数和I/O相关函数与之对应。考虑到配置文件应该方便后期进行修改,我们将相关代码从键盘功能本身独立出来,并以源代码的方式存储在 硬件参数硬件参数的结构体如下所示。 typedef struct {
struct {
uint8_t total_cnt;
uint8_t col_cnt;
uint8_t row_cnt;
const uint8_t *col_pins;
const uint8_t *row_pins;
const uint8_t *layout_from;
} matrix;
struct {
uint8_t total_cnt;
uint8_t col_cnt;
uint8_t row_cnt;
} layout;
struct {
uint16_t scan_period_ms;
uint16_t max_jitter_ms;
uint8_t debounce_algo;
} scan;
struct {
uint8_t layer_cnt;
uint8_t default_layer;
const smk_keycode_type *keymaps;
} map;
} smk_keyboard_hardware_type; 其中:
I/O相关函数I/O相关函数通常与使用的MCU相关,因此我们将进行I/O相关操作的函数封装好,以便配置文件中进行设置。 I/O相关函数包括:
其中, 键盘事件键盘事件是键盘扫描部分进行信息传输的重要载体。为了方便键盘扫描以外的其它代码与之进行交互,我们将键盘事件统一为如下结构体: typedef struct {
uint8_t class;
uint8_t subclass;
smk_event_data_type data;
TickType_t timestamp;
} smk_event_type; 其中:
键盘事件可以通过FreeRTOS提供的Queue实现为事件FIFO,从而可以在多任务处理的过程中保证事件的同步和处理方式的统一。 键值键盘扫描部分使用的键值在
对于USB HID部分,通常只接受合法的键值,因此传递到USB HID的键值需要保证合法性(包括排除 按键扫描对于主函数而言,按键扫描主要有两个函数是需要被直接调用的,分别是初始化函数
|
TapEngine设计与使用为了使得键盘支持Tap-Hold及相关功能,这里我们引入了TapEngine的相关设计,并给出了基本的使用方法。 Tap-Hold简介Tap-Hold(短按-长按)是机械键盘键位映射的核心功能之一,基本思路是根据按键时间的长度来判断当前按键的使用状态,从而赋予单个按键以更多的功能。 Tap-Hold有以下几种基本应用:
对于Tap-Hold来说,以下几个参数可以决定相关的特性:
当然,与Tap-Hold相关的特性还有很多(典型的是与其它按键的交互,或者与Layer之间的相互影响)。这里我们主要集中于Fn键的相关应用,其它应用留待后续开发者自由发挥。 设计思路一般来说,Tapping相关的特性与键值有关,因此我们将其放在KeyMap的相关部分;不过,由于TapEngine的设计相对复杂,并且不影响其它功能的设计,我们将其作为KeyMap的独立子模块进行设计较为合适。 对于已有的KeyMap代码,我们还需要添加不同的结构体和函数。
|
@metro94
The text was updated successfully, but these errors were encountered: