シリアル送信を行うVerilog-2001のコードの一例を以下に示します。
module putc(
input [7:0] char,
input req,
output txd,
output reg ack,
output ready,
input clk9600, clock);
reg [9:0] sr;
assign txd = sr[0];
reg [4:0] cnt;
assign ready = ~|cnt;
reg last_clk, clk_buf;
always @(posedge clock) begin
last_clk <= clk_buf;
clk_buf <= clk9600;
if(~last_clk & clk_buf) begin
if(|cnt) begin
sr <= {1'b1, sr[9:1]};
cnt <= cnt - 5'h1;
end
else if(req & ~ack) begin
sr <= {1'b1, char, 1'b0};
cnt <= 5'd10;
ack <= 1'b1;
end
else if(~req) ack <= 1'b0;
end
end
endmodule
モジュールputcは、信号線reqをHにすることで入力ポートcharにセットされた送信データを取り込み、clk9600に 与えられた送信クロックに同期して出力ポートtxdにシリアル送信信号を出力します。
シリアル送信に用いるクロック信号clk9600の周波数は、FPGA内部の信号処理に使用するクロック信号 (clock)と異なる、通常、非常に低い周波数が使用されます。上位モジュールとの通信に使用する信号線への値の 設定はclockに同期して行われるため、これらの信号線の参照もclockに同期して行う必要があります。
そこで、モジュールputcはclockに同期して動作させることとし、送信動作をclk9600の立ち上がりを検出して これに同期して行うようにします。立ち上がりの検出は、clk9600の信号レベルをclk_bufに取り込み、その前回値を last_clkに取り込み、clk_bufがHで、かつlast_clkがLの場合を立ち上がりであるとすることによって行います。
送信すべきビットはシフトレジスタsrに格納されており、送信すべき残りのビット数はカウンタcntにセットされています。 clk9600の立ち上がりが検出された際の動作は次のようになります。
まず、cntがゼロでない場合、シフトレジスタsrを1 bit右シフトし、カウンタを一つ減じます。シフトレジスタの最下位 sr[0]はtxdにアサインされていますので、srを右シフトすることでシリアル送信信号線txdに次のビットを送り出すことができます。
送信すべき残りのビット数を示すカウンタcntがゼロである場合は次の送信動作を行う準備ができていることを意味します。 この状態はassign ready = ~|cntで形成されるready信号として出力ポートにも出力されています。
出力要求を受け付ける入力ポートreqがHであれば、送信データcharにスタートビット1'b0とストップビット1'b1を前後に加えた ものをシフトレジスタsrにセットし、送信すべき残りのビット数を10とします。また、送信要求を受け付けたことを上位モジュールに 伝達するための信号線ackをHとします。
信号線ackはまた、送信動作を1回だけ確実に行うためにも使用されます。つまり、ackがLであることを送信動作を行う条件 としており、送信動作の実行と同時にackをHとすることで送信動作は1回だけ実行されることになります。
putcに送信要求を出した(reqをHとした)上位モジュールはputcの出力するack信号がHとなったことを確認してreqをLとします。 モジュールputcはreqがLとなったことを確認してack信号をLに戻します。
以下に、モジュールputcを用いて1バイトの送信動作を行うための上位モジュールのコードを示します。
if(ready_putc & ~req_putc & ~ack_putc) begin char_putc <= "A"; req_putc <= 1'b1; end else begin if(ack_putc) req_putc <= 1'b0; end
上のコードで、上位モジュールの信号線は、モジュールputcの入出力ポートの信号線名に_putcを付加した名前としています。 送信要求はready_putc(モジュールputcのready)がHであり、req_putcがLであり(即ち送信要求がまだ行われておらず)、ack_putc がLである場合のみに実行します。また、ack_putcがHになったこと(モジュールputcが送信要求を受け付けたこと)を確認して req_putcをLに戻します。
上の例では、モジュールputcを操作する部分のみが示されていますが、実際のコードは送受信部分をステートマシンとして構成 する等の処理が必要となります。即ち、特定のステートの際(ないし特定のフラグが立っている場合)にのみ送信処理要求を出し、 送信処理要求を出した際にはステートを進める等の処理を組み合わせることになります。
1バイトのシリアル受信を行うVerilog-2001のコードの一例を以下に示します。
module getc(
output reg [7:0] char,
output reg gotten,
input ack,
input rxd,
input rclk16,
input clock);
reg [3:0] state;
reg [4:0] next;
reg [6:0] past7;
wire [1:0] sum01 = past7[0] + past7[1];
wire [1:0] sum23 = past7[2] + past7[3];
wire [1:0] sum45 = past7[4] + past7[5];
wire [2:0] sum03 = sum01 + sum23;
wire [2:0] sum46 = sum45 + {1'b0, past7[6]};
wire [3:0] sum06 = sum03 + sum46;
wire mj = ~|sum06[3:2];
reg last_clk, clk_buf;
always @(posedge clock) begin
clk_buf <= rclk16;
last_clk <= clk_buf;
if(clk_buf & ~last_clk) begin
past7 <= {past7[5:0], ~rxd};
case(state)
4'd0: begin
if(~mj) begin // IDLE
next <= 5'd24;
state <= 4'd1;
end
else if(ack) gotten <= 1'b0;
end
4'd1, 4'd2, 4'd3, 4'd4, 4'd5, 4'd6, 4'd7, 4'd8: begin
if(|next) begin
next <= next - 5'd1;
end
else begin
char <= {mj, char[7:1]};
state <= state + 4'd1;
next <= 5'd15;
end
end
4'd9: begin // stop bit
if(|next) next <= next - 5'd1;
else begin
state <= 4'd0;
gotten <= 1'b1;
end
end
default: state <= 4'd0;
endcase
end
end
endmodule
このコードは、多数決論理により耐ノイズ性を改善しています。このため、ボーレートを16倍した周波数のクロック信号 clk16で動作させています。多数決論理は次のように構成されます。
まず、受信信号は、rclk16に同期する、ボーレートの16倍の周波数で7ビットのシフトレジスタpast7に取り込まれます。 past7の各ビットは1ビット幅の整数としてそれぞれ加算され、これらの和sum06が4以上の場合(ビット2またはビット3がHの 場合)にmjがHとなります。つまり、Hが多数派であるということになります。
モジュールgetcはレジスタstateの値によって動作を選択するステートマシンとして構成されています。状態stateの それぞれの値の意味と動作内容は次のようになります。
state=0の場合:無信号(アイドル)状態であり、スタートビットを待ちます。スタートビットが検出された場合、即ち mjがLとなった場合は、次回の検出タイミング(next)を24にセットしてstateを1とします。
state=1〜8の場合:データビットの検出中であり、検出タイミングとなるまでの間、検出タイミングカウンタnextを1 ずつ減じます。nextが0となった場合は多数決結果を受信レジスタcharの上位よりシフトインし、stateをインクリメント するとともに、次回の検出タイミング(next)を15にセットします。
state=9の場合:ストップビット待ち状態であり、ストップビット検出タイミングまでnextを減じながら待ちます。 ストップビット検出タイミングとなったら、受信完了フラグgottonをHとし、stateを0に戻します。
このモジュールは、ボーレートの16倍のクロックレートで作動するため、受信信号のそれぞれのビットは16クロック 毎に検出されます。このため、各ビットを受信するごとにnextを15とし、16クロック後に次のビットを検出するようにします。
例外はスタートビット検出後のnextの設定です。スタートビットは(ノイズがなければ)Lを4回受信した時点で検出 されますが、スタートビットの幅は16クロック分あることが期待され、この後12クロックの間がクロックビットの受信中という ことになります。多数決論理は7ビットの幅で検出を行いますが、受信期間の前後4〜5ビットを除いた中心部の7ビット 幅で多数決を行うことが最適です。そこでスタートビットを検出した際はnextを24として、次回のビット検出を信号の 中心部で行うようにしています。
モジュールgetcは、どの瞬間にシリアルデータが送られてくるか予測できないため、アイドル状態でスタートビットを検出 した際には、常に次のデータの受信を開始します。即ち、受信データの処理が完了する前に次の受信動作が開始される 可能性があります。これによるデータのとりこぼしを防ぐためには、受信が完了した後、速やかに受信データを転送しなけれ ばなりません。
シリアル送受信を確実に行うためには、CTS, RTS信号線を用いたハードウエアハンドシェークを行うのが理想的です。 今回は、私の用いた試験環境にハードウエアハンドシェークの信号線が準備されていないため、これらの機能は用いて おりません。このような場合は、PC側の受信バッファがオーバーフローしないようにFPGAからの送信を行うとともに、FPGA が受信を完了した際には速やかに関連する処理を完了するなどの配慮が必要になります。、