無線分離式人體工學鍵盤Mitosis的介紹與分析

Posted on 2022-01-09

Mitosis 是一款使用 QMK 作爲韌體所開發的無線分離式鍵盤,它不僅僅是與電腦之間無線,它的左右兩部分之間也沒有實體連線,可謂是「真 • 無線」。就我所知,有許多基於 QMK 的無線分離式鍵盤都是受到 Mitosis 的啓發。

本文將會概略性地介紹 Mitosis 是如何做到無線的。

硬體與基本架構

首先,Mitosis 是擁有並需要自製的專用接收器,而 QMK 實際上只在此接收器上運作。

Mitosis 的架構中,主要擁有這些硬體:

  • 1 個 Pro Micro(ATmega32U4)。接收器的一部分,QMK 實際上只在 Pro Micro 上運作,以 USB 線連接電腦。
  • 3 個 nRF51822。這是一個整合了 BLE(Bluetooth Low Energy,藍牙低功耗)等無線功能的 SoC(System On Chip)。
    • 第 1 個 nRF51822 作爲接收器的一部分,負責接收來自左右兩部分鍵盤的訊號,並將其透過 UART 傳給 Pro Micro。
    • 第 2、3 個 nRF51822 分別在左右兩鍵盤上,負責讀取鍵盤上的按鍵狀態,並將其透過 Gazell 傳給接收器的 nRF51822。
 1             PC
 2              |
 3            <USB>
 4              |
 5        Pro Micro(QMK)
 6              |
 7           <UART>
 8              |
 9         nRF51822(#1)
10          /        \
11     <Gazell>      <Gazell>
12        /              \
13  nRF51822(#2)        nRF51822(#3)
14      |                   |
15 Left Keyboard      Right Keyboard

可以看出,Mitosis 的架構其實很簡單。雖然這樣的架構要用上更多的 IC,以導致它感覺起來不夠精簡,但這也其容易達成、理解或修改。

總的來說,左右鍵盤上的 nRF51822 會處理各自的按鍵狀態,並各自將其透過 Gazell 傳輸給接收器上的 nRF51822,接收器受到新的按鍵狀態後,會將左右部分的按鍵狀態組合在一起,並透過 UART 傳給 Pro Micro,Pro Micro 收到來自 UART 的封包後就解析按鍵狀態,並交由 QMK 處理。

程式

從基本架構可以得知,Mitosis 總共有 4 個 MCU(1 個 Pro Micro 的 ATmega32U4,3 個 nRF51822),而它們執行的程式當然也不一樣,以下就一一介紹不同部分的程式。

左右手鍵盤(nRF51822)

首先,這部分的程式在:reversebias/mitosis/mitosis-keyboard-basic/。主要有:

  • main.c 是主程式。
  • config/mitosis.h 是包含了腳位設定的標頭檔。

左右手鍵盤上 nRF51822 的程式是同一個,僅透過 #define COMPILE_RIGHT#define COMPILE_LEFT 來切換不同的腳位設定和通道編號(Pipe number)而已。

在這裡有幾個重要的函數(僅列出函數名稱):

  • read_keys()
  • send_data()
  • handler_maintenance()
  • handler_debounce()

handler_debounce()

先看到 handler_debounce() 這個函數,它負責處理按鍵防彈跳(Debounce)。內容如下:

 1// 1000Hz debounce sampling
 2static void handler_debounce(nrf_drv_rtc_int_type_t int_type)
 3{
 4    // debouncing, waits until there have been no transitions in 5ms (assuming five 1ms ticks)
 5    if (debouncing)
 6    {
 7        // if debouncing, check if current keystates equal to the snapshot
 8        if (keys_snapshot == read_keys())
 9        {
10            // DEBOUNCE ticks of stable sampling needed before sending data
11            debounce_ticks++;
12            if (debounce_ticks == DEBOUNCE)
13            {
14                keys = keys_snapshot;
15                send_data();
16            }
17        }
18        else
19        {
20            // if keys change, start period again
21            debouncing = false;
22        }
23    }
24    else
25    {
26        // if the keystate is different from the last data
27        // sent to the receiver, start debouncing
28        if (keys != read_keys())
29        {
30            keys_snapshot = read_keys();
31            debouncing = true;
32            debounce_ticks = 0;
33        }
34    }
35
36    // looking for 500 ticks of no keys pressed, to go back to deep sleep
37    if (read_keys() == 0)
38    {
39        activity_ticks++;
40        if (activity_ticks > ACTIVITY)
41        {
42            nrf_drv_rtc_disable(&rtc_maint);
43            nrf_drv_rtc_disable(&rtc_deb);
44        }
45    }
46    else
47    {
48        activity_ticks = 0;
49    }
50}

handler_debounce() 每秒會觸發 1000 次(也就是以 1000 Hz運作,RTC1 處理)。

它會先判斷目前是否在防彈跳中(if (debouncing)),如果沒有的話會去判斷目前的按鍵狀態是否和最後一次一樣,如果不一樣代表有按鍵按下或放開了,透過 read_keys() 讀取目前的按鍵狀態,並儲存爲快照 keys_snapshot,同時開始防彈跳(將 deboducing 設爲 true)。

一旦開始防彈跳,它就會一直確認快照與目前的按鍵狀態是否一樣,一旦不一樣就停止防彈跳,若累計達到設定的防彈跳次數就會承認快照的按鍵狀態,並將快照的值給目前的鍵值 keys,並呼叫 send_data() 開始傳送。

handler_maintenance()

1// 8Hz held key maintenance, keeping the reciever keystates valid
2static void handler_maintenance(nrf_drv_rtc_int_type_t int_type)
3{
4    send_data();
5}

此函數的功能顯而易見,就是以 8 Hz 的頻率次數呼叫 send_data() 傳送資料。此函數由 RTC0 處理

send_data()

 1// Assemble packet and send to receiver
 2static void send_data(void)
 3{
 4    data_payload[0] = ((keys & 1<<S01) ? 1:0) << 7 | \
 5                      ((keys & 1<<S02) ? 1:0) << 6 | \
 6                      ((keys & 1<<S03) ? 1:0) << 5 | \
 7                      ((keys & 1<<S04) ? 1:0) << 4 | \
 8                      ((keys & 1<<S05) ? 1:0) << 3 | \
 9                      ((keys & 1<<S06) ? 1:0) << 2 | \
10                      ((keys & 1<<S07) ? 1:0) << 1 | \
11                      ((keys & 1<<S08) ? 1:0) << 0;
12
13    data_payload[1] = ((keys & 1<<S09) ? 1:0) << 7 | \
14                      ((keys & 1<<S10) ? 1:0) << 6 | \
15                      ((keys & 1<<S11) ? 1:0) << 5 | \
16                      ((keys & 1<<S12) ? 1:0) << 4 | \
17                      ((keys & 1<<S13) ? 1:0) << 3 | \
18                      ((keys & 1<<S14) ? 1:0) << 2 | \
19                      ((keys & 1<<S15) ? 1:0) << 1 | \
20                      ((keys & 1<<S16) ? 1:0) << 0;
21
22    data_payload[2] = ((keys & 1<<S17) ? 1:0) << 7 | \
23                      ((keys & 1<<S18) ? 1:0) << 6 | \
24                      ((keys & 1<<S19) ? 1:0) << 5 | \
25                      ((keys & 1<<S20) ? 1:0) << 4 | \
26                      ((keys & 1<<S21) ? 1:0) << 3 | \
27                      ((keys & 1<<S22) ? 1:0) << 2 | \
28                      ((keys & 1<<S23) ? 1:0) << 1 | \
29                      0 << 0;
30
31    nrf_gzll_add_packet_to_tx_fifo(PIPE_NUMBER, data_payload, TX_PAYLOAD_LENGTH);
32}

此函數就是將目前的鍵值 keys 打包成資料封包並傳輸出去。keys 的值會在 handler_debounce() 中更新。

PIPE_NUMBER 的值左右鍵盤不同(在 mitosis.h 中定義),接收器藉此判斷收到的資料是來自左還是右鍵盤。

read_keys()

1// Return the key states, masked with valid key pins
2static uint32_t read_keys(void)
3{
4    return ~NRF_GPIO->IN & INPUT_MASK;
5}

此函數的功能也是很直觀,就是讀取並回傳所有的按鍵狀態。

從這裡也可以得知,Mitosis 是不用矩陣掃描(Matrix scan)的,畢竟它的按鍵數本來就比較少(左右各 23 鍵),又是分離式的鍵盤,一個 nRF51822 的 GPIO 足以分配到每個按鍵上,自然不用掃描,直接讀值就好。

接收器(nRF51822)

這部分的程式在:reversebias/mitosis/mitosis-receiver-basic/。主要有:

  • main.c 是主程式。

其中有幾個重要的函數(僅列出函數名稱):

  • nrf_gzll_host_rx_data_ready()
  • main()

nrf_gzll_host_rx_data_ready()

 1// If a data packet was received, identify half, and throw flag
 2void nrf_gzll_host_rx_data_ready(uint32_t pipe, nrf_gzll_host_rx_info_t rx_info)
 3{   
 4    uint32_t data_payload_length = NRF_GZLL_CONST_MAX_PAYLOAD_LENGTH;
 5    
 6    if (pipe == 0)
 7    {
 8        packet_received_left = true;
 9        left_active = 0;
10        // Pop packet and write first byte of the payload to the GPIO port.
11        nrf_gzll_fetch_packet_from_rx_fifo(pipe, data_payload_left, &data_payload_length);
12    }
13    else if (pipe == 1)
14    {
15        packet_received_right = true;
16        right_active = 0;
17        // Pop packet and write first byte of the payload to the GPIO port.
18        nrf_gzll_fetch_packet_from_rx_fifo(pipe, data_payload_right, &data_payload_length);
19    }
20    
21    // not sure if required, I guess if enough packets are missed during blocking uart
22    nrf_gzll_flush_rx_fifo(pipe);
23
24    //load ACK payload into TX queue
25    ack_payload[0] =  0x55;
26    nrf_gzll_add_packet_to_tx_fifo(pipe, ack_payload, TX_PAYLOAD_LENGTH);
27}

這是接收處理函數。當接收到資料時,以 pipe 判斷這是來自左還是右鍵盤,並設定好資料。

main()

以下省略一些不重要的程式:

 1int main(void)
 2{
 3    /* 省略部分程式 */
 4	
 5    // main loop
 6    while (true)
 7    {
 8        // detecting received packet from interupt, and unpacking
 9        if (packet_received_left)
10        {
11            packet_received_left = false;
12
13            data_buffer[0] = ((data_payload_left[0] & 1<<3) ? 1:0) << 0 |
14                             ((data_payload_left[0] & 1<<4) ? 1:0) << 1 |
15                             ((data_payload_left[0] & 1<<5) ? 1:0) << 2 |
16                             ((data_payload_left[0] & 1<<6) ? 1:0) << 3 |
17                             ((data_payload_left[0] & 1<<7) ? 1:0) << 4;
18
19            data_buffer[2] = ((data_payload_left[1] & 1<<6) ? 1:0) << 0 |
20                             ((data_payload_left[1] & 1<<7) ? 1:0) << 1 |
21                             ((data_payload_left[0] & 1<<0) ? 1:0) << 2 |
22                             ((data_payload_left[0] & 1<<1) ? 1:0) << 3 |
23                             ((data_payload_left[0] & 1<<2) ? 1:0) << 4;
24
25            data_buffer[4] = ((data_payload_left[1] & 1<<1) ? 1:0) << 0 |
26                             ((data_payload_left[1] & 1<<2) ? 1:0) << 1 |
27                             ((data_payload_left[1] & 1<<3) ? 1:0) << 2 |
28                             ((data_payload_left[1] & 1<<4) ? 1:0) << 3 |
29                             ((data_payload_left[1] & 1<<5) ? 1:0) << 4;
30
31            data_buffer[6] = ((data_payload_left[2] & 1<<5) ? 1:0) << 1 |
32                             ((data_payload_left[2] & 1<<6) ? 1:0) << 2 |
33                             ((data_payload_left[2] & 1<<7) ? 1:0) << 3 |
34                             ((data_payload_left[1] & 1<<0) ? 1:0) << 4;
35
36            data_buffer[8] = ((data_payload_left[2] & 1<<1) ? 1:0) << 1 |
37                             ((data_payload_left[2] & 1<<2) ? 1:0) << 2 |
38                             ((data_payload_left[2] & 1<<3) ? 1:0) << 3 |
39                             ((data_payload_left[2] & 1<<4) ? 1:0) << 4;
40        }
41
42        if (packet_received_right)
43        {
44            packet_received_right = false;
45            
46            data_buffer[1] = ((data_payload_right[0] & 1<<7) ? 1:0) << 0 |
47                             ((data_payload_right[0] & 1<<6) ? 1:0) << 1 |
48                             ((data_payload_right[0] & 1<<5) ? 1:0) << 2 |
49                             ((data_payload_right[0] & 1<<4) ? 1:0) << 3 |
50                             ((data_payload_right[0] & 1<<3) ? 1:0) << 4;
51
52            data_buffer[3] = ((data_payload_right[0] & 1<<2) ? 1:0) << 0 |
53                             ((data_payload_right[0] & 1<<1) ? 1:0) << 1 |
54                             ((data_payload_right[0] & 1<<0) ? 1:0) << 2 |
55                             ((data_payload_right[1] & 1<<7) ? 1:0) << 3 |
56                             ((data_payload_right[1] & 1<<6) ? 1:0) << 4;
57
58            data_buffer[5] = ((data_payload_right[1] & 1<<5) ? 1:0) << 0 |
59                             ((data_payload_right[1] & 1<<4) ? 1:0) << 1 |
60                             ((data_payload_right[1] & 1<<3) ? 1:0) << 2 |
61                             ((data_payload_right[1] & 1<<2) ? 1:0) << 3 |
62                             ((data_payload_right[1] & 1<<1) ? 1:0) << 4;
63
64            data_buffer[7] = ((data_payload_right[1] & 1<<0) ? 1:0) << 0 |
65                             ((data_payload_right[2] & 1<<7) ? 1:0) << 1 |
66                             ((data_payload_right[2] & 1<<6) ? 1:0) << 2 |
67                             ((data_payload_right[2] & 1<<5) ? 1:0) << 3;
68
69            data_buffer[9] = ((data_payload_right[2] & 1<<4) ? 1:0) << 0 |
70                             ((data_payload_right[2] & 1<<3) ? 1:0) << 1 |
71                             ((data_payload_right[2] & 1<<2) ? 1:0) << 2 |
72                             ((data_payload_right[2] & 1<<1) ? 1:0) << 3;
73        }
74
75        // checking for a poll request from QMK
76        if (app_uart_get(&c) == NRF_SUCCESS && c == 's')
77        {
78            // sending data to QMK, and an end byte
79            nrf_drv_uart_tx(data_buffer,10);
80            app_uart_put(0xE0);
81
82            /* 省略部分程式 */
83        }
84        // allowing UART buffers to clear
85        nrf_delay_us(10);
86        
87        /* 省略部分程式 */
88    }
89}

這裡就是來負責將 Gazell 接收到的左右鍵盤按鍵狀態重新打包,只要確認了來自 QMK 的輪詢請求(s),就透過 UART 傳送出去。

傳給 QMK 的封包除了按鍵狀態外,還有一個 0xE0 作爲結束封包。

QMK / 接收器(Pro Micro)

這部分的程式在:qmk/qmk_firmware/keyboards/mitosis。主要有:

  • rules.mk
  • config.h
  • matrix.c

rules.mk

 1# MCU name
 2MCU = atmega32u4
 3
 4# Bootloader selection
 5BOOTLOADER = caterina
 6
 7# Build Options
 8#    change yes to no to disable
 9#
10BOOTMAGIC_ENABLE = no  # Enable Bootmagic Lite
11MOUSEKEY_ENABLE = yes  # Mouse keys
12EXTRAKEY_ENABLE = yes  # Audio control and System control
13CONSOLE_ENABLE = yes   # Console for debug
14COMMAND_ENABLE = yes   # Commands for debug and configuration
15CUSTOM_MATRIX = yes    # Remote matrix from the wireless bridge
16NKRO_ENABLE = yes      # Enable N-Key Rollover
17# BACKLIGHT_ENABLE = yes  # Enable keyboard backlight functionality
18UNICODE_ENABLE = yes   # Unicode
19
20# # project specific files
21SRC += matrix.c serial_uart.c

這裡可以注意到作者使用了 QMK 的「Custom Matrix」功能 (CUSTOM_MATRIX = yesSRC += matrix.c),因爲 Mitosis 不像一般的鍵盤透過矩陣掃描得知按鍵狀態,而是讀取來自 nRF51822 透過 UART 傳送的封包。

config.h

config.h 主要是設定 QMK 中的各種東西,稍微熟悉 QMK 的人都不陌生。這裡僅列出重要的地方,也就是 UART 的相關設定:

1//UART settings for communication with the RF microcontroller
2#define SERIAL_UART_BAUD 1000000
3#define SERIAL_UART_RXD_PRESENT (UCSR1A & _BV(RXC1))
4#define SERIAL_UART_INIT_CUSTOM       \
5    /* enable TX and RX */            \
6    UCSR1B = _BV(TXEN1) | _BV(RXEN1); \
7    /* 8-bit data */                  \
8    UCSR1C = _BV(UCSZ11) | _BV(UCSZ10);

matrix.c

matrix.c 是爲了使用 QMK 的「Custom Matrix」功能所必要的檔案。

重點在 matrix_scan()

 1uint8_t matrix_scan(void)
 2{
 3    uint32_t timeout = 0;
 4
 5    //the s character requests the RF slave to send the matrix
 6    SERIAL_UART_DATA = 's';
 7
 8    //trust the external keystates entirely, erase the last data
 9    uint8_t uart_data[11] = {0};
10
11    //there are 10 bytes corresponding to 10 columns, and an end byte
12    for (uint8_t i = 0; i < 11; i++) {
13        //wait for the serial data, timeout if it's been too long
14        //this only happened in testing with a loose wire, but does no
15        //harm to leave it in here
16        while(!SERIAL_UART_RXD_PRESENT){
17            timeout++;
18            if (timeout > 10000){
19                break;
20            }
21        }
22        uart_data[i] = SERIAL_UART_DATA;
23    }
24
25    //check for the end packet, the key state bytes use the LSBs, so 0xE0
26    //will only show up here if the correct bytes were recieved
27    if (uart_data[10] == 0xE0)
28    {
29        //shifting and transferring the keystates to the QMK matrix variable
30        for (uint8_t i = 0; i < MATRIX_ROWS; i++) {
31            matrix[i] = (uint16_t) uart_data[i*2] | (uint16_t) uart_data[i*2+1] << 5;
32        }
33    }
34
35
36    matrix_scan_quantum();
37    return 1;
38}

首先此函數會傳送一個 s 以請求 nRF51822 開始傳送按鍵狀態封包。

接著,一個 for 迴圈會處理來自 UART 的按鍵狀態封包。當接收完成後,判斷結束封包是否正確(爲 0xE0),如果沒問題的話就將按鍵狀態封包處理並賦值給 matrix[],接下來就是讓 QMK 去處理了。

結語

本次簡單地介紹 Mitosis 鍵盤是如和達成無線的,但我其實沒用過 nRF51822,對 QMK 的瞭解也還很粗淺,很多細節沒辦法講解,而如果上述內容有任何錯誤也請指正。

撰寫本文時的 Mitosis 相關 repo 資訊:

文章修改記錄 2022/02/23:原本寫的各個 nRF51822 之間的通訊方式是 BLE,但應該是 Gazell,故更新內容。

相關文章



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

comments powered by Disqus