2018年04月22日

自作キーボード ソフト編(2)

次は、I2C デバイスの処理である。当初 USI だけ対応する予定だったのだが、TWI にも対応することになった。これについても、だらだら書いてきたものがあるのだが、改めて検討する。

ソフト編(1) で検討した内容を踏まえて検討する。

I2C デバイスのやり取り

    I2C では、WRITE もしくは READ パケットをホストが送出する。WRITE では、
        アドレス(2B) データ1 データ2 データ3 ...
    こんな感じで レジスタへの WRITE データを送ってくる。データの数は決まっていないが、デバイス側で上限を設けることはできる。1B 毎にハンドシェークするのだが、上限に達したときに NAK を返す。(NAK は、これ以上受け取れないという意味で、直前のデータは受信)

    HID over I2C では、Command Register に対し

    Op Code , (Report Type | Report ID)
    10: Input
    20: Output
    30: Feature

    という 2 バイトをまず送ってくる。
    Op Code で Linux が対応しているものは、

      01 RESET
      02 GET_REPORT
      03 SET_REPORT
      08 SET_POWER

    だけである。また

      0E VENDOR

    という好きに使ってよいコマンドがある。

      ただし HID の定義では、

        04 GET_IDLE 必須
        05 SET_IDLE 必須
        06 GET_PROTOCOL ブートキーボードでは必須
        07 SET_PROTOCOL ブートキーボードでは必須

      ということになっている。なお SET_REPORT は キーボードの LED 操作であり必須ではない。

    例えば、SET_REPORT を送ったとしよう。次は、Output Register に対しリポートを Write してくる。
        アドレス(2B) レングス(2B) レポートの中身
    あるいは
        アドレス(2B) レングス(2B) レポートID(1B) レポートの中身
    どちらになるかは、Report Descriptor の内容で決まる。

    例えば、GET_REPORT。 キーボードの Input Report を Input Register に対し Read してくる。
        (WRITE) アドレス(2B)
        (READ) レングス(2B) レポートの中身
    あるいは、
        (WRITE) アドレス(2B)
        (READ) レングス(2B) レポートID(1B) レポートの中身
    ということになる。

     ・ https://docs.mbed.com/docs/ble-hid/en/latest/api/md_doc_HID.html
    実際の中身について、ここに説明がある。

      [modifier, reserved, Key1, Key2, Key3, Key4, Key5, Key6]
      modifier:
      bit 0: left control
      bit 1: left shift
      bit 2: left alt
      bit 3: left GUI (Win/Apple/Meta key)
      bit 4: right control
      bit 5: right shift
      bit 6: right alt
      bit 7: right GUI

      null report: [0, 0, 0, 0, 0, 0, 0, 0]
      'a' report: [0, 0, 4, 0, 0, 0, 0, 0]
      'A' report: [2, 0, 4, 0, 0, 0, 0, 0]

      モデファイア以外の同時押しが 6 つまでで、6 つを超えると "Keyboard ErrorRollOver" コードに全部書き換える。作ったキーボードでは、left control, left shift, left alt の3つがモデファイア。Fn キーは内部で処理するので送出しない。同時押し可能なキーは 他に ESC,TAB,BS,ENTER と 4 つあり、6 個を埋める場合がある。

    HID では、常に 8バイト使って、状態を送る。HID over I2C の場合は、2 バイトの Length または、2 バイトの Length + 1 バイトの Report ID が前に付くということである。

常にレジスタを指定し、WRITE または READ するわけだが、そのレジスタはどこにあるのか?

    HID over I2C の場合、HID Descriptor というものがある。USB での ディスクリプタのようなもの。
    面倒なので Linux からコピー

      struct i2c_hid_desc {
      __le16 wHIDDescLength;
      __le16 bcdVersion;
      __le16 wReportDescLength;
      __le16 wReportDescRegister;
      __le16 wInputRegister;
      __le16 wMaxInputLength;
      __le16 wOutputRegister;
      __le16 wMaxOutputLength;
      __le16 wCommandRegister;
      __le16 wDataRegister;
      __le16 wVendorID;
      __le16 wProductID;
      __le16 wVersionID;
      __le32 reserved;
      } __packed;

    HID Descriptor 自体の長さから始まって version ... と続く 30 バイトである。
    この中でレジスタが、

      ReportDescRegister
      InputRegister
      OutputRegister
      CommandRegister
      DataRegister

    と 5 つ定義されている。ReportDescRegister は、Report Descriptor を読むアドレス。このアドレスを WRITE して、READ する。
    InputRegister, OutputRegister は、USB でいう所の エンドポイント。ホストが受信する Report は、InputRegister をまず指定して Read するということらしい。DataRegister は、コマンドに対するステータスとか?

    Report Descriptor だが、こちらは HID を定義する デスクリプタ。その内容はというと ... USB と同じ。
    では、HID Descriptor はどこから読むのか? ... ACPI で定義せよ?

実装方針

    HID over I2C について、あまり分かったわけではないが、それと共存できる独自方式のやりかたは分かった。
     -- レジスタを指定(WRITE)して 続いて データを送る。
     -- レジスタを指定(WRITE)して 改めて READ する。
     -- アドレス空間は 2 バイトあるが、1 バイトだけ使っても構わない
    基本はこういうことである。

    1) FIFO の読み出し → READ バッファの読み出し(レジスタ)
    ソフト編(1)では、FIFO としたが、ちょっと変える。 READ バッファということにして、スキャンでは、 READ バッファに詰めていく。READ が来たら、全部持って行ってもらう。フォーマットは 最初に Length 2 バイトを付ける。

    これは、Input Register と共用できる。

    なお、READ の最中は、READ バッファを更新してはならない。スキャンを禁止しないといけない。
    ここが FIFO と大きく違う。

    2) 機能の指定 (レジスタ)

      0E xx で 機能を指定する
        00 なし
        01 ローカルスキャンコード
        02 ASCII
        03 スキャンコード1

    これは、Command Register と共用できる。

    3) 現在の状態の読み出し(レジスタ)

    14 バイト(7 バイト)のテーブルを作っているのだから、これを勝手に読み出せば良い。

    サイズが大きく READ バッファを使えない読み出しなので、専用のレジスタにする。統一するため 最初に 2バイトの Length を付ける(かも知れない:未確定)

    4) 独自ディスクリプタ読み出し(レジスタ)

    使える機能とかを定義して、ROM から読み込む。
    専用のレジスタにするが、仕組みは、HID Descripor , Report Descriptor と共用にする。

    最低限必要なのは、こんなところ。ステータスを返す Data Registor とか必要になるかも知れないが、作りながら検討しよう。

I2C の制御

    USI については、以前作ったものがある。それを元にするつもりで、TWI 対応もそれに準じた作りにする予定。
    それは良いのだが、すっかり忘れてしまった。復習しないといけない。

    教材としては、AVR311 (TWI slave), AVR312(USI slave) がある。AVR.jp に日本語訳もある。あとは各データシート。

    これらの教材は、デバイスの使い方であって、どう処理するかは設計しないといけない。

設計方針

    基本は、割り込みである。

    割り込みが来たら、WRITE では、1 バイトを受け取り、READ では送信データをセットする。これらをどう処理するか検討していく。

    処理の基本は、ステートマシンである。1 バイト目のWRITE で何かをして、2 バイト目のWRITE を受け取る状態にする・・・みたいな処理。

    1 バイト目 アドレス(L) : カレントレジスタの設定

      例えば、独自ディスクリプタ読み出しレジスタが設定されたら、読み込むアドレスとレングス、ROM からの読み出しを示すフラグを設定する。

    2 バイト目 アドレス(L) :無視

    3 バイト目 コマンド:

      カレントレジスタが、コマンドレジスタであり、コマンドが 0E のときのみ次に進む

    4 バイト目 機能選択:

      メインループ側に 機能が変わったということを通知

    それ以降:無視
    終了条件:初期状態にリセット

    簡単なものは、こんな感じ。

    READ では、
    1 バイト目 レングス(L) :
    2 バイト目 レングス(H) :
    それ以降:

      カレントレジスタの設定で設定された情報に従って 送信データをセットする。
      (レングスが 0 になっているときは、0 をセット)

    終了条件:初期状態にリセット

      レングスを 0 にする・・・だけで良いのだろうか?
      繰り返し READ することをサポートするのであれば、ちょっと処理が変わる。

      繰り返し READ が来た場合、同じデータが先頭から読めないといけないようだ。HIDDescriptor の読み込みでいきなり、それが起きる。

      他に RESET が来たときは、READY になったら 00 00 が読めなければならない。最後まで READ した場合は、レングスを 0 にするが、途中でやめたときは、READ 前に戻すことにしよう。

    必ず終了条件が来るので、ステートマシンとしてそう複雑なものではない。状態数は多くないので、すべてを 1 バイトに収める。分かりやすくするために状態変数を分けたりはしない。

役割分担と排他制御

    割り込み処理が絡むので何をメインループで処理させるか? メインループと競合がおきないようどう排他制御するか設計しないとならない。ここは非常に重要である。
    READ バッファ

      READ バッファ自体は、メインループだけが変更する。割り込みでは、READ バッファを読み込むが、更新をーひいてはスキャンを停止させなければならない。メインループで処理が一段落したら、次のループからスキャンを行わない。I2C の次の割り込みまで間があるので、十分時間的猶予がある。

       ・READ バッファの読み出しレジスタがセットされたら、一時停止フラグを立てる。
       ・現在の状態の読み出しレジスタも対象にする
       ・READ が(正しく)完了したら一時停止フラグを落とす。
      これだけでは、ずっとスキャンが停止してしまうかも知れない。
       ・WRITE が来たら、まず一時停止フラグを落とす。

      これだけ入れておけば良いような気がする。なにも要求が来なければ、なにもしないで良いのである。なにか要求が来たら、割り込み終了後ただちに スキャンすることになる。

    READ でどこから読み込むかの情報

      これは、割り込み側でのみ操作する情報。メインループ側では関知しない。READ バッファも レングスを転記する。

    動作モード

      メモリに置いて、動作モード変更フラグは GPIOR に置く。割り込み側でのみ変更。
      動作モードは、メインループの一回のループで一度だけ読む。volatile 属性。

      変更フラグを先にチェックし、フラグが立っていたら、READ バッファを クリアし、フラグを落とす。-- こういう処理も割り込みでやってはいけない。

      忘れていたが、READ バッファの読み込み完了の通知も 動作モード変更フラグ を流用しよう。一時停止フラグを落としたら、このフラグを立てる。


    HID over I2C はいきなりは対応しないが、
    ・READ バッファの生成要求
    というのも作っておかないといけない。これもフラグ1つ。生成が終わったらフラグを落とす。
    フラグを立てるタイミングは、GET_REPORT が来たときで良いのではないかと思う。
    これらフラグは、GPIOR に詰め込んでいく。メモリとは全くコストが違うのである。

    排他制御が重要と書いたが、基本緩いフラグ処理で行けそうな気がする。

    なお、HID over I2C はいきなりは対応しないと書いたが、USB が先である。HID を理解し動かしてから、I2C に入れていく。

ここまでで、一旦検討終了。作り出すことが出来そう。
今のところ、version 1 2313版 の基板を作る部品だけ揃っている。作っているうちに 861 版 も揃うだろう。version 3 t88a 版はまだまだ。一か月ぐらいみておかないと。

なお HID over I2C の実装は、USB キーボードの実装後。
 ・http://d.hatena.ne.jp/hanya_orz/20140425/p1
ここを読んで HID を理解しておこう。


付録

レジスタアドレス定義

XX00 LocalDescRegister
XX01 HIDDescRegister
XX02 ReportDescRegister
XX03 InputRegister
XX04 OutputRegister
XX05 CommandRegister
XX06 DataRegister
XX07 KeystatRegister

LocalDescriptor 定義


HIDDescriptor 定義
__le16 wHIDDescLength = 30;
__le16 bcdVersion = 0x0100;
__le16 wReportDescLength;
__le16 wReportDescRegister = 2;
__le16 wInputRegister = 3;
__le16 wMaxInputLength = 10;
__le16 wOutputRegister = 4;
__le16 wMaxOutputLength;
__le16 wCommandRegister = 5;
__le16 wDataRegister = 6;
__le16 wVendorID;
__le16 wProductID;
__le16 wVersionID = 0x0000;
__le32 reserved = 0;

GPIOR フラグ一覧
タイマー(Tick)フラグ
オートリピート中 ?
オートリピート2個目以降 ?

モード変更イベント?
ROM からの読み出しを示すフラグ
スキャンの一時停止フラグ
READ バッファの生成要求
posted by すz at 16:41| Comment(0) | TrackBack(0) | I2CKEYBOARD
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/183033100
※ブログオーナーが承認したトラックバックのみ表示されます。

この記事へのトラックバック