4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

USBキーボードをファミリーベーシックのキーボードにする

Last updated at Posted at 2022-06-07

USBキーボードをファミリーベーシックのキーボードにする

以前、ファミリーベーシックのキーボードをUSBキーボードにする という記事を書いた。 ファミリーベーシックのキーボドを、USBキーボードとして使うというものだ。
これが動いたので、逆もやってみたくなるのが人情だ。というわけで、とうとうファミリーベーシックのカートリッジを入手した。(送料込みで¥4000)

また、ファミコンももうとっくの昔に捨ててしまっていたので、今回、ハードオフで500円で買ってきた。2chが映るテレビももうないので、USB動作、AVファミコン化した。

ファミリーベーシックのキーボードをUSBキーボードにする という記事も、ほとんどの人にとって役に立たないと思うが、USBキーボードをファミリーベーシックにつなぐ人はもっと少なそうだ。USBホストの処理は、USBデバイスの処理よりも複雑で規模も全然違う。

しかし、情報はEnri's Home PAGEにあり、ファミリーベーシックのカセットも、キーボードも、ファミコンもある。全部そろってしまったので仕方ない。

必要なもの

今回は部品が多いので、表にしてある。PC、PICの書き込み機器(PicKITなど)は別途必要になる。今回使用するマイコン、pic32mx270f256bは、mplab SNAPが使用できる。プログラムが大きいので、PICKIT3 だと転送に時間がかかる。mplab SNAP がお勧め

この部品表には、ファミコンのAV化部品は含んでいない。

一般部品

名称 デバイス名|
C1,C4,C5,C6 0.1μF セラミックコンデンサ
C2 47μF 電解コンデンサ
C3 10μF 電解コンデンサ
J1,J2,J3 1x1 ピンヘッダ(拡張用)
LED1,LED2 LED デバッグ用。家にあるのでOK
J4 1x10 2mmピッチピンヘッダ(ファミコン接続用)
P1 1x5 ピンヘッダ(ICSP)
P3 1x2 ピンヘッダ(5V電源用)
P4 1x4 ピンソケット(USBモジュール用)
R1,R2 300Ω カーボン抵抗。なんでもよい。家にあるのでOK
R3 10KΩ カーボン抵抗。なんでもよい。家にあるのでOK
u2 3.3vレギュレータ なんでもよい。例えばLP2950L-3.3Vなど

モジュール・CPUなど

名称 デバイス名|
PICマイコン pic32mx270f256b マイコン。秋月通商の販売ページ
USBコネクタDIP化キット AE-USB-A-DIP 秋月電子販売ページ
接続ケーブル キーボードとファミコンを接続するケーブル

デバッグ時に、ストロベリーリナックス製 i2c液晶を接続したが、ファミコンのキーボードスキャンやUSBのレポート送信は非常に高速で、I2Cで文字を出しているとまったく間に合わない。ほとんど役に立たなかった。回路上は残っているが接続しない。

  • 接続ケーブルは、単品販売がない。ファミリーベーシックのキーボードをUSBキーボードにする で作った際にキーボードから取り外したものを使用する。
  • 今回、マイコンは大容量のものが必要になる。(600円!!)USBキーボードを接続するため、USBのホストになる必要がある。このUSBホストスタックとFreeRTOSが、それだけで64Kbytesを占有してしまう。128KBytes版(350円)だとぎりぎり厳しい。
  • PICには、5Vトレラントの入力端子があるが、全部の信号を使うには足りない。本来レベル変換をすべきなのだろうが、素人工作なのでやらかなった。5Vトレラントには、ファミコン⇒PIC方向のリセット信号、セレクト信号を接続し、PIC⇒ファミコンに送るキースキャンデータは、3.3Vでそのまま使用した。きちんと作りたい人はこういうモジュールを使用して レベル変換すべきだと思う。

ソフトウェア

  • MPLAB X IDE (無料) ソースからビルドする場合。
  • MPLAB XC8 コンパイラ (無料) ソースからビルドする場合。ダウンロードとインストール方法はgoogleなどで検索するとたくさん出てくる。
  • [MPLAB X IPE](MPLAB X IDE (無料) バイナリの書き込みだけの場合。インストーラーは MPLAB X IDEと同じ。インストール中に Select Programs の部分で選択できる。

ファミコンとの接続

ファミコンの外部端子は、DSUB15ピンと微妙にサイズが違う。無理やりに接続することもできるが、今回はファミリーベーシックキーボードから外したケーブルを使用する。

開発中は、ユニバーサル基板に作った回路に、下の写真のように接続した。

KBケーブル(FC15ピン/2.0mピッチメス)--- 2.0mmピンヘッダ -- ユニバーサル基板(2.0mmコネクタ/2.5mmピンヘッダ)

キーボードケーブルは貴重なので、なるべく抜き差しでのダメージを抑えたい。そのため、2mmのピンヘッダを経由して接続している。2mmのピンヘッダはロング(連結ヘッダ)があればよかったのだが、秋月では売っていないようだ。そのため、普通のピンヘッダを細工して、ピンを同じ長さになるように押し込んで使用した。接続には問題ないようだ。

ユニバーサル基板は、FCの外部端子から、信号線6本と、5V/GNDを引き出している。 5VのラインはUSB用に使用する。マイコンは3.3V駆動なので、レギュレータで降圧して使用する。

ファミコンとの通信

Enri's Home PAGEでは、プロトコルについて書かれているが、
詳しいタイミングがわからない。

キーボードの認識

ファミリーベーシックは、キーボドを接続しないと起動しない。(手元のVer2.0。3.0では、何かメッセージが出るらしい)
DATA0~4のどれか1つでも、GNDになると起動する。はじめはこれがわからず、買ったカセットが不良かと思った。

キースキャン

下の図は、1回のキースキャン部分のもの。上段がRESET信号、下段がSELECT信号。

prot1.png

Enriさんのページにあるように RESET信号がストローブされ(カウンタがリセット)、その後、SELECT信号が繰り返されている。
SELECT信号の立下りで、カウンタが+1される。

キースキャンの繰り返しパターン

キースキャンの繰り返しパターンは、メニュー画面とBASICプロンプトが出ている状態で異なる。

メニュー画面

キースキャンは32m秒ごとに繰り返される。
prot2.png

BASICプロンプト

BASICプロンプトの表示状態の場合、キースキャンの間隔は非常に短くなる。

prot3.png

このパターンの場合、一般的なUSBキーボードのUSBレポートの間隔よりも、キースキャンの間隔の方が短くなる。 USBレポートは、キーの押下状態しか通知されない(キーのリリースは通知されない)ため、そのまま作ってしまうと問題が発生してしまう。ファミコンからのキーボドマトリックスの2回のスキャンの間に、1つもUSBレポートが来なかった場合、一回キーが離された、と誤認してしまうことになる。USBレポートが来たかどうか、の処理が必要になる。

ハードウェア

前回の、ファミリーベーシックをUSBキーボードにする、の記事では適当なCPUモジュール(USBやクロックなどが組み込まれたパーツ)があったが、今回のハードウェアで要求されるCPUにはそういうモジュールがない。そのため、クロックやキャパシタなども回路図に含める必要があり、部品点数が多くなってしまう。ただ、その分部品代は安くなる。

開発中の様子

開発に使った機材は、PCを除けば下の写真のものがすべてとなる。

dev.png

開発は、ユニバーサル基板で行った。今回、極薄のノンスルーホール基板を使ってみた。ノンスルーホールは、両面に部品を配置できるので、とても便利だった。非常に薄く、はさみで切れるのも助かった。が・・・、小さいうちは良いのだが、今回の場合部品点数が多くなり、裏側にスズメッキ線を付けていくと、基板が反ってきてしまうのが困った。ZIFソケットなど、重い部品を付けると、それだけで反ってしまう。
ノンスルーホールは便利だが、薄い基板は規模を考えると使いにくい。

白い箱はMPLab SNAP(リンクはマルツ。全部秋月だと回し者のように思われるので)。最近は半導体不足の影響か、品切れが増えている。私が購入したとき、3,800円くらいだったと記憶しているが、昔はもっと安かったらしい。

上中央はADALM2000 。簡易オシロスコープ、ロジックアナライザ、ネットワーク・アナライザなど多くの機能を持った学習用の計測器。ロジアナとしてOSC001 PCB Scopeというのを使っていたが、早い信号はほとんど取れず、記録できる時間も瞬間で調べがはかどらなかったが、これしか知らなかったので「こんなもんか」と思っていた。
YouTubeのイチケンさんのコンテンツで見て、買ってみることにした。実際使ってみると非常に便利。特に、ロジアナはプロトコルを指定するとそれにあわせて表示してくれる。(ロジアナ使いの人には当たり前なのだろうが…) i2cなどで正しい信号が出ているかを1バイトづつ確認していた時間がバカみたいだ。

ファミコンはAV化してある。ファミコンのAV化はリビジョンが新しいと非常に簡単。カセットのソケットに金属のカバーがあるとかなりの高確率で新しいリビジョンになる。

回路図とPCBデータ

全体の回路図は次のようになった。

sch1.png

見るべきものはほとんどない。PICの各ポートにコネクタをつないだだけ。すべての処理はソフトウェアで行われる。左下は電源。今回使用したPIC32MXシリーズは、3.3V定格で5Vでは使用できない。そのため、レギュレータで降圧してPIC側に提供している。5Vはそのまま、USB(キーボード)コネクタに供給される。

回路図では、クロックにセラロックを使用している。定数がないが、20MHzを使用する。

※ 繰り返しになるが、3.3VのGPIO(OUT)を、5Vのファミコン(IN)に直結している。問題ないと思うが万一、「商品化しよう!!」と思っている人がいたら、きちんとレベル変換してほしい。

KiCAD用のファイルは、github で公開している。
PCBは片面基板用で、表側の配線はジャンパを飛ばす。

PCBのサイズごとに2つ公開している。

55mmX57mm版。何も考えずに作ったもの。作った後、しっくりくるケースが見つからないことに気づいた。タカチだと、SW- 85くらいだが、かなり大きくなってしまう。また、入手も難しい。(私の場合、秋月で売っていない=入手が難しい、になる)
また、よくある生基板のサイズ、100x150だと、歩留まりが悪く2枚しかとれない。

45mmX60mm版。タカチのSW- 65(45x65x25)~SW-75(50x75x30)などに入るサイズ。SW65dだと、まったく余裕がないので、USBソケットなどの引き出しが大変だと思われる。aitendoだと、C80x50x20C80X50X25が適合サイズになる。100x150の生基板で4枚取れる。
また、手持ちのLP2950L-3.3Vがなくなったので、3端子レギュレータの部分のパターンを変更して、HT7133Aのように、GND-Vin-Vout順に並んでいるレギュレータを使用する場合のパターンを追加した。(今までは、Vout-GND-Vinのものだけしかパターンを用意していなかった)当然だが、どちらか片方だけつければよい。

完成した基板

完成した基板の様子は次のようになる(55x57mmサイズ)。ファミコンとの接続は、2mmピッチのピンヘッダを立てて、ファミコンのキーボードの線をそのまま接続した。緑の太い線は、本来はキーボード内部のキーボードユニットのシャシに設置されていたGNDだが、接続しない。

この基板も、0.3mm厚の生基板を使っているが、ユニバーサル基板と同様、基板が柔らかすぎて安定しない。特に、28PのCPUソケットなどを指すと、基板がたわんでしまう。穴あけも手でやっているので、精度が低く、そこに無理やり長い部品を差し込むと、部品に引っ張られて基板が歪んでしまう。はさみで切れる、という以外のメリットが全くない。

pcb2.png

下の写真は、開発中のユニバーサル基板で組んだものと、PCBで組んだもの。回路自体のフットプリントは実はあまり変わらない。開発中の基板との一番の違いはファミコンケーブルの接続方法。PCBでは、直接ファミコンケーブルが刺さるようにピン配置を合わせてある。
ユニバーサル基板で組んだものは、必要な信号だけをピンソケットで出しておき、ファミコンから来たケーブルはいったん変換基板で受け、2.54mmに変換してから必要な信号を取り出している。開発中、この接続がいちいち面倒だったので、ケーブルを直接させるようにしてある。

pcb1.png

USBは2.0mmピンソケットの上に、ピンヘッダで取り出してある。これを延長して、壁面取付用のUSB-Aメスソケットを接続する(予定)。

ソフトウエアの概要

ほとんどの処理はソフトウェアで行われる。

ファミコンへのデータ送信については、一般的なGPIOで行うのでMPLabXや、Harmonyなどライブラリのコーディング自体は(動くかどうかは別にして)難しくない。最初のステップでつまづくのは、USBホストスタックをどうやって実装するかだと思われる。 Harmonyをどうやって設定するか、生成されたコードのどこに何を追加するかがわからない、ということだと思う。(少なくとも私はそうだった)

ソースコードとバイナリは、github で公開している。 

Harmony 3 (MHC)プロジェクト

USBHostスタックは、MPLabX Harmony 3で標準で用意されているため、それを使用する。

Harmony3 の構成~クロック

Harmony 3で、すべての人が「便利」と思うのはこの機能ではないだろうか?(他は、Configuration bitsか)
ポイントは、USBへの供給クロックを48MHzにすること。こうしないと、USBは接続できない。

clk1.png

Harmony3 の構成~プロジェクトグラフ

Mplab Harmony3は今も頻繁に開発が続けられているようで、情報が錯綜しており使い方がよくわからない。(探し方が下手なだけか…)
ここでは、試行錯誤の結果行ったことを記すが、誤りや冗長な部分があると思われる。

MPLab Harmony Configuratior (MHC)での、プロジェクトグラフは次のようになる。実際にMHCを使わず、絵だけ見ていると「すげぇ、こうやってハードウェアの知識がなくても色々組めるんだ!」と思うかもしれないが、それは間違いだと思う。

ドキュメントは少なく、実際には作ったプロジェクトグラフから生成されるソースを見て、「あー、このオブジェクトはこうなってんのか」と考えていく必要がある。質が良いサンプルとドキュメントがたくさん用意されている方がありがたい。

mch1.png

プロジェクトグラフでは、Libraries > USB > Host Stack > Host Layerを追加する。これが、USBのホストスタックとなる。これを追加すると、芋づる式にいろいろなモジュールが追加され、最終的にFreeRTOSのプログラムが生成される。この、USBホストスタックに、ドライバとして接続するデバイスのドライバを追加していく。

今回のケースでは、キーボードを接続するため、HID Client Driverを追加する。Host Layerで Consumerに指定(下図)しても良いし、左側のLibrary > USB > Host Stack > HID Client Driverを追加した後に、SatisfierにHost Layerを追加しても良い。

mch2.png

USBキーボードを接続するためのHost Layerと HID Client Driver のプロパティは次のようになる。

mch3.png

生成されるソースでは、USBの処理が1つのアプリケーション(FreeRTOSのプロセス)に割り当てられることになる。このプロセスは、Coreモジュールに定義されている。下の図は、自動生成されたapp1(USBの処理)と、ファミコンからの信号処理用に app2 というタスクを追加した状態。スタックサイズは微妙で、少なすぎると実行時に例外が発生するし、多すぎるとビルドに失敗する。

mch4.png

また、FreeRTOSモジュールでは、ヒープサイズや使用する関数などを指定しておく必要がある。特に、ヒープサイズも微妙で、大きすぎるとコンパイルに失敗する。ユーザーアプリケーションでヒープは使わない、と決めても、OSやUSBスタックが使用するフットプリントがあるため、ゼロにできるわけではない。
また、vTaskDelayは有効にしておく。RTOSでは、待ち時間を無限ループで組むことはマナー違反。別のタスクにCPU資源を渡して、OS側でタスクスケジューリングから一定時間外してもらう必要がある。そのための関数で、vTaskDelayを使用する。ここで指定したTick の間、タスクにはCPU資源が回ってこない。 Tickの値は、このプロパティで Tick Rateとして定義されている。

mch4.png

その他、Harmony 3では細かいプロパティの調整が必要になる。

Harmony3 の構成~ピンアサインと割り込み指定

ピンアサイン

ピンのアサインでは、次のピン定義を行った。

ピンID 用途 備考
RA0/RA1 ICSP用
RB0~RB3 DATA4~1 FCのD4~D1に接続。接続ケーブルでは4~7番
RB4、RA4 SDA/SCL I2C液晶用
RB7 SELECT (5Vポート) FCのOUT1に接続。接続ケーブルでは11番。ポート変更割り込み有効
RB8 RESET (5Vポート) FCのOUT0に接続。接続ケーブルでは12番。ポート変更割り込み有効
RB13、RB15 LED 接続状態表示用(とデバッグ用)のLED|

※FC接続ケーブルのピン配置

con5.png

実際の設定は次の通り。

mch4.png

割り込み設定

RB7とRB8(SELECTとRESET信号)では、ポート変更の割り込みを指定してある。それに伴い、Systemのプロパティで、Enable Change Notice interruptを有効にする。

mch5.png

USBホストのスケルトン

ソースコードを生成すると、USBホストスタックのライブラリを組み込んだソースコードが生成されるが、これだけでは何もできない。Harmonyが生成したタスクの中に、USBホストとしての機能を実装していく必要がある。

USB hostのサンプルソースは、 <ライブラリのディレクトリ>\Libraries\apps\usb\host\hid_keyboard\firmware\demo_src に存在するが、これはUSキーボードだったりと、色々使いにくい。そこで、YS電子工作ラボさんのサンプルソースを流用する。キーボードを接続し、USBレポートを読み取るだけのスケルトンプログラムは次のようになる。

生成された、app1.h、app1.cをこの内容に入れ替える。

このスケルトンでは、コメントにしてあるが、RB13のLEDが、USBキーボードが接続されたら点滅するようになっている。

ヘッダファイル(app1.h)

#ifndef _APP1_H
#define _APP1_H

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include "configuration.h"
#include "usb/usb_host_hid_keyboard.h"
#include "driver/driver_common.h"
// DOM-IGNORE-BEGIN
#ifdef __cplusplus  // Provide C++ Compatibility

extern "C" {

#endif

typedef enum {
    /* Application pixels put*/
    APP1_STATE_INIT=0,
    APP1_STATE_OPEN_HOST_LAYER,
    APP1_STATE_WAIT_FOR_HOST_ENABLE,
    APP1_STATE_HOST_ENABLE_DONE,
    APP1_STATE_WAIT_FOR_DEVICE_ATTACH,
    APP1_STATE_DEVICE_ATTACHED,
    APP1_STATE_READ_HID,
    APP1_STATE_DEVICE_DETACHED,
    APP1_STATE_CHANGE_DEVICE_PARAMETERS,
    APP1_USART_STATE_DRIVER_OPEN,
    APP1_USART_STATE_CHECK_FOR_STRING_TO_SEND,
    APP1_USART_STATE_DRIVER_WRITE,
    APP1_STATE_ERROR
    } APP1_STATES;

// これは今回使用しないが、一応残しておく
typedef struct
{
    /* Application last data buffer */
    USB_HOST_HID_KEYBOARD_DATA data;

} APP1_DATA_LAST_DATA;

typedef struct
{

    /* USB Application's current state*/
    APP1_STATES state;
    /* USART Application task state */
    // APP1_STATES usartTaskState;          
    /* Unique handle to USB HID Host Keyboard driver */
    USB_HOST_HID_KEYBOARD_HANDLE handle;
    /* Unique handle to USART driver */
    DRV_HANDLE usartDriverHandle;
    /* Number of bytes written by the USART write*/
    uint32_t nBytesWritten;
    /* Size of the buffer to be written */
    uint32_t stringSize;
    /* Buffer used for USART writing */
    uint8_t string[64];
    /* Holds the current offset in the string buffer */
    uint16_t currentOffset;
    /* Flag used to determine if data is to be written to USART */
    bool stringReady;
    /* Flag used to select CAPSLOCK sequence */
    bool capsLockPressed;
    /* Flag used to select SCROLLLOCK sequence */
    bool scrollLockPressed;
    /* Flag used to select NUMLOCK sequence */
    bool numLockPressed;
    /* Holds the output Report*/
    uint8_t outputReport;
    /* Application current data buffer */
    USB_HOST_HID_KEYBOARD_DATA data;
    /* Application last data buffer */
    APP1_DATA_LAST_DATA lastData;
} APP1_DATA;

void APP1_Initialize ( void );
void APP1_Tasks( void );

#ifdef __cplusplus
}
#endif
#endif 

ソースコード (app1.c)

#include "app1.h"
#include <string.h>
#include <stdlib.h>
#include "FreeRTOS.h"
#include "task.h"

// 動作確認のためのLED
//#include "config/Test4default/peripheral/gpio/plib_gpio.h"


APP1_DATA app1Data;


// USBホストのイベントハンドラ
USB_HOST_EVENT_RESPONSE APP_USBHostEventHandler (USB_HOST_EVENT event, void * eventData, uintptr_t context)

{
        
    switch(event) {
        case USB_HOST_EVENT_DEVICE_UNSUPPORTED:{
            break;
        }
        default: {
            break;
        }
    }
    return USB_HOST_EVENT_RESPONSE_NONE;
}


// USB キーボードのイベントハンドラ
void APP_USBHostHIDKeyboardEventHandler(USB_HOST_HID_KEYBOARD_HANDLE handle, 
                                        USB_HOST_HID_KEYBOARD_EVENT event, void * pData)
{   

    switch (event) {

        case USB_HOST_HID_KEYBOARD_EVENT_ATTACH:{
            app1Data.handle = handle;
            app1Data.state =  APP1_STATE_DEVICE_ATTACHED;
            app1Data.nBytesWritten = 0;
            app1Data.stringReady = false;
            memset(&app1Data.string, 0, sizeof(app1Data.string));
            memset(&app1Data.lastData, 0, sizeof(app1Data.lastData));
            app1Data.stringSize = 0;
            app1Data.capsLockPressed = false;
            app1Data.scrollLockPressed = false;
            app1Data.numLockPressed = false;
            app1Data.outputReport = 0;
            break;
        }
        case USB_HOST_HID_KEYBOARD_EVENT_DETACH:{
            app1Data.handle = handle;
            app1Data.state = APP1_STATE_DEVICE_DETACHED;
            app1Data.nBytesWritten = 0;
            app1Data.stringReady = false;
            //app1Data.usartTaskState = APP1_USART_STATE_CHECK_FOR_STRING_TO_SEND;
            memset(&app1Data.string, 0, sizeof(app1Data.string));
            memset(&app1Data.lastData, 0, sizeof(app1Data.lastData));
            app1Data.stringSize = 0;
            app1Data.capsLockPressed = false;
            app1Data.scrollLockPressed = false;
            app1Data.numLockPressed = false;
            app1Data.outputReport = 0;
            break;
        }
        case USB_HOST_HID_KEYBOARD_EVENT_REPORT_RECEIVED:{
            app1Data.handle = handle;
            app1Data.state = APP1_STATE_READ_HID;
            /* Keyboard Data from device */
            memcpy(&app1Data.data, pData, sizeof(app1Data.data));
            break;
        }
        default:
            break;
    }
    return;
}


// FreeRTOSの初期化処理

void APP1_Initialize ( void )
{
    // ステートマシンを初期状態に設定する
    app1Data.state = APP1_STATE_INIT;
}
// USBキーボードから受け取ったキーコードを保存する変数(このスケルトンでは使用しない)
volatile USB_HID_KEYBOARD_KEYPAD keyCode;

// RTOSのタスク
void APP1_Tasks ( void )
{
   // USB関連処理のステートマシン
    switch ( app1Data.state ) {
        // 初期状態。USBホストスタックの初期化処理を行う
        case APP1_STATE_INIT:{
            USB_HOST_EventHandlerSet(APP_USBHostEventHandler, 0);
            USB_HOST_HID_KEYBOARD_EventHandlerSet(APP_USBHostHIDKeyboardEventHandler);
			USB_HOST_BusEnable(0);
			app1Data.state = APP1_STATE_WAIT_FOR_HOST_ENABLE;
            break;
        }
        // USBホストが有効になるのを待つ
		case APP1_STATE_WAIT_FOR_HOST_ENABLE:{
            if(USB_HOST_BusIsEnabled(0)) {
                // USBホストが有効になったら、次の状態に移る
                app1Data.state = APP1_STATE_HOST_ENABLE_DONE;
            }
            break;
        }
        // USBホストが有効になったら、デバイスの接続待ち状態に移る
        case APP1_STATE_HOST_ENABLE_DONE: {

            app1Data.stringSize = 64;
            app1Data.state = APP1_STATE_WAIT_FOR_DEVICE_ATTACH;
            break;
        }
        // デバイスの接続待ち。
        case APP1_STATE_WAIT_FOR_DEVICE_ATTACH: {
            // USBキーボードの接続待ち。この状態は APP_USBHostHIDKeyboardEventHandler 内で解決される。
            break;
        }
        // デバイスが接続されたら、HIDデバイスからのレポート読み込みを待つ
        case APP1_STATE_DEVICE_ATTACHED: { 

            // デバイスが接続された
            app1Data.stringReady = true;
            app1Data.stringSize = 64;
            // GPIO_RB13_Set();                // テスト用にLEDを点灯させる
            break;
        }

        case APP1_STATE_READ_HID:{ 
            // キーボードからレポートが送信された。レポートはキー入力が無くても定期的に送信されるので、
            // ここに入ったからと言って必ずしもキーが押されたとは限らない
            uint8_t count = 0;

            // レポート内には、6つのキー押下情報を含めることができる。修飾キー(SHIFTなど)は別
            for(count = 0; count < 6; count++) {
                if(app1Data.data.nonModifierKeysData[count].event == USB_HID_KEY_PRESSED) {
                    // キーが押されていたら、LEDを点滅させる
                    //GPIO_RB13_Toggle();
                    // キーコードを取得する
                    keyCode = app1Data.data.nonModifierKeysData[count].keyCode;
                }
            }
            break;
        }

        case APP1_STATE_DEVICE_DETACHED:
            //GPIO_RB13_Set();                // デバイスが切断されたらLEDは消灯
            app1Data.state = APP1_STATE_HOST_ENABLE_DONE;
            break;
        case APP1_STATE_ERROR:
            // ここに来ることは想定していない。
            
            break;
        default:
            break;
    }

}


プログラム詳説

実際に、作成されたソフトウェアについて、ソースコードを読むうえでわかりにくいと思われる部分について説明する。

HID Reportの構造

USBキーボードからの情報は、HID Report という情報でやり取りが行われる。HID Reportは、HID Report Descriptorという構造になっている。 

Human Interface Devices (HID) Information
Device Class Definition for Human Interface Devices (HID)
ヒューマン インターフェイス デバイス (HID) の概要

ただ、多くの面倒な処理はMHC内でやってくれる。今回のコードでは、app1.hの中にAPP1_DATA```という型が定義され、app1.cの中では、APP1_DATA app1Data;としてタスク固有の情報が保存されている。 この中にある、USB_HOST_HID_KEYBOARD_DATA;が、実質的なHID Reportの内容になる。(この外側には、インデックスやレポートIDなどを含むエンベロープがあるが、これらが剝がされてデータ部分だけがユーザプログラムに到着する。)

この、USB_HOST_HID_KEYBOARD_DATAは、<プロジェクトフォルダ>/config/default/usb/usb_host_hid_keyboard.h に定義されている。関連する部分を抜き出すとこうなる。

HIDキーボードレポートの定義ファイル ソースコード(usb_host_hid_keyboard.h)

// *****************************************************************************
/* USB Host HID Keyboard Non Modifier Keys Data Object

  Summary:
    Defines the USB Host HID Keyboard Non Modifier Keys Data Object.

  Description:
    Defines the USB Host HID Keyboard Non Modifier Keys Data Object.

  Remarks:
    None.
*/

typedef struct
{
    /* USB_HID_KEY_EVENT will determine if the key has been pressed or released.
     * On key press, event will be USB_HID_KEY_PRESS. On key release
     * USB_HID_KEY_RELEASE will be notified as event value. */
    USB_HID_KEY_EVENT  event;
    /* The usage value of the key for which this event is notified of. */
    USB_HID_KEYBOARD_KEYPAD keyCode;
    
    uint64_t sysCount;
    
} USB_HOST_HID_KEYBOARD_NON_MODIFIER_KEYS_DATA;


typedef struct
{
    uint8_t leftControl :1;
	uint8_t leftShift :1;
	uint8_t leftAlt :1;
	uint8_t leftGui :1;
	uint8_t rightControl :1;
	uint8_t rightShift :1;
	uint8_t rightAlt :1;
	uint8_t rightGui :1;
    
} USB_HID_KEYBOARD_MODIFIER_KEYS_DATA;

// *****************************************************************************
/* USB Host HID Keyboard Data Object

  Summary:
    Defines the USB Host HID Keyboard Data Object.

  Description:
    Defines the USB Host HID Keyboard Data Object.

  Remarks:
    None.
*/

typedef struct
{
    /* This holds the key state for all modifier Keys Data. On key press,
	 * event will be USB_HID_KEY_PRESS. On key release USB_HID_KEY_RELEASE
	 * will be notified as event value. */
	USB_HID_KEYBOARD_MODIFIER_KEYS_DATA modifierKeysData;
	/* This value determines how many instances of nonModifierKeysData[]
	 * is valid. */
    size_t nNonModifierKeysData;
	/* This holds all the non modifier keys state that has been changed
     * from the last time USB_HOST_HID_KEYBOARD_EVENT_REPORT_RECEIVED event
	 * was notified. If a non modifier key has been kept pressed
	 * continuously, that key will also be reported here. */
	USB_HOST_HID_KEYBOARD_NON_MODIFIER_KEYS_DATA nonModifierKeysData[6];

} USB_HOST_HID_KEYBOARD_DATA;

一つ注意が必要なのが、この生成されるソースの次のコメントは必ずしも正確ではないということだ。

    /* USB_HID_KEY_EVENT will determine if the key has been pressed or released.
     * On key press, event will be USB_HID_KEY_PRESS. On key release
     * USB_HID_KEY_RELEASE will be notified as event value. */

Device Class Definition for Human Interface Devices (HID)によると

Non-modifier keys must be reported in Input (Array, Absolute) items. Reports must contain a list 
of keys currently pressed and not make/break codes (relative data)

となっており、「一般キーキーは押下状態を通知するが、メイク/ブレイク(オン/オフ)コードではない」とのことで、一般キーは、USB_HID_KEY_RELEASE は通知されない。押されている間、USB_HID_KEY_PRESSが連続して送信されるので、メイク/ブレイクを直接知ることができない。キーのメイク/ブレイクについては後で述べる。

HID Reportの処理

前述のコードでは、キーボードから HID Reportが送られると、 case APP1_STATE_READ_HID:{ の部分が呼び出されるので、このcaseの中で、今回来たキーコードを処理することになる。

完成したプログラムでは、app1.cの中で次のような処理を行っている。

キーボードレポートの処理 ソースコード(app1.c)

      :
      :
KeyInfo kiMod;
bool setUnset;
kiMod = findKey(0xE6);
setUnset = app1Data.data.modifierKeysData.rightAlt;

if (setUnset) keyMap[0][kiMod.Base.row] |= kiMod.Base.colmn; else keyMap[0][kiMod.Base.row] &= ~kiMod.Base.colmn;

kiMod = findKey(0xE2);
setUnset = app1Data.data.modifierKeysData.leftAlt;
if (setUnset) keyMap[0][kiMod.Base.row] |= kiMod.Base.colmn; else keyMap[0][kiMod.Base.row] &= ~kiMod.Base.colmn;
            :
            :
            :
for(count = 0; count < 6; count++) {
    // Non-modifier-key
    if(app1Data.data.nonModifierKeysData[count].event == USB_HID_KEY_PRESSED) {
        KeyInfo ki =  findKey(app1Data.data.nonModifierKeysData[count].keyCode);
        keyMap[0][ki.Base.row] |= ki.Base.colmn;
    } 
}
UpdateUSBKeyReport();
            :
            :
            :

前半のif が並んでいる部分が修飾キーの処理で、次のような処理が連続している。

(1) kiMod = findKey(0xE1);
(2) setUnset = app1Data.data.modifierKeysData.leftShift;
(3) if (setUnset) keyMap[0][kiMod.Base.row] |= kiMod.Base.colmn; else keyMap[0][kiMod.Base.row] &= ~kiMod.Base.colmn;

これは、0xE1(左SHIFT)の場合の処理で、findkey (keyCodeDef.c内)で、(1) 左SHIFTがファミリーベーシックキーボードのマトリックス上、どの行のどのビットかを求め、(2)レポートディスクリプタでキーの押下状態を求め、(3)でファミリーベーシックキーボードのマトリックスのビットを操作している。

これ以降の処理は、ファミコンのプロトコルとUSB HIDキーボードのタイミングの差を吸収するための処理になる。キーのメイク/ブレーク判定方法で詳しく解説した。
FC側キースキャンが遅すぎる場合の対応
FC側キースキャンが早すぎる場合の対応
を事前に参照のこと。

後半のforループが、一般のキー処理で、USB_KEY_PRESSEDがあった場合、そのキーコードに対応するマトリックスの位置をfindkey (keyCodeDef.c内)で求め、キーマップに反映している。
マトリックス操作について、ビットを立てるが、KEY_PRESSがなくてもビットを下ろしていない。このままだと、Aが押されて、離されてもキーボードマトリックス上、Aが押しっぱなしとなってしまう。

これは、今回のプログラムでの特徴で、ファミコン側からのキースキャンのタイミングが非同期で、時には32msと非常に長いこともある。そのため、キースキャンとキースキャンの間で、キーがメーク/ブレークされてしまうと、ファミコン側でそのキーを取りこぼしてしまう。修飾キーでは取りこぼしても問題ないが、一般キーではキーの取りこぼしはあってはならない。
そのため、キーブレークのタイミングはファミコンからのキースキャンに合わせて行うようになっている。

最後にある、UpdateUSBKeyReport(keyCodeDef.c内)は、キーレポートが行われたカウンタ。キーブレークのタイミング制御のために用意されている。ファミコン側のキースキャンの間隔は長いだけでなく、1.5mSと短いこともある。そのため、キースキャン中に一度もUSBレポートが来ないことがある。すると、本来押しっぱなしのはずのキーが、ブレークされた、と誤認する可能性が出てくる。そのため、キースキャン中に一度もUSBレポートが来なかった場合、キースキャンを空打ちさせて前回と同じ状態として扱うようになっている。

キーマトリックスの生成

ファミコンは、下のようなマトリックスを順番にスキャンして、キーボド入力を受け取る。Enri's Home PAGEは、;と:のキーが逆になっているかもしれない。私の手元にあるキーボドでは、下の表のようになっている。

 
行1 F8 RETURN [ 「
 ロ 
] 」
 °
カナ  右
シフト

STOP
行2 F7  @ 
 レ 
: *
 _
; +
 モ
 ␣ 
 ン
/ ?
 ヲ
- =
 ラ
^  
 リ
行3 F6 O ペ
 ヘ 
L  
 メ
K  
 ム
 < 
, ョ
 ヨ 
 > 

 ワ
P ポ
 ホ

 ノ
行4 F5 I プ
 フ 
U ピ
J 
  ミ
M ュ
 ユ
/N ャ
 ヤ
 )

 ネ
 (

 ヌ
行5 F4 Y パ
 ハ 
G  
 ソ
H  
 マ
B  
 ト
V  
 テ
 ’
7  
 ニ
 &
6  
 ナ
行6 F3 T  
 コ 
R  
 ケ
D  
 ス
F  
 セ
C ッ
 ツ
 %
5 ォ
 オ
 $
4 ェ
 エ
行7 F2  W 
 キ 
S  
 シ
A  
 サ
X  
 チ
Z  
 タ
E  
 ウ
 #
3 ゥ
 ウ
行8 F1  ESC Q  
 カ
 CTR  左
シフト
GRPH  !
1 ァ
 ア
 ”
2 ィ
 イ
行9 CLR
HOME
 ↑  →  ←  ↓ スペース DEL INS

ファミコンがキーマトリックスを読み出す方法は次のようになる。

  1. RESET信号をストローブする。行カウンタを0にする。
  2. SELECT信号をLにする。行カウンタを+1にして、これから読み出すデータが1行目であることを示す。
  3. DATA1~4で、行カウンタの行の0ビット目~3ビット目の下位4ビットを読み出す。
  4. SELECT信号をHにする。
  5. DATA1~4で、行カウンタの行の4ビット目~8ビット目の上位8ビットを読み出す。
  6. 2.に戻る、を、すべての行(9行目)までくりかえす。

このプロトコルに従い、RESET/SELECTの信号の状態に応じて、DATA1~DATA4ポートに値を出力すればよい。出力するデータを示す、このマトリックスは、プログラム上でバイト(uint8_t)の配列として実装されている。KeyCodeDef.c では、次の配列を用意している。

キーボードのマトリックステーブル定義 ソースコード(KeyCodeDef.c)

uint8_t keyMap[2][10];              // キーマップ。ダブルバッファにする。

1バイト(uint8_t)が9列あるので、単に [9] の配列があればよいが、実際には[10]の配列を2セット用意している。[9]にしなかった理由は特にない。なんとなく[0]の部分を「なにも押されていない状態」として用意してみたが、特に使わなかった。2セット用意しているのは、このバッファをダブルバッファにしているため。

受信したキーボードからのキー情報は、KeyCodeDef.c / KeyCodeDef.hでマトリックスに展開される。
KeyInfo findKey(uint8_t keyCode) 関数は、USBキーコード(HID レポートで通知される)を引数に受け取り、この関数は、CVTTable構造体のテーブルを参照して何行目の何ビット目にあたるかを返す関数。 CCTTable構造体は次のようになっている。

キーコードの変換テーブル ソースコード(KeyCodeDef.c)

static struct CVTTable{
    uint8_t keyCode;
    uint8_t Row;
    uint8_t Column;
} cvt[2][122] = 
{
    {
     :
     :
        {0x04,7,0b00010000},//  31 A   
        {0x05,5,0b00001000},//  50 B   
        {0x06,6,0b00000100},//  48 C   
        {0x07,6,0b00010000},//  33 D   
  :

例えば、USBのキーコード 0x04 は[A]キーで、これがキーボドマトリックスでは7行目の第4ビット(0b00010000)に相当する。これがキーの数分並んでいる。つまり、このテーブルを書き換えれば、自由にキーマッピングを変更できる。キーテーブルは2つもち、F11とF12 (ファミコンキーボードには存在しない)で切り替えられるようになっている。

USBのキーコードから、この変換テーブルを検索し、マトリックスの行とビット列がわかったら、これを内部に配列keyMapとして用意したマトリックスに書き込んでいく。

USBのキー押下によるマトリックスへの書き込みと、ファミコンからのデータ読み込みが非同期に行われる。 そのためバッファが競合するのを避け、ファミコンからのデータ読み込みの終了時に行われるマトリックスのクリア(キーボードのブレークとみなす)により、新しく来たキーボードの押下信号がクリアされてしまうのを避ける。

KeyMapがダブルバッファになっているのはこの競合が発生するのを避けるため。配列[0]側にUSBレポートからのキーボードマトリックス更新、配列[1]側はファミコンからのリクエストに基づいてデータの送信に使用する。データの転送開始時(RESET信号のストローブ)に、[0]から[1]にデータをコピーして[0]側をクリアする。(キーボードのブレーク処理)

当初、この部分はダブルバッファではなく、シングルバッファでセマフォを使って競合を避けていた。しかし、こうするとキーボードのスキャン中、マトリックスへのアクセスが待たされることになり、その間に来たUSBレポートが失われてしまう。
セマフォ制御をせずに競合に対して対処しないと一見問題がなくなるが、今度はキーボードのスキャンが早く(1.5mS)なった時に問題が発生する。キーボードのスキャン中にUSBレポートでキーの押下が発生すると、キーボードスキャン終了時に行われるバッファクリアによりキーの押下が消されてしまう。結果として、キーを押し続けているにも関わらず、一瞬だけキーが離されるような状態となってしまう。それでも一般キーでは特に問題が発生しないのだが、ファンクションキーやRETRUNキーの場合、素早く離して再度押す、という処理をすると、ファミリーベーシックが誤動作(BREAKと画面に表示されコマンドが実行されない)する。
タイミング問題とその対応については、別途後述する。

関連する処理は、app1.c、KeyCodeDef.c、app2.cにまたがって実装されている。あまり良いコーディングではない。試行錯誤が多かったのでこうなってしまった。

USBのHIDレポートを受け取ると、(1)で、バッファ[0]のマトリックスを更新する。

キーボードレポートからマトリックスを更新 ソースコード(app1.c)

for(count = 0; count < 6; count++) {
    // Non-modifier-key
    if(app1Data.data.nonModifierKeysData[count].event == USB_HID_KEY_PRESSED) {
         KeyInfo ki =  findKey(app1Data.data.nonModifierKeysData[count].keyCode);
(1)      keyMap[0][ki.Base.row] |= ki.Base.colmn;
    } 
    :
    :

ファミコンからのキースキャンが開始(RESET信号の立下り)を検知すると、(2)でSwapKeyMapを呼び出して、キーバッファの[0]を[1]にコピーする。

RESETを受け取るとマトリックステーブルを切り替える ソースコード(app2.c)

static void RESET_Handler(GPIO_PIN pin, uintptr_t context)
{
    if (RESET_Get()==0) {
        kvWk  = 0;

        //LED_GREEN_Set();
(2)        bool isSwapped = SwapKeyMap();                           // バックで更新されたキーテーブルを読み出しようにフロントに持ってくる
        if (isSwapped == false) {
            //LED_ORANGE_Toggle();
        }
        KeyIdxReset();                          // キーのインデックスを0にする
        isStartRound  = true;

SwapKeyMapでは、[0]を[1]にコピーして、[0]をクリアする。

マトリックステーブルの切り替え処理 ソースコード(keyCodeDef.c)

bool  SwapKeyMap(void)
{
    if (USBReportCount == 0) return false;;
    
    for (uint8_t i=1;i<=9;i++) {
        keyMap[1][i]=keyMap[0][i];
        keyMap[0][i] = 0;
    }
    clearCnt++;
    USBReportCount = 0;
    return true;
}

キーのメイク/ブレーク判定方法

前述のとおり、HIDレポートは、キーのリリース情報が来ない。そのため、レポート内にキーの情報が最初に含まれた時点をキーのメイク、キー情報が含まれなくなった時点でキーのブレイクと判断するのが一般的な方法になる。HIDレポートの処理では、あるキーについてUSB_KEY_PRESSが新しく入ったらメーク、前回あったUSB_KEY_PRESSがなくなったらブレーク、とすればよい。

キーはHIDレポートに含まれて送られるので、実際のメイク/ブレイクとUSBホストが認識するメイク/ブレイクには若干の違いがあるが、ほとんどの場合、問題にならない。HIDレポートは、キーボードの場合、最長でも定められたアイドルレート間隔で定期的に送信する必要がある。(アイドルレートはホストから設定可能)

そのため、[A]キー、[B]キーが実際に押された後、押し続けられて何もイベントが発生しない場合でも定期的に送信を続けるし、すべてのキーが押されなくなって何も起こらなくても送信は続けられる。

図:標準的な方法でのメイク/ブレイク判定
makebrk.png

この標準的な方法を使ってメイク/ブレイクを判定し、キーボードマトリックスにフラグをセット/リセットするわけだが、今回のように一定間隔でファミコンからキーマトリックスがスキャンされるという場合、この方法ではうまくいかない。

FC側キースキャンが遅すぎる場合の対応

ファミコン側のスキャン間隔がUSBのレポート間隔よりも長い場合、2回のキースキャン中にメイク/ブレイクが入ってしまうと、キースキャン2では、キーが押されたことを知ることができない。結果として、キーを取りこぼすことになってしまう。

図:標準的な方法でメイク/ブレイクを処理するとキーを取りこぼす
tim3.png

そのため、このプログラムでは、USBレポートにキー押下が含まれなくなっても、キーのブレイクとはみなさない。キーのブレイクは、キースキャン2で、[A]キーの押下を読み取った後で行われる。

図:キー押下なしをブレイクとみなさなければキーを取りこぼさない
tim4.png

このファミコン側からのキースキャン時にキーがブレイクされる、「みなしブレイク」は、キーが押しっぱなしの場合でも処理を変えない。キーが押しっぱなしの場合、キースキャン2により、みなしブレイクとなるが、HIDレポートは十分時間が短いので、すぐに次のキーのメイクが発動される。結果的に、キースキャン2、もその次のキースキャン3も、[A]キーが押された、という情報をファミコン側に送ることになる。

図:キーが押し続けれられた場合も特別な処理は不要
tim5.png

USBのHIDレポートを受け取ると、(1)でキーマトリックスを更新するが、このとき |= として既存のフラグは落とさない。

キーマトリックスの更新では既存のフラグは保持 ソースコード(app1.c)

      case APP1_STATE_READ_HID:{                  // レポートが来た
            :
            for(count = 0; count < 6; count++) {
                // Non-modifier-key
                if(app1Data.data.nonModifierKeysData[count].event == USB_HID_KEY_PRESSED) {
                    KeyInfo ki =  findKey(app1Data.data.nonModifierKeysData[count].keyCode);
(1)                    keyMap[0][ki.Base.row] |= ki.Base.colmn;
                } 

ファミコン側からのリセット信号を受け取ると、キースキャンを開始する。この時、(2)でデータの切り替え処理を行う。

リセット信号でキーバッファを切り替える ソースコード(app2.c)

static void RESET_Handler(GPIO_PIN pin, uintptr_t context)
{
    if (RESET_Get()==0) {
        kvWk  = 0;

        //LED_GREEN_Set();
(2)        bool isSwapped = SwapKeyMap();                           // バックで更新されたキーテーブルを読み出しようにフロントに持ってくる
        if (isSwapped == false) {
            //LED_ORANGE_Toggle();
        }
        KeyIdxReset();                          // キーのインデックスを0にする
        isStartRound  = true;
        SelectCount = 0;
        uint8_t SelVal = SELECT_Get();          // 最初の(1行目の)データをバスに出力する

SwapKeyMapでは、(3)で、今まで(1)が |=で更新を続けてきたバッファを読み取り側に移し、(4)でマトリクスをクリアする。これにより、キーが押されていない状態に変化させる。
バッファ切り替え処理でマトリックスをクリア ソースコード(keyCodeDef.c)

static int8_t clearCnt = 0;
bool  SwapKeyMap(void)
{
    if (USBReportCount == 0) return false;;
    
    for (uint8_t i=1;i<=9;i++) {
(3)     keyMap[1][i]=keyMap[0][i];
(4)     keyMap[0][i] = 0;
    }
    clearCnt++;
    USBReportCount = 0;
    return true;
}

FC側キースキャンが早すぎる場合の対応

USBレポートに比べて、ファミコン側のキースキャンの間隔が十分長いのが下のような状態。たとえば、Aキーを押し続けた場合、キースキャンとキースキャンの間に、[A]キー押下が送信される。キースキャン[2]で、マトリックスの情報がクリアされるが、キースキャン3の前に、[A]キー押下のレポートが来るため、キースキャン3でもAキーが押されたと判断される。結果として、キースキャン2~3で、Aキーは押し続けられた、と判断される。ファミリーベーシックの、メニュー画面などは、キースキャン間隔が長いので、この状態となる。

図:キースキャン間隔が長ければ、キーを押し続けることを正しく認識できる

tim1.png

BASICのプロンプトが出ると、キースキャンの間隔が短くなる。すると、キースキャンの間に、USBのレポートが一つも来ない、という状態が発生するようになる。
下の図では、キースキャン3と4の間に、HIDレポートが到着していない。すると、キースキャン4の時には、何もキーが押されていない、という情報がファミコン側に通知されてしまう。結果として[A]キーを、キースキャン3と4の間の非常に短い間だけ離し、そしてすぐに押した、という状態になってしまう。

図:キースキャン間隔が短いと、キーがいったん離されたと判断されてしまう

tim2.png

キーが一瞬離されても、押しっぱなしでもたいした違いがない、と思ったのだが実際はそうでもなかった。一般のキー(キーリピートするキー)では問題がないが、キーリピートしない、ファンクションキーやENTERキーの場合、このように非常に素早く押したり離したり、という動作をすると、ファミリーベーシックが誤動作して、画面にBREAKと出力してコマンドが実行されない。

対処方法としてはシンプルで、キースキャンとキースキャンの間に、一度もHIDレポートが来なかったら、そのキースキャンでは、前回と同じキーマトリックスを出力するようにした。HIDレポートでは、キーが押し続けられていれば、必ずそのキーがレポートに含まれる。そのため、レポートが来たか、来なかったか、という判断で構わない。

USBのHIDレポートを受け取ると、APP1_STATE_READ_HIDの最後で、UpdateUSBKeyReport (keyCodeDef.c)を呼び出して、HIDレポートの到着をインクリメントする。

キーボードレポートを受け取るとレポート到着数を増やす ソースコード(app1.c)

      case APP1_STATE_READ_HID:{                  // レポートが来た
            :
            UpdateUSBKeyReport();
           //LED_ORANGE_Clear();

UpdateUSBKeyReport()では、HIDレポートの到着数を増やす。 ファミコンからのキースキャンを開始するときに呼び出される、バッファの切り替え処理 SwapKeyMapでは、(2)で、HIDレポートが来ていなければ何もせずに戻る。これにより、ダブルバッファが切り替わらないので、ファミコン側には前回と同じ情報が送信されることになる。
USBReportCountが0ではない場合、バッファの切り替えを行い、(3)でHIDレポートのカウンタをクリアする。

キーボードレポートが到着していないときは空打ちさせる ソースコード(keyCodeDef.c)

void UpdateUSBKeyReport()
{
(1)    USBReportCount++;    
}

static int8_t clearCnt = 0;
bool  SwapKeyMap(void)
{
(2) if (USBReportCount == 0) return false;;
    
    for (uint8_t i=1;i<=9;i++) {
        keyMap[1][i]=keyMap[0][i];
        keyMap[0][i] = 0;
    }
    clearCnt++;
(3)    USBReportCount = 0;
    return true;
}

ファミコンへのキーボード情報送信

一連の処理で、キーボードレポートをキーマトリックスの配列に変換することができた。これを、ファミコンに送信することになる。

ファミコンに送るデータは、RESET信号とSELECT信号で制御される。ルールは次の4通り。

  1. RESET信号が0→1に立ち上がった時に、行カウンタはリセットされる
  2. SELCT信号が0の時は、下位4ビットをDATA1~4に出力、1の時は上位4ビットをDATA1~4に出力
  3. SELECT信号が0→1に立ち下がった時、行カウンタは+1される
  4. SELECT信号の立下りは、行が9になるまで繰り返される。

sig2.png

これを踏まえて、プログラムではRESETとSELECTにポート変化割り込み(CNEN)を設定してある。ピンアサインを参照

RESETのハンドラは次のようになっている。

RESETのハンドラ(app2.c)_

static bool isStartRound = false;              // RESETからの一連のシーケンスが動いているかのフラグ
static uint8_t SelectCount = 0;                 // SELECTパルスの数

static void RESET_Handler(GPIO_PIN pin, uintptr_t context)
{
(1)    if (RESET_Get()==0) {
          :
(2)     KeyIdxReset();                          // キーのインデックスを0にする
        isStartRound  = true;
(3)     SelectCount = 0;
(4)     uint8_t SelVal = SELECT_Get();          // 最初の(1行目の)データをバスに出力する
(5)     Out2DataPort(SelVal);
         :
    }
}

RESETでは、(1)で立下りのみ処理を追加している。ポート変化割り込みはエッジでかかるので、立ち上がりは無視する必要がある。
(2)では、行カウンタをリセットしている。
(3)は、SELECT信号のカウンタをリセットしている。SELECT信号の立下りは、9回検出して一連の動作が終了することになるので、そのカウンタとして使用する。
(4)では、SELECT信号の状態を取り出す。ここは、0になっているはず。
(5)でそれをDATA1~4に出力する。SELECT信号の最初の立下りは、RESET信号の立ち上がりと同じタイミングになる。そのため、最初のSELECT信号の立下りは、RESET信号の立下りでは検出することができない。そのため、最初のデータ出力はRESET信号の立下りで実行する。

SELECT信号のハンドラは次のようになる。

SELECTのハンドラ(app2.c)

static void GPIO_Change_Handler(GPIO_PIN pin, uintptr_t context)
{
(1) if (isStartRound == false) {                // RESETからのシーケンスが始まっていなければ何もしない
        return;
    }
    if (pin == SELECT_PIN) {  
(2)     uint8_t SelVal = SELECT_Get();
(3)     if (SelVal == 0) {      // 立下りの場合
          KeyIdxInc();
       } else {
(4)         SelectCount++;      // 立ち上がった時はカウントをあげる。
            if (SelectCount == 9) {
                isStartRound = false;
            }
       }
(5)     Out2DataPort(SelVal);
    }
}

SELECTの処理では、立ち上がり、立下り双方で異なる作業が行われる。立下りの場合行を+1する。

(1)で、RESET信号の発生をチェックする。RESET信号が来ていない(通常は起こりえない。取り逃したなど)場合、SELECT信号は無視する。
(2)で、SELECT信号の現在の値を取得する。
(3)で、SELECTが0(立下り)の場合、マトリックスのインデックスを+1する。
(4)で、SELECTが1(立ち上がり)の場合、処理した数を+1して、9になっていれば一連のキースキャンは完了した、と判断する。
(5)で、DATA1~4に値を出力する。これは、SELECT信号が0でも1でも、どちらでも実行される。

ポートへのデータ出力は、Out2DataPort(SelVal)で行われる。引数には、SELECT信号の現在の状態(0か1)を受け取り、上位4ビットか下位4ビットかを出力する。今回、初めて知ったが、PIC32はPORTBのほかにLATBというラッチ付きのポートが用意されていた。

ポートへのデータ出力(app2.c)

void Out2DataPort(uint8_t SelVal)
{
(1) uint32_t kv = KeyIdxValue();

    kvWk = (kvWk | kv);
    // こっちだと 1μS
(2)    if (SelVal == 0) {          // SELECT が Hなので上位4bitを出力
(3)     if (kv & 0b10000000) DATA1_Set(); else DATA1_Clear();
        if (kv & 0b01000000) DATA2_Set(); else DATA2_Clear();
        if (kv & 0b00100000) DATA3_Set(); else DATA3_Clear();
        if (kv & 0b00010000) DATA4_Set(); else DATA4_Clear();
    } else {                    // SELECTがLなので、下位4bitを出力
        if (kv & 0b00001000) DATA1_Set(); else DATA1_Clear();
        if (kv & 0b00000100) DATA2_Set(); else DATA2_Clear();
        if (kv & 0b00000010) DATA3_Set(); else DATA3_Clear();
        if (kv & 0b00000001) DATA4_Set(); else DATA4_Clear();
    }
}

(1) では、keyCodeDef.c内にある、マトリックスから値を取得する関数を呼び出し、現在のキーボードの押下状態を求めている。何行目のデータを取り出すかは、KeyCodeDef内にある変数で制御している。
(2)で、現在のSELECT信号の値をもとに分岐し、上位ビットか下位ビットかを出力する。
(3)以降の処理が実際にポートを出力する処理。ここにあるDATA1_Clear などの関数は、Harmony で自動生成される関数で、config/default/peripheral/gpio/plib_gpio.h 内で定義されている。

ソフトウェア~カスタマイズ

キーアサインの変更

キーアサインは、KeyCodeDef内の、CVTTable構造体、cvt変数で変更できる。配列の各エントリは、USBキーコード、キーボドマトリックスの行、キーボードマトリックスのビットマップに対応している。これを変更すれば、任意のキーを任意の文字に変更できる。

キーボードの変換テーブル(keycodedef.c)

static struct CVTTable{
    uint8_t keyCode;
    uint8_t Row;
    uint8_t Column;
} cvt[2][122] = 
{
    {//あ
        {0x00,0,0b00000000},//        
        {0x01,0,0b00000000},//         
        {0x02,0,0b00000000},//       
        {0x03,0,0b00000000},//       
        {0x04,7,0b00010000},//  31 A   
        {0x05,5,0b00001000},//  50 B   
        {0x06,6,0b00000100},//  48 C   
        {0x07,6,0b00010000},//  33 D   
        {0x08,7,0b00000010},//  19 E   

また、この構造体は2つの配列になっており、F11キーとF12キーで切り替えられる。この2つは、バックスペースキーの割り当てが異なっている。変更するときの参考にすることができる。

F11を押したとき(デフォルト)

        {0x2A,1,0b00000001},//  15 Backspace   ⇒[STOP]
                   :
                   :
        {0x35,0,0b00000000},//  1 半角/全角   ⇒割り当てなし

F12を押したとき

        {0x2A,9,0b00000010},//  15 Backspace   ⇒[DEL]
                   :
                   :
        {0x35,1,0b00000001},//  1 半角/全角   ⇒ [STOP]

ケースの作成

基板サイズを45mmX60mmにしたものを、aitendoのC80x50x20や[C80X50X25]に組み込む。このケースは、パーツの上部の薄いフタと本体に分かれている。今回は、このフタ側を下にして、そこに基板を配置する。上の本体側パーツには、USBなどの穴をあけることになる。サイズ的にはフタ側にピッタリ収まる感じになる。

c0.png

端子配置は次のようになった。

c3.png

ケースはファミコン「風」に塗装してみた。上パーツをつや消し白、下パーツをえんじ色にしたものがこれ。

c1.png

下1/3くらいまでえんじ色を増やしたものがこんな感じ。

c2.png

ファミコン下部の色は、はじめ 艦底色にしようと思ったが、実際に塗ってみるとかなり暗くなってしまったので、えんじ色にしてある。aitendoのケースはABSなのでそのままでは溶剤系の塗料は使用できない。タミヤのプライマーを下地に使用した。この下地は透明なので、どれくらい吹いたのかよくわからない。プラモデル作りが趣味の人はどうしているのだろう?

この写真では見えないが、後ろ側にはファミコンにつながるケーブルと、ICSPの5ピンが出ている。

使用方法

  • LEDが2つあり、それぞれがUSBとファミコンとの接続状況を表している。点滅が未接続、点灯が正常。実際に、動き出してしまうと、このLEDは邪魔なだけで意味がない。ケースに組んだ時も、特にLEDを見えるようにはしなかった。
  • F11キーを押すと、ファミリーベーシックに近いキーボードが押される。F12は、キー配置を変更してあり、USBキーボードのBSキーは、ファミリーベーシックのDELキー、半角/全角キーがSTOPキーになる。テスト中に、BSキーを間違えて押す、ということがあまりに多かったため。
4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?