最初は、規模が小さい MachXO2-256 で作れるものとして検討してみよう。もちろん、規模が小さいわけだから、一般的なコントローラの機能は作れない。では何を作るのか?
レベル 0)
パケットを送出し、受け取る。そういうものならまずは可能だろう。ホストから来た 0/1 のシーケンスをそのまま送るのは、難しくなさそうだ。
受信を考えると、まず相手のクロックに同期させなければならない。4 倍クロックを使って、最初の 信号の立ち上がりに合わせようかと思う。ここのところは、少々難しそうだが、USART などと似た様なものだから、それほどでもないだろう。
パケットの開始(SOP)は、送信側からみると、入力状態から、アイドル状態と同じ値を一旦出力することらしい。アイドル状態は、出力しない状態で Low-Speed では、D- がプルアップされているわけだから D- = H , D+ = L 。Full-Speed では、逆になる。
続いて SYNC データを送出する。データは LSB first で 0x80 。これを NRZI (Non Return Zero Invert) エンコードすると H-L-H-L-H-L-H-H (Low-Speed での D+/Full-Speed D-)。 最初に L → H と変わるから ここを拾って 受信クロックを同期させるわけだ。 クロックが同期してなければ、1 つ遅延させる。 6 回チャンスがあるから、最後の H-H までに同期するはず。
パケットの終了(EOP)は D- , D+ 共に L 出力 x 2クロックの後 、アイドル状態と同じ値を一旦出力して、入力状態。受信では、D- , D+ 共に Lを検出したら、1 クロック待って 受信を終了させれば良い。
あと難しそうなのが、FIFO が必須ということ。途切れなく 送受信しなければならないので、データが来たからといって、すぐに 送信すれば良いものでもない。受信では、FULL にならないようにする配慮も必要。
ちなみに、Full-Speed での最大パケットサイズは、512 Bytes だったと思う。MachXO2-256では、合計で 64B ぐらいしか取れそうにない。なかなか考えどころ。
レベル1)
USB では、NRZI (Non Return Zero Invert) エンコーディング というのを行なっている。レベル1では、エンコード/デコードする 。後で説明するが、NRZI を 論理レベルに変換すると、MCU 側の処理が楽になる。コントローラ側でも 送受信の単位が バイト単位になって FIFO まわりが少し楽になる。
レベル2)
パケットの形式を知った上で、いくつかの機能をサポート。パケット ID(PID) が正しいかどうかの検出や、CRC のチェック/生成など。
このレベルより高度なものは規模的に無理。このレベルでも怪しい。一応目標にはするが、レベル1になるかも。
さて、こういうものを作るとして、MCU との 送受信インターフェイスはどうしよう。FIFO を使うから基本は、8bit パラレル。
ただ、パラレルでは、ピンを消費する上に AVR なんかだと SPI より遅くなる。やはり SPI かと考えている。まぁこれは、出来た後でも良い。
ところで、USB PHY のインターフェイスは、UTMI というものが定義されているらしい。SMSC では、これのピン数を減らした ULPI( UTMI + Low Pin Interface) の PHY を製品化している。これに合わせることは、考えていないが、出来た後、再度検討してみよう。まずは、上記のものを設計しなければ。
USBでの信号の扱い
基本は、差動信号だが、上で出てきたように例外がある。(両方 L レベル)
FPGA は一般に 差動出力も持っているのだが、これをサポートするため、シングルエンド x 2 で操作すれば良いだろう。受信側も 同じ事情。差動入力もできるが、シングルエンドも必要。FPGA は 1つのポートで 複数の入力の仕方ができるものなのだろうか? その上 出力にも切り替えられないといけないのだが ...
多分無理だと考えて、差動入力をパラレルに接続することにしよう。ポートが勿体なければ、シングルエンドで 値を読むことにしても良い。
さて、デバイスとして構成するなら 1.5K のプルアップが必要だ。ポートが余っていれば 2つのポートに抵抗を付けることで、設定可能にしても良いだろう。ホスト側は、15K のプルダウン。これは、FPGA のプルダウン機能でいけそう。
こういうことなので、最大は D-,D+ にそれぞれ 3 つづつ 計 6 個のポートが必要。
LRZI について
簡単に説明してしまうと 論理 0 出力は反転、論理 1 出力は 状態保持。
そして、同じ状態が 6 クロック続くと 次に 0 を挿入する。-- 要するに論理 11111 は、0 または 1 の状態が 6 クロック続くので 0 が挿入される。
Full-Speed は、物理的な差動出力が逆になるわけだが、受信側から見ると、同じ値になる。初期状態が違うだけで、反転-保持の関係は変わらないからだ。
以上の理解で、レベル1は設計できるはず。さあ設計してみよう。
クロックジェネレータ
48 MHz を入力して、12 MHz を作る。Low-Speed の場合は、6MHz 入力で 1.5 MHz を作る。
PLL のない MachXO2-256 で 48 MHz をどうするのか? という問題があるが、とりあえず考えない。あとクロックは、送信中、受信中のみ 生成することにしよう。
module udrv_clkgen (
input CLK_4X
, input DP
, input SYNC_REQ
, output SYNC_ACK
, output CLK
);
reg [1:0] clk_ph = 0;
reg r_dp = 1'b0;
reg r_done = 1'b0;
assign CLK = clk_ph[1];
assign SYNC_ACK = r_done;
always @(posedge CLK_4X)
begin
r_dp <= DP;
if (~SYNC_REQ | ~(r_dp ^ DP) | (clk_ph == 3) )
clk_ph <= clk_ph + 1;
if (~SYNC_REQ)
r_done <= 1'b0;
else if ((r_dp ^ DP) & (clk_ph == 3))
r_done <= 1'b1;
clk_ph <= clk_ph + 1;
end
endmodule
// _______
// CLK |_______|
//
// clk_ph 3 | 0 | 1 | 2 | 3 | 0
// _____________
// DP >< ><
//
// _________
// SYNC_ACK __|
//
SYNC_REQ は、上位モジュールからの受信要求で、SYNC_ACK が 1 になると 受信準備が出来たということで、受信モジュールを enable するつもり。
(clk_ph == 3) 以外で DP が変化すると 1/4 クロック遅らせる。OK になれば、SYNC_ACK が 1 になるので、(上位モジュールが) SYNC_REQ を 0 にする。
受信モジュール
DP 以外に SE0 という入力信号を作ることにした。これは、DP-,DP+ を シングルエンドとして見て共に 0 のとき 1 にする。RECV_EN で受信開始だが、SYNC の途中なので、1 - 1 または 0 - 0 が来た次から 採取開始。データは、FIFO に入れることにする。FIFO が溢れても関知しない。
module udrv_reciever (
input CLK
, input RECV_EN
, input DP
, input SE0
, output [7:0] RECV_DATA
, output RECV_DATA_RDY
, input RECV_DATA_ACK
);
reg recv_stat = 0;
reg r_dp = 0;
reg [7:0] data_out;
reg data_rdy = 0;
reg [7:0] r_data;
reg [2:0] data_count = 0;
reg [2:0] one_count = 0;
assign RECV_DATA[7:0] = r_data[7:0];
assign RECV_DATA_RDY = data_rdy;
always @(posedge CLK)
begin
r_dp <= DP;
if (~RECV_EN | SE0)
begin
recv_stat <= 0;
data_count <= 0;
r_data <= 0;
one_count <= 0;
end
else if (recv_stat == 0)
begin
if (~(r_dp ^ DP)) // end of SYNC
recv_stat <= 1;
end
else
begin
if (~(r_dp ^ DP)) one_count <= one_count + 1;
else one_count <= 0;
if (one_count <= 5)
begin
r_data <= { ~(r_dp ^ DP), r_data[7:1] };
data_count <= data_count + 1;
if (data_count == 7)
data_out <= { ~(r_dp ^ DP), r_data[7:1] };
end
end
if ( RECV_EN & ~SE0 & (recv_stat == 1)
& (one_count <= 5) & (data_count == 7) )
data_rdy <= 1'b1;
else if (RECV_DATA_ACK)
data_rdy <= 1'b0;
end
endmodule
送信モジュール
SEND_EN のとき送信開始。まず SYNC を送出し、次からデータを送る。データが途切れると送信終了。
送信側は、シングルエンドとしてのポート状態を生成する。ちょっと変なのだが、(SEND_DONE != 0) のとき DP,DM を出力することを想定している。
一応 受信モジュールとのペアで、パラレルの通信ができるようにした。送信が途切れると パケットを終了し、再開すると自動的に SYNC が付く。CRC などはないから、再送できないので実用的ではないが、ロジックのデバッグには役立ちそうだ。
send_stat は、6 状態になった。シーケンスがあるので、そんなものだろう。
send_stat = 0 : アイドル
send_stat = 1 :
send_stat = 2 : SYNC とデータ
send_stat = 3 : SOP 1 (SE0)
send_stat = 4 : SOP 2 (SE0)
send_stat = 5 : SOP 3
module sender (
input CLK
, input SEND_EN
, output SEND_DONE
, output DP
, output DM
, input [7:0] SEND_DATA
, input SEND_DATA_RDY
, output SEND_DATA_ACK
);
reg r_dp;
reg r_dm;
reg [2:0] send_stat = 0;
reg r_out = 0;
reg [6:0] data_in;
reg data_ack = 0;
reg [7:0] data_count = 0;
reg [7:0] one_count = 0;
assign DP = r_dp;
assign DM = r_dm;
assign SEND_DONE = (send_stat == 0);
assign SEND_DATA_ACK = data_ack;
always @(posedge CLK)
begin
if (~SEND_EN)
begin
send_stat <= 0;
end
if ( (send_stat == 0) & SEND_EN )
begin
send_stat <= 1;
data_count <= 7;
data_in[7:0] <= 7'b1000000;
r_out <= 1'b0;
end
else if (send_stat == 1)
send_stat <= 2;
else if ( (send_stat == 2) & (one_count <= 5) & (data_count == 0) )
if (SEND_DATA_RDY)
begin
data_count <= 7;
data_in[6:0] <= SEND_DATA[7:1];
r_out <= SEND_DATA[0];
end
else // exit sending
send_stat <= 3;
else if ( (send_stat == 2) & (one_count <= 5) )
begin
data_count <= data_count - 1;
data_in[6:0] <= { 1'b0, data_in[6:1] };
r_out <= data_in[0];
end
else if (send_stat == 3) send_stat <= 4;
else if (send_stat == 4) send_stat <= 5;
else if (send_stat == 5) send_stat <= 0;
if ( (send_stat == 3) | (send_stat == 4) ) // SE0
begin
one_count <= 0;
r_dp <= 1'b0;
r_dm <= 1'b0;
end
else if (send_stat == 2)
begin
if ( (~r_out) | (one_count > 5) )
begin
one_count <= 0;
r_dp <= ~r_dp;
r_dm <= r_dp;
end
else one_count <= one_count + 1;
end
else // if ( (send_stat == 1) | (send_stat == 5) )
begin
one_count <= 0;
r_dp <= (FS) ? 1'b1 : 1'b0;
r_dm <= (FS) ? 1'b0 : 1'b1;
end
data_ack <= SEND_EN & SEND_DATA_RDY & ( (send_stat == 1)
& (one_count <= 5) & (data_count == 0) );
end
endmodule
FIFOモジュール
FIFOモジュールも自分で作る。Dual PORT の分散RAM ベース。32 バイトで 30 スライス 、64 バイトだと 50 スライス。たぶん 32 バイトしか選べない。
32 バイト増えて 20 スライス増えている。2 バイトあたり 1 スライス だから 16bit x 1bit が 2 LUT の計算。Dual PORT ならこの値が適切。
送受信は、同時には行わない。FIFO を 2 つ持つのは、もったいないので可能なら共用にしたい。
module udrv_fifo # (
parameter DEPTH = 5
// parameter DEPTH = 6
) (
input CLK
, input RST
, input [7:0] I_DATA
, input I_DATA_RDY
, output I_DATA_ACK
, output I_DATA_FULL
, output [7:0] O_DATA
, output O_DATA_RDY
, input O_DATA_ACK
);
reg [5:0] mem [0:2**DEPTH-1];
reg [DEPTH-1:0] i_addr = 0;
reg [DEPTH-1:0] o_addr = 0;
wire [DEPTH-1:0] i_next = i_addr + 1;
wire [DEPTH-1:0] o_next = o_addr + 1;
reg i_ack = 0;
reg o_rdy = 0;
assign I_DATA_FULL = (i_addr != o_next);
assign I_DATA_ACK = i_ack;
assign O_DATA_RDY = o_rdy;
always @(negedge CLK)
begin
if (RST)
begin
i_addr <= 0;
o_addr <= 0;
i_ack <= 1'b0;
o_rdy <= 1'b0;
end
else if (I_DATA_RDY & ~i_ack)
begin
if (~I_DATA_FULL)
begin
i_addr <= i_next;
mem[i_next] <= I_DATA;
end
i_ack <= 1'b1;
end
else
i_ack <= 1'b0;
if ( O_DATA_ACK & (i_addr != o_next))
begin
o_addr <= o_next;
o_rdy <= 1'b1;
end
else if (i_addr != o_next)
o_rdy <= 1'b1;
else if ( O_DATA_ACK )
o_rdy <= 1'b0;
end
assign O_DATA = mem[o_addr];
endmodule
以上が今回作ったもの。シミュレーションとかデバッグは未だ。あとインターフェイスに SPI を付けなければならない。そうしないとピンが多すぎて、QFN32 に収まらないのだ。
で、専用の SPI モジュールを作ったのだが、問題がある。パケットより小さな FIFO で、Full-Speed に対応するには、12 M bps 前後で通信できないといけない。なかなかに厳しい条件だが、それは置いておく。問題は、SPI の SCK が CLK より早い場合にも対応しないといけないところ。FIFO との ハンドシェークが鍵なのだが、うまく行っていないかも。
とりあえず QFN32 に収めるために、8bit データを inout に変更するのが良さそう。
ところで、作ったは良いがどうやって使うのか? ... V-USB のコードを改造すればうまく制御できるのではないかと思っている。V-USB のなかで見たくもない部分は、通信のところで、アセンブラになっている。これをまるごと置き換えてしまえば 良さそうなのだ。ただ、Full-Speed に対応するには、48 MHz が必要になってしまう。Low-Speed で良いなら V-USB で良いし。結局のところ 規模の大きいFPGA のコアで使うぐらいか。
なかなか使いドコロがないが、CRC のコードも作りたいし、それだけ追加してみて、シミュレータでチェックして、ひとまずの完成としよう。
この後、送信をシミュレータでデバッグした。

Full-Speed(FS) の例。FS では、D+(DP) がプルアップされているわけだから DP = H がアイドル。Hi-Z から DP = H なって、LHLHLHLL と SYNC コードを送出している。次に 0x33 を送るのだが、 ちゃんと LLHLLLHL となっている。

終わりは、DP/DM 共に L を 2 クロックで、次に アイドルと同じ DP = H で最後に Hi-Z 。
結構バグがあったが、なんとかデータを送ってくれるようになった。上で貼り付けたコードは、バグがあるままなので注意。正しいのは、最新のソースコードのみである。
送信でインターフェイスを若干変更。ひとつは、送信STARTを指示するフラグ。FIFO にパケットを全部送ってから、送信というのが妥当だと思えたので。-- 以前に AT90USB162 のコードを作ったが 最大パケットサイズは、32 か 64 にしか出来なかった。それで十分でもあった。その程度ならば FIFOに全部入るのだ。
もうひとつは、DP/DM を出力にするのを指示するフラグ。SEND 中というフラグで代用しようと思っていたのだが 1 クロックずれる。
次は受信をデバッグしよう。送信データを受信側とつなぐのだが、CLK がずれるので、ちょっと工夫がいる。

受信も一応動かせた。受信要求を出すと SYNC_REQ が 1 になって、CLK をずらす。確かに 180°ずれて SYNC が完了しているのだが、SYNC_REQ が ON/OFF を繰り返していて、ぎりぎりで 受信開始になっている。... まずそう。
それは、おいおい直すとして ... 今回は、送信用、受信用 2 つのモジュールを接続したのだが、pull-up/pull-down が分からなかったので論理レベルで接続することにした。このため、LB_TEST という define を追加。

0x33,0x34,0x35,0x36 を送信しているのだが、その 4つをちゃんと受け取って、受信終了。その後、RECV_REQ が ON/OFF を繰り返している。(受信前は、SYNC_REQ)。一応動きはしたが、やっぱりまずそうだ。
CRC について(基礎編)
まずは基礎から。
USBでは、CRC-5 と CRC-16 の2通りの CRC を使っている。CRC-5 は SYNC 込み 32bit のパケット専用で、それより大きいパケットでは CRC-16 を使う。CRC-16 と言っても、計算式はひと通りしかないわけではないらしい。USB での方法を知らないといけないのだ。
(16bit) x^15 + x^14 + x^2 + 1
(5bit) x^5 + x^2 + 1
生成多項式にはこれを使う。初期値には all 1 を使い、出力は ビット反転した上で LSB と MSB も反転。
コードで示した方が早そうだ。
CRC-16:
unsigned int r_crc = 0xffff;
unsigned int In;
for (i=0; i< XX; i++) {
w = (r_crc ^ In) & 1;
r_crc = (r_crc >> 1) & 0x7fff;
if (w) {
r_crc ^= (1<<0);
r_crc ^= (1<<13);
r_crc ^= (1<<15); // w
}
In = (In >> 1);
printf("%04x r %04x\n", r_crc, ~r_crc & 0xffff);
}
CRC-5:
unsigned int r_crc = 0x1f;
unsigned int In;
for (i=0; i< XX; i++) {
w = (r_crc ^ In) & 1;
r_crc = (r_crc >> 1) & 0xf;
if (w) {
r_crc ^= (1<<4);
r_crc ^= (1<<2);
}
In = (In >> 1);
printf("%02x r %02x\n", r_crc, ~r_crc & 0x1f);
}
出力は、~r_crc (bit反転したもの)なので注意。
さて、受信で CRC値が含まれているが、これも含めて CRC 計算してしまうと ... 特定の値になる。CRC-5 では 5'b11001 , CRC-16 では 16'h4FFE 。(bit反転前の値(r_crc) なので注意)
CRC について(応用編)
応用編というより実装編。
module udrv_crc # (
parameter WIDTH=16
// parameter WIDTH=5
, parameter SEQUENTIAL_MATCHING=0
) (
input CLK
, input [1:0] SEL
, input I // LSB first
, output O // LSB first
, output MATCH
, output [WIDTH-1:0] CRC
);
SEL で 動作を指定するのだが、基本的に 2 つのモードがある。ひとつは、01 で CRC 計算。もうひとつは、10 で、計算結果をビットストリームにして出力 -- 送信などで使う。
00 は、なにもしない。-- USB では、0 が挿入される場合があるから、これが必要なのだ。あと、11 は、初期値にする 。
パラレルの CRC は、シミュレータのチェック用で使わないのが基本。
同時に CRC の一致検出 もつけた。やりかたには 2 通りある。ひとつは、計算結果を出力して、入力と比べるというやりかた(SEQUENTIAL_MATCHING=1)。もうひとつは、最後まで計算して特定の値になったかどうか チェックするやりかた(SEQUENTIAL_MATCHING=0)。計算結果を出力してしまうと、以降計算はできないので両立はできない。
(SEQUENTIAL_MATCHING=0) は、受信での CRC-16 チェックに使う。USB ではパケットを受け取り終わってはじめて、2 バイト前が CRC だったと分かる。だから、これを使う以外にはない。
(SEQUENTIAL_MATCHING=1) は、CRC-5 に使おうかと思っている。
reg [WIDTH-1:0] r_crc = (-1);
wire I2 = (r_crc[0] ^ I);
assign CRC [WIDTH-1:0] = ~r_crc[WIDTH-1:0] ;
assign O = ~r_crc[0];
reg r_match = 0;
assign MATCH = (SEQUENTIAL_MATCHING) ? r_match
: (WIDTH == 5) ? ( ~r_crc == 5'b11001 )
: (WIDTH == 16) ? ( ~r_crc == 16'h4FFE )
: 0;
always @(negedge CLK)
begin
if ( SEL == 2'b11 ) // RST
begin
r_crc <= (-1);
r_match <= 1;
end
else if ( SEL == 2'b10 ) // Extract bitstream
begin
r_match <= (I ^ r_crc[0]) & r_match;
r_crc <= { 1'b0, r_crc[WIDTH-1:1] };
end
else if ( SEL == 2'b01 ) // Calc.
if (WIDTH == 16)
r_crc <= { I2 , r_crc[15]
, ( r_crc[14] ^ I2) , r_crc[13:2]
, ( r_crc[ 1] ^ I2) } ;
else if (WIDTH == 5)
r_crc <= { I2, r_crc[4]
, ( r_crc[ 3] ^ I2), r_crc[2:1] };
end
endmodule
実装はこれ。短いとみると長いとみるか...
これを 送受信に組み込むことは成功した。送信・受信でモジュールを兼用。-- わずか 3 スライスほどだが、節約できた。合計の規模は、110 になった。ただし、FIFO のサイズを 32 バイト にしたときの話。これで FIFO のサイズを 64 バイトにするのは無理になった。
実は、ピンの数の方が切実。JTAGENB を使うようにすれば、21 pin 使えるのだが、既に 19 pin 使っている。受信では CRC エラーの出力でさらに 1pin 増えた。送信では、CRC を生成するかどうかの 設定を 2 種類入れたかったのだが、無理なので自動で 生成することにした。
CRC-5 は、計算結果を入れるかどうかの判断をするタイミング(3バイト目 残り 5bit)で、次のデータがあるかどうか を見て なければ、入れる。そこできちんと終わらせないといけない。次のデータがあれば CRC-16 を入れることにする。( データを受け取り終わったら CRC-16 の 2 バイトが挿入される。)
こういうロジックを入れて 110 スライスなのだ。CRC をサポートせず生のデータを送信・受信するなら 79 スライスになった。31 スライスも使っているが、CRC 自体は小さい。制御が大変なのだ。
さて、入れたいメカニズムは入ったのだが ... 入出力をどうするのか まだ。
SPI はひとつの案だが、詳細は詰めないと。それに今の 入出力 。これも一般的な仕様にする必要がある。
ところで、48 MHz の件、 『クリスタルオシレータ(48MHz) SG-8002DC(3.3V)』 -- PLL なしだとこれを使うしかないようだ。
入出力のハンドシェーク
これまで、全部が CLK に同期して動いていた。だが、別クロック系統のものと接続するには、具合が悪い。パラレルで MCU と接続する場合も今のままではダメだし、SPI と接続する場合も同様。
まずは、パラレルのところの整備から始める。
// ______________________ _______
// DATA <______________________><_______ // // _______ _____ // RDY ________| |______________| // // ________ // ACK _____________| |________________ // // // CLK | | | | | //
今考えているのを図にしてみた。
1. ACK が 0 のとき DATA をセットできる。セットしたら RDY を 0 にする。
2. DATA が確定したので、RDY を 1 にする。
3. 受け取る方は、受信後、ACK を 1 にする。
4. ACK が 1 になったら、RDY を 0 にする。
5. 受け取る方は、RDY が 0 になったら ACK を 0 にする。
最短 3 クロック。
このルールにしたがうようにまず変更した。規模は 110 で変わらない。
さて、BUS を使うなら、一般的なデバイス -- RD/WR で制御できるもの と同じようにしておきたい。ついでにコントロールレジスタも付けて ピンも減らしたい。
RDについては、
assign RECV_DATA_ACK = ~RD;
で制御できる。データがあれば、ACK の前後で 安定している。なければ、前のデータが 読めるだけ。RD が H になった後 次のデータがセットされるので、読み込みサイクルが早すぎるとマズイ。なんなら FIFO のクロックを 4倍速にしたり マスター側に合わせて良い。それが出来るように ハンドシェークを見直したのだ。
RECV_DATA_RDY は、BUS に接続するなら コントロールレジスタから読めると便利。だが、AVR などでポート制御するなら、ピンに出しておいた方が良い。
assign DB[7:0] = (~RD & ~RS) ? RECV_DATA[7:0]
: (~RD & RS) ? CTRL_REG[7:0]
: 8'bz;
assign SEND_DATA[7:0] = DB[7:0];
出力制御は、こんな風にする。
次に WRのほう。
assign SEND_DATA_RDY = ~WR;
WR を L にしたときデータが安定しているという条件なら、これでも良い。ACK は無視。勝手に L→ H→ L になる。不安なら、WR を遅延させるのも良いかも知れない。
ここまで作ったところ 115 スライスになった。わずかな増加だが、これを元に SPI を付けるのだ。残り はわずか 13 スライス。
SPI インターフェイス
これで、 SCK が CLK より早かろうが遅かろうが問題なくなった。いよいよ最後の機能として SPI を付ける。
module udrv_spi (
input MOSI
, input SCK
, input CS
, output MISO
, input [7:0] SPI_DATA_I
, output SPI_DATA_I_ACK
, input SPI_DATA_I_RDY
, output [7:0] SPI_DATA
, output SPI_DATA_RDY
, input SPI_DATA_ACK
);
モジュール単体は、こう。SPI を パラレルに変換しているだけ。CLK もない。
続いて本体
reg [3:0] r_count = 0;
reg [7:0] r_spi;
reg [7:0] spi_out;
reg r_mosi = 1'b0;
reg o_data_ack = 1'b0;
reg o_data_set = 1'b0;
reg o_data_rdy = 1'b0;
assign SPI_DATA_RDY = o_data_rdy;
reg i_data_ack = 1'b0;
assign SPI_DATA_I_ACK = i_data_ack;
assign SPI_DATA = spi_out[7:0];
assign MISO = r_spi[7];
always @(posedge SCK)
begin
r_mosi <= MOSI;
if (r_count == 1)
i_data_ack <= 1'b1;
else if (~SPI_DATA_I_RDY)
i_data_ack <= 1'b0;
end
always @(CS , negedge SCK)
begin
if (CS) // LOAD
begin
r_count <= (-1);
r_spi <= SPI_DATA_I;
end
else if (r_count == 7) // LOAD
begin
r_count <= 0;
r_spi <= SPI_DATA_I;
spi_out <= {r_spi[6:0], r_mosi };
end
else // SHIFT
begin
r_count <= r_count + 1;
r_spi <= {r_spi[6:0], r_mosi };
end
o_data_ack <= SPI_DATA_ACK;
o_data_set <= (~CS & (r_count == 7));
if (o_data_set)
o_data_rdy <= 1'b1;
else if (~o_data_ack & SPI_DATA_ACK)
o_data_rdy <= 1'b0;
end
endmodule
悩ましいのは、動作のタイミングが足りないところ。
通常、最後のタイミングは、negedge SCK で (r_count == 7) 。ここでデータ自体は受け取れる。o_data_set を 1 にすることも出来る。だが、o_data_rdy を 1 に出来ない。
CS の変化も条件に入れてようやく 1 にできる。ただし、0 には出来ない。
これでもシミュレータでは動く。だが、always @(CS , negedge SCK) という記述を期待通りにやってくれるかどうか? 不安だったりする。前に Xilinx で試したときは、ダメだったような ...
それも問題だが、上位レイヤーでどう使うか?
コントロールレジスタをサポートしたいのだが、どういうプロトコルにするか ... RS が邪魔なのだ。まぁ、余裕もなさそうだから、ピンから入力することにしよう。
送信の場合、RS=0 にして、パケット分のデータを 送り、RS=1 にして、コントロールレジスタに書き込むことで、SEND_START を 1 にする 。FIFO が空になれば、一定時間後に 送信完了。
受信が問題。通常は受信モード。FIFO が空でなければ、受信中。一定時間後に 受信は完了する。... なんだか苦しいが、ピンもロジックも足りない。内部ロジックには、送信中、受信中というのがあるので、コントロールレジスタをポーリングすれば、分かるようにしておこう。
こんなところか。で、これを実際に作りこむと .. 124 スライスにまでなった。
128 スライスなど小さいと思っていたのだが、udrv.v は、なんと 859 行もある。コメントはそれなりにはあるが、コード自体に冗長な部分はない。メモリを使わずにコードだけで埋めるのは結構しんどい。

シミュレータで、送信側を確認。
シミュレータでは、negedge CS , posedge CS が動作ポイントに入るのは確認できた。
r_count は、4bit 。最初に negedge CS で動いたときに 7 にするのは まずい。F にしている。
negedge CS で F それから 0 1 ... 7 でデータ出力。最後は、negedge SCK で データ出力できている。posedge CS でかろうじて o_data_rdy を 1 。これでデータは受け取ってもらえる。ACK は半端な状態だが、次に送信するときに、 続きから処理される。
o_data_rdy を 1 クロック早めるのは、データを確実に渡せない気がする。ただ、FIFO 側で 遅延して受け取るような変更なら出来る。決めたルールに例外を持ち込むことになるが、このようなケースなら止むを得ないか。
追記: 間違っていた。
always @(CS , negedge SCK)
begin
if (CS) // LOAD
begin
r_count <= (-1);
r_spi <= SPI_DATA_I;
この部分なのだが、先頭の negedge CS で条件が成立していない。
受信の動作確認で分かった。
reg r_cs = 0;
always @(CS or negedge SCK)
begin
r_cs <= CS;
if (r_cs) // LOAD
begin
r_count <= 0;
r_spi <= SPI_DATA_I;
こう変えたら動いたのだが、どうも釈然としない。 negedge SCK だけの場合は、変更前の SCK が見えたのに、こう書くと変更後?

受信しているところ。送信側も SPI だから両方ちゃんと動いている。
FIFO に遅延読み込みのパラメータを付け o_data_rdy を 1 SCK クロック 前にした。
入れたい機能は入れた。QFN32 の 128 スライスという条件をほぼフルに使った。もう満足だったりする。随分と勉強にはなったが、この後どうしよう?
Full-Speed/Low-Speed の切り替えを付けたい。あと差動入力を使うオプション。だが、これ以上のロジック追加は無理だろう。今回は、MachXO2-256 でどこまで出来るか? というテーマなので、いずれ別の機会にしよう。
思い出したので、メモ。
FIFO は、Dual-Port の RAM を使っている。いま想定している使い方だとパケットを受け取り終わってから、READ することになりそう。送信の場合も同じ。なら Single-Port でも良さそうな気がする。制御ロジックが似た様なものなら 2 倍の容量が使える。FIFO まわりを差し替えるだけで済むなら検討してみたい。
... やってみたが、却って規模が増えた。.. 失敗。
あと FIFO は、同期 FIFO を非同期で使えるようにしている。非同期 FIFO というものもある。それと差し替えるとどうなるか試してみたい。 (参考: 『FPGAの部屋:同期FIFOと非同期FIFO』)
残っているものとしては、JTAG 通信と ADC 。DAC は一応 設計してみたのだから、ADC もその程度のものはやりたい。
検討もれ
ひといきつきたいのだが、検討もれがあるかないかが気になっている。マズそうなものをリストアップしていこう。ただし、すぐには直さないかも。
(EOP だけのパケット)
Low-Speed では、これがあるそうだ。
まずこれを送るインターフェイスがない。送信では、SYNC が自動で付加される。0 バイトを送出することもできない。最短は、SYNC - PID - EOP 。
受信でも検出できない。だけでなく、マズイケースがありそうだ。受信中にまで行くと EOP でともかく状態を抜けるのだが、少々不安。あと、受信は完了したが、0 バイトというインターフェイスがいる。
(タイムアウト)
反応が遅すぎてタイムアウトになる可能性がある。 3ms バスが アイドル状態だとサスペンドだそうだ。
心配だったのだが、AVR の USB だと ACK/NAK/STALL を返すのは、上位レイヤーだったことを思い出した。... たぶん大丈夫ではないかと思う。
ただちょっと確認。1 ms というのは、1.5 MHz クロックだとして 1500 クロック。8 バイトのパケットは、SYNC , PID , CRC を含まないそうだから 1(SOP) + 14 x 8 + 3(EOP) の 96 クロックが 最短。Low-Speed だとしても 1ms に対して 十分に短い 感じ。反応が悪いと性能が出ない。
ところで、FIFO は 32 バイトだが、PID + データ を収めないといけない。だが 32 バイトのパケットとは、データのサイズが 32 バイトのようだ。なら収まらない。これもなんとか対処する方法を考えないと。
幸い、受信部分と FIFO の間には、1 バイトのバッファがある。最後のデータを待たせればなんとか。送信も SPI だとバッファがある。
追記:使い方編
使い方を考えながら、インターフェイスを見なおした。
まずは、BUS の信号
// _ ____________________________________ _____
// RS _><____________________________________><_____
//
// _____ _______
// nCS |________________________________|
//
// ______________ _____________
// nWR/nRD |_____________|
//
// _______________
// DB[7:0] --------------<_______________>----------
//
LCD とかのタイミングを参考にするとこんな感じか。信号名も nWR とか nXX にしてみた。
READ は、これで問題ない。WRITE はどうしたものか。どうも posedge nWR でデータ採取しているみたいに見えるのだが ... ここは negedge nWR から遅延させて採取にしたい。
SPI は、普通のタイミング ... だが 、最後のデータは、CS を H にしたとき 、上位に 受け取られる。
とりあえずは、これでいこうと思っている。
ところで、FIFO 付きの BUS/SPI コードを作ったことになる。DAC で欲しかったものだ。DAC もこれをくっつけようと思う。
さて、どう使うかまとめてみた。
ステータスレジスタ
bit7: CRC_ERROR
bit6: DATA_FULL
bit5: DATA_EMPTY
bit4: DATA_RDY
bit3: RECV_EN (受信中)
bit2: SEND_EN (送信中)
bit1: r_recv_mode (送受信モード ) (0: 送信 / 1: 受信)
bit0: r_start (送受信スタート) (1: スタート / 0: 完了)
内部の状態をステータスレジスタとして見せる。この内いくつかは ピンにも出力している。
対応するピン出力( 負論理 )
nCRC_ERROR
nDATA_EMPTY
nDATA_FULL
nDATA_RDY
nBUSY ( ~r_start )
信号の説明
r_recv_mode (送受信モード )
状態は、0: 送信モード か 1: 受信モードのどちらかに大別される。
どちらでもない状態は存在しない。
r_start/BUSY (送受信スタート)
1 にすると 送信モードでは送信要求、受信モードでは 受信要求。
送信が完了すると、続いて受信スタートになる。(r_recv_mode = 1)
受信が完了 すると 0 になる。
CRC_ERROR :
受信完了後有効で、1 になると CRC_ERROR が起きたことを示す。
DATA_FULL :
1 で、FIFO が溢れたことを示す。(データロスト)
FIFO をリセットするまで有効
DATA_EMPTY :
1 で FIFO が空であることを示す。データが入れば 0 になる。
DATA_RDY :
1 で 受信データがあることを示す。
ハンドシェーク用で、DATA_EMPTY とは変化のタイミングが異なる。
※) FIFO は、受信モード→送信モードに切り替えたときと、受信中になったタイミングでリセットされる。
※) ステータスレジスタの読み込み:
(SPI) RS=1 で 0x00 を書き込む。
(BUS) RS=1 で読み込む
コントロールレジスタ
bit7: 書き込み要求
bit1: r_recv_mode (送受信モード ) (0: 送信 / 1: 受信)
bit0: r_start (送受信スタート) (1: スタート / 0: 完了)
動作をさせる信号は、コントロールレジスタとした。書き込み要求は、SPI のみで必須だが、インターフェイスを合わせるために、BUS でも必須ということにしておく。
※) コントロールレジスタの書き込み:
1)送信 モード
(SPI) RS=1 で 0x80 を書き込む。
(BUS) RS=1 で 0x80 を書き込む。
2)送信スタート
(SPI) RS=1 で 0x81 を書き込む。
(BUS) RS=1 で 0x81 を書き込む。
3) 受信スタート
(SPI) RS=1 で 0x83 を書き込む。
(BUS) RS=1 で 0x83 を書き込む。
サポートするのは上記の 3 つ。
送信方法
1) 送信 モードに する
2) PID+データを書き込む。(合計 31 バイトまで)
3) 送信 スタート にする
4) (タイミングを見て) 追加のデータを書き込む。
やはり、規模的に これが限界。最大パケットサイズを 32 バイトにすれば、追加のデータは、高々 2 バイト。すぐに送信が始まることは保証できるので、ステータスを見なくても タイミングは計れるはず。空になる前に送り込めるだろう。

シミュレータで確認したが、こんな感じ。

続いて送信されるわけだが、FIFO が空になると CRC を付けて 送信完了。CRC-5 を使う場合も自動で元データと CRC を入れ替える。
※ シミュレータ結果で i_recv_mode が 1 になっているが、過渡のものなので気にしないで欲しい。
受信方法
1) 受信 スタート にする

データが来ると nDATA_RDY が 0 になるので データを受け取る。nBUSY が 0 になった後は、新たなデータは来ない。 nBUSY = 0 かつ nDATA_EMPTY = 0 を確認して終わる。
終わるときに、nDATA_FULL = 0 か nCRC_ERROR = 0 なら、エラーにする。

FIFO が溢れなければ良いので、高々 2 バイト余分に受け取ることは可能だろう。
nDATA_RDY=0 で割り込みをかけるとする。ここから 12 MHz で 8 x 31 クロックもある。この間に SPI で 2 バイト 受け取れば溢れない。AVR なら問題なさそうに思える。
以上だが、パケットの間隔というのが気になる。送信→受信、受信→受信で、データを受け取り損ねるとマズイ。特に受信→受信。このケースには、SETUP や OUT がある。
まずいなら、仕様を変更しないとならない。... どうもその必要がありそうだ。
ソースコードは、qfn32samples-11.zip の予定(準備中)。
追記: ソースコードのブラッシュアップとバグ修正
仕様を変更するにも、まずは規模を減らさないといけない。無駄なところがないかチェックしてみた。
そうしたら、... 送信での CRC 生成がエンバグしていた。
後で use_i16 , use_i5 という レジスタを作ったのだが、全く不要なばかりか、バグの原因になっていた。
次、CRC のモジュールは 送信・受信で共有している。特に CRC-5 は、配線の切り替えの方が重いような気がしたので、2 つに分けてみた .... これは 1 スライスの増加に終わった。残念。
次、受信での CRC の制御。こっちもバグっていた。まず、CRC-5 か CRC-16 のどちらの結果を取るか示す MATCH_SEL というレジスタがあるのだが、完了でリセットされる。これではまずいので 状態を残すように修正。
次、MATCH_SEL 自体を良くみたら 受信 4 バイト以上という条件だったので、data_bytes の bit を 1 つ増やして data_bytes[2] に変更。
次、FIFO リセットの条件を 受信モード・送信モードを切り替えたときに変更。受信→受信に対応するベースにする。FIFO_FULL は、受信モードでずっと残るが、その方が都合が良いようにも思える。その上で、受信モードは常に受信待ちということにした。r_start は、1つめを受信したときに 0 になる。完了が必要ならば、r_startを再度 1 にする。
さて、 送信→受信のパターン。DATA0/DATA1 を送ったあとの ACK が相当するようだ。タイミングによって ACK を取りこぼしてしまう。やはり送信後すかさず受信モードにすることにした。r_start は、0 になるので、次の受信の完了は検出できない。あと、送信時の FIFO_FULL も検出できない。
ここまでやった。そして ... なんとか 128 スライスに収まった。
まだ続く
もう FIX したいのだが、まだ手直しするところが ...
どれぐらいのクロックで動くのかの確認をするついでに、DCMA モジュールというのを入れてみた。これは、クロックセレクタで、ここを通すとグローバルクロックにつなげられる。で、やってみると DCMA に入力できない。... ピン配置を変えると OK みたいなので、ピン配置を見なおした。
ついでに RS ... データとコントロールレジスタの選択だが、一般に RS=0 が コントロールレジスタみたいなので論理を逆転。
あと見直していたら、CRC-5 を 2 個使うようになっていた。これで問題なさそうなので 2 個使う USE_DUAL_CRC5
を標準にした。
ついでに SUPPORT_SEND_EOP というのを作りかけていたのだが、ヤメ。コード削除。
で、動作周波数の確認。... ボトルネックを見ていたら、r_recv_mode1 が引っかかった。1 クロック遅延させるつもりが、半クロックになっていたので変更。さらに FIFO のクロックを CLK_4X にしていたのだが、SPI では必要ないので通常の CLK に。
最終的に CLK_4X は、67.7 MHz になった。本来 48 MHz だが、これぐらい余裕があれば安心。 規模は 127 スライスでぎりぎり OK。
あと、CLK_OUT を付けた。CLK_4X の反転出力。水晶があれば、発振させられるかも。ソースコードは、-11 を上書き。
ソフト側の構想
V-USB を改造すればいけるんじゃないかと上で書いたが、少し確認してみよう。本格的にやる場合は別記事にするが、とりあえず、あたりを付けよう。
extern usbRxBuf, usbDeviceAddr, usbNewDeviceAddr, usbInputBufOffset
extern usbCurrentTok, usbRxLen, usbRxToken, usbTxLen
extern usbTxBuf, usbTxStatus1, usbTxStatus3
extern usbSofCount
; extern unsigned usbCrc16(unsigned char *argPtr, unsigned char argLen);
; extern unsigned usbCrc16Append(unsigned char *data, unsigned char len);
; extern unsigned usbMeasurePacketLength(void);
アセンブラで extern を grep すれば、こういうのが見つかる。
下の 3 つは、アセンブラから呼び出すサブルーチンで無視して良い。CRC などは機能にあるし、usbMeasurePacketLength というのも NRZI 関係だろう。
さて、上は全部変数。この変数を制御しているのが、usbdrv.c 。同じインターフェイスにすれば、わずかな変更でいけそうだ。たぶんアセンブラは INT0 割り込みで動作する。udrv では、なにか受信すると DATA_RDY が L になる。この変化を INT0(か 1) で拾えば同じような処理ができるはず。
受信は割り込みベース、送信はポーリングでも良いかも。12 MHz でデータが来るが、16 バイト受信ぐらいまでに応答するとすれば、10 us 以内。20 MHz なら 200 クロックもある。これなら受信ルーチンも C で良いかも知れない。
memo : 受信側
uchar usbRxBuf[2*USB_BUFSIZE];
uchar usbInputBufOffset;
volatile schar usbRxLen;
uchar usbCurrentTok;
uchar usbRxToken;
このあたりが受信で重要そう。
len = usbRxLen - 3;
if(len >= 0){
usbProcessRx(usbRxBuf + USB_BUFSIZE + 1 - usbInputBufOffset, len);
こういうコードがある。usbRxLen には、先頭の PID と 最後の CRC-16 が含まれるから -3 する。
PID の場所は、usbRxBuf + USB_BUFSIZE - usbInputBufOffset ということらしい。
CRC-16があるということは、DATA0/1 パケット。その前に SETUP か OUT が来ているはずで、それが、その前に入っているのかも。この udrv でも同じ仕様が都合が良さそう。
usbRxToken は、OUT or SETUP (の PID)が入っている。usbCurrentTok は、OUT だった場合に エンドポイント番号が格納される。
あと、受信では 1 フレームの送受信を一気にやってしまうようだ。IN が来たら予め用意されたデータを送る。OUT or SETUP では、ACK などの応答を返してしまう。
なんと、まだ続く
V-USB のコードを眺めていたら CRC-16 は、DATA0/1 パケットに使うことが分かった。サイズは関係ない -- 誤解してた。あと、CRC-5 の送信側のコードがない。.... デバイスは 指示パケットを送信しないのであった。
仕様を誤解していたのだから、まずは修正。CRC-5 の送信側のフラグは、crc5_extract というものなのだが、ややこしいので、整理したら規模が少し減った。さらにデバイス専用にも出来るようにして、少し規模を削れるように parameter を導入したところ、論理が変わらないのに規模が減った。今は 123 スライス。デバイス専用だと 121 スライス。
ついでなので、0 バイト送信で SOP のみを送る機能は、Low-speed のみなので FS パラメータ(1 で Full-speed) に連動するように変更。さらに DP , SE0 状態を negedge CLK でサンプリングするよう変更。こうしておかないと 受信毎にクロックを 180°ずらすことになる。ソースコードは、-12 版準備中。
最終的な仕様を記録しないとわけが分からなくなりそうだ。新記事を起こそう。
関連記事
・ 『QFN32の FPGA』
・ 『TTL ALU 74281』
・ 『FPGA時計の設計』
・ 『MCPU -- A Minimal 8 Bit CPU』
・ 『DACを設計してみよう』
・ 『USBコントローラの設計』
最新ソースコード (2012/07/1)
・ qfn32samples-10.zip
qfn32samples-10.zip で一応の完成とした。最終的な規模を記録しておこう。GSR は、1 つしかないリソースで、SPI で使った非同期リセットで 使われたらしい。
BUS SPI
Number of registers: 117 126
PFU registers: 115 123
PIO registers: 2 4
Number of SLICEs: 116 122 /128
SLICEs(logic/ROM): 32 32 /32
SLICEs(logic/ROM/RAM): 84 90 /96
As RAM: 12 12 /96
As Logic/ROM: 72 78 /96
Number of logic LUT4s: 200 214
Number of distributed RAM: 12 12
Total number of LUT4s: 224 238
Number of PIO sites used: 20 14 /22
Number of GSRs: 0 1 /1
最新ソースコード (2012/07/05)
・ qfn32samples-11.zip
最新ソースコード (2012/07/06)
・ qfn32samples-12.zip
今度こそ.. ちなみに、実際に動かそうと思えるレベルかどうかが、『一応の完成』の基準。(動かせばバグは出るだろう)
規模を記録しておく。僅かながら変更する余裕ができた。SUPPORT_CRC5S=0 とすると、送信時 CRC-5 は生成しない(device では不要) が、-2 スライスになる。
BUS SPI (CRC 分)
Number of registers: 123 148 45
PFU registers: 121 145 45
PIO registers: 2 3
Number of SLICEs: 112 123 /128 27
SLICEs(logic/ROM): 32 32 /32
SLICEs(logic/ROM/RAM): 80 91 /96 33
As RAM: 12 12 /96
As Logic/ROM: 68 79 /96
Number of logic LUT4s: 197 218 64
Number of distributed RAM: 12 12
Total number of LUT4s: 221 242 64
Number of PIO sites used: 21 14 /22
Number of GSRs: 0 1 /1
参考文献(データシート等)
・ 『MachXO2 ファミリデータシート(日本語 pdf)』
・ 『MachXO2 テクニカルノート(日本語)』
・ 『Lattice Diamond 1.4マニュアル』