BUS版と SPI版
HOST とのインターフェイスは 2 種類ある。LCD で良く使われる 8080 バス仕様と SPI 仕様。
BUS 版。自作モジュールのピン配置を示す。
// XO2-256 UDRV XO2-256
// 27 DP 1 24 VCC
// 28 DM 2 23 nBUSY 25
// 29 nRD 3(TMS) 22 nCRC_ERROR 23
// 30 nWR 4(TCK) 21 CLK_OUT 21
// 32 DB0 5(TDI) 20 CLK_4X 20
// 1 DB1 6(TDO) 19 nDATA_FULL 17
// 4 DB2 7 18 nDATA_EMPTY 16
// 5 DB3 8 17 N.C. (JTAGENB)
// 8 DB4 9 16 nDATA_RDY 14
// 9 DB5 10 15 nCS 13
// 10 DB6 11 14 RS 12
// GND 12 13 DB7 11
(JTAGENBを除いて)1つのピンも余らない。コンフィグデータを書き込むときには、JTAGENB を H にして、JTAG ポートから書き込む。
// _ ____________________________________ _____
// RS _><____________________________________><_____ // // _____ _______ // nCS |________________________________| // // ______________ _____________ // nWR/nRD |_____________| // // WRITE: _________________ // DB[7:0] ----------------<_________________>--------
// |---| CLK_4X (MAX)
// |---------------------| CLK_4X x 2 (MIN)
//
// READ: _____________
// DB[7:0] --------------<_____________>----------
//
// |-| gate level delay
アクセスのタイミング。こうはしたが、RS は、nWR/nRD を L にしたときに確定していれば良い。nCS は、単なる出力コントロールなので、nRD と 同時に変化させても構わない。
nWR は、普通 posedge で採取するものなのだが、内部クロックに同期して採取しているので無理。negedge から間をおいて採取になる。negedge から CLK_4X の 1T (Full-speed なら 20.8 ns) 以内に WRITE データを確定させないといけない。そこから 最小 1T 確定させておかなければならない。 Low-Speed なら 1T が 167 ns にもなる。注意が必要。
RS は、A0 とも表記される レジスタ選択。L:データ/H:コントロール
BUS 版を 一応は作ってみたが、実際に使うことは想定していない。
実際に使うのは、SPI 版
// XO2-256 UDRV SPI XO2-256
// 27 DP 1 24 VCC
// 28 DM 2 23 nBUSY 25
// 29 nCS(2) 3(TMS) 22 nCRC_ERROR 23
// 30 SCK(2) 4(TCK) 21 CLK_OUT 21
// 32 MOSI(2) 5(TDI) 20 CLK_4X 20
// 1 MISO(2) 6(TDO) 19 nDATA_FULL 17
// 4 7 18 nDATA_EMPTY 16
// 5 8 17 N.C. (JTAGENB)
// 8 9 16 nDATA_RDY 14
// 9 MISO 10 15 nCS 13
// 10 MOSI 11 14 RS 12
// GND 12 13 SCK 11
ピン配置は、こうした。(2) と書いてあるのは、第二の選択。
JTAG のポートを空けるならば、オリジナルのピン配置にする。ただ、SPI を通してコンフィグもやりたい(できる)なら、(2) の方が便利。
SPI自体のビットレートには制限はあまりない。FIFO とのハンドシェークが制限になるだけ。SPI では、FIFO を CLK (12MHz) で駆動している。ハンドシェークに 4 CLK かけるとすると 24 MHz が上限。FIFO のクロックを CLK_4X にすれば、96 MHz までいく。(FPGA の SCK の上限はもっと高い)
共通のピンの説明
DP/DM
USB の D+/D- 。Low-Speed デバイスは、DMを 1.5 K でプルアップする。Full-Speed デバイスは DP の方。FPGA の ピン設定では、抵抗値が高く役に立たないので注意。
HOST 向けでは、両方をプルダウンする。こちらは、FPGA の ピン設定で良い。現状では、Low-Speed/Full-Speed の両方には対応できない。
CLK_4X/CLK_OUT
Full-Speed では、48 MHz , Low-Speed では 6MHz を CLK_4X に入力する。CLK_OUT は、反転出力で水晶を直付け可能なように配慮。
nDATA_RDY
受信データがあるときに L になる。受信ハンドシェーク用で、割り込み線として使う。
nBUSY/nCRC_ERROR/nDATA_FULL/nDATA_EMPTY
RS=1 で READ すれば、同じ情報が読めるので必須ではない。特に nDATA_EMPTY 。nDATA_RDY と論理とタイミングが違うだけなので、信号すら不要。これらは、LED とかでのモニタ用として一応 残してある。
信号自体は、後で説明。
define と parameter
変更可能だと思っているものについて説明しておく。
(define) USE_SPI
SPI にするか BUS にするかの選択。( USE_BUS は常に define で構わない )
(parameter) SUPPORT_CRC5S=1
デバイスでは、CRC-5 を使う トークンパケットを出さないので、CRC-5 生成は不要。
SUPPORT_CRC5S=0 とすると僅かに規模が減る。
Full-Speed 用
(parameter) FS=1
(parameter) FIFO_DEPTH = 5
Low-Speed 用
(parameter) FS=0
(parameter) FIFO_DEPTH = 4
Low-Speed では、SOP のみの送信をサポートする。規模が増えるので、FIFO を減らすことで対処する。
Full-Speed 用の FIFO は 32 バイト。最大パケットサイズ 32 を想定しているが、MCU 側の 応答時間+転送速度によっては、もっと大きなパケットにも対応可能。逆に 32バイトのパケット全部は FIFO に入らない。送信時にちょっと細工がいる。
データ送受信
BUS 版では、
RS=0 としてデータを書けば FIFO に入り、読めば FIFO のデータを読める。
FIFO が溢れれば nDATA_FULL が L になり、FIFO をリセットするまで FULL 状態が維持される。
データがないのに読めば、前のデータが読める。nDATA_RDY を見てのハンドシェークが必要。
SPI では、
(送受信モード = 0 のとき) RS=0 として データを書けば FIFO に入る。DATA_FULL の動作は同じ。
(送受信モード = 1 のとき) RS=0 として データを読めば FIFO のデータを読める。nDATA_RDY との関係は同じ。
送受信モード, FIFO リセット については、次で説明する。
コントロールレジスタ
RS=0 にして書き込むことで 以下の状態を制御する。
0x80 送受信モードを 0 (受信モード)
0x82 送受信モードを 1 (送信モード)
0x81 送信要求を 1 (送信モード+送信スタート)
RS=0 にして読み込むと以下の状態が読める。
bit7: CRC_ERROR
bit6: DATA_FULL
bit5: DATA_EMPTY
bit4: DATA_RDY
bit3: RECV_EN (受信中)
bit2: SEND_EN (送信中)
bit1: 送受信モード (0: 送信 / 1: 受信)
bit0: 送信要求 (1: スタート / 0: 完了)
送信では、送受信モード=0 にしてから、FIFO にデータを送る。ある程度送ったら 送信要求=1 。残りのデータを FIFO にデータを送る。
FIFO のサイズを大きく超えるデータ量だと、この制御は難しい。規模に余裕があるので、DATA_EMPTY をフロー制御用の信号に置き換えるべき。
旧
assign I_DATA_EMPTY = (i_addr == o_addr);
新
wire [DEPTH-2:0] data_len = (i_addr[DEPTH-1:1] - o_addr[DEPTH-1:1]);
assign I_DATA_EMPTY = (data_len < 2**(DEPTH-2) );
誤差 -1 だから 引き算の結果は、±1 。FIFO のデータ量が
0 〜 14 なら 1
(15 〜 16 なら どちらになるか分からない)
17 〜 31 なら 0
という制御なら入った。後日変更しよう。
(まとめ終わり・本題が続く)
V-USB
V-USB は、AVR のみで Low-Speed デバイスにするドライバで、商業利用でないなら、GPL v2 のもとに利用できる。(USB の ID を利用するのは、また別)
AVR のみで動くのならば、必要な処理すべてが入っているわけだ。一部置き換えることで、作ったコントローラ用になるはず。
一体どこをどう変えるのか? ... 目星は付けた。
V-USB は、アセンブラと C に分かれている。インターフェイスを見たがどうも、アセンブラを置き換えるだけで済むようだ。C を変更することになったとしても変更は僅かになりそう。
コンフィグは、実にややこしくなっている。ここは変更すべきところが出てくる。
まずは、この方針で V-USB というものを理解する。
V-USB のアセンブラコード・インターフェイス(1)
udbdrv.c: usbPoll() をまず見よう。受信後どんなデータを見ているのかが分かる。
uchar usbRxToken;
uchar usbCurrentTok;
volatile schar usbRxLen;
uchar usbInputBufOffset;
uchar usbRxBuf[2*USB_BUFSIZE];
:
len = usbRxLen - 3;
if(len >= 0){
usbProcessRx(usbRxBuf + USB_BUFSIZE + 1 - usbInputBufOffset, len);
usbRxLen = 0;
}
usbProcessRx:
if(usbRxToken < 0x10){
usbFunctionWriteOut(data, len);
return;
}
if(usbRxToken == (uchar)USBPID_SETUP){
:
if(type != USBRQ_TYPE_STANDARD){
replyLen = usbFunctionSetup(data);
}else{
:
大胆にロジックを削った。
受信データがある場合、usbRxLen に PID , CRC-16 を含めたデータサイズが格納される。
ただし、0 オフセットではなく、USB_BUFSIZE - usbInputBufOffset から格納。
受信データとは何かというと DATA0/1 パケット。このパケットの前には、SETUP か OUT のトークンが来ている。それは、usbRxToken に格納され、後で処理の切り分けに使う。OUT でのエンドポイント番号は、usbCurrentTok に格納されるらしい。
まずは、ここまで。
どうも usbRxLen がカナメらしい。これをセットすることで上位レイヤーの受信処理が動き出す。
自作 USBコントローラ(以下 udrv) で対応させる場合、ルールさえ守れば後は自由にして良さそうだ。
まず、どの契機で動かすか ... Full-Speed では、次々にデータが来るから nDATA_RDY=L を割り込みの契機にする。一度フレームが来たら、1 フレーム分 全部処理し終わるまで 割り込みから抜けない。というもので良さそう。
ただ、Low-Speed でそれでは面白くない。余裕があるからだ。受信完了を契機にしたいところ。nBUSY は、送信完了で H になるのだが、L になるようセットしておけば、受信完了でも H になる。
Full-Speed では、FIFO が溢れる恐れがあるし、連続受信で、L になるようセットするのが間に合わないケースが出てくる。Low-Speed では問題ないようにできるはず。
Full-Speed でも、FIFO が 半分を超えるか、受信完了で 割り込むという手がある。送信の場合は、FIFO が 半分を切れば追加のデータを送るのが良さそう。送信完了自体は検出する必要はない。そういう信号線(IRQ)が作れないか検討してみよう。
DATA_EMPTY はなくしても良いし、nCRC_ERROR と nDATA_FULL はエラーが起きたという意味にして 1 つにまとめても良いし、なくしてさえ良い。nBUSY は動いているのを目視したいから残したいが、送信中+受信中に変えたい。
//assign nBUSY = r_start;
assign nBUSY = ~(RECV_EN | SEND_EN);
assign nIRQ = ~r_irq;
always @(posedge CLK)
begin
r_irq &= ~UNDER_HALF ^ r_start ^ i_recv_mode;
end
こんな風に変えるのなら可能だった。(UNDER_HALF は、変更後の I_DATA_EMPTY )
送信モードでは、nIRQ の 最初の状態は H 。
r_start == 0 すなわち 送信データ詰め込み中ならば、半分埋まったところで L になる。
r_start = 1 すなわち 送信をスタートさせると nIRQ は反転。
半分以上埋めているなら、半分を切れば L になる。(半分以下なら、r_start=1 で L )
送信完了後は、受信モードになる。最初は 空 だから L のまま。
ここで r_start = 1にすると H に反転。
受信データが半分を超えるか、受信が完了すれば L になる。
受信完了後、自動で r_start = 1にすることは可能だった。
if (r_se0)
begin
`ifdef AUTO_RECV_AFTER_SEND
if (~r_recv_mode) r_recv_mode <= 1;
`endif
r_start <= 1'b0;
end
else if (~r_recv_en1 & r_recv_en)
r_start <= 1'b1;
上記(2ヶ所ある)の最後の else が追加分。
そこまでやると、H → L の変化だけが重要になる。送信完了後にやるべきことはないのだ。
送信で重要なのは、途切れずにデータを送り込むこと。
半分を切れば割り込みが起きるから、16 バイト+αを送り込む。送り込む速度が 12Mbps よりかなり速くないと 64 バイトのパケット送信は無理だが。送り込んでしまえば、あとは受信待ち。
受信で重要なのは、FIFO を溢れさせないことと 確実な 受信完了の受け取り。両方 H → L 。
さて、受信データは、usbRxBuf に貯めこんでいく。usbRxLen が 0 になっていたら、貯めるインデックスを 0 にする。負の値にならないようにするなら、最大パケット長に 128 以上は取れない。Full-speed でも 64 が精々。
受信データの前に SETUP か OUT が来ている。これは、PID を usbRxToken に格納して、廃棄? -- データをそのまま残しても良いかも知れない。
V-USB のアセンブラコード・インターフェイス(2)
1 パケットの受信完了で何をしているか ... アセンブラを眺めてみると
受け取ったパケットのPID によって次の処理を行う。
DATA0/1 の場合 : handleData
( usbDeviceAddr を見て チェック : ignorePacket )
IN の場合 : handleIn
OUT の場合 : handleSetupOrOut
SETUP の場合 : handleSetupOrOut
どれでもない場合 : ignorePacket
handleSetupOrOut では、一旦リターンする。(思っていたのと違った)
handleIn は、送るべきデータが用意されていなければ NAK を送ってリターン
ある場合は、usbSendAndReti これが送信ルーチンだが、終了後 usbNewDeviceAddr を見て usbDeviceAddr を更新している。
udrv の場合、usbSendAndReti に相当する部分は、かなり簡略化できる。データをそのまま送れば良いからだ。
どうすれば良いか大分イメージできた。次は、送信データを受け取るところを見てみよう。あと、ハンドシェーク。ACK/NAK/STALL をどういう場合に送っているか(あるいは送らないか)もチェックしよう。
V-USB のアセンブラコード・インターフェイス(3)
typedef struct usbTxStatus{
volatile uchar len;
uchar buffer[USB_BUFSIZE];
}usbTxStatus_t;
extern usbTxStatus_t usbTxStatus1, usbTxStatus3;
#define usbTxLen1 usbTxStatus1.len
#define usbTxBuf1 usbTxStatus1.buffer
#define usbTxLen3 usbTxStatus3.len
#define usbTxBuf3 usbTxStatus3.buffer
volatile uchar usbTxLen = USBPID_NAK;
uchar usbTxBuf[USB_BUFSIZE];
usbBuildTxBlock:
usbTxBuf[0] ^= USBPID_DATA0 ^ USBPID_DATA1; /* DATA toggling */
len = usbDeviceRead(usbTxBuf + 1, wantLen);
if(len <= 8){
usbCrc16Append(&usbTxBuf[1], len);
len += 4; /* length including sync byte */
}else{
len = USBPID_STALL;
}
usbTxLen = len;
usbDeviceRead:
if(usbMsgFlags & USB_FLG_USE_USER_RW){
len = usbFunctionRead(data, len);
}else{
if(usbMsgFlags & USB_FLG_MSGPTR_IS_ROM){ /* ROM data */
:
}else{ /* RAM data */
:
usbSetInterrupt:
usbGenericSetInterrupt(data, len, &usbTxStatus1);
usbSetInterrupt3:
usbGenericSetInterrupt(data, len, &usbTxStatus3);
とりあえず、送信データは、3 種類ある。usbTxBuf と usbTxBuf1/usbTxBuf3。 これらのフォーマットはすべて同じで、PID と CRC-16 を含むデータ。usbTxLen には、NAK , STALL などの PID が入る場合がある。 そうでない場合は、SYNC の分を含む 1 多い値。
IN がきた時 endpoint が 0 なら usbTxBuf を使う。USB_CFG_EP3_NUMBER に一致すれば usbTxBuf3 , それ以外なら usbTxBuf1を使う。
udrv に対応させる場合、CRC-16 は余計なのだが、インターフェイスを変えない。内部で -2 すれば良いだけの話だからだ。オーバヘッドが大きければ、CRC-16 の計算を止める。
こうしておくと udrv の送信で CRC-16 の計算をサポートしないという選択枝が出来る。
ついでに気がついたのでメモ: データサイズが 8 バイト以内だという のがハードコーディングされている所がある。ここは、Full-Speed 対応で変更しないと。
パケットのフロー制御
パケットが来たら ACK/NAK/STALL のどれかを送ったり、あるいは送らないという処理が必要。
いったい V-USB ではどうしているのだろう。
まずは、予備知識
HOST | device | HOST
---------------+--------------------------+----------
SETUP DATA0 | ACK/(none) |
| |
OUT DATA0/1 | ACK/NAK/STALL/(none) |
| |
IN | DATA0/1 or NAK/STALL | ACK/(none)
代表的なのは、仕様上こういうことらしい。だが、実際のコードはもっと細い。どういう風になっているか まとめておかないと、コードが作れないのだ。
SETUP/OUT の後 の DATA0/1
NAK : (usbRxLun != 0) のとき 受信バッファが空いていないので受け取らない。
ACK : 0 バイト受信のとき 、受信バッファの状態を変更せず ACK
STALL : 該当ケースなし。
(none) : 前が SETUP/OUT でないとき
IN
NAK : (usbRxLun != 0) のとき 受信バッファが空いていないので送らない。
( すぐに ACK が来るが受け取れない )
送信するデータがないとき。
上位から指定されたとき
STALL : 上位から指定されたとき
(その他)
SETUP/OUT (none)
受信バッファオーバーフロー (none)
想定外のトークン / エンドポイント 範囲外 (none)
どうも、これだけのようだ。CRC エラーはないが、たぶん想定外と同じ。
受信バッファが空いていないので受け取らない。というのは、若干面倒。FIFO には受け取った上で NAK を送信すれば良いのだが、受信完了を待つのが ちょっと。
IN での NAK は、自力では、思いつかなさそう。
あと、SETUP の後、NAK は送らないのだが ... 送るケースがあるような ...これは、そういうことがないようにしている?
usbCurrentTok にストアするタイミング
SETUP/OUT (none)
受信バッファオーバーフロー (none)
想定外のトークン / エンドポイント 範囲外 (none)
これらは、ACK/NAK を返さないだけでなく、usbCurrentTok を更新している。ちなみに DATA0/1 の後の (none) では、更新しない。わざわざそうしているから、理由があるのかも。
ドライバーの設計
ようやく本題に入る。ドライバーをどうやって作るのか検討を始めよう。
その前に 仕様が大分変わった。
・ udrv-13-spec.txt
変更案をいろいろ書いたが、このスペックをもとにする。
ISR(INT0_vect) {
static uint8_t recv_offset = 0;
static int8_t send_len = 0;
static uint8_t *send_ptr;
uint8_t i;
まずは、C で全部記述することにする。AVR だとこんな感じで割り込みを定義できる。
初期化で、negedge nIRQ で INT0割り込みが 起きるようにしておく。
if (send_len > 0) {
:
return;
}
送信で、送りそこなった分があれば送るコードが必要かも知れない。割り込みは、FIFO が半分になったとき起きる。ただ、SPI が十分速くないと最大パケットサイズを大きくできない。遅いと 最大パケットサイズ を 32 にせざるを得ないが、そうなると、送信をキックするところで全部送れてしまう。たぶん必要ないので、これは後回しにしよう。
次、受信だが、割り込みは、受信完了か FIFO が半分を超えたところで起きる。
if (bit_is_clear(nDATA_RDY_PIN, nDATA_RDY_BIT)) {
re_recv:
do {
if (recv_offset < USB_BUFSIZE) {
usbRxBuf[recv_offset++] = spi_read();
} else
spi_read();
} while (bit_is_clear(nDATA_RDY_PIN, nDATA_RDY_BIT));
}
i = ctrl_write(CTRL_READ_STATUS);
if (i & ST_BUSY) return; // RECV in progress
if (i & ST_DATA_RDY) goto re_recv;
if (i & ST_CRC_ERROR) {
recv_offset = 0;
return;
}
if (i & ST_DATA_FULL) {
recv_offset = 0;
ctrl_write(CTRL_SEND_MODE);
ctrl_write(CTRL_RECV_MODE);
return;
}
nDATA_RDYが L で buffer に入る限りは、とにかく入れる。H になれば、FIFO が空ということ。ループを抜けてチェックにはいる。ctrl_write() でステータスが読めるとしておく。
ST_BUSY なら、まだ受信が続いているということ。割り込みを一旦抜けて、次の割り込みを待つ。
受信が終了したとしても、僅かな期間で 最後のバイトが受信されたかも知れない。ST_DATA_RDY ならば、もう一度 読み込む。
エラーが起きたら、無視することになっている。ST_DATA_FULLだと、一旦 送信モードにしないとステータスが消えない。
ここで バッファに受信データが入った。次はパケットの解析。
usbRxToken = usbRxBuf[0];
if ((usbRxToken == USBPID_SETUP) | (usbRxToken == USBPID_OUT)) {
if ( recv_offset < 4 ) {
if (usbRxLen) recv_offset = USB_BUFSIZE + 1;
return; // RECV DATA0/1
}
if (recv_offset == USB_BUFSIZE + 1) { // buffer busy
recv_offset = 0;
goto send_nak;
}
if (recv_offset == USB_BUFSIZE ) { // buffer overrun
recv_offset = 0;
return;
}
// data top : USB_BUFSIZE - usbInputBufOffset
usbInputBufOffset = USB_BUFSIZE - 3;
usbRxLen = recv_offset - 3;
recv_offset = 0;
send_handshake(USBPID_ACK);
return;
SETUP か OUT の場合、次に DATA0/1 パケットが来る。続けて 読み込んで バッファにくっつけてしまう。ただし、usbRxLen が 0 でなければ、受け取った後、NAK を送る。recv_offset = USB_BUFSIZE + 1 として後で分かるようにしておく。DATA0/1 パケットを受け取った後、バッファが溢れていたら NAK は送らない。
このコードで DATA0/1 トークンのチェックはしていない。後で入れておく必要がある。
あと、RxLen が 0 でないときでも、DATA パケットが開始されるまでに SETUP/OUT を受け取らないと、DATA パケットを受け取ってしまう。ここは直すの面倒。
最後に、usbRxLen をセットして、ACK を送り割り込みを抜ける。
IN が来たら送信。usbRxLen が 0 以外なら NAK を送るのは、SETUP/OUT と同じ。アドレスが違うときも NAK 。
次に エンドポイント番号によって、送信バッファを選択。send_len には、SYNC と CRC-16が含まれるので udrv では、-3 する。これで送信の準備は出来た。ちなみに、これ以外のパケットが来ても何もしない。
} else if ( usbRxToken == USBPID_IN ) {
if ( usbRxLen < 0 ) goto send_nak;
i = (usbRxBuf[1] >> 1);
if (i & (i != usbDeviceAddr)) goto send_nak;
i = (usbRxBuf[1] << 3) & 0x08 | (usbRxBuf[2] >> 5) & 0x07;
if (i == 0) {
send_len = usbTxLen;
send_ptr = usbTxBuf;
usbTxLen = USBPID_NAK;
} else if ( i == USB_CFG_EP3_NUMBER) {
send_len = usbTxLen3;
send_ptr = usbTxBuf3;
usbTxLen3 = USBPID_NAK;
} else {
send_len = usbTxLen1;
send_ptr = usbTxBuf1;
usbTxLen1 = USBPID_NAK;
}
if (send_len > 3) send_len -= 3; // remove SYNC , CRC-16
usbDeviceAddr = usbNewDeviceAddr;
(送信処理)
} else {
recv_offset = 0;
return;
}
いよいよ送信にはいる。まず、send_len が負の場合 ACK などのハンドシェークの PID を送る仕様。0 なら、送るものがないので NAK 。
いよいよ送信だが、まずは、送信モードにする。最初は FIFO が空で nIRQ は H 。詰めていって 半分を超えると L になる。ここまで詰めたら送信スタート。半分に満たない場合は、スタートと共に nIRQ が L になる。
割り込み要求ビットが立っているので、送信スタートの直後にクリア。次にまだまだ詰められるので 20 を上限に詰める。
最後に 全部詰められたら、割り込み要求ビットをクリア。
if (send_len < 0) {
send_handshake(send_len);
send_len = 0;
return;
}
if (send_len == 0) goto send_nak;
ctrl_write(CTRL_SEND_MODE);
bit_clear(nCS_PORT, nCS_BIT);
do {
spi_write(*send_ptr++);
while ( (--send_len) && bit_is_set(nIRQ_PIN, nIRQ_BIT) );
while (spi_busy())
;
bit_set(nCS_PORT, nCS_BIT);
ctrl_write(CTRL_SEND_START);
bit_set(EIFR, INTF0); // clear INT0 req
re_send:
if (send_len > 0) {
bit_clear(nCS_PORT, nCS_BIT);
i = 20;
if (send_len < i) i = send_len + 1;
while (--i) {
spi_write(*send_ptr++);
--send_len;
}
while (spi_busy())
;
}
bit_set(nCS_PORT, nCS_BIT);
if (send_len == 0)
bit_set(EIFR, INTF0); // clear INT0 req
return;
だいたいこんな感じだが、送信はちょっとグダグダ。SPI が遅ければどうせ全部詰めることになるのだ。nIRQ など見ないで 24 など 規定数詰めたらスタート 。残りを順次詰めて、最後に 割り込み要求ビットをクリアで良いと思う。
まぁ AVR 以外も想定してロジックを考えてみたのだが、24MHz 以上とかの 高速なSPI とかじゃないと意味なさそう。... Low-Speed を忘れていたが、こちらは最大パケット長が 8 なので余裕。全部詰めてスタートで良い。
追記: バグがいくつか
送信で、NAK などハンドシェークパケットを送る際に Length の方にセットする仕様なのだが、変更する必要があった。
ハンドシェーク の PID は以下のとおり
/* handshake */
#define USBPID_ACK 0xd2 /* 1101 0010 */
#define USBPID_NAK 0x5a /* 0101 1010 */
#define USBPID_STALL 0x1e /* 0001 1110 */
Low-Speed では、DATA パケットを送る場合 の Length の最大値は 12 なのだ。V-USB では、Length の bit4 が 1 だと ハンドシェークと判断する。この仕様だと 32 バイトのパケットサイズすら対応できない。
PID の bit0 が 0 なのを利用して、0x80 | (PID >> 1) とすることにした。
ただ、こうやって仕様変更すると usbdrv.c の方を結構修正しないといけない。10 ヶ所ぐらいある。あと usbTxLen & 0x10 などとして判断しているところもあってこれも 0x80 に変更しないといけない。
次に受信のほう。パケットを受けられるだけ受けるというのは、どうも無理がありそうだ。
SOF → SETUP → DATA0
例えばこんな風にパケットが来たらくっつけてしまうやりかただと対応が面倒。
どうも 受信中にパケットの切れ目を判断してパケット毎に処理しないとダメそうだ。判断は簡単だが、ループの内側に if 分が 2 つほど入ることになる。ちょっと面白くないが、とりあえずは、やむを得ないということにしよう。
IN (device → host) のデータ再送未対応。
V-USB も分かっていながら未対応にしている。作っているのは、サイズの制限など気にしてないので、対応させておきたい。
バスリセット 未対応
ホストがバスリセットすると、たぶんおかしくなる。
ACK や NAK を返すタイミングについて
どうも気になって ググってみると ... ACK や NAK を返すタイミングは相当にタイトらしい。Low-Speed で 18 クロックだとか。( USB の仕様書を見てみたが、 Full-Speed も同じだった)
いつまでも待っていたら HOST が次を始められない。それはそうだ。
データがぶつかるのは、IN の後 デバイスから送られる DATA0/1 と NAK/STALL ハンドシェーク。HOST が DATA0/1 を送った後の デバイスから送られる ACK/NAK/STALL ハンドシェーク。
... となると いままで作ってきた udrv の仕様では全然ダメだ。全然間に合わない。変更するにも規模が足りない。
DATA0/1 を受けとれない状態なら NAK か STALL。受け取った後 エラーが起きなければ ACK 。こういうものだけなら、規模がもうすこしあれば行けそうな気がする。だが、DATA を送信するのは一体どうするのだろう?最初からデータを入れとかないと無理なのでは?
あと、Low-Speed 専用ならどうだろう? 1.5 MHz で 18 clk ということは、20 MHz で 240 clk 。受信完了し続いての動作だから、可能なやりかたはありそうだ。
だが、12MHz で 18clk は、20 MHz で 30 clk 。SPI では 1バイトを送る時間もなさそう。これは諦める。対応する余裕はない。
Full-Speed device に対応するには、エンドポイント毎の バッファを持ったモジュールを MCU の代わりに入れて、高レベルのコントローラにするしかないような気がする。いま作っているのは、単なる物理層だから無駄にはならない。だが、高レベルのコントローラまで設計するつもりは今はない。
ただ、Low-Speed でも 受信完了してから 受信データを受け取り ACK を返すのでは遅すぎる。受信したら即受け取りでないと ダメかも知れない。
逆に HOST 専用ならどうだろう? HOST が制御しているのだから、送信同士がぶつからないように制御はできる。USB の仕様書でも、送信同士がぶつかるケースについてだけ言及しているように見える。
ところで、device 側から見ると、ACK が来なかったら、(たぶん)次の要求は再送。このあたり、V-USB ではどうなっているのだろう? 気になってきた。
; Comment about when to set usbTxLen to USBPID_NAK:
; We should set it back when we receive the ACK from the host. This would
; be simple to implement: One static variable which stores whether the last
; tx was for endpoint 0 or 1 and a compare in the receiver to distinguish the
; ACK. However, we set it back immediately when we send the package,
; assuming that no error occurs and the host sends an ACK. We save one byte
; RAM this way and avoid potential problems with endless retries. The rest of
; the driver assumes error-free transfers anyway.
なんかわざわざコメントが書いてあった。やっぱり 送信後 バッファを捨ててしまうのはまずそうな ... たぶん ACK が来たときに usbTxLen を更新してバッファを捨てれば良い。
さて、device では タイムアウト が必要ということではなさそうだ。デバイス依存になるかも知れないが、Full-Speed の HOST は可能かも知れない。
ホストの検討
なかなか問題が多そうだが、デバイス側はどうすれば良いか分かってきた。では、ホストはどうしたら良いのだろう。これも問題がありそうだが、強引に検討だけはしておこう。
実現できたとしても、実用上の利点というのは、ほとんどなさそう。デバイス側が USB の勉強になったように、これもまた勉強。
作るためには、ホストの制御を基本から理解する必要がある。逆にこれが使えるぐらいの知識が出来たら、他の OTG 付き MCU で制御できるようになるはず。V-USB の HOST 版というものもたぶん作れるようになる。
OTG 付きの MCU というと、秋月の PIC32MX220F032B がわずか 220 円で手に入るようになった。これを使いこなせば実にいろいろできそうな気がする。
さて、HOST にしてなにを制御するのか? 汎用的なものは一切考えない。それは PC でやるべきことだ。一応電子工作として テーマを 2 つ
(1) キーボード や マウスの制御
これができるようになると、PS/2 変換器が作れそう。ただ、V-USB のような 直接制御が可能そうなので、実際には udrv で作る必要はないだろう。あと、HUB には対応するのは、面倒そうだが ... いったいどうするのか 後回しにするがいずれ検討したい。
(2) BlueTooth ドングル
USB BlueTooth ドングルは、妙に安いので これが作れると嬉しいような気がする。実際に作るなら、PIC32MX220F032B で変換器を作るのが最も安くあがりそうだが ... まずは udrv で検討する。
はたして、可能なのかどうか? 最初の壁はパケットサイズ。MaxPS=64 みたいなので、結構厳しいものがある。
目標はできたが、どのように設計するのかについて条件がある。まずはそこから。
(1) V-USB のアセンブラコードを最小限度の変更で利用できるようなインターフェイスにする。
Low-Speed の HOST なら ピンを直接制御しても 作れそう。ただ、送信・受信をするコードを 一から作れるような気はしないので、V-USB のコードを流用するにはどうしたら良いかについて、最初から考慮しておく。
(2) PIC32MX220F032B が最終ターゲット
AVR にも HOST 付きのものはあるが、どうも 最近は device only のものばかりになっている。XMEGA にも USB 付きが出てきているが device ばかり。
まだ、AVR を中心に使おうとは思っているのだが、PIC32MX を専用IC にしてしまうような使い方なら 使ってみたいとは思っている。なにしろ値段が安い。220 円ならホストインターフェイスを買うより安くなってしまう。
(3) MicroChip の USB Framework は使わない。
これを使うと MicroChip 以外のものを扱うとき困ってしまう。API も合わせない。作りたいのは、上記の 2 つぐらいだし、理解するために試しに作ってみるだけなので、いきなり高い完成度のものは狙わない。
もっと言うと、MicroChip が出している開発ツールを一切使わないで開発できるようにしたい。なかなか厳しそうではあるが、使いたい MIPS チップは他にもある。ちなみに、書き込みは、 OpenOCD が既に対応しているそうだ。問題なのはライブラリだが、(他のMIPSチップ向けだが)作りかけたものがある。
こういう条件で作り出すのは良いのだが、さてどうしよう。
最初に PIC32MXの USB インターフェイスがどういうものなのか、あたりを付けないといけないだろう。
・ 秋月の PIC32MX220F032B
ここにデータシートがあるので 見てみる。で、レジスタの名前 例えば U1OTGIR で検索すると ... PIC24F の 日本語ドキュメント がヒットする。
構成図を見ると、PIC32MX と PIC24F の USB モジュールは実に似ている。どうもほとんど同じようだ。なら、調査は、PIC24Fの方で済ますことにする。
つらつらと眺めてみたが、HOST の場合、それほど高機能ではなさそう。エンドポイントも 1 つだけを使う。なら udrv と大差ないんじゃないかと言う気がしてきた。udrv 向けに普通に作れば対応可能だろう。
あと気になったのは、U1SOF レジスタ。HOST のみの機能で、送信同士のぶつかりを防ぐためのタイムアウト値を格納する。最大値は 255 。なるほど、変更できるかどうか別にして、HOST では、タイムアウトが必ずあるものだと思った方が良さそうだ。逆に、デバイスで ACK 待ちのタイムアウト値はレジスタとしてはない。デバイスでは、タイムアウトという概念はなくても良いのかも知れない。
次に、HOST がどうやって送信するのか? V-USB の device のコードは、見てきたわけだが、HOST から来る 指令に対応するだけのものなのだ。だからこの知識だけでは、HOST が どのような送信の仕方をするのかは全然分からない。
ぐぐってみると PICFUN さんのところの、
・ http://www.picfun.com/usb03.html
が引っかかった。随分前にも見たことがあるのだが、理解する必要もなかったので忘れていた。
・ 概念的には、SOF で始まるフレームがある。
・ フレームのなかには、SETUP - DATA0 - ACK とか IN - DATA0/1 - ACK とか一連の シーケンスからなる トランザクションがある。
・ バルク転送というのは、本来 フレーム内で必要なトランザクションを行ったあと、余った帯域で 行うもの。(優先度が低い)
・ インタラプト転送というのは、優先度が高いが、頻繁に行うものではない。
・ フレームについて概念的にと書いたのは、デバイスから見ればフレームがあってもなくても処理に大差ないため。今まで作ったもののレベルだと、リセットするのを防ぐとかその程度の意味しかないような感じ。
今の理解はこんな感じ。分からないのは、
・ Low-Speed では、EOP のみを送るルールがある。
・ そうなるとフレームはなくても良さそうなもの。
・ SOF は送らなくとも良いのか or 送ってはダメなのか ?
ホストの設計
随分前置きが長くなった。さぁ、設計をしてみよう。
Low-Speed では、必ずしもフレームを構成しなくても良さそうなのだが、定期的に EOP を送らなければならない。そこから考えると、1ms 周期のタイマ割り込みを作りそこから、一連の 送受信処理につなげていく 構造が良さそうだ。
Full-Speed では、最初に SOF を送る。Low-Speed では、SOF は送らない? 結局送信すべきものがないと EOP だけ送る。
トランザクションの途中では、かならず受信待ちが入るわけだが、ここで割り込みを一旦抜ける。返ってこなかったら、次の 1ms のタイマ割り込みで タイムアウトの処理を行うことにする。これで良いのか? という気もするのだが、動作ポイントは、タイマ割り込みと 受信割り込みということで進めよう。
次にバッファの受け渡し。受信待ちに入れば、タスクの方が動作可能になる。まず送信について考えてみると、そこで 次のデータを準備したりできるわけだ。だが、ACK が返らない限り バッファが開放できない。送信では(少なくとも)ダブルバッファが必要そうに思える。
そもそも 1 フレームで 1 つしか送信できないとすると、MaxPS=64 でも 64KB/sec にしかならない。BlueTooth のことを考えると これじゃ帯域が低すぎるような気がする。ダブルバッファでは足りず FIFO にしないといけないかも知れないが、まずは ダブルバッファということにしておこう。
では受信ではどうなのか? データを受け取り ACK を返せば受信成功だ。で、すぐに次の受信待ちになる。このときに、タスクで受信処理をできるかも知れない。やっぱりダブルバッファは必要そう。
バッファの構造はどうしよう。
V-USB では、受信バッファに 全部収めて、さらに usbRxLen / usbRxToken とかの情報を設定するルールだった。ここは、ダブルバッファにするが、同じようなものにすればよさそうだ。
送信は、3 つのバッファがあって、IN を受信すると エンドポイントに対応したバッファを選んで送信する仕組みだった。送るデータは ACK/NAK などを送るか、バッファに格納されたデータを送るかの 2 種類。
HOST の場合、ACK を 受信完了で送ることはあるが、NAK/STALL を送信することはない。そのかわり SETUP/OUT に続いて DATA0/1 を送ることになる。また、IN/OUT/SETUP のトークンを送るには、アドレスとエンドポイントの情報が必要。ついでに CRC-5 を生成しないといけなくなった。
V-USB の送信コードを有効に使うならば、Length とバッファのアドレスを指定して送信する機能と ACK を送信する機能を使うことになる。どちらも 割り込みから抜けてしまうことになっているが、そこは 戻ってきてもらわないと困る。
あと、SETUP/OUT + DATA で 送信バッファを 2 つ使うのではバッファの効率が悪い。1 つのバッファに詰めることにする。トークンより大きいデータが入っていたら 2 回に分けて送信するようなイメージ。call したい ところは 2 ヵ所だが、サブルーチンにする必要はないかもしれない。
一方 ACK を返すのは、たったの一ヶ所。こちらは無条件ジャンプで事足りる。
ここまでがドライバの仕事。要するにバッファの受け渡しで 事が進む。タスク側の処理がこれに加えて必要になる。あと、接続しただとか、切断しただとか状態管理も別途必要で、これについては後で検討しよう。まずは、中核となる部分だが、ここのイメージはできた。
状態監視とか
接続後の動作しか検討していなかったのだが、気になってきた。今の udrv で 接続や切断、リセットに対応できるのだろうか?
なにも接続されていない状態では、D+/D- ともに L で SE0 状態。接続されていれば idle 状態。
この状態をホストがどのように検出できるか?
受信をスタートさせると SE0 なのですぐに bit0: 送受信要求 が 0 になる。これは、nIRQ が L になることでも検出できる。続けて 2 回実行しても同じ結果なら、接続されていないことが分かる。
受信をスタートさせても、nIRQ が H なら、接続はされていて idle 状態。
これを定期的にポーリングしてやれば良いのだろう。
逆に、ホストがリセットしたい場合、50ms 以上 SE0 状態にする。
device 側が、これを検出する仕組みは、ホストと同じ。だが、ホスト側で 強制的に SE0 状態にする仕組みを作っていない。
実をいうと -13 版では、コントロールレジスタに 0x84 を書き込むことで、ENABLE ピンを L にする 仕組みを入れた。このピンでプルアップすることで、device では、切断状態を作り出せる。
ホストでは、0x88 を書き込むことで、D+ , D- ともに L 出力ということにしようかと思う。ただ、この程度の変更すら規模的に厳しい。ただ、既に SUPPORT_CRC5S=0 としていてそのおかげで入った。要するに ホストで使うトークンや SOF に対して CRC-5 は自動生成しない。生成済みのデータを送る必要があるが、僅か 11 bit 分の CRC 計算だし重くはないだろう。
関連記事
・ 『QFN32の FPGA』
・ 『TTL ALU 74281』
・ 『FPGA時計の設計』
・ 『MCPU -- A Minimal 8 Bit CPU』
・ 『DACを設計してみよう』
・ 『USBコントローラの設計』
・ 『USBコントローラの設計(2)』
ソースコード (2012/07/06)
・ qfn32samples-12.zip
最新ソースコード (2012/07/12)
・ qfn32samples-13.zip
udrv: 仕様を src/udrv-13-spec.txt に変更。古いのは src/old へ
udrv: src/udrv-driver.c 添付。
参考文献(データシート等)
・ 『MachXO2 ファミリデータシート(日本語 pdf)』
・ 『MachXO2 テクニカルノート(日本語)』
・ 『Lattice Diamond 1.4マニュアル』