2020年2月19日水曜日

エルゴノミック Realforce を作ろう(6)トッププレート編

前回(3x4+1 キーパッド編)までは、どちらかというと、静電容量無接点スイッチの押下検出回路について書いてきた。そして、それも前回でひと段落したので、今回はトッププレートについての試行錯誤を書いてみる。


3x3 トッププレート


NiZ 製ハウジングに付いているトッププレートへの引っ掛かりのツメの高さは 1.5mm なのだけど、まぁ、1.6mm PCB でも何とかツメがかかったりしてくれないかな?と甘い期待をして、あまり深く考えず、1.6mm 厚の PCB で発注した。

ただ、多少でも厚みが出ないようにと思って、裏面は銅箔もレジストも無しにしてみた。こういう時は、「片面」の PCB を指定するようだ。いつも通りに「両面」設定で注文してしまっていたので、発注直後に pcbgogo からツッコミが入り、修正して注文し直した。

そして、届いた基板にハウジングを取り付けてみると、案の定、ツメが微妙なところでかからない・・・。

ハウジングの右手前の真ん中にあるツメが押し込まれていて出ていない

ハウジングをプレートに嵌めても、ぐにゅっと入るだけで、ツメがカチッとは言わない。

そこで、ツメがかかるところをヤスリで少しだけ削ってやると、かかるようにはなった。

ツメが出ている(レジストを削るのはいかにも残念だと思ったので、裏面のツメが当たるところを削ってみた)

トッププレートが厚くてツメがかからなくても、ハウジングの固定自体には支障は無い。だけど、キーを底打ちした時に、微妙にトッププレートに近過ぎるのか、打鍵感があまりよくない気がする。

また、低頭とは言え、ネジをハウジングのギリギリに寄せたのはまずかった。底打ち時にキーキャップがネジに当たってしまい、カチャカチャ音がすることがある。底打ちすると、キーキャップは結構下まで降りることが今さらながら分かった・・・。

ネジはハウジングギリギリに寄せたので、上から見ると、キーキャップとかぶっている

トッププレートを裏返すとこんな感じ(手持ちで写しているのは、そうしないと、ラバードームとバネがキーキャップに押されて浮いてしまうため)。

下段の紫ラバーが左下と右下のネジにかかってしまう

ラバードームがネジにかかっていて、ネジを締める時に挟み込んでしまう。。。そういう意味でも、ネジはキーキャップにかぶらない方が良い。

ちなみに、上段左の2個が NiZ のラバードームで、その右端と中段は Realforce、下段はその後買ってみた BKE のラバードーム(だけど、BKE のタクタイル感はあまり自分の好みではなかったので、最終版では使わないことにあっさりと決定・・・)。

あと、BKE のラバードームは、厚みが微妙に分厚いようだ。スペーサのサイズの選び方に違いが出てきそうだ。

BKE(紫)はピッタリだけど、Realforce(灰)と NiZ(乳白)は少し浮いている

また、しっかりとハウジングを固定するには、トッププレートの締め付けが大事なので、大きさ優先で M2 ネジにしていたけど、M2 では心許ない気がする。


3x3 での教訓


  • 1.6mm PCB は分厚すぎて、ハウジングのツメがかからない
  • ネジは低頭でもキーキャップにかぶらないようにすること
  • あと、M2 ネジでは固定が弱いかも


4x4トッププレート


そこで、4x4 キーパッドでは、ちょうど Realforce の中古品を手に入れたのもあり、

  • 1.2mm PCB
  • M3 ネジを使う
  • M3 ネジがキーに干渉しないように配置
  • NiZとTopre キーを半分ずつ

という仕様にした。ハウジングの穴の大きさは、NiZ が 14x14mm で、Topre が 14x14.6mm になっている。

左手前 2 列が 14x14mm の NiZ 用で、奥の右側 2 列が 14x14.6mm の Topre スイッチ用

1.2mm PCB だと、さすがにツメはかかる。ハウジングを嵌めると、早々にカチッと言うけど、スキマがあるので、少し動かすことができる。これもネジで締めれば、ハウジングの固定自体に問題はない。

ツメの下に少し(0.3mm)のスキマが出来ている

ネジもキーキャップから離したので、押下時にも干渉しない。

上面では、ネジはキーキャップから離れている

裏面でも、ネジはラバードームに干渉しない(ラバーは上段から NiZ, BKE Light, BKE Ultra Light, Realforce)

ネジは M3 にしたので、頑丈感もある。しかし、トッププレートと下の回路基板のネジ穴を、どちらも同じ φ3.2 にしておいたら、片方にネジを取り付けて、もう片方の板にはめこもうとすると、微妙にネジが引っかかる。どうやら、どちらも φ3.2 というのはギリギリ過ぎるようだ。少し遊びがあった方がよかった。

それとあと、横から見ると、微妙に PCB がたわんでいる気がする。真ん中あたりが膨らんでいるような?

微妙だけど、膨らんでいる?

正面から見ても、やはり真ん中辺りが膨らむようにたわんでいる気がする

4x4 ともなると、4 隅の 4 ヶ所のネジ止めだけでは足りないようだ。2 列に 1 つはネジを配置して、たわみが出ないように、しっかりと固定しておきたい。

最後に、3x3 でも 4x4 でも、トッププレートと回路基板の間に 5mm のスペーサを挟んでいたのだけど、これはちょっと高過ぎたようだ。ハウジングとラバードームの間にスキマが空いている。

ラバードームの厚みとスキマ
紫の BKE Ultra Light だとちょうどよさそうだけど、最終的には、右端の乳白色の NiZ を使おうと思っているので、もう少し低くした方がよさそうだ。


4x4 での教訓


  • トッププレートのネジ穴径は、回路基板と同じだと少し狭くて引っかかる
  • トッププレートのたわみを減らすために、ネジが多めに必要
  • スペーサの高さは、NiZ 製ラバードームには、5mm だと高過ぎる


3x4+1 トッププレート


上記の 4x4 の教訓をさらに踏まえ、3x4+1 キーパッドでは、

  • トッププレートのネジ穴は φ3.4 にする
  • ネジをなるべくたくさん配置する
  • スペーサは 4.5mm にして、0.2mm 厚や 0.3mm 厚のワッシャで調節する

という改善を入れた。

組み立ててみると、トッププレートのネジ穴を広げたので、ネジはすんなりと通った。

ネジ穴をたくさん用意したけど、とりあえず 6 カ所をネジ止め

とりあえず 4 隅と中程の 6 カ所をネジ止めして、横から見てみた。

若干のたわみもあるかもしれないけど、しっかりとした固定感がある

スペーサは 4.5mm に変えて、0.2mm のワッシャを一つ挟んでいる。これでちょうどよさそうだ。

また、今回は LED を付けるために、ラバードームを NiZ のもので統一している。トッププレートを裏返すとこうなっている。

回路を接続して右の基板の丸いところを指で触ると、タッチセンサとしても機能する

組み立てながら気づいたのは、スペーサは、まずトッププレートにネジで取り付けて、それから回路基板をネジで取り付けた方がよい。ラバードームと円錐バネは、上のようにトッププレートをひっくり返してハウジングに載せておき、その上へ回路基板を裏返して載せた状態で、最後にネジを取り付けた方が楽だからだ。なので、ネジ穴を広げておくのは、トッププレートではなくて、下の回路基板の方が適切だった。


3x4+1 での教訓


  • ネジ穴を φ3.4 にするのは、トッププレートではなくて、回路基板の方がよい


まとめ


では、最後に、今回得られた知見をまとめておきたい。

  • トッププレートの厚みについて、
    • 1.6mm PCB は分厚すぎて、ハウジングのツメがかからない
    • 1.2mm PCB だとスキマができるけど、ハウジングの固定に支障はない
  • ネジは低頭でもキーキャップにかぶらないようにすること
    • キーキャップはトッププレート直近まで降りてくる
  • ネジは M3 程度の大きさにして、ハウジングをしっかりと固定できるようにする
  • ネジ穴径は、ネジをトッププレート側に先に取り付ける前提で、以下のようにする
    • トッププレート: φ3.2
    • 回路基板: φ3.4
  • トッププレートのたわみを軽減するために、ネジはキーの 2~3 列/行に1つは欲しい
  • スペーサの高さは、NiZ 用には、4.5mm とし、0.2mm 厚のワッシャで調整する

ちなみに、3x4+1 トッププレートについては、この後 1.5mm 厚のアルミ板でも作ってみたので、また別途報告したい。





2020年2月7日金曜日

STM32F042K6T6 で USB マウス & キーボード & LED を実装する

USB キーボード & マウスになっている STM32F042K6T6 ブレークアウト基板(右端の LED が CAPS 状態)

STM32CubeIDE を使って USB マウスや USB キーボードを作る方法は、既に色々なサイトで分かり易く解説されているので、自分でもそれに倣って動かすことが出来た。

だけど、一歩進んで、

  • マウスとキーボードの両方として動かすには?
  • CAPS ロックなどのインジケータの状態を取得するには?

というところはすぐには分からず、ちょっとつまづいたので、色々調べて試行錯誤して分かった方法をここにメモしておく。

動いてしまえばなんてことないんだけど、動くまでが大変だよね。


はじめに


USB マウスや USB キーボードを作る時は、STM32CubeMX で HID Class を指定している例が多い。しかし、ここでは Custom HID Class を使って雛型コードを生成する。その理由は、HID Class だと、

  1. CubeMX でコード生成をする度に、レポート デイスクリプタが HID マウスのものに毎回書き戻されてしまう
  2. HID の雛型コードでは、OUT 方向(ホストからデバイスの向き)のエンドポイント(データ入出力の FIFO)である EPOUT が作られないので、LED 表示のデータを受け取れない

という(難)点があるからだ。ただし、1については、Custom HID Class を使っても、完全には書き戻しを避けられない。


環境


以下では、

  • macOS Catalina 10.15.2
  • STM32CubeIDE Version 1.2.0
  • STM32F042K6T6 ブレークアウト基板(自作)

を使用している。


ボード


冒頭の写真にあるように、STM32F042K6T6 ブレークアウト基板に、外付けタクトスイッチを 4 個付けたものを使った。

ブレークアウト基板自体には、BOOT0 兼用スイッチと、表示用 LED が 2 個付いている。

そして、SWD 無しの最小構成で、DFU 経由でバイナリを書き込む。これだとデバッガは使えないけど、USB ケーブル 1 本で作業が出来る。


CubeMX での設定

USB の設定


"Connectivity" の "USB" で、"Device (FS)" にチェックを入れる。


また、"Middleware" の "USB_DEVICE" で、"Custom Human Interface Device Class (HID)" を選択し、


以下のパラメータを変更する:

  • CUSTOM_HID_FS_BINTERVAL = 0x1
  • USBD_CUSTOM_HID_REPORT_DESC_SIZE = 119

最初のは HID イベントを送る周期で、単位は ms。ここではホストから 1ms でポーリングしてもらう設定にする。2 つ目については、後述するキーボードとマウスのレポート ディスクリプタのサイズを指定している。


クロックの設定


"System Core" の "RCC" で、"CRS SYNC Source USB" を選択する。


また、ここで "Clock Configuration" タブをクリックすると、"Do you want to run automatic clock issues solver?" と聞かれるので、"Yes" をクリックする。
HSI48 が使われていれば問題ないと思う。これで USB に同期した 48MHz クロックで動作するのだと思う。


タイマーの設定


"Pinout & Configuration" タブに戻り、"Timers" の "TIM14" の "Activated" をチェックし、1ms 毎に処理を行いたいので、以下のパラメータを変更する。

  • Presaler = 48 - 1
  • Counter Period = 1000 - 1


また、"NVIC Settings" で Enabled をチェックして割り込みを許可する。



GPIO の設定


"System Core" の "GPIO" で、タクトスイッチと LED 用の設定をする。


SW0 は BOOT0 兼用で、ブレークアウト基板にプルアップ抵抗と共に備付けられているタクトスイッチ。そのため、SW0 はプルアップもプルダウンも無しにする。

それ以外のタクトスイッチは、すべてプルアップに設定する。各スイッチの反対側は GND に落としている。

また、ブレークアウト基板上の LED 2 つ用の出力ピン LED1, LED2 も設定する。


USB 関連コードの変更

レポート ディスクリプタの定義


マウス用とキーボード用のそれぞれを盛り込むんだレポート ディスクリプタを定義する。基本は、 usb.org の

Device Class Definition for HID 1.11

にある hid1_11.pdf の

  • p.69 Appendix E.6 Report Descriptor (Keyboard)
  • p.71 Appendix E.10 Report Descriptor (Mouse)

からのコピペだけど、マウスなのかキーボードなのかを識別するために、ReportID をそれぞれの先頭に追加する。

USB_DEVICE/App/usbd_custom_hid_if.c の 91 行目にある CUSTOM_HID_ReportDesc_FS を以下のように書き換える。

/** Usb HID report descriptor. */
__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END =
{
  /* USER CODE BEGIN 0 */
  /* 47+18 */
  0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
  0x09, 0x06,                    // USAGE (Keyboard)
  0xa1, 0x01,                    // COLLECTION (Application)
  0x85, 0x01,                    //   REPORT_ID (1)
  0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
  0x19, 0xe0,                    //   USAGE_MINIMUM (Keyboard LeftControl)
  0x29, 0xe7,                    //   USAGE_MAXIMUM (Keyboard Right GUI)
  0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
  0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
  0x75, 0x01,                    //   REPORT_SIZE (1)
  0x95, 0x08,                    //   REPORT_COUNT (8)
  0x81, 0x02,                    //   INPUT (Data,Var,Abs)
  0x95, 0x01,                    //   REPORT_COUNT (1)
  0x75, 0x08,                    //   REPORT_SIZE (8)
  0x81, 0x03,                    //   INPUT (Cnst,Var,Abs)

  0x95, 0x05,                    //   REPORT_COUNT (5)
  0x75, 0x01,                    //   REPORT_SIZE (1)
  0x05, 0x08,                    //   USAGE_PAGE (LEDs)
  0x19, 0x01,                    //   USAGE_MINIMUM (Num Lock)
  0x29, 0x05,                    //   USAGE_MAXIMUM (Kana)
  0x91, 0x02,                    //   OUTPUT (Data,Var,Abs)
  0x95, 0x01,                    //   REPORT_COUNT (1)
  0x75, 0x03,                    //   REPORT_SIZE (3)
  0x91, 0x03,                    //   OUTPUT (Cnst,Var,Abs)

  0x95, 0x06,                    //   REPORT_COUNT (6)
  0x75, 0x08,                    //   REPORT_SIZE (8)
  0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
  0x25, 0x65,                    //   LOGICAL_MAXIMUM (101)
  0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
  0x19, 0x00,                    //   USAGE_MINIMUM (Reserved (no event indicated))
  0x29, 0x65,                    //   USAGE_MAXIMUM (Keyboard Application)
  0x81, 0x00,                    //   INPUT (Data,Ary,Abs)

  0xc0,                          // END_COLLECTION

  /* 54 */
  0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
  0x09, 0x02,                    // USAGE (Mouse)
  0xa1, 0x01,                    // COLLECTION (Application)
  0x09, 0x01,                    //   USAGE (Pointer)
  0xa1, 0x00,                    //   COLLECTION (Physical)
  0x85, 0x02,                    //     REPORT_ID (2)
  0x05, 0x09,                    //     USAGE_PAGE (Button)
  0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
  0x29, 0x03,                    //     USAGE_MAXIMUM (Button 3)
  0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
  0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
  0x95, 0x03,                    //     REPORT_COUNT (3)
  0x75, 0x01,                    //     REPORT_SIZE (1)
  0x81, 0x02,                    //     INPUT (Data,Var,Abs)
  0x95, 0x01,                    //     REPORT_COUNT (1)
  0x75, 0x05,                    //     REPORT_SIZE (5)
  0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)
  0x05, 0x01,                    //     USAGE_PAGE (Generic Desktop)
  0x09, 0x30,                    //     USAGE (X)
  0x09, 0x31,                    //     USAGE (Y)
  0x09, 0x38,                    //     USAGE (Wheel)
  0x15, 0x81,                    //     LOGICAL_MINIMUM (-127)
  0x25, 0x7f,                    //     LOGICAL_MAXIMUM (127)
  0x75, 0x08,                    //     REPORT_SIZE (8)
  0x95, 0x03,                    //     REPORT_COUNT (3)
  0x81, 0x06,                    //     INPUT (Data,Var,Rel)
  0xc0,                          //   END_COLLECTION
//0xc0,                          // END_COLLECTION
  /* USER CODE END 0 */
  0xC0    /*     END_COLLECTION              */
};

/* USER CODE BEGIN PRIVATE_VARIABLES */

これ全体で 119 バイトなので、先にディスクリプタのサイズ USBD_CUSTOM_HID_REPORT_DESC_SIZE にこの値を指定したのだった。


EPIN サイズを 9 に変更


次に、ホストへ送るエンドポイント EPIN のサイズを、マウスのレポート構造体と、キーボードのレポート構造体の、サイズの大きい方に書き換える。ここでは、キーボードの構造体の先頭に ReportID を付け加えた、9 バイトを指定する。

Middlewares/ST/STM32_USB_Device_Library/Class/CustomHID/Inc/usbd_customhid.h の 45 行目の CUSTOM_HID_EPIN_SIZE の値を 0x09U に変更する。

/** @defgroup USBD_CUSTOM_HID_Exported_Defines
  * @{
  */
#define CUSTOM_HID_EPIN_ADDR                 0x81U
#define CUSTOM_HID_EPIN_SIZE                 0x09U

この値は、CubeMX がコードを生成する度にデフォルト値の 2 に上書きされてしまうので注意!

そこで、書き戻された時に、すぐ気付けるよう、#error を仕込んでおこう。

USB_DEVICE/App/usbd_custom_hid_if.c の 65 行目に、

/* USER CODE BEGIN PRIVATE_DEFINES */
#if CUSTOM_HID_EPIN_SIZE != 0x09U
#error "CUSTOM_HID_EPIN_SIZE is not 0x09U!"
#endif
/* USER CODE END PRIVATE_DEFINES */

としておいて、この値が 9 じゃなかったら、コンパイルエラーになるようにしておく。


EPOUT コールバック関数の実装


一方、LED ステータス用のデータにも、上記 ReportID が付加されて返ってくるようなので、EPOUT 側は ReportID とステータスを合わせた 2 バイトが必要だけど、こちらは CUSTOM_HID_EPOUT_SIZE のデフォルト値が 2 になっているので、変更は要らない。

Middlewares/ST/STM32_USB_Device_Library/Class/CustomHID/Inc/usbd_customhid.h の 48 行目に定義されている。

#define CUSTOM_HID_EPOUT_ADDR                0x01U
#define CUSTOM_HID_EPOUT_SIZE                0x02U

そして、PC から LED ステータスが送られてきた時の処理は、
USB_DEVICE/App/usbd_custom_hid_if.c の 245 行目の CUSTOM_HID_OutEvent_FS() に書く。

/**
  * @brief  Manage the CUSTOM HID class events
  * @param  event_idx: Event index
  * @param  state: Event state
  * @retval USBD_OK if all operations are OK else USBD_FAIL
  */
static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state)

この関数は 2 つの引数を持っているけど、この関数を呼んでいる
Middlewares/ST/STM32_USB_Device_Library/Class/CustomHID/Src/usbd_customhid.c(642)
を見ると、これら 2 つの引数は、受信したデータの 1 バイト目と 2 バイト目になっていることが分かる。

static uint8_t  USBD_CUSTOM_HID_DataOut(USBD_HandleTypeDef *pdev,
                                        uint8_t epnum)
{

  USBD_CUSTOM_HID_HandleTypeDef     *hhid = (USBD_CUSTOM_HID_HandleTypeDef *)pdev->pClassData;

  ((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->OutEvent(hhid->Report_buf[0],
                                                            hhid->Report_buf[1]);

  USBD_LL_PrepareReceive(pdev, CUSTOM_HID_EPOUT_ADDR, hhid->Report_buf,
                         USBD_CUSTOMHID_OUTREPORT_BUF_SIZE);

  return USBD_OK;
}

今回の状況では、1 バイト目がキーボードの ReportID の 1 になっていて、2 バイト目が、CAPS ロックなどの LED ステータスが入っているようだ。

この LED ステータスの中身はというと、先のキーボードのレポート ディスクリプタの "USAGE PAGE (LEDs)" で指定されたものになっている。今度は usb.org の

HID Usage Tables 1.12

に置かれている hut1_12v2.pdf の p.61 "11 LED Page (0x08)" に、各ビットが何を意味するかが定義されている。

普通は最初の 5 ビットを送ってもらうようにするようだ。つまり、2バイト目 state の最初の 5 ビットには、

  1. Num Lock
  2. Caps Lock
  3. Scroll Lock
  4. Compose
  5. Kana

のステータスが格納されている。

今回は、CAPS がオンの時に、LED2 を点灯させることにする。ちょっとかっこ悪いけど、変更量を最小限にするために、まず
USB_DEVICE/App/usbd_custom_hid_if.c の 26 行目で main.h をインクルードし、LED2 の情報が取れるようにしておいて、

/* USER CODE BEGIN INCLUDE */
#include "main.h"
/* USER CODE END INCLUDE */

USB_DEVICE/App/usbd_custom_hid_if.c の 245 行目で、

static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state)
{
  /* USER CODE BEGIN 6 */
  HAL_GPIO_WritePin( LED2_GPIO_Port, LED2_Pin, (state & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET );
  return (USBD_OK);
  /* USER CODE END 6 */
}

というように、state の 2 ビット目が立っていたら、LED2 を点灯するようにする。


main.c の変更

HID クラス


28 行目で usbd_custom_hid_if.h をインクルードし、
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "usbd_custom_hid_if.h"
/* USER CODE END Includes */

48行目で usb デバイスのインスタンスを extern 宣言する。

/* USER CODE BEGIN PV */
extern USBD_HandleTypeDef hUsbDeviceFS;
/* USER CODE END PV */

レポート用構造体の定義


マウスとキーボードのレポート用の構造体には、それぞれ先頭に ReportID のフィールドを付加して、ディスクリプタ通りの固定値を書いておく。

少し戻って、30 行目で構造体を定義。
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
struct keyboardHID_t {
  uint8_t report_id;
  uint8_t modifiers;
  uint8_t reserved;
  uint8_t key[6];
};
struct mouseHID_t {
  uint8_t report_id;
  uint8_t buttons;
  int8_t x;
  int8_t y;
  int8_t wheel;
};
/* USER CODE END PTD */

そして、main() 関数の最初で初期化する。

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  struct keyboardHID_t keyboardHID;
  keyboardHID.report_id = 1;
  keyboardHID.modifiers = 0;
  keyboardHID.reserved = 0;
  for (int8_t i = 0; i < 6; ++i) {
    keyboardHID.key[i] = 0;
  }

  struct mouseHID_t mouseHID;
  mouseHID.report_id = 2;
  mouseHID.buttons = 0;
  mouseHID.x = 0;
  mouseHID.y = 0;
  mouseHID.wheel = 0;
  /* USER CODE END 1 */


タイマー待ち


73 行目に、TIM14 を待って USB 処理を開始するための volatile 変数と、割り込みハンドラを記述する。

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
static volatile uint8_t timer_intr = 0;
void HAL_TIM_PeriodElapsedCallback( TIM_HandleTypeDef* htim )
{
  if (htim->Instance == TIM14) {
    timer_intr = 1;
  }
}
/* USER CODE END 0 */

そして、while ループの手前で、タイマを開始する。

  /* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT( &htim14 );
  /* USER CODE END 2 */


while ループ


最後に、メインの while ループの中を以下のように実装する。

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  uint8_t idx = 0;
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    while (timer_intr == 0);
    timer_intr = 0;

    const uint8_t sw0_on = (HAL_GPIO_ReadPin( SW0_GPIO_Port, SW0_Pin ) == GPIO_PIN_SET)   ? 1 : 0;
    const uint8_t sw1_on = (HAL_GPIO_ReadPin( SW1_GPIO_Port, SW1_Pin ) == GPIO_PIN_RESET) ? 1 : 0;
    const uint8_t sw2_on = (HAL_GPIO_ReadPin( SW2_GPIO_Port, SW2_Pin ) == GPIO_PIN_RESET) ? 1 : 0;
    const uint8_t sw3_on = (HAL_GPIO_ReadPin( SW3_GPIO_Port, SW3_Pin ) == GPIO_PIN_RESET) ? 1 : 0;
    const uint8_t sw4_on = (HAL_GPIO_ReadPin( SW4_GPIO_Port, SW4_Pin ) == GPIO_PIN_RESET) ? 1 : 0;

    idx += 1;
    if (idx & 1) {// keyboard
      keyboardHID.key[4] = (sw3_on) ? 0x04 : 0; // 'a'
      keyboardHID.key[5] = (sw4_on) ? 0x39 : 0; // CAPS
      USBD_CUSTOM_HID_SendReport( &hUsbDeviceFS, (uint8_t*) &keyboardHID, sizeof( struct keyboardHID_t ) );
    } else {// mouse
      mouseHID.buttons = sw0_on;
      mouseHID.x = sw1_on - sw2_on;
      USBD_CUSTOM_HID_SendReport( &hUsbDeviceFS, (uint8_t*) &mouseHID, sizeof( struct mouseHID_t ) );
    }
    const uint8_t led1_on = sw1_on ^ sw2_on;
    HAL_GPIO_WritePin( LED1_GPIO_Port, LED1_Pin, (led1_on) ? GPIO_PIN_SET : GPIO_PIN_RESET );
  }
  /* USER CODE END 3 */
}

ここでは、各タクトスイッチを以下の操作に対応させてみた。

  • SW0:マウスの左クリック
  • SW1:マウスカーソルの右移動
  • SW2:マウスカーソルの左移動
  • SW3:キーボードの a キー
  • SW4:キーボードの CAPS キー

また、テスト目的で、キーボードの構造体の最後の方(インデックスが 4 と 5)を使うようにしてみた。EPIN のサイズが足りていないと、ここが送られないので、SW3, 4 が動かなくなることが確認できる。

LED1 については、マウスカーソルが移動する時に点灯するようにしてみた。

あと、どうするのがベストなのかは分からないけど、上ではキーボードとマウスのレポートを交互に送信するようにしてみた。


ファームウエアの DFU 書き込み


では、Release バイナリをビルドして書き込もう。

まず、MPU を BOOTLOADER モードにするために、BOOT0 スイッチを押しながら、Reset スイッチを長めに(2〜3秒?)押す。そして、システム情報の USB の一覧で
のように出ていること確認する。もしダメなら USB ケーブルを一旦抜いて、挿し直す。

BOOTLOADER モードになっていることが確認できたら、以下のコマンドをコマンドラインから実行する。

/Applications/STM32CubeIDE.app/Contents/Eclipse/plugins/com.st.stm32cube.ide.mcu.externaltools.cubeprogrammer.macos64_1.1.0.201910081157/tools/bin/STM32_Programmer_CLI -c port=usb1 -w STM32CubeIDE/workspace_1.2.0/UsbKeyMouse/Release/UsbKeyMouse.elf

以下に実際の実行画面を示す。
そして、システム情報の USB の一覧が
のようになっていることを確認する。Reset をかけるか、再度 USB ケーブルの抜き差しが必要かも知れない。


動作テスト


上記の実行画面で、最後のプロンプトの "AAaaAAaa" という文字列が選択されている部分が、今回のボードの各スイッチを使って、地味に

  1. 大文字小文字の a を入力
  2. マウスの横移動
  3. 左ダブルクリックで全選択

をしたもの(笑)

あとは、SW4 の CAPS を押して、LED の点灯・消灯を確認できたら完成!

macOS と Windows では、CAPS キーがキーボードごとに個別か、全キーボードで共通か、の違いがあるようだった。


参考サイト





2020年2月4日火曜日

エルゴノミック Realforce を作ろう(5)3x4+1キーパッド編

製作した 3x4+1 キーパッド

前回は 4x4 キーパッドを製作した。Topre スイッチ(静電容量無接点スイッチ)を押下した時の過渡波形は、思った通りに再現・計測できるようになったのだけど、前回の投稿では触れていなかった、未解決の事案がある。

アナログマルチプレクサの入力切り替え


それは、アナログマルチプレクサの sel 入力を使って、キーマトリックスからの各 row 線を切り替えた時に、検出コンデンサの電圧が一瞬落ち込んでしまう事だ。そのため、それが定常状態の中間電位に回復するまで、100μs の待ち時間を実は入れていた。


前回の回路図


まず、前回の 4x4 キーパッドでは、下の回路図のように、アナログマルチプレクサの 8 入力(A0 ~ A7)のうち、4 本(A0~A3)は Topre キーマトリックスの各 row 線に、1 本(A5)は中間電位に、そして、残り 3 本(A4, A6, A7)は GND に落としていた。

出力端子(A)は row としてオペアンプにつながっており、8 本ある入力のどれをこれに接続するかは、S0, S1, S2 の 3 ビットの sel 入力で行う。

前回の 4x4 キーパッドのマルチプレクサ入力:A4, A6, A7 を GND に接続し、A5 のみ中間電位


キー押下検出時の波形


この回路で、
  1. ADC を開始する
  2. 10μs 待つ
  3. アナログマルチプレクサの sel 線(S0, S1, S2)を設定して、row0/1/2/3 を選択する
  4. 50μs 待つ
  5. col 線を High にする
  6. 30μs 待つ
  7. col 線を Low にする
  8. ADC が 126μs 分を取得して DMA が完了するのを待つ
  9. 次のキーに移るために 1 に戻る
というのを各キーについて行った時の測定波形を下に示す。
4 行 2 列分のキー押下検出波形(キーはすべて非押下)
ここで、赤い線は各キー毎の切れ目を表していて、それぞれのキーの ADC の DMA 転送が終わり、また、その次のキーの ADC の DMA 転送の始まりに対応している。実際はその間が時間的にあいているけど、ADC は止まっているので、波形は取り込まれていない。

キーの順番としては、左から、以下の表のようになっている。

赤枠12345678
row選択row0row0row1row1row2row2row3row3
col選択col0col1col0col1col0col1col0col1

つまり、4 行 row0, row1, row2, row3 に対して、最初の 2 列 col0, col1 ずつの波形を表示している。


入力切り替え時の波形


さて、各キー波形の後半の上下インパルスは、col 線を High にして Low にする、押下検出波形に対応する。

一方、奇数枠の波形、すなわち、row 選択が切り替わるところでは、波形の最初のところで下方向にインパルス波形が現れている。これがより分かり易いように、col 線の上げ下げを行わず、row 線の切り替えだけにすると、測定波形は以下のようになる。
S0, S1, S2 の設定による、row 線の切り替えのみの波形
この波形は、キー非押下時とは言え、検出波形の大きさと同程度になってしまっているので嬉しくない。

上記のステップ 3 のマルチプレクサの入力切り替えは、

    const uint8_t sel = row_sel[row];
    HAL_GPIO_WritePin( ROWSEL2_GPIO_Port, ROWSEL2_Pin, (sel & 4) ? GPIO_PIN_SET : GPIO_PIN_RESET );
    HAL_GPIO_WritePin( ROWSEL1_GPIO_Port, ROWSEL1_Pin, (sel & 2) ? GPIO_PIN_SET : GPIO_PIN_RESET );
    HAL_GPIO_WritePin( ROWSEL0_GPIO_Port, ROWSEL0_Pin, (sel & 1) ? GPIO_PIN_SET : GPIO_PIN_RESET );

というコードで行っている。各ピンは順番に書き換わるので、row0 (A3) から row1 (A0) に切り替える場合、上記のコードだと、


状態選択S2S1S0
初期A3 (row0)011
S2 切替A3 (row0)011
S1 切替A1 (row2)001
S0 切替A0 (row1)000


というように、一瞬 A1 につながる瞬間がある。


選択信号 S0, S1, S2 の同時設定


これはあまりよろしくなさそうだ。そこで、ちょうど sel0, sel1, sel2 は全て同じ PA* グループにマップしてあったので、レジスタで直接同時に設定するようにしてみる。

    const uint8_t sel = row_sel[row];
    uint32_t val = ROWSEL0_GPIO_Port->ODR;
    val &= ~(ROWSEL2_Pin | ROWSEL1_Pin | ROWSEL0_Pin);
    if (sel & 1) val |= ROWSEL0_Pin;
    if (sel & 2) val |= ROWSEL1_Pin;
    if (sel & 4) val |= ROWSEL2_Pin;
    ROWSEL0_GPIO_Port->ODR = val;

この時の波形は以下のようになり、下向きインパルスの大きさが半分程度になった。
レジスタ直打ちで同時に S0, S1, S2 を設定した時の波形

多少の改善は見られたが、まだ波形が出ている。

とは言え、50μs も待てば十分だし、各 row ごとにキーをスキャンすることにすれば、row 線の切り替え回数は、全部で 100 個のキーがあっても 10 回以下には出来そうだ。そうすれば、この波形による待ち時間は、50μs x 10rows = 0.5ms 程度・・・!?馬鹿には出来ない(笑)出来るなら削っておきたい性分。

う〜む。

ひょっとしてだけど、マルチプレクサの入力の一部 (A4, A6, A7) が GND につながっているのが悪さをしているのではないだろうか?

S0, S1, S2 が切り替わる時に、最適化された内部の論理回路がガチャガチャっと切り替わる過程で、A4, A6, A7 の GND が漏れつながったりしてないだろうか?

そう思って、今回は、この部分を変えてみることにした・・・。


余談:A5 の中間電位


実は、上記の回路図で、A5 端子の中間電位は、スイッチの押下検出が終わったら、row 出力、すなわち検出コンデンサをここにつなぎ変えることで、その放電を早く終わらせるつもりで用意しておいたものだった。そのため、1kΩ と小さめの抵抗につないである。

キーを1つ検出するごとに、A5 の中間電位につないだ場合の波形を以下に示す。ただし、col 線の上げ下げは外してある。
1キー検出ごとに、中間電位 (A5) にいったんつなぐ
こうすると、各キーごとに、最初に A5 の中間電位から A0 ~ A3 の各 row 線に選択し直すことになるので、各キーごとに、切り替わり時の波形が最初に出る。

一方、検出波形の後半で、下向きインパルスをちょっと確認したら A5 につなぎ替える事で、確か放電は早く出来るようになる(後半でピコッと波形が下がっているタイミング)。だけど、結局最初の切り替え時の波形を待つ時間が増えるので、あまり意味が無かったようだ。残念。


3x4+1 キーパッドの設計


ということで、今回の 3x4+1 キーパッドでは、アナログマルチプレクサの入力には、キーマトリックスからの row 線 4 本と、残りの 4 本は全て中間電位につないで、GND につながる入力がないようにしてみた。

今回のマルチプレクサ入力:A0, A1, A2, A3 すべてを中間電位に接続し、GND にはつながない

また、それでもダメな時用に、Enable 信号も使えるようにしておいた(上の回路図の nen 信号)。


3x4+1 キーパッドの回路図


また、後述するように、今回はフル LED 仕様にしたので、上記の変更に加えて、LED 用の回路と配線を追加した。


3x4+1 キーパッド全体の回路図

そして PCB の配線は、ベジェ曲線や、半径が線形補間される円弧などを使って、丸みを付けてみた。外形含め、すべての配置・配線を Python で行っているので、正月含めて、まぁ結構な時間がかかっている・・・。やりたいだけとしか言えない。

PCB の表裏面

それと、今回初めて、エルゴノミックな要素として、放射状にキーを配置して、親指キーも 1 つ置いてみた。でも 100x100mm のサイズにして PCB 代を抑えたいので(ケチ!)、ホームポジションにする予定の位置から 1 つ右の親指キーにしている。

この放射状のキー配置については、自分の手の指を曲げ伸ばししながら決めたのだけど、実際に組み上げてみると、角度を素直に付けすぎたかなぁ、という感じ。ここ1年ほど使っている

X-Bows Mechanical Ergonomic Keyboard – X-Bows Store

と比べてみると、角度の広がり方が大袈裟すぎることがよく分かる。

製作したトッププレートと普段使いの X-Bows キーボードの比較(I-K-, 列の位置を合わせた)


マルチプレクサ切り替え時のインパルス波形


さてさて、それでは、今回の当初の目的に戻って、アナログマルチプレクサの sel 線切り替え時のインパルス波形はどうなっただろう?

4x4 キーパッドの時と同様に、まず、
  1. ADC を開始する
  2. 10μs 待つ
  3. アナログマルチプレクサの sel 線(S0, S1, S2)を設定して、row0/1/2/3 を選択する
  4. 50μs 待つ
  5. col 線を High にする
  6. 30μs 待つ
  7. col 線を Low にする
  8. ADC が 126μs 分を取得して DMA が完了するのを待つ
  9. 次のキーに移るために 1 に戻る

とした場合の、各キーの測定波形を以下に示す。

3x4+1 キーパッドの 4 行 2 列分のキー押下検出波形(キーはすべて非押下、最後はキー自体がない)
う〜〜む?!

特に変わってない!?

やはり先程と同様に、col 線を上げ下げするのをやめてみる。
3x4+1 キーパッドで、S0, S1, S2 の設定による、row 線の切り替えのみの波形

う〜〜〜む。

無念!やっぱりほとんど変わってない・・・。


Enable 入力


ならば!期待の  Enable 入力を使ってみよう。以下のように、S0, S1, S2 を切り替える時に、出力を disable するようにしてみる。
  1. ADC を開始する
  2. 5μs 待つ
  3. アナログマルチプレクサを Disabled にする
  4. 5μs 待つ
  5. アナログマルチプレクサの sel 線(S0, S1, S2)を設定して、row0/1/2/3 を選択する
  6. 20μs 待つ
  7. Enabled にする
  8. ADC が 126μs 分を取得して DMA が完了するのを待つ
  9. 次のキーに移るために 1 に戻る
Disabled 中に S0, S1, S2 を切り替えた時の波形

う〜〜〜〜む!?

波形の形は変わり、S0, S1, S2 の切り替え時にインパルス波形は出なくなったけど、代わりに、Disable にしたり Enable にする時に波形が出てしまっている。結局、あまり改善しているとは言えない・・・。

この程度の過渡波形が出てしまうは仕方がないのだろうか??

インパルス波形の振幅は非押下時の Topre スイッチと同程度なので、1pF 程度の容量がアナログスイッチの入出力間に寄生していると思われる。

そう思って 74HC4051 のデータシートを見てみると、そのものズバリの Feedthrough capacitance の典型値が 0.95pF と書いてあった・・・。値もほぼ符合する・・・。


まとめ


ということで、今回はアナログマルチプレクサについて、

  • 空いている入力端子を GND ではなく、すべて中間電位につないでも、Sel 切り替え時に過渡的なインパルス波形が生じてしまう
  • 切り替え時にマルチプレクサを Disable にしても、別の過渡波形が出るので、今回の用途ではあまり効果はなかった
  • インパルス波形の原因は、マルチプレクサの入出力間の浮遊容量だと思われる
  • その浮遊容量は、Topre スイッチの非押下時の静電容量と同程度の大きさ(データシートとも符合)

という知見が得られた。けど、インパルス過渡波形は消えなかった・・・。


おまけ: LED 追加


でも今回、NiZラバードームには、2x3mm の LED 用の切り欠きがあることに気付いた。

各ドームの右斜め上 45 度に 2x3mm の LED 用切り欠きがある

また、SK6812 のシリーズで、 2.0x2.2mm の大きさで、手ハンダ出来そうなものがあるのを見付けたので、色気を出して、フルカラー LED も並べることにした。

50-1000Pcs WS2812 2020 LED Chip 4pins Mini SMD White PCB Addressable Digital RGB Full Color LED Chip DC5V for LED strip Screen

フル LED は諦めていたのだけど、やっぱり諦めないことにして、LED を追加する。ラバードームもせっかく半透明だしね。

LED のハンダ付け用に、治具も作ってみた。切り欠きのところに LED を置くと、位置と向きが揃うようになっている。

LED ハンダ付け用治具


3.3V問題


だけど、SK6812 の電源電圧は 5V で、STM32 は 3.3V なところが気になる。LED の電源は USB の VBUS をそのまま持ってくるにしても、信号線の DIn が、STM32F042 の GPIO 出力で足りるだろうか?

SK6812 のデータシートでは、入力が H と見做される最小電圧は 0.7Vdd となっているので、3.5V が必要のはずだ。でも色々ググって出てくるソースコードでは、GPIO そのままで動かしているっぽい?!

しかも、今使っているレギュレータは、テスターで測ると 3.2V ぐらいの出力電圧だ・・・。「まぁ、でも、大丈夫なのかな?」と思いつつも、やっぱり気になるので、マイコン側はオープンドレイン出力として、
  • H 出力の時に 3.6V
  • L 出力の時に 0.9V
ぐらいになる抵抗分圧のパターンを用意しておくことにした。

LED 周りの回路図(再掲)

一応、これでも動いている。分圧なしで、GPIO 出力そのままでも動いていた。

でもどちらの場合も、ジッと見ていると、時々ピカッと全体がフラッシュするように光ったり、暗いはずの LED が、ジリジリと鈍く光ったりするので、何か不安定な要素が残っているようだ。基板スペースの都合上、パスコンを3分の1に削ったので、それがまずかったかも?それともタイマー割り込みの優先度とかで、タイミングがずれているのか・・・。


PWM+DMA で制御


ファームウェアについては、こちらの

STM32のタイマとDMAを組み合わせてNeoPixelでLチカする

を使わせてもらって、PWM+DMA で実装した。今後、右左で2つの GPIO で並列に LED 信号を出したかったので、マルチチャンネル化が容易な PWM を選択することにした。

ただ、同じ SK6812-2020 でも、データシートか製品の版によって、データ信号のタイミングがまちまちなのはちょっと困る・・・。0, 1 シンボルでの 0, 1 信号の幅と、Reset 信号の長さだけ変更して調整する。


3.3V 問題への別アプローチ


その後、さらに調べてみると、同じ 3.3V 問題への対処として、他にも方法が色々見つかった。

  • 初段の SK6812 だけ、Vdd にダイオードを一つ挟んで Vddを4.4V にする。0.7 Vdd = 3.1V となり、3.3V で間に合う
  • 5V tolerant ピン (FT)を使って、5V にプルアップ抵抗を付けて、オープンドレインにする。一部のピンは 5V がかかっても大丈夫に作られているようだ
  • NPN トランジスタでレベルシフトする

2番目の方法が簡単なので、次回はそちらにしようと思う。