[自製QMK鍵盤-番外] 在Custom Matrix中使用UART與控制滑鼠遊標,並加上無線模組

系列:自製QMK鍵盤 Posted on 2022-02-05

這篇文章中我簡單地介紹了 Mitosis 這個基於 QMK 的無線分離式人體工學鍵盤,而在這篇文章中,我將參考其架構來做出一個我自己的無線分離式鍵盤的雛形。

要達成這樣的功能,會需要用到 QMK 的 Custom Matrix 和 UART 功能,並且使用 LoRa 無線通訊模組 HC-12 來暫時替代藍牙作爲無線通訊。

在 QMK 中使用 Custom Matrix 與 UART

由於 Mitosis 不是和一般的鍵盤一樣透過按鍵掃描來取得按鍵狀態,而是藉由 UART 通訊,所以我們需要改變 QMK 的掃描程式,改成使用 UART 取得按鍵狀態。以下將會說明要如何達成。

rules.mk

首先,要完整地啓用「Custom Matrix」功能的話,要在 rules.mk 中增加 CUSTOM_MATRIX = yesSRC += matrix.c,並在鍵盤資料夾中增加 matrix.c 檔案。而自定的掃描程式就要按照格式寫在 matrix.c 中。

然後,因爲我們還會需要使用 UART 功能,所以在 rules.mk 中還要增加 SRC += uart.c 。因此,rules.mk 大概會長這樣:

 1# MCU name
 2MCU = atmega32u4
 3
 4# Processor frequency.
 5#     This will define a symbol, F_CPU, in all source code files equal to the
 6#     processor frequency in Hz. You can then use this symbol in your source code to
 7#     calculate timings. Do NOT tack on a 'UL' at the end, this will be done
 8#     automatically to create a 32-bit value in your source code.
 9#
10#     This will be an integer division of F_USB below, as it is sourced by
11#     F_USB after it has run through any CPU prescalers. Note that this value
12#     does not *change* the processor frequency - it should merely be updated to
13#     reflect the processor speed set externally so that the code can use accurate
14#     software delays.
15F_CPU = 8000000
16
17#
18# LUFA specific
19#
20# Target architecture (see library "Board Types" documentation).
21ARCH = AVR8
22
23# Input clock frequency.
24#     This will define a symbol, F_USB, in all source code files equal to the
25#     input clock frequency (before any prescaling is performed) in Hz. This value may
26#     differ from F_CPU if prescaling is used on the latter, and is required as the
27#     raw input clock is fed directly to the PLL sections of the AVR for high speed
28#     clock generation for the USB and other AVR subsections. Do NOT tack on a 'UL'
29#     at the end, this will be done automatically to create a 32-bit value in your
30#     source code.
31#
32#     If no clock division is performed on the input clock inside the AVR (via the
33#     CPU clock adjust registers or the clock division fuses), this will be equal to F_CPU.
34F_USB = $(F_CPU)
35
36# Bootloader selection
37#   Teensy       halfkay
38#   Pro Micro    caterina
39#   Atmel DFU    atmel-dfu
40#   LUFA DFU     lufa-dfu
41#   QMK DFU      qmk-dfu
42#   ATmega32A    bootloadHID
43#   ATmega328P   USBasp
44BOOTLOADER = caterina
45
46# Interrupt driven control endpoint task(+60)
47OPT_DEFS += -DINTERRUPT_CONTROL_ENDPOINT
48
49
50# Boot Section Size in *bytes*
51OPT_DEFS += -DBOOTLOADER_SIZE=4096
52
53
54# Build Options
55#   comment out to disable the options.
56#
57BOOTMAGIC_ENABLE ?= yes	# Virtual DIP switch configuration(+1000)
58MOUSEKEY_ENABLE ?= yes	# Mouse keys(+4700)
59EXTRAKEY_ENABLE ?= yes	# Audio control and System control(+450)
60CONSOLE_ENABLE ?= no	# Console for debug(+400)
61COMMAND_ENABLE ?= no    # Commands for debug and configuration
62SLEEP_LED_ENABLE ?= no  # Breathing sleep LED during USB suspend
63NKRO_ENABLE ?= yes		# USB Nkey Rollover - if this doesn't work, see here: https://github.com/tmk/tmk_keyboard/wiki/FAQ#nkro-doesnt-work
64BACKLIGHT_ENABLE ?= no  # Enable keyboard backlight functionality
65AUDIO_ENABLE ?= no
66RGBLIGHT_ENABLE ?= no
67ENABLE_VIA = yes
68POINTING_DEVICE_ENABLE = yes
69CUSTOM_MATRIX = yes
70
71SRC += matrix.c uart.c

matrix.c

自行新增的程式檔案 matrix.c 是用來放自定的掃描程式的,我們要在掃描程式中使用 UART 進行通訊。

根據 QMK 文件的說明,matrix.c 需要實作以下的函式:

 1/* Implement the following functions in a matrix.c file in your keyboard folder: */
 2matrix_row_t matrix_get_row(uint8_t row) {
 3    // TODO: return the requested row data
 4}
 5
 6void matrix_print(void) {
 7    // TODO: use print() to dump the current matrix state to console
 8}
 9
10void matrix_init(void) {
11    // TODO: initialize hardware and global matrix state here
12
13    // Unless hardware debouncing - Init the configured debounce routine
14    debounce_init(MATRIX_ROWS);
15
16    // This *must* be called for correct keyboard behavior
17    matrix_init_quantum();
18}
19
20uint8_t matrix_scan(void) {
21    bool matrix_has_changed = false;
22
23    // TODO: add matrix scanning routine here
24
25    // Unless hardware debouncing - use the configured debounce routine
26    debounce(raw_matrix, matrix, MATRIX_ROWS, changed);
27
28    // This *must* be called for correct keyboard behavior
29    matrix_scan_quantum();
30
31    return matrix_has_changed;
32}
33
34/* And also provide defaults for the following callbacks: */
35__attribute__((weak)) void matrix_init_kb(void) { matrix_init_user(); }
36__attribute__((weak)) void matrix_scan_kb(void) { matrix_scan_user(); }
37__attribute__((weak)) void matrix_init_user(void) {}
38__attribute__((weak)) void matrix_scan_user(void) {}

對我們來說,只需要注意 matrix_init()matrix_scan() 這兩個函式就好了。matrix_init() 就是初始化矩陣掃描(只會被呼叫一次),我們要在此函式中完成 UART 的初始化,而 matrix_scan() 就是矩陣掃描的程式,也就是每次要進行掃描是要執行的程式,我們要在此函式中接收 UART 的封包並告訴 QMK 有哪些按鍵狀態改變了(被壓下或釋放)。

一個簡單的測試程式大概長這樣:(我根據 Mitosis 的程式進行修改的,未檢查是否有不必要的程式)

  1/*
  2Copyright 2012 Jun Wako
  3Copyright 2014 Jack Humbert
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 2 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <http://www.gnu.org/licenses/>.
 17*/
 18#include <stdint.h>
 19#include <stdbool.h>
 20#if defined(__AVR__)
 21#    include <avr/io.h>
 22#endif
 23#include "wait.h"
 24#include "print.h"
 25#include "debug.h"
 26#include "util.h"
 27#include "matrix.h"
 28#include "timer.h"
 29#include "uart.h"
 30//#include "quantum.h"
 31
 32#if (MATRIX_COLS <= 8)
 33#    define print_matrix_header() print("\nr/c 01234567\n")
 34#    define print_matrix_row(row) print_bin_reverse8(matrix_get_row(row))
 35#    define matrix_bitpop(i) bitpop(matrix[i])
 36#    define ROW_SHIFTER ((uint8_t)1)
 37#elif (MATRIX_COLS <= 16)
 38#    define print_matrix_header() print("\nr/c 0123456789ABCDEF\n")
 39#    define print_matrix_row(row) print_bin_reverse16(matrix_get_row(row))
 40#    define matrix_bitpop(i) bitpop16(matrix[i])
 41#    define ROW_SHIFTER ((uint16_t)1)
 42#elif (MATRIX_COLS <= 32)
 43#    define print_matrix_header() print("\nr/c 0123456789ABCDEF0123456789ABCDEF\n")
 44#    define print_matrix_row(row) print_bin_reverse32(matrix_get_row(row))
 45#    define matrix_bitpop(i) bitpop32(matrix[i])
 46#    define ROW_SHIFTER ((uint32_t)1)
 47#endif
 48
 49/* matrix state(1:on, 0:off) */
 50static matrix_row_t matrix[MATRIX_ROWS];
 51
 52__attribute__((weak)) void matrix_init_kb(void) { matrix_init_user(); }
 53__attribute__((weak)) void matrix_scan_kb(void) { matrix_scan_user(); }
 54__attribute__((weak)) void matrix_init_user(void) {}
 55__attribute__((weak)) void matrix_scan_user(void) {}
 56
 57inline uint8_t matrix_rows(void) { return MATRIX_ROWS; }
 58inline uint8_t matrix_cols(void) { return MATRIX_COLS; }
 59
 60void matrix_init(void) {
 61    uart_init(9600);
 62    matrix_init_quantum();  // This *must* be called for correct keyboard behavior.
 63}
 64
 65uint8_t matrix_scan(void) {
 66    if (uart_available()) {
 67        uint8_t indata = uart_read();
 68        switch (indata) {
 69            case 0x00:
 70                matrix[0] = 0;
 71                break;
 72
 73            case 0x01:
 74                matrix[0] = 1;
 75                break;
 76
 77            case 0x10:
 78                matrix[1] = 0;
 79                break;
 80
 81            case 0x11:
 82                matrix[1] = 1;
 83                break;
 84
 85            default:
 86                break;
 87        }
 88    }
 89
 90    matrix_scan_quantum();  // This *must* be called for correct keyboard behavior.
 91    return 1;
 92}
 93
 94inline bool matrix_is_on(uint8_t row, uint8_t col) { return (matrix[row] & ((matrix_row_t)1 << col)); }
 95
 96inline matrix_row_t matrix_get_row(uint8_t row) { return matrix[row]; }
 97
 98void matrix_print(void) {
 99    print_matrix_header();
100
101    for (uint8_t row = 0; row < MATRIX_ROWS; row++) {
102        print_hex8(row);
103        print(": ");
104        print_matrix_row(row);
105        print("\n");
106    }
107}
108
109uint8_t matrix_key_count(void) {
110    uint8_t count = 0;
111    for (uint8_t i = 0; i < MATRIX_ROWS; i++) {
112        count += matrix_bitpop(i);
113    }
114    return count;
115}

上面這段程式比較重要的有幾點:

  • #include "uart.h":引用 QMK 的 UART 功能,否則會編譯錯誤。
  • uart_init(9600):在 matrix_init() 中初始化 UART,並將鮑率(Baud Rate)設定爲 9600 bps。
  • uart_available():有用過 Arduino 的 Serial Port 的人應該都看得懂這一段,就是只要 UART 的接收緩衝區有值(有接收到資料),就使用 uart_read() 將收到的資料讀出,在透過 switch-case 來處理並改寫 matrix[] 的值,以完成按鍵狀態的更新。

利用 QMK 移動滑鼠遊標

因爲我要做的無線分離式鍵盤上預計裝有軌跡球,所以我也一併測試了 QMK 要如何控制滑鼠遊標。

首先,在 rules.mk 中增加 MOUSEKEY_ENABLE = yesPOINTING_DEVICE_ENABLE = yesPOINTING_DEVICE_DRIVER = custom 就可以啓用滑鼠與遊標的相關功能。

matrix.c 中加入 #include "quantum.h",並將剛剛的 matrix_scan() 的程式改成:

 1#include "quantum.h"
 2
 3uint8_t matrix_scan(void) {
 4    if (uart_available()) {
 5            uint8_t indata = uart_read();
 6            report_mouse_t report = {};
 7            report.x = (int8_t)indata;
 8            pointing_device_set_report(report);
 9            pointing_device_send();
10        }
11    }
12
13    matrix_scan_quantum();
14    return 1;
15}

其中,report_mouse_t 就是 QMK 中滑鼠遊標的 Data type,其原型爲:

 1// File:qmk_firmware/tmk_core/protocol/report.h
 2// URL:https://github.com/qmk/qmk_firmware/blob/master/tmk_core/protocol/report.h
 3
 4typedef struct {
 5#ifdef MOUSE_SHARED_EP
 6    uint8_t report_id;
 7#endif
 8    uint8_t buttons;
 9    int8_t  x;
10    int8_t  y;
11    int8_t  v;
12    int8_t  h;
13} __attribute__((packed)) report_mouse_t;
  • xy 分別代表滑鼠遊標 X 軸與 Y 軸移動的距離,範圍是 -128 ~ 127
  • vh 代表滑鼠滾輪垂直與水平滾動的距離,範圍是 -128 ~ 127
  • buttons 代表各個滑鼠按鈕按下的情況。

LoRa 無線通訊模組 HC-12

因爲我手邊沒有其它適合的無線通訊模組,所以就先拿「HC-12」這款 LoRa 模組來使用。

這個模組的好處是使用簡單方便,就自己把它當成一般的 UART 就好,Tx 與 Rx 接好,不用特別設定什麼就可以無線通訊了。

而我用來控制 HC-12 的是 Nucleo-F302R8(STM32F302R8),因爲只是要簡單的測試無線通訊及 QMK,所以就寫了一個按下按鈕會透過 UART 傳送特定資料的程式作爲測試。STM32 韌體函式庫使用「libopencm3」,IDE 爲「PlatformIO for VS Code」。

  1/**
  2 * @file   main.c
  3 */
  4
  5#define CFG_0
  6//#define CFG_1
  7
  8#include <libopencm3/stm32/rcc.h>
  9#include <libopencm3/stm32/gpio.h>
 10#include <libopencm3/stm32/usart.h>
 11
 12#define USART (USART2)
 13
 14/* USART2-Tx = PA2 */
 15#define USART_TX_PORT (GPIOA)
 16#define USART_TX_PIN (GPIO2)
 17
 18/* User-LED = PB13 */
 19#define LED_PORT (GPIOB)
 20#define LED_PIN (GPIO13)
 21
 22/* User-Button = PC13 */
 23#define BUTTON_PORT (GPIOC)
 24#define BUTTON_PIN (GPIO13)
 25
 26uint8_t state = 0;
 27
 28void rcc_setup(void)
 29{
 30  rcc_periph_clock_enable(RCC_GPIOA);
 31  rcc_periph_clock_enable(RCC_GPIOB);
 32  rcc_periph_clock_enable(RCC_GPIOC);
 33  rcc_periph_clock_enable(RCC_USART2);
 34}
 35
 36void usart_setup(void)
 37{
 38  /* Setup Tx pin. */
 39  gpio_mode_setup(USART_TX_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, USART_TX_PIN);
 40  gpio_set_af(USART_TX_PORT, GPIO_AF7, USART_TX_PIN);
 41
 42  /* Setup UART config with 9600, 8-N-1. */
 43  usart_set_baudrate(USART, 9600);
 44  usart_set_databits(USART, 8);
 45  usart_set_stopbits(USART, USART_STOPBITS_1);
 46  usart_set_parity(USART, USART_PARITY_NONE);
 47  usart_set_flow_control(USART, USART_FLOWCONTROL_NONE);
 48  usart_set_mode(USART, USART_MODE_TX);
 49
 50  /* Enable. */
 51  usart_enable(USART);
 52}
 53
 54void led_setup(void)
 55{
 56  gpio_mode_setup(LED_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, LED_PIN);
 57  gpio_set_output_options(LED_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, LED_PIN);
 58}
 59
 60void button_setup(void)
 61{
 62  gpio_mode_setup(BUTTON_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, BUTTON_PIN);
 63}
 64
 65int main(void)
 66{
 67  rcc_setup();
 68  led_setup();
 69  button_setup();
 70  usart_setup();
 71
 72  usart_send_blocking(USART, 'O');
 73  usart_send_blocking(USART, 'K');
 74#if defined(CFG_0)
 75  usart_send_blocking(USART, '0');
 76#elif defined(CFG_1)
 77  usart_send_blocking(USART, '1');
 78#else
 79#error CFG_0 or CFG_1
 80#endif
 81  usart_send_blocking(USART, '\r');
 82  usart_send_blocking(USART, '\n');
 83
 84  while (1)
 85  {
 86    if (gpio_get(BUTTON_PORT, BUTTON_PIN) == 0)
 87    {
 88      // Pressed.
 89      gpio_set(LED_PORT, LED_PIN);
 90
 91#if defined(CFG_0)
 92      usart_send_blocking(USART, 0x01);
 93#elif defined(CFG_1)
 94      usart_send_blocking(USART, 0x11);
 95#else
 96#error CFG_0 or CFG_1
 97#endif
 98state = 1;
 99    }
100    else if(state != 0)
101    {
102      // Not pressed.
103      gpio_clear(LED_PORT, LED_PIN);
104#if defined(CFG_0)
105      usart_send_blocking(USART, 0x00);
106#elif defined(CFG_1)
107      usart_send_blocking(USART, 0x10);
108#else
109#error CFG_0 or CFG_1
110#endif
111state = 0;
112    }
113  }
114
115  return 0;
116}

最終效果如影片所示:

結語

這次簡單地分享了 QMK 使用 Custom Matrix、UART 和控制滑鼠遊標的方法,有些功能我自己也是找了不少資料才知道要怎麼做,並且也測試了很多次。

然而對 QMK 的瞭解也還很粗淺,很多細節沒辦法講解,而如果上述內容有任何錯誤也請指正。

相關文章



留言可能不會立即顯示。若過了幾天仍未出現,請 Email 聯繫:)

comments powered by Disqus