野火指南者F103开发板之二: 移植 qmk 开源键盘固件

前言

连接效果

在前文中, 我介绍了移植野火开发板 stm32f103开发板的 dfu bootloader, 接下来要介绍的是如何迁移qmk到开发板上.
这篇文章会涉及修改 qmk 源码,添加 stm32f103 键盘, 配置测试小键盘的布局, 最后通过 stm32duino 的 dfu 模式刷入固件.
一同解释 qmk 的一些基础知识.
后面的文章逐步深入到 RGB 和存储等.

源码仓库 https://github.com/hitsmaxft/qmk-kb-bheznz , 最新的代码在我的qmk-firmware 中, 这一份是手工尽量同步.

开发板

指南者拥有接近 100 个引脚, 这次搭建键盘原型只会用到很小一部分引脚.

在本文这个测试键盘中, 我用的是一个简单的 2x4 微动按钮矩阵用于测试键盘功能.

  • 微动键盘: 4x2 8个独立按键, 9个引脚.

图片

每个按钮都是独立引脚, 直接连接到开发板, 还有一个 GND 接地.

初始化键盘 源代码

1
qmk new-keyboard

我这里给键盘起的名字是 bheznz

  • layout 选最后一个 62. none of the above, 后面再改
  • mcu 选 40. STM32F103

qmk会添加一下文件.

1
2
3
4
5
6
keyboards/bheznz
keyboards/bheznz/keymaps
keyboards/bheznz/keymaps/default
keyboards/bheznz/keymaps/default/keymap.c
keyboards/bheznz/readme.md
keyboards/bheznz/keyboard.json

keyboard.json 是最新的配置格式, 通过 json 配置大部分键盘核心配置. 但是剩下的定制选项还是需要通过其他文件来完成.

由于后续我还根据按键个数拆分了不同的外设, 发现一个坑, 就是同一个键盘下多个型号, 需要多份 keyboard.json 的时候, 这里的 keyboard.json 会导致报错, 又被我手动重新改名回 info.json.

实际移植中, 我后续手动添加以下文件, 来完成迁移 qmk 的所有功能

1
2
3
4
5
6
keyboards/bheznz/bheznz.h //键盘公共头文件
keyboards/bheznz/bheznz.c //键盘自定义行为代码, 比如实现 qmk的钩子函数, 在这个文件实现不需要额外的构建配置
keyboards/bheznz/config.h //入口头文件, 大部分通过宏定义的配置项都需要放在这里, 最高优先级加载.
keyboards/bheznz/halconf.h //chibios hal 模块开关, 比如开启 dma,时钟等功能, 参考 https://github.com/hitsmaxft/qmk-kb-bheznz/blob/master/halconf.h
keyboards/bheznz/mcuconf.h //定制 mcu 配置, 比如那些 STM32 开头的宏变量. 参考 https://github.com/hitsmaxft/qmk-kb-bheznz/blob/master/mcuconf.h
rule.mk //编译选项, 键盘的功能需要在这里开启和配置, 现在大部分在 keyboard.json/info.json 里配置了, 少部分选项必须通过 rule.mk 开启.

后面需要改的时候再一一说明.

配置键盘布局

移植和构建qmk固件的第一步, 是需要让键盘按键逻辑运作起来.
这部分配置不需要写代码, 只需要修改 json 配置.

我这里选择了开发板上给 led 屏幕留的插槽部分和 gnd, 一共9个引脚.

tft引脚图

根据物理的外观, 首先配置好按键在板子上行列情况.

本来独立按钮是没有必要配置行列的, 它并没有行列扫描的逻辑, 直接按一行8列配置就行. 这里只是为了和行列的保持一致, 按照板子上的排列配置了4行2列.
接下来 json 的配置都是 keyboard.json 这个文件.

这是键盘上按键的物理布局, 4 rows 2 column (4行2列), 坐标从 (0,0) 到 (3,2).

┌──┐┌──┐
│K1││K2│
└──┘└──┘
┌──┐┌──┐
│K3││K4│
└──┘└──┘
┌──┐┌──┐
│K5││K6│
└──┘└──┘
┌──┐┌──┐
│K7││K8│
└──┘└──┘

1
2
3
4
5
6
7
8
9
10
11
12

连接的 PIN/引脚 按上面, 排列如下

```json
"matrix_pins": {
"direct": [
["D15", "D1"], //0,0|0,1
["E8", "E10"],
["E12", "E14"],
["D8", "D10"]
]
}
考虑到键盘一般是横向的, 内置的 layout 里也只有 2x4/2行4列, 接下来会键盘的物理定义顺时针旋转了 90 度. 原因后面会补充. 计算了一下键盘的按键物理布局和引脚行列编号的对应关系, 是下面这个情况. 列出
1
2
3,0|2,0|1,0|0,0
3,1|2,1|1,1|0,1
接下来需要配置 layout, 也就是键盘上从左到右, 从上到下, 从 0 开始编号至最后一个按钮. 配置中需要声明每个按钮对应的引脚的行列位置, 以及它在键盘中的物理位置. > matrix: 当前按钮对应的行和列, pin 分成了4列2行, 但做了90度旋转, 所以需要换成实际的pin位置 > x: 按钮左边的偏移量, 从0开始. 所以每个键的x, 其实**左边所有按键的宽度**累加起来. > y: 同x, 是垂直方面, 上面所有行高度累加, 默认是1. 像分裂键盘就会存在高度超过 1 的按钮. > w: 键盘正常是 1U , mod 键一般是1.25u, 还有更长的 shift 和 space, 我这里都当成1u, 也就不用特地写上 w 参数了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"layouts": {
"LAYOUT_ortho_2x4": {
"layout": [
{"matrix": [3, 0], "x": 0, "y": 0},
{"matrix": [2, 0], "x": 1, "y": 0},
{"matrix": [1, 0], "x": 2, "y": 0},
{"matrix": [0, 0], "x": 3, "y": 0},
{"matrix": [3, 1], "x": 0, "y": 1},
{"matrix": [2, 1], "x": 1, "y": 1},
{"matrix": [1, 1], "x": 2, "y": 1},
{"matrix": [0, 1], "x": 3, "y": 1}
]
}
}

这里做了按键到pin的换算, 是处于教程的目的. 实际的键盘设计中, 是会存在一些优化手段, 按键的实际位置和 pin 的行列位置是不同的. 这么做也是为了节省引脚. 比如61/64键键盘布局 5x14, 实际上能容纳下70个键, 这就导致存在很多空位. 同过把第 14 列的按键放在其他行的空位上. 这样子 5x13 列的 pin 布局就可以容纳下物理外观是 5x14, 最多65键的键盘. 节省了一个引脚可以用于其他作用. 最终效果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

> qmk info -kb bheznz -l

....

Layouts: LAYOUT_ortho_2x4
Processor: STM32F103
Bootloader: stm32duino
Matrix for "LAYOUT_ortho_2x4":
┌──┐┌──┐┌──┐┌──┐
│3A││2A││1A││0A│
└──┘└──┘└──┘└──┘
┌──┐┌──┐┌──┐┌──┐
│3B││2B││1B││0B│
└──┘└──┘└──┘└──┘

`LAYOUT_ortho_2x4` 是`qmk`源码库中内置的布局名称, `qmk`编译的时候, 会按这个布局生成对应的 c 代码宏指令. 下面通过 `keymap.c` 配置键位功能, 代码中会用到这个生成的宏命令. 以下是我根据需要定制自己的测试 keymap 代码.
title:keymap.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include QMK_KEYBOARD_H

#include "keycodes.h"
#include "quantum.h"
#include "bheznz.h"

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
/*
* ┌───┬───┬───┬───┐
* │ A │ S │ D │ F │
* ├───┼───┼───┼───┤
* │ Z │ X │ C │ V │
* └───┴───┴───┴───┘
*/
[0] = LAYOUT_ortho_2x4(
LT(2, KC_A), KC_S, KC_D, KC_F,
LT(1, KC_Z), KC_X, KC_C, KC_V
),
[1] = LAYOUT_ortho_2x4(
_______, RGB_VAI, RGB_HUI, RGB_HUD,
_______, RGB_VAD, RGB_MOD, RGB_RMOD
),
[2] = LAYOUT_ortho_2x4(
_______, RGB_TOG, _______, _______,
QK_BOOT, QK_DEBUG_TOGGLE, RGB_VAI, RGB_VAD
),
};

## 开发板和 chibios 代码相关 前面只是罗列了开发板的一些引脚信息, 让固件跑起来, 除了配置布局, 还需要正确地配置 qmk 中的开发板配置和引脚信息. qmk 的 HAL (硬件抽象层) 是 chibios 这个嵌入式框架提供的. 关于开发板的定制, 首先了解一下 chibios 中开发板代码组织方式. * platforms/chibios/boards 下是qmk提供的内置开发板, f103 的板子只有 `STM32_F103_STM32DUINO` * lib/chibios/os/hal/boards 下则是 chibios 默认支持的开发板信息, 可以看到 ST Nuclear 的板子比较多 本来我以为需要自己写板子的配置, 但是仔细测试之后发现, qmk 的bootloader选项一旦配置成 `stm32duino`, 会强行指定 `BOARD=STM32_F103_STM32DUINO`. 也就是说, 不能自己定义开发板源码. 所以这里我们也不用特地去关心开发板的代码了, 自定义的行为代码, 都需要放在 keyboard 的源代码中. 所以, 现在确认一下 `keyboard.json` 中, bootloader 是正确地写上 `stm32duino`.
1
"bootloader": "stm32duino",
开发板的准备工作到这里就告一段落了, 我们已经完成了 bootloader 开发板信息是默认的, 键盘的布局和keymap也配置完毕. 接下来还需需要一些改动, 让固件能够正常地工作起来. ### 解决 qmk 每次启动都进入 bootloader 虽然开发板代码不能改, 这里还需要提醒一下, qmk 固件刷了之后, 每次启动都会默认进入 bootloader, 这是开发板源码里一个bug导致的. 先看一下开发板的默认初始化代码
title: platforms/chibios/boards/STM32_F103_STM32DUINO/board.c
1
2
3
4
5
6
7
8
9
10
11
12

/*
* Board-specific initialization code.
*/
void boardInit(void) {
//JTAG-DP Disabled and SW-DP Enabled
AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE;
//Set backup register DR10 to enter bootloader on reset
//我们需要注释掉这句话
BKP->DR10 = RTC_BOOTLOADER_FLAG;
}

`BKP` 是 stm32 的备份寄存器, 指南者板子上自带了纽扣电池, `BKP` 和 `RTC` 时钟寄存器在电池供电的情况下, 即时拔掉外部供电, 也不会丢失. 而 `stm32duino-bootloader` 会读取 `BKP DR10` , 一旦发现这个值是 RTC_BOOTLOADER_FLAG, 会停留在 bootloader 的DFU 模式, 等待刷固件. > RTC Flag 还有额外集中情况, 待补充. 所以默认的 boardInit 会引起每次上电都进入刷固件模式. 但根本不存在这种需求, 需要刷固件按 reset 不就行了. 复写这个函数, 需要在 bheznz.c, 也就是键盘的自定义代码中覆盖这个函数
title: keyboard/bheznz/bheznz.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "bootloader.h"
#include "bheznz.h"

void bootloader_jump(void) {
//写入 bootloader 标记, 下次重启自动进入bootloader
BKP->DR10 = RTC_BOOTLOADER_FLAG;
NVIC_SystemReset();
}

void mcu_reset(void) {
BKP->DR10 = RTC_BOOTLOADER_FLAG;
NVIC_SystemReset();
}

void boardInit(void) {
//JTAG-DP Disabled and SW-DP Enabled 这句话我还不知道干嘛的, 继续保留
AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE;
}
`bootloader_jump()` 和 `mcu_reset()` 分别是按下 QK_BOOT 和 开发板reset 按钮执行的操作. 现在改成写入 bootloader 标记位, 重置或者重新上电, bootloader 会识别并进入 DFU 模式. boardInit 这里就没必要动寄存器了, 正常引导就行了. > 原始代码见 `platforms/chibios/bootloaders/stm32duino.c` ### 配置 usb disc 引脚 上文说到 野火的usb 有个开关引脚 PD6, 这个引脚如果处于高电平状态, 会导致usb无法工作. chibios 已经考虑到 usb 初始化的定制需求. 预留了 usb 的扩展宏. 这里需要修改 `board.h` 中, 以下两个宏命令. 往其中添加 PD6 引脚的状态修改代码.
1
2
#define usb_lld_connect_bus(usbp) palClearPad(GPIOD, 6); palSetPadMode(GPIOA, 12, PAL_MODE_INPUT);
#define usb_lld_disconnect_bus(usbp) palSetPadMode(GPIOA, 12, PAL_MODE_OUTPUT_PUSHPULL); palClearPad(GPIOA, 12); palSetPad(GPIOD, 6);
tmk 的源码中可以找到调用的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//lib/chibios/os/hal/include/hal_usb.h:400
#define usbConnectBus(usbp) usb_lld_connect_bus(usbp)

//tmk_core/protocol/chibios/usb_main.c:363:
void init_usb_driver(USBDriver *usbp) {
for (int i = 0; i < USB_ENDPOINT_IN_COUNT; i++) {
usb_endpoint_in_init(&usb_endpoints_in[i]);
usb_endpoint_in_start(&usb_endpoints_in[i]);
}

for (int i = 0; i < USB_ENDPOINT_OUT_COUNT; i++) {
usb_endpoint_out_init(&usb_endpoints_out[i]);
usb_endpoint_out_start(&usb_endpoints_out[i]);
}

/*
* Activates the USB driver and then the USB bus pull-up on D+.
* Note, a delay is inserted in order to not have to disconnect the cable
* after a reset.
*/
usbDisconnectBus(usbp);
usbStop(usbp);
wait_ms(50);
usbStart(usbp, &usbcfg);
usbConnectBus(usbp);
}
从源码中可知, tmk 在初始化usb 驱动时, 会先调用 disc 函数关闭 usb , 完成状态重置, 然后重新连接 usb bus. > `platforms/chibios/boards/STM32_F103_STM32DUINO/board/board.h` 这里也有部分代码考虑到 USB DISC pin 但是都处于注释状态, 并没有提供选项. 我打算提个 PR 优化一下, 就当做公益了. ## 编译和刷写固件 接下来就是运行编译命令, 碰到什么问题一个一个解决了.
1
qmk compile -kb bheznz -km default 
### 通过 qmk flash 刷写固件 qmk cli 中内置了 stm32 dfu 更新的命令 按下 reset 让开发板进入 dfu 模式, 然后运行
1
qmk flash -kb bheznz -km default 
`qmk` 会通过 `dfu-tool` 发现处于 `dfu` 状态的设备, 然后把编译好的固件写入到键盘中. 这部分没啥好介绍的, 参考前面的文章, 把 `stmd32duino-bootloader` 刷好. 开机按住 k1 或者按一下 reset 都能看到 bootloader 指示灯闪烁, 就是成功进入 dfu 模式 ## 结尾 到这里, 指南者的 qmk 固件就迁移完毕了. 刷完固件, 重置开发板, bootloader 如果没有进入 DFU 模式, 接下来就会加载用户代码部分, 也就是 qmk 键盘固件. 正常启动的话, 就可以在电脑中看到键盘了. ![连接效果](https://img.bhe.ink/IMG_2091.jpeg) > 基础的键盘部分只需要配置对 PIN 和 bootloader, 保证 USB 功能正常启动, 测试一下键位对不对就行了. 后面再继续写复杂一点的, 支持 RGB, EEPROM 和OELD等部分.