50%キーボードの製作メモ
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変換の際、F6
〜F10
が近くて押しやすいです。(左手で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 |
タップダンスは、こちらを参考にしました。
タップダンスの使い方の概要
詳細は上記リンクのとおりですが、概要はこんな感じでした。
rules.mkでタップダンスを有効にする。
TAP_DANCE_ENABLE = yes
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押下時にその都度変数に格納するようにしました。
// 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;
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_mhen
とb_henk
は、無変換と変換の状態を格納しているbool型の変数です。
b_caps
は、CapsLockの状態を格納するbool型の変数です。
b_tg_num
とb_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