⌨️

50%キーボードの製作メモ

2023/02/26に公開

1.はじめに

約50%キーボードを設計・製作したので、特徴や今回覚えたことを書いてみます。
キーボードの名前は「kplj51」です。

作ったもの

  • 一体型の60%キーボードから、数字の行をなくしたサイズのキーボードです。
  • ケースは、アクリルサンドイッチにしました。
  • ProMicroを使用しました。

製作動機

数字の行の9(0)-_=+あたりが少し遠くて押しにくいと感じていたので、前回製作した60%キーボードで数字の行を使わないキーマップを作って試したところ、あまり違和感なく使えたので、製作してみることにしました。

2.設計方針

60%キーボードから数字の行をなくしたサイズにする。横幅は詰めない。

横方向のキー数・キー幅は、現状で不便を感じていないため。
また、一番左と一番右の列のキー幅を詰めようとしても、それにあったスカルプチャードのキーキャップが見つからなかったため。

ProMicroを使う。

PCBに直接実装するスキルがないため。

英語配列にする。

前回製作した60%キーボードで英語配列に慣れたため。
ただし、OS側は日本語キーボード設定を前提とする。

3.キーレイアウト・キーマップ

このようにしました。

_BASEレイヤー

主に文字入力用です。
Pの右は、押す頻度が高い-_=+にしました。
長い右Shiftはスペースがもったいないので、1u削って右側にFn2をつけました。

_NUMレイヤー

数字入力用です。
普通のキーボードの1!から=+のキーを、Tabの行に配置しました。
CapsLock'"をシングルホールドしている間、レイヤーをONにします。離すとOFFです。
また、CapsLockのダブルタップでON継続、シングルタップでOFFにします。
CapsLock'"のダブルホールドは、Shift+シングルホールドと同じにします。(指の移動軽減)
Fn1をシングルタップした場合もOFFにします。

_CURSORレイヤー

vi風カーソル、各種移動、右クリメニュー、よく使うファンクションキー用です。
変換または無変換を押している間、レイヤーをONにします。離すとOFFです。
また、Fn1のダブルタップでON継続、シングルタップでOFFにします。

_FUNCレイヤー

ファンクションキー入力用です。
変換または無変換を押しながら、CapsLockをシングルホールドしている間、レイヤーをONにします。離すとOFFです。
また、変換または無変換を押しながら、CapsLockのダブルタップでON継続、シングルタップでOFFにします。
Fn1をシングルタップした場合もOFFにします。
IME変換の際、F6F10が近くて押しやすいです。(左手でCapsLock無変換を押さえて、右手でY〜を押す)

_FUNC2レイヤー

音量と液晶の明るさなどのキー入力用です。
Fn2をホールドしている間、レイヤーをONにします。離すとOFFです。

4.ProMicroの配置

QキーとAキーの裏側に配置しました。
一部のピンがAキーと干渉するので、その部分のピンは使わず隣にスルーホールを設けて、リード線で配線することにしました。

Type-CのProMicroを使ったのですが、Micro-BのものよりUSB端子側の基板がやや長く、少しはみ出しました。

PCB単体はこんな感じです。

裏側にProMicroを配置する都合上、厚みが出てしまいました。ピンヘッダは少し切って短くします。

プレートはこんな感じです。ボトムは2枚です。

5.タップダンス

50%キーボードはレイヤー切替キーを押す頻度が増えるので、ホールドの押し変えを少し減らす目的で、ところどころタップダンスを使い、ダブルタップでShift同時押しのシングルタップと同じ入力ができるようにしました。
具体的には、`~ ;: '" です。

例えば、"1:00〜"と入力したいときは、このように打つイメージです。

キー入力 入力される文字 備考
'ホールド+Qタップ 1 'ホールドは_NUMレイヤーON
;ダブルタップ : Shift+;でも同じ
CapsLockホールド+P*2回タップ 0 0 CapsLockホールドは_NUMレイヤーON
変換ホールド+Qダブルタップ 変換ホールド+Shift+Qシングルタップでも同じ。変換ホールドは_CURSORレイヤーON

タップダンスは、こちらを参考にしました。
https://docs.qmk.fm/#/ja/feature_tap_dance
https://thomasbaart.nl/category/mechanical-keyboards/firmware/qmk/qmk-basics/

タップダンスの使い方の概要

詳細は上記リンクのとおりですが、概要はこんな感じでした。

rules.mkでタップダンスを有効にする。

rules.mk
TAP_DANCE_ENABLE = yes

keymap.cに必要な型、インスタンス、関数を追加する。

keymap.c
// タップダンス状態
typedef enum {
    TD_NONE,
    TD_UNKNOWN,
    TD_SINGLE_TAP,
    TD_SINGLE_HOLD,
    TD_DOUBLE_TAP,
    TD_DOUBLE_HOLD,
    TD_DOUBLE_SINGLE_TAP,
    TD_TRIPLE_TAP,
    TD_TRIPLE_HOLD
} td_state_t;

// タップダンス状態を入れる型
typedef struct {
    bool is_press_action;
    td_state_t state;
} td_tap_t;

// タップダンス定義用の列挙型
enum {
    TD_XXX
};

// タップダンスの状態判断用の関数
td_state_t cur_dance(qk_tap_dance_state_t *state) {
    // 後述
}
td_state_t cur_dance2(qk_tap_dance_state_t *state) {
    // 後述
}

// タップダンス用のインスタンス
static td_tap_t XXX_tap_state = {
    .is_press_action = true,
    .state = TD_NONE
};

// タップダンス判定時の処理
void XXX_finished(qk_tap_dance_state_t *state, void *user_date) {
    XXX_tap_state.state = cur_dance(state);
    // 処理内容
}

// リセット時の処理
void XXX_reset(qk_tap_dance_state_t *state, void *user_date) {
    // 処理内容
}

// 押下時の処理(必要による)
void XXX_each_tap(qk_tap_dance_state_t *state, void *user_date) {
    // 処理内容
}

// タップダンスのアクションの設定
qk_tap_dance_action_t tap_dance_actions[] = {
    // each_tapがない場合
    [TD_XXX] = ACTION_TAP_DANCE_FN_ADVANCED(NULL, XXX_finished, XXX_reset),
    // each_tapがある場合
    [TD_XXX] = ACTION_TAP_DANCE_FN_ADVANCED(XXX_each_tap, XXX_finished, XXX_reset),
};

// キー別名
#define TDD_XXX TD(TD_XXX)

// キーマップ
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [_BASE] = LAYOUT_1(
        ..., TDD_XXX, KC_YYY, ...
    )
};

割り込みの扱い

タップ時間中(TAPPING_TERM経過前)に別のキーが押されたとき(=割り込みがあったとき)に、タップとして扱いたいキーとホールドとして扱いたいキーがあるので、タップダンスの状態判断用の関数は2種類作りました。

割り込み時はタップ
td_state_t cur_dance(qk_tap_dance_state_t *state) {
    switch (state->count) {
        case 1: {
            if (state->interrupted || !state->pressed) return TD_SINGLE_TAP;
            else return TD_SINGLE_HOLD;
        } break;

        case 2: {
            if (state->interrupted) return TD_DOUBLE_SINGLE_TAP;
            else if (state->pressed) return TD_DOUBLE_HOLD;
            else return TD_DOUBLE_TAP;
        } break;

        case 3: {
            if (state->interrupted || !state->pressed) return TD_TRIPLE_TAP;
            else return TD_TRIPLE_HOLD;
        } break;

        default: return TD_UNKNOWN;
    }
}
割り込み時はホールド
td_state_t cur_dance2(qk_tap_dance_state_t *state) {
    switch (state->count) {
        case 1: {
            if (!state->interrupted && !state->pressed) return TD_SINGLE_TAP;
            else return TD_SINGLE_HOLD;
        } break;

        case 2: {
            if (!state->interrupted && !state->pressed) return TD_DOUBLE_TAP;
            else return TD_DOUBLE_HOLD;
        } break;

        case 3: {
            if (!state->interrupted && !state->pressed) return TD_TRIPLE_TAP;
            else return TD_TRIPLE_HOLD;
        } break;

        default: return TD_UNKNOWN;
    }
}

Shiftキーと同時押し

タップダンスの処理の中では、RSFT(KC_〇〇)のような書き方はできなかったので、register_code(KC_RSFT)tap_code(KC_〇〇)unregister_code(KC_RSFT)のようにしました。

OS日本語設定のまま英語配列のタップダンス

仕事用PCのユーザー権限の都合上、OS側は日本語キーボード設定から変更できないため、前作60%キーボードと同様にkey_overrideを使います。
しかしながら、タップダンスの処理の中からはkey_overrideが使えなかったため、上記のShiftキーと同時押しの要領で書きました。

Shift押下状態の格納

タップダンスの処理の中でget_mod()するのが正しいような気がしたものの、下記のようにShift押下時にその都度変数に格納するようにしました。

keymap.cの最初のほう
// shiftキーが押されているかどうか
static bool b_rsft = false;
static bool b_lsft = false;

// タップダンス状態を入れる型(shiftを気にしない場合)
typedef struct {
    bool is_press_action;
    td_state_t state;
} td_tap_t;

// タップダンス状態を入れる型(shift同時押しも気にする場合)
typedef struct {
    bool is_press_action;
    td_state_t state;
    bool shifted;
} td_tap2_t;
keymap.cの最後のほう
bool process_record_user(uint16_t keycode, keyrecord_t *record){
    // 前略
    switch (keycode) {
        // 中略

        case KC_RSFT: {
            if (record->event.pressed) {
                b_rsft = true;
            } else {
                b_rsft = false;
            }
            return true;
        } break;

        case KC_LSFT: {
            if (record->event.pressed) {
                b_lsft = true;
            } else {
                b_lsft = false;
            }
            return true;
        } break;

        default: {
        } break;
    }
    return true;

作ったタップダンス

Fn1キー

ダブルタップ(のリセットのタイミング)で_CURSORレイヤーをON、それ以外で_BASEレイヤー以外をOFFにします。

void fn_finished(qk_tap_dance_state_t *state, void *user_date) {
    fn_tap_state.state = cur_dance(state);
}

void fn_reset(qk_tap_dance_state_t *state, void *user_date) {
    switch (fn_tap_state.state) {
        case TD_SINGLE_TAP:
        case TD_SINGLE_HOLD:
        case TD_DOUBLE_HOLD:
        case TD_TRIPLE_HOLD: {
            layer_off(_CURSOR);
            layer_off(_NUM);
            layer_off(_FUNC);
            layer_off(_FUNC2);
        } break;

        case TD_DOUBLE_TAP:
        case TD_DOUBLE_SINGLE_TAP:
        case TD_TRIPLE_TAP: {
            layer_on(_CURSOR);
            layer_off(_NUM);
            layer_off(_FUNC);
            layer_off(_FUNC2);
        } break;

        default: break;
    }
    fn_tap_state.state = TD_NONE;
}
LAltキー(RAltも同様)

シングルホールドはただのAlt、ダブルホールドは_NUMレイヤーをONにしてAltにします。
Alt+数字のショートカットを押しやすくるためです。(Gnome-Terminalのタブなど)

void lalt_finished(qk_tap_dance_state_t *state, void *user_date) {
    lalt_tap_state.state = cur_dance2(state);
    switch (lalt_tap_state.state) {
        case TD_SINGLE_TAP:
        case TD_SINGLE_HOLD:
        case TD_DOUBLE_TAP:
        case TD_DOUBLE_SINGLE_TAP:
        case TD_TRIPLE_TAP: register_code(KC_LALT); break;

        case TD_DOUBLE_HOLD:
        case TD_TRIPLE_HOLD: {
            layer_on(_NUM);
            register_code(KC_LALT);
        } break;

        default: break;
    }
}

void lalt_reset(qk_tap_dance_state_t *state, void *user_date) {
    switch (lalt_tap_state.state) {
        case TD_SINGLE_TAP:
        case TD_SINGLE_HOLD:
        case TD_DOUBLE_TAP:
        case TD_DOUBLE_SINGLE_TAP:
        case TD_TRIPLE_TAP: unregister_code(KC_LALT); break;

        case TD_DOUBLE_HOLD:
        case TD_TRIPLE_HOLD: {
            layer_off(_NUM);
            unregister_code(KC_LALT);
        } break;

        default: break;
    }
    lalt_tap_state.state = TD_NONE;
}
CapsLockキー

シングルホールドしている間、_NUMレイヤーをONにします。離すとOFFです。
ダブルタップでON継続、シングルタップでOFFにします。
ダブルホールドは、Shift+シングルホールドと同じにします。
変換または無変換が押されているときは、_NUMレイヤーではなく_FUNCレイヤーになります。
b_mhenb_henkは、無変換と変換の状態を格納しているbool型の変数です。
b_capsは、CapsLockの状態を格納するbool型の変数です。
b_tg_numb_tg_fncは、_NUMレイヤーと_FUNCレイヤーのトグル状態格納用のbool型の変数です。
(同じタップ操作でのトグル式にしなかったので、今のところ無意味)

void caps_finished(qk_tap_dance_state_t *state, void *user_date) {
    caps_tap_state.state = cur_dance2(state);
    switch (caps_tap_state.state) {
        case TD_SINGLE_TAP: {
            layer_off(_NUM);
            layer_off(_FUNC);
            b_tg_num = false;
            b_tg_fnc = false;
            if (b_mhen || b_henk) {
                layer_on(_CURSOR);
            }
        } break;

        case TD_DOUBLE_TAP:
        case TD_TRIPLE_TAP: {
            if (!b_mhen && !b_henk) {
                layer_on(_NUM);
                layer_off(_FUNC);
                b_tg_num = true;
                b_tg_fnc = false;
            } else {
                layer_off(_NUM);
                layer_on(_FUNC);
                b_tg_num = false;
                b_tg_fnc = true;
                layer_on(_CURSOR);
            }
        } break;

        case TD_SINGLE_HOLD: {
            if (!b_mhen && !b_henk) {
                layer_on(_NUM);
                layer_off(_FUNC);
            } else {
                layer_off(_NUM);
                layer_on(_FUNC);
            }
        } break;

        case TD_DOUBLE_HOLD:
        case TD_TRIPLE_HOLD: {
            register_code(KC_RSFT);
            if (!b_mhen && !b_henk) {
                layer_on(_NUM);
                layer_off(_FUNC);
            } else {
                layer_off(_NUM);
                layer_on(_FUNC);
            }
        } break;

        default: break;
    }
}

void caps_reset(qk_tap_dance_state_t *state, void *user_date) {
    switch (caps_tap_state.state) {
        case TD_DOUBLE_TAP:
        case TD_TRIPLE_TAP: {
            unregister_code(KC_RSFT);
        } break;

        case TD_SINGLE_HOLD: {
            layer_off(_NUM);
            layer_off(_FUNC);
            if (b_mhen || b_henk) {
                layer_on(_CURSOR);
            }
        } break;

        case TD_DOUBLE_HOLD:
        case TD_TRIPLE_HOLD: {
            unregister_code(KC_RSFT);
            layer_off(_NUM);
            layer_off(_FUNC);
            if (b_mhen || b_henk) {
                layer_on(_CURSOR);
            }
        } break;

        default: break;
    }
    b_caps = false;
    caps_tap_state.state = TD_NONE;
}

void caps_each_tap(qk_tap_dance_state_t *state, void *user_date) {
    switch (state->count) {
        case 1: {
            b_caps = true;
            if (!b_mhen && !b_henk) {
                layer_on(_NUM);
                layer_off(_FUNC);
            } else {
                layer_off(_NUM);
                layer_on(_FUNC);
            }
        } break;

        case 2: {
            b_caps = true;
            register_code(KC_RSFT);
        } break;

        default: b_caps = true; break;
    }
}
クォーテーション、ダブルクォーテーション

シングルタップは'、ダブルタップとShift+シングルタップは"です。
シングルホールドしている間、_NUMレイヤーをONにします。離すとOFFです。
ダブルホールドは、Shift+シングルホールドと同じにします。

void quot_finished(qk_tap_dance_state_t *state, void *user_date) {
    quot_tap_state.state = cur_dance2(state);
    switch (quot_tap_state.state) {
        case TD_SINGLE_TAP: {
            if (!quot_tap_state.shifted) {
                register_code(KC_LSFT);
                tap_code(KC_7);
                unregister_code(KC_LSFT);
            } else {
                register_code(KC_LSFT);
                tap_code(KC_2);
                if (!b_lsft) unregister_code(KC_LSFT);
            }
        } break;

        case TD_SINGLE_HOLD: layer_on(_NUM); break;

        case TD_DOUBLE_TAP:
        case TD_DOUBLE_SINGLE_TAP:
        case TD_TRIPLE_TAP: {
            register_code(KC_LSFT);
            tap_code(KC_2);
            if (!b_lsft) unregister_code(KC_LSFT);
        } break;

        case TD_DOUBLE_HOLD:
        case TD_TRIPLE_HOLD: {
            layer_on(_NUM);
            register_code(KC_RSFT);
        } break;

        default: break;
    }
}

void quot_reset(qk_tap_dance_state_t *state, void *user_date) {
    switch (quot_tap_state.state) {
        case TD_SINGLE_TAP: {
            layer_off(_NUM);
        } break;
        case TD_DOUBLE_TAP:
        case TD_DOUBLE_SINGLE_TAP:
        case TD_TRIPLE_TAP: {
            layer_off(_NUM);
            unregister_code(KC_RSFT);
        } break;

        case TD_SINGLE_HOLD: layer_off(_NUM); break;

        case TD_DOUBLE_HOLD:
        case TD_TRIPLE_HOLD: {
            layer_off(_NUM);
            unregister_code(KC_RSFT);
        } break;

        default: break;
    }
    quot_tap_state.state = TD_NONE;
    quot_tap_state.shifted = false;
}

void quot_each_tap(qk_tap_dance_state_t *state, void *user_date) {
    switch (state->count) {
        case 1: layer_on(_NUM); break;
        case 2: register_code(KC_RSFT); break;
        default: break;
    }
    quot_tap_state.shifted = (b_rsft || b_lsft);
}
セミコロン、コロン

シングルタップは;、ダブルタップとShift+シングルタップは:です。
(でもなぜかShift+シングルタップが*になってしまうので、調査中。)

void scln_finished(qk_tap_dance_state_t *state, void *user_date) {
    scln_tap_state.state = cur_dance(state);
    switch (scln_tap_state.state) {
        case TD_SINGLE_TAP: {
            if (!scln_tap_state.shifted) {
                tap_code(KC_SCLN);
            } else {
                unregister_code(KC_RSFT);
                unregister_code(KC_LSFT);
                tap_code(KC_QUOT);
                if (b_rsft) register_code(KC_RSFT);
                if (b_lsft) register_code(KC_LSFT);
            }
        } break;

        case TD_DOUBLE_TAP:
        case TD_DOUBLE_SINGLE_TAP:
        case TD_TRIPLE_TAP: {
            tap_code(KC_QUOT);
        } break;

        default: break;
    }
}

void scln_reset(qk_tap_dance_state_t *state, void *user_date) {
    scln_tap_state.state = TD_NONE;
    scln_tap_state.shifted = false;
}

void scln_each_tap(qk_tap_dance_state_t *state, void *user_date) {
    scln_tap_state.shifted = (b_rsft || b_lsft);
}
グレイブ(バッククォート)、チルダ
void grv_finished(qk_tap_dance_state_t *state, void *user_date) {
    grv_tap_state.state = cur_dance(state);
    switch (grv_tap_state.state) {
        case TD_SINGLE_TAP: {
            if (!grv_tap_state.shifted) {
                register_code(KC_LSFT);
                tap_code(KC_LBRC);
                unregister_code(KC_LSFT);
            } else {
                register_code(KC_LSFT);
                tap_code(KC_EQL);
                if (!b_lsft) unregister_code(KC_LSFT);
            }
        } break;

        case TD_DOUBLE_TAP:
        case TD_DOUBLE_SINGLE_TAP:
        case TD_TRIPLE_TAP: {
            register_code(KC_LSFT);
            tap_code(KC_EQL);
            unregister_code(KC_LSFT);
        } break;

        default: break;
    }
}

void grv_reset(qk_tap_dance_state_t *state, void *user_date) {
    grv_tap_state.state = TD_NONE;
    grv_tap_state.shifted = false;
}

void grv_each_tap(qk_tap_dance_state_t *state, void *user_date) {
    grv_tap_state.shifted = (b_rsft || b_lsft);
}
タップダンスのアクションの設定
qk_tap_dance_action_t tap_dance_actions[] = {
    [TD_FN] = ACTION_TAP_DANCE_FN_ADVANCED(NULL, fn_finished, fn_reset),
    [TD_LALT] = ACTION_TAP_DANCE_FN_ADVANCED(NULL, lalt_finished, lalt_reset),
    [TD_RALT] = ACTION_TAP_DANCE_FN_ADVANCED(NULL, ralt_finished, ralt_reset),
    [TD_CAPS] = ACTION_TAP_DANCE_FN_ADVANCED(caps_each_tap, caps_finished, caps_reset),
    [TD_QUOT] = ACTION_TAP_DANCE_FN_ADVANCED(quot_each_tap, quot_finished, quot_reset),
    [TD_SCLN] = ACTION_TAP_DANCE_FN_ADVANCED(scln_each_tap, scln_finished, scln_reset),
    [TD_GRV] = ACTION_TAP_DANCE_FN_ADVANCED(grv_each_tap, grv_finished, grv_reset),
};
キーの別名
#define TDD_FN TD(TD_FN)
#define TDD_LALT TD(TD_LALT)
#define TDD_RALT TD(TD_RALT)
#define TDD_CAPS TD(TD_CAPS)
#define TDD_QUOT TD(TD_QUOT)
#define TDD_SCLN TD(TD_SCLN)
#define TDD_GRV TD(TD_GRV)
#define MO_FUNC2 MO(_FUNC2)

6.変換、無変換

変換または無変換を押している間、_CURSORレイヤーをONにします。離すとOFFにします。
変換と無変換のラップした押し変えは、押しっぱなしとして扱います。
シングルタップは、普通の変換、無変換として扱います。
CapsLockと同時押しでの_FUNCレイヤーONは、CapsLockのあとに変換または無変換が押されても機能するようにします。

static uint16_t pressed_henk_time = 0;
static uint16_t pressed_mhen_time = 0;

static uint16_t mem_keycode = 0;

bool process_record_user(uint16_t keycode, keyrecord_t *record){
    uint16_t prev_keycode = mem_keycode;
    bool is_tapped = ((!record->event.pressed) && (keycode == prev_keycode));
    mem_keycode = keycode;

    switch (keycode) {
        case JP_MHEN: {
            if (record->event.pressed) {
                b_mhen = true;
                pressed_mhen_time = record->event.time;
                if (!b_caps) {
                    layer_on(_CURSOR);
                } else {
                    layer_on(_FUNC);
                }
            }
            else {
                b_mhen = false;
                if (!b_henk) {
                    layer_off(_CURSOR);
                    if (b_caps) {
                        layer_on(_NUM);
                    }
                }
                if (is_tapped && (TIMER_DIFF_16(record->event.time, pressed_mhen_time) <= TAPPING_TERM)) {
                    tap_code(keycode);
                }
            }
            return false;
        } break;

        case JP_HENK: {
            if (record->event.pressed) {
                b_henk = true;
                pressed_henk_time = record->event.time;
                if (!b_caps) {
                    layer_on(_CURSOR);
                } else {
                    layer_on(_FUNC);
                }
            }
            else {
                b_henk = false;
                if (!b_mhen) {
                    layer_off(_CURSOR);
                    if (b_caps) {
                        layer_on(_NUM);
                    }
                }
                if (is_tapped && (TIMER_DIFF_16(record->event.time, pressed_henk_time) <= TAPPING_TERM)) {
                    tap_code(keycode);
                }
            }
            return false;
        } break;

        case KC_RSFT: {
            // 前述のとおり
        } break;

        case KC_LSFT: {
            // 前述のとおり
        } break;

        default: {
        } break;
    }
    return true;
}

7.おわりに

次に製作するときは、Atmega32u4をPCBに直接実装し、薄くできるようにしたいなと思いました。
また、プレートの寸法が15u×4uサイズ(キーキャップと同じ幅・奥行き)だと、アクリルの強度の限界のようだったので、金属プレートにしたいです。

Discussion