ErgoDoxにLCDをつけてみた話

こんにちは!PIC MANです。
今回はErgoDoxにLCDをつけてみたのでまとめてみました。
(この記事は以前PIC MANが所属団体で書いた記事を加筆修正したものです)

1.そもそもErgoDoxって何?

ErgoDoxはオープンソースエルゴノミクスキーボードです。
キーボードの基板データからファームウェアまで全部公開されています。キーの押し心地やキーキャップの色を選んだり、キー配列も自由に変えることができます。CapsLockのないキーボードも作れます。
公式サイトはこちら
いやーかっこいいですよね左右分離型のキーボード。親指周りのキーが充実していて良さげです。
体格の良い方には快適だ、などと言った効果もあるようです。さすがエルゴノミクスキーボード。個人的にErgoDoxの魅力はオープンソースであることだと思います。ものづくりが好きな方には思う存分カスタマイズができます。
さてこのキーボード、今年度の頭頃に知り合いの先輩から基板を複数発注すると聞き、1セット買わせていただきました。
それ以外のパーツ(キースイッチやキャップ)は同先輩や友人に協力をいただき揃えることができました。ありがとうございます。
そして出来上がったキーボードがこちらになります。

キーキャップは無刻印で、中華茶軸を使っています。
https://www.gearbest.com/keyboards/pp_367442.html (ディスコンになってしまったようです)
外装は特に何も用意してません。基板むき出しのほうがかっこよくない?え?そんなことはない?

一応滑り止めのため、裏面にはラバーの足を複数くっつけています。流石に裏面はショートを防ぐためになにかでカバーしたほうがいい気もします…
費用的にはこんな感じです。

さて、ErgoDoxは通常使われるキーボードよりもキー数が少なく、どうやってすべてのキーを割り当てるの?と思う方もいるかも知れません。ErgoDoxの公式のファームウェア(qmk firmwareを利用してます)では最大で32のレイヤに切り替えて使うことができます。つまり、一つのキーに複数のキーを割り当て、それをレイヤによって切り替えることができるのです。
ちなみにぼくのかんがえたさいきょうのれいあうとはこんなかんじです。

これがデフォルトのレイヤです。左手だけでコピペ、BS, Enterが押せるので便利です。
右下のwinキーはwin+Lでロックするためだけに置いてます。この記事書いてるときに思ったのですがマクロで組んで別のところに置けばいいじゃん…_(:3」∠)_
レイヤ1ではファンクションキーを、レイヤ2ではマウスとメディア操作、レイヤ3はテンキーとなっています。
利用環境はwindowsで英字キー設定にしてます。

2.レイヤ切り替えの問題点

レイヤを切り替えることができるので一つのキーを複数の機能に割り当てることができますが、このままでは現在どのレイヤに居るのかちょっとわかりにくいです。
実は右手側キーボードの中心部分にLEDを3つほどつけることができ、各レイヤによって点灯パターンを切り替えることもできるのですがあんまりパッとしませんでした。

↑とりあえず一個だけつけてみた時の写真です。
そんなこんなで「レイヤ情報が表示されるような仕組みを作りたいな」ということでLCDを載せようと決意しました。

3 本題:LCDを載せて現在のレイヤを表示したい

まずは載せるLCDの選定です。
レイヤ情報を載せるだけと考えれば、キャラクタ液晶で事足りそうです。無駄にグラフィック液晶を載せたりして処理が重くなりキー入力すら快適に受け付けなくなったりすると困るので。
ここで,ErgoDoxでは各キーの入力を左側キーボードではIOエキスパンダが、右側キーボードはteensy(メインのマイコン)が受け取っています。
このIOエキスパンダはteensyとI2Cという通信方式を用いて情報のやり取りを行っております。
I2Cという通信方式は複数の機器をパラレルに接続し制御することが可能なのでこれを利用しようと思います。
よってI2C制御可能なキャラクタ液晶に的を絞ります。
…………
みんな大好き秋月電子さんで検索した結果、こちらを使うことにしました。

I2C接続小型キャラクタLCDモジュール(16x2行・3.3V/5V)ピッチ変換キット

価格も手頃でとてもコンパクトです。デバイスアドレスもIOエキスパンダ(MCP23018)とかぶっていないようなので安心です。
はんだ付けした変換基板の向きが逆だったけどまあいっか

4. LCDの接続

LCDの接続方法です。
先に書いたようにI2C接続なので電源(5V, GND)とSDA, SCLを並列につなぐだけです。
ぼくはこんな感じで左側キーボードの部分からつなぎました。

5. ファームウェアの書き換え

まずは自前のPCでファームウェアをビルドする環境を整えます。
windowsからでもWSLを使えばかんたんにビルドできます。ココらへんを参考にどうぞ。
環境が整ったら、qmk_firmware\keyboards\ergodox\keymapsにあるdefaultフォルダを中身ごとコピーし、フォルダ名を「LCD」などとお好きなように変えて置きましょう。
このフォルダの中にある「keymap.c」が主にソースを書き換えるファイルになります。
またこのkeymap.cはErgoDoxのGUIキーマップ作成ツールから落としてきたソースコードに置き換えることもできます。

5.1 ファームウェア:LCDの初期化処理

さてまずLCDの初期化処理の記述をしましょう。φ(‘ᴗ’」)
先程keymap.cが主にいじるファイルと言いましたが、このファイルでは初期化処理を終え、キー入直判定のループ処理の一部が記述されています。よって一回だけの処理をkeymap.cにフラグとか立てて書くより、IOエキスパンダの初期化処理の部分を探しそこに付け加えたいと思います。
そのファイルは qmk_firmware\keyboards\ergodox\ez\ez.c です。
このファイル中にinit_mcp23018()という関数があるのでそこにお邪魔します。
大体90行目くらいにout:というラベル(!)があり、直下でi2c_stop();という関数が呼ばれています。直前のIOエキスパンダの初期化処理中にエラーが帰ってきた場合、ここ(out:)へジャンプするようになっているようです。久しぶりに見ましたgoto文。
この次にLCDの初期化処理を書きましょう。これに関しては秋月電子さんの参考資料を元に作成しました。

ここで気をつけたいのはPower/ICONcontrol/Contrast setレジスタへの書き込みです。今回LCDは5V電源を利用していますのでBonビットは0にする必要があります。よってここで書き込む値は0x52となります。

// 頭の方に定義
// LCD
#define LCD_ADDRESS 0b0111110
#define LCD_ADDRESS_WRITE ( (LCD_ADDRESS<<1) | 0 )

// ~~~~~~中略~~~~~~

/***************************
LCDコマンドデータ関数
t_data : 送信データ
***************************/
void writeCommand(unsigned char t_data)
{
    i2c_start(LCD_ADDRESS_WRITE);
    i2c_write(0x00);
    i2c_write(t_data);
    i2c_stop();
    // 初期化時多めにdelay入れないとうまく表示されなかった
    _delay_ms(1);
}

uint8_t init_mcp23018(void)
{

    // ~~~~~~中略~~~~~~

    out:

    i2c_stop();

    // SREG=sreg_prev;
    // こんなところにびっくりLCDの初期化処理入れちゃうよ
    _delay_ms(40);
    writeCommand(0x38);
    writeCommand(0x39);
    writeCommand(0x14);
    writeCommand(0x73);
    writeCommand(0x52);
    writeCommand(0x6C);
    _delay_ms(200);
    writeCommand(0x38);
    writeCommand(0x01);
    writeCommand(0x0C);
    
    // とりあえずDefaultって出しとく
    const char text_layer[16] = "Layer:Default ";
    i2c_start(LCD_ADDRESS_WRITE);
    i2c_write(0x40);
    for(int i = 0; i<16; i++) i2c_write(text_layer[i]);
    i2c_stop();
    _delay_ms(1);
    return mcp23018_status;
}

こんな感じで書きました。I2CのライブラリはIOエキスパンダで使用してるので、そちらを流用しました。
ここまで書き換えられるとビルド(と言うよりmake keymap=LCD ※LCDはコピーして名前を変えたフォルダ名)してhexファイルを作り、teensyに書き込めばLCDに”Layer:Default”と表示されるかと思います。

5.2 ファームウェア:レイヤ切替時の処理

ここからがメインになります。
ErgoDoxの各キースイッチの状態をスキャンするごとにkeymap.cのmatrix_scan_user()関数が呼ばれているようです。
この関数内ではデフォルトでレイヤに応じてLEDの点灯パターンを変えていることがわかります。
というわけでmatrix_scan_user()関数をこのように書き換えました。

 void matrix_scan_user(void) {

    static uint8_t old_layer = 0;
    uint8_t layer = biton32(layer_state);

    if(old_layer != layer){

        lcd_update(layer);
        ergodox_board_led_off();
        ergodox_right_led_1_off();
        ergodox_right_led_2_off();
        ergodox_right_led_3_off();

        switch (layer) {
            case 1:
                ergodox_right_led_1_on();
                break;

            case 2:
                ergodox_right_led_2_on();
                break;
            
            case 3:
                ergodox_right_led_3_on();
                break;
            
            case 4:
                ergodox_right_led_1_on();
                ergodox_right_led_2_on();
                break;
            
            case 5:
                ergodox_right_led_1_on();
                ergodox_right_led_3_on();
                break;
            
            case 6:
                ergodox_right_led_2_on();
                ergodox_right_led_3_on();
                break;
            
            case 7:
                ergodox_right_led_1_on();
                ergodox_right_led_2_on();
                ergodox_right_led_3_on();
                break;

            default:
                break;
        }
    }
    old_layer = layer;
}

lcd_update()関数でLCDの表示している情報の更新を行います。(後述)
またold_layer変数を追加しています。これはレイヤの変更があった場合のみLEDやLCDの更新を行うためです。なぜこうするかというと,キーマトリックスのスキャン毎にLCDの更新を行うと,LCDへのI2C通信に長い時間が掛かるのでキー入力がもたつくように感じられたからです.
LEDを取り付けていないのならばswitch文周りは消しちゃってもいいでしょう。
肝心なlcd_update()関数は次のようになっています。

// keymap.cの頭の方に書いてね
// I2C関連
#include "ez/i2cmaster.h"
#define LCD_ADDRESS 0b0111110
#define LCD_ADDRESS_WRITE ( (LCD_ADDRESS<<1) | I2C_WRITE )

// プロトタイプ
void lcd_contrast(bool);
void lcd_set_cursor(uint8_t, uint8_t);
void lcd_update(uint8_t);
// ここまで頭の方に書いてね

/***************************
カーソル位置合わせ関数
raw : 行(1, 2)
col : 列(1..16)
***************************/
void lcd_set_cursor(uint8_t row, uint8_t col) {

    if(row <= 2 && col <= 16){
        // LCD文字アドレス
        uint8_t addr = 0x80 + col -1;
        // 二行目の場合先頭アドレスは0x40からなので足す
        if(row == 2 ) addr += 0x40;
        // 書き込み
        i2c_start(LCD_ADDRESS_WRITE);
        i2c_write(0x00);
        i2c_write(addr);
        i2c_stop();
        _delay_ms(10);
    }
}

/***************************
液晶更新関数
layer : レイヤ番号(0..3)
***************************/
void lcd_update(uint8_t layer_num) {

    // 表示文字の配列
    const char L2_text[5][10] = {   "Default ",
                                    "Function ",
                                    "Mouse ",
                                    "Numeric ",
                                    "Other "
                                };

    // 一行目の七文字めにカーソルをあわせる
    lcd_set_cursor(1, 7);
    i2c_start(LCD_ADDRESS_WRITE);
    i2c_write(0x40);
    if(3 < layer_num) layer_num = 4;
    for(int i = 0; i<10; i++) i2c_write(L2_text[layer_num][i]);
    i2c_stop();
    _delay_ms(1);
}

lcd_update()では引数に現在のレイヤ番号を取っています。
はじめに液晶のカーソル(n文字目の字を選択し書き換えるよ、ということ)を合わせるための関数lcd_set_cursor()を読んでいます。lcd_set_cursor(1, 7)はつまり一行目の7文字目、初期化処理で表示させた”Layer:Default”のDに合わせています。ここから文字を書き換えれば毎回”Layer:”と表示する分のデータを通信する必要がなくなりLCDの更新にかかる時間が減らせますね.

カーソルを合わせたら、初期化処理のときと同じように文字を書き込みます。
2次元配列のL_text変数の行がレイヤ番号に、列に文字を対応させています。この文字は頭の方に紹介したぼくのかんがえたさいきょうのれいあうとに対応しています。各々のキーマップに合わせて変更してください。ただし10文字以上にすると二行目に改行してしまいます。
さて実際に動かしてみるとこんな感じになります。

6. おわりに

ほしいと思った機能がいい感じに実装できたので満足です。
今後何かに使えたらと思ってLCDの二行目は丸々空けているのですが今のところさっぱり思いつきません。
マクロを使ってパスワード生成させるとか…?
なにか良いアイディアがあったらコメントください。質問等も是非どうぞ。
皆様よきErgoDoxライフを。

PIC MAN

ソフトとハードの両方の目線を持てるようになりたいです.

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です