Skip to content

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

發佈

這篇文章中我簡單地介紹了 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 大概會長這樣:

# MCU name
MCU = atmega32u4
 
# Processor frequency.
#     This will define a symbol, F_CPU, in all source code files equal to the
#     processor frequency in Hz. You can then use this symbol in your source code to
#     calculate timings. Do NOT tack on a 'UL' at the end, this will be done
#     automatically to create a 32-bit value in your source code.
#
#     This will be an integer division of F_USB below, as it is sourced by
#     F_USB after it has run through any CPU prescalers. Note that this value
#     does not *change* the processor frequency - it should merely be updated to
#     reflect the processor speed set externally so that the code can use accurate
#     software delays.
F_CPU = 8000000
 
#
# LUFA specific
#
# Target architecture (see library "Board Types" documentation).
ARCH = AVR8
 
# Input clock frequency.
#     This will define a symbol, F_USB, in all source code files equal to the
#     input clock frequency (before any prescaling is performed) in Hz. This value may
#     differ from F_CPU if prescaling is used on the latter, and is required as the
#     raw input clock is fed directly to the PLL sections of the AVR for high speed
#     clock generation for the USB and other AVR subsections. Do NOT tack on a 'UL'
#     at the end, this will be done automatically to create a 32-bit value in your
#     source code.
#
#     If no clock division is performed on the input clock inside the AVR (via the
#     CPU clock adjust registers or the clock division fuses), this will be equal to F_CPU.
F_USB = $(F_CPU)
 
# Bootloader selection
#   Teensy       halfkay
#   Pro Micro    caterina
#   Atmel DFU    atmel-dfu
#   LUFA DFU     lufa-dfu
#   QMK DFU      qmk-dfu
#   ATmega32A    bootloadHID
#   ATmega328P   USBasp
BOOTLOADER = caterina
 
# Interrupt driven control endpoint task(+60)
OPT_DEFS += -DINTERRUPT_CONTROL_ENDPOINT
 
 
# Boot Section Size in *bytes*
OPT_DEFS += -DBOOTLOADER_SIZE=4096
 
 
# Build Options
#   comment out to disable the options.
#
BOOTMAGIC_ENABLE ?= yes	# Virtual DIP switch configuration(+1000)
MOUSEKEY_ENABLE ?= yes	# Mouse keys(+4700)
EXTRAKEY_ENABLE ?= yes	# Audio control and System control(+450)
CONSOLE_ENABLE ?= no	# Console for debug(+400)
COMMAND_ENABLE ?= no    # Commands for debug and configuration
SLEEP_LED_ENABLE ?= no  # Breathing sleep LED during USB suspend
NKRO_ENABLE ?= yes		# USB Nkey Rollover - if this doesn't work, see here: https://github.com/tmk/tmk_keyboard/wiki/FAQ#nkro-doesnt-work
BACKLIGHT_ENABLE ?= no  # Enable keyboard backlight functionality
AUDIO_ENABLE ?= no
RGBLIGHT_ENABLE ?= no
ENABLE_VIA = yes
POINTING_DEVICE_ENABLE = yes
CUSTOM_MATRIX = yes
 
SRC += matrix.c uart.c

matrix.c

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

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

/* Implement the following functions in a matrix.c file in your keyboard folder: */
matrix_row_t matrix_get_row(uint8_t row) {
    // TODO: return the requested row data
}
 
void matrix_print(void) {
    // TODO: use print() to dump the current matrix state to console
}
 
void matrix_init(void) {
    // TODO: initialize hardware and global matrix state here
 
    // Unless hardware debouncing - Init the configured debounce routine
    debounce_init(MATRIX_ROWS);
 
    // This *must* be called for correct keyboard behavior
    matrix_init_quantum();
}
 
uint8_t matrix_scan(void) {
    bool matrix_has_changed = false;
 
    // TODO: add matrix scanning routine here
 
    // Unless hardware debouncing - use the configured debounce routine
    debounce(raw_matrix, matrix, MATRIX_ROWS, changed);
 
    // This *must* be called for correct keyboard behavior
    matrix_scan_quantum();
 
    return matrix_has_changed;
}
 
/* And also provide defaults for the following callbacks: */
__attribute__((weak)) void matrix_init_kb(void) { matrix_init_user(); }
__attribute__((weak)) void matrix_scan_kb(void) { matrix_scan_user(); }
__attribute__((weak)) void matrix_init_user(void) {}
__attribute__((weak)) void matrix_scan_user(void) {}

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

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

/*
Copyright 2012 Jun Wako
Copyright 2014 Jack Humbert
 
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
 
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdint.h>
#include <stdbool.h>
#if defined(__AVR__)
#    include <avr/io.h>
#endif
#include "wait.h"
#include "print.h"
#include "debug.h"
#include "util.h"
#include "matrix.h"
#include "timer.h"
#include "uart.h"
//#include "quantum.h"
 
#if (MATRIX_COLS <= 8)
#    define print_matrix_header() print("\nr/c 01234567\n")
#    define print_matrix_row(row) print_bin_reverse8(matrix_get_row(row))
#    define matrix_bitpop(i) bitpop(matrix[i])
#    define ROW_SHIFTER ((uint8_t)1)
#elif (MATRIX_COLS <= 16)
#    define print_matrix_header() print("\nr/c 0123456789ABCDEF\n")
#    define print_matrix_row(row) print_bin_reverse16(matrix_get_row(row))
#    define matrix_bitpop(i) bitpop16(matrix[i])
#    define ROW_SHIFTER ((uint16_t)1)
#elif (MATRIX_COLS <= 32)
#    define print_matrix_header() print("\nr/c 0123456789ABCDEF0123456789ABCDEF\n")
#    define print_matrix_row(row) print_bin_reverse32(matrix_get_row(row))
#    define matrix_bitpop(i) bitpop32(matrix[i])
#    define ROW_SHIFTER ((uint32_t)1)
#endif
 
/* matrix state(1:on, 0:off) */
static matrix_row_t matrix[MATRIX_ROWS];
 
__attribute__((weak)) void matrix_init_kb(void) { matrix_init_user(); }
__attribute__((weak)) void matrix_scan_kb(void) { matrix_scan_user(); }
__attribute__((weak)) void matrix_init_user(void) {}
__attribute__((weak)) void matrix_scan_user(void) {}
 
inline uint8_t matrix_rows(void) { return MATRIX_ROWS; }
inline uint8_t matrix_cols(void) { return MATRIX_COLS; }
 
void matrix_init(void) {
    uart_init(9600);
    matrix_init_quantum();  // This *must* be called for correct keyboard behavior.
}
 
uint8_t matrix_scan(void) {
    if (uart_available()) {
        uint8_t indata = uart_read();
        switch (indata) {
            case 0x00:
                matrix[0] = 0;
                break;
 
            case 0x01:
                matrix[0] = 1;
                break;
 
            case 0x10:
                matrix[1] = 0;
                break;
 
            case 0x11:
                matrix[1] = 1;
                break;
 
            default:
                break;
        }
    }
 
    matrix_scan_quantum();  // This *must* be called for correct keyboard behavior.
    return 1;
}
 
inline bool matrix_is_on(uint8_t row, uint8_t col) { return (matrix[row] & ((matrix_row_t)1 << col)); }
 
inline matrix_row_t matrix_get_row(uint8_t row) { return matrix[row]; }
 
void matrix_print(void) {
    print_matrix_header();
 
    for (uint8_t row = 0; row < MATRIX_ROWS; row++) {
        print_hex8(row);
        print(": ");
        print_matrix_row(row);
        print("\n");
    }
}
 
uint8_t matrix_key_count(void) {
    uint8_t count = 0;
    for (uint8_t i = 0; i < MATRIX_ROWS; i++) {
        count += matrix_bitpop(i);
    }
    return count;
}

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

利用 QMK 移動滑鼠遊標

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

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

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

#include "quantum.h"
 
uint8_t matrix_scan(void) {
    if (uart_available()) {
            uint8_t indata = uart_read();
            report_mouse_t report = {};
            report.x = (int8_t)indata;
            pointing_device_set_report(report);
            pointing_device_send();
        }
    }
 
    matrix_scan_quantum();
    return 1;
}

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

// File:qmk_firmware/tmk_core/protocol/report.h
// URL:https://github.com/qmk/qmk_firmware/blob/master/tmk_core/protocol/report.h
 
typedef struct {
#ifdef MOUSE_SHARED_EP
    uint8_t report_id;
#endif
    uint8_t buttons;
    int8_t  x;
    int8_t  y;
    int8_t  v;
    int8_t  h;
} __attribute__((packed)) report_mouse_t;

LoRa 無線通訊模組 HC-12

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

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

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

/**
 * @file   main.c
 */
 
#define CFG_0
//#define CFG_1
 
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/usart.h>
 
#define USART (USART2)
 
/* USART2-Tx = PA2 */
#define USART_TX_PORT (GPIOA)
#define USART_TX_PIN (GPIO2)
 
/* User-LED = PB13 */
#define LED_PORT (GPIOB)
#define LED_PIN (GPIO13)
 
/* User-Button = PC13 */
#define BUTTON_PORT (GPIOC)
#define BUTTON_PIN (GPIO13)
 
uint8_t state = 0;
 
void rcc_setup(void)
{
  rcc_periph_clock_enable(RCC_GPIOA);
  rcc_periph_clock_enable(RCC_GPIOB);
  rcc_periph_clock_enable(RCC_GPIOC);
  rcc_periph_clock_enable(RCC_USART2);
}
 
void usart_setup(void)
{
  /* Setup Tx pin. */
  gpio_mode_setup(USART_TX_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, USART_TX_PIN);
  gpio_set_af(USART_TX_PORT, GPIO_AF7, USART_TX_PIN);
 
  /* Setup UART config with 9600, 8-N-1. */
  usart_set_baudrate(USART, 9600);
  usart_set_databits(USART, 8);
  usart_set_stopbits(USART, USART_STOPBITS_1);
  usart_set_parity(USART, USART_PARITY_NONE);
  usart_set_flow_control(USART, USART_FLOWCONTROL_NONE);
  usart_set_mode(USART, USART_MODE_TX);
 
  /* Enable. */
  usart_enable(USART);
}
 
void led_setup(void)
{
  gpio_mode_setup(LED_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, LED_PIN);
  gpio_set_output_options(LED_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, LED_PIN);
}
 
void button_setup(void)
{
  gpio_mode_setup(BUTTON_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, BUTTON_PIN);
}
 
int main(void)
{
  rcc_setup();
  led_setup();
  button_setup();
  usart_setup();
 
  usart_send_blocking(USART, 'O');
  usart_send_blocking(USART, 'K');
#if defined(CFG_0)
  usart_send_blocking(USART, '0');
#elif defined(CFG_1)
  usart_send_blocking(USART, '1');
#else
#error CFG_0 or CFG_1
#endif
  usart_send_blocking(USART, '\r');
  usart_send_blocking(USART, '\n');
 
  while (1)
  {
    if (gpio_get(BUTTON_PORT, BUTTON_PIN) == 0)
    {
      // Pressed.
      gpio_set(LED_PORT, LED_PIN);
 
#if defined(CFG_0)
      usart_send_blocking(USART, 0x01);
#elif defined(CFG_1)
      usart_send_blocking(USART, 0x11);
#else
#error CFG_0 or CFG_1
#endif
state = 1;
    }
    else if(state != 0)
    {
      // Not pressed.
      gpio_clear(LED_PORT, LED_PIN);
#if defined(CFG_0)
      usart_send_blocking(USART, 0x00);
#elif defined(CFG_1)
      usart_send_blocking(USART, 0x10);
#else
#error CFG_0 or CFG_1
#endif
state = 0;
    }
  }
 
  return 0;
}

最終效果如影片所示:

結語

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

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

相關文章


[自製QMK鍵盤-番外] 為QMK鍵盤加上Bluetooth藍牙無線功能
無線分離式人體工學鍵盤Mitosis的介紹與分析

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