シリアル通信で Hello, FPGA (2)

実用的な回路の設計・実装と動作確認を通じて,ハードウェア記述言語 (HDL) を使った FPGA 上のディジタル回路の設計について学ぶコースの第2回です。

前回は、順序回路を HDL で記述するときの基礎をいくつか確認しました。今回は、前回に学んだことを踏まえて、シリアル通信 (UART) による文字送信の回路を HDL を用いて設計していきます。

シリアル通信 (UART) とは

UART の概要

機器の間で1ビットずつデータを送る通信方式のことを、シリアル通信といいます。 UART (Universal Asynchronous Receiver Transmitter) は、PC やその周辺機器のシリアル通信の規格である RS-232 で使われる通信方式です。 元々 UART というのはこの通信方式を実現する IC を指すものでしたが、今では通信方式自体もしばしば UART とよばれています。 結局のところ、シリアル通信・UART・RS-232 はほぼ同じ意味で使われる言葉になっています (厳密には RS-232 規格は電圧レベルなどが異なりますが……)。

RS-232 の端子には、D-Sub という D 字型の端子 (9ピンまたは25ピン) が使われており、 産業用 PC では今でも産業機器との接続のために使われています。しかし一般向けの PC では、端子の大きさや通信速度の遅さ、電源を入れたまま機器を抜き差しできないことなど様々なデメリットがあり、USB が普及した2000年ごろからは USB に取って代わられるようになりました。今や D-Sub 端子をもつ一般向けの PC は皆無です。

一方で、通信方式としての UART は、(このあと説明しますが) 複雑な取り決めが必要なく、FPGA と PC との通信手段としては非常に手軽で便利です。そのため最近の FPGA 評価ボードには、UART と USB との変換用の LSI (FTDI 社の FT232 がよく使われます) が備わっていて、USB でボードと PC を接続すると仮想的なシリアル通信のポートが利用できるようになるものが多いです。ACRi の FPGA 検証環境で利用できる Arty や PYNQ でも、この仕組みで UART が利用できます。

UART の信号

UART の通信は、基本的に送信・受信でそれぞれが1本ずつの信号を使います。送信・受信の信号をそれぞれ TXD (Transmit Data)、RXD (Receive Data) とよびます。FPGA と PC のどちら側から見るかによって、「送信」と「受信」の意味が逆転するので注意してください。ここでは、FPGA から見て、FPGA がデータの送信に使う信号を TXD としましょう。

UART では、Asynchronous (非同期) の名の通り、送受信器の間でクロック信号を用いたタイミングの同期を取りません。送信するデータがないときには信号は常に ‘1’ としておき、データを送信するときには送信側が信号を一定時間’0’にすることによって、そのことを受信側に伝えます。データの始まりを示すこの ‘0’ をスタートビットとよびます。その後、送信したいデータを最下位ビットから順に1ビットずつ、一定時間おきに送信します。最後にデータの終わりを示す ‘1’ (ストップビットとよびます) を一定時間送信したら、1回のデータの送信は終了です。送受信器の間で、あらかじめ通信速度 (ボーレートともいいます) や一度に送信するデータのビット数などを取り決めていて、送受信器が各々の正しいタイミングで回路を動作させれば、この方法で正しく通信ができます。

通信速度には RS-232 にならい、9,600の倍数、特に 115,200 bit/s がよく用いられます。また、1つのデータは8ビット、ストップビットの長さは1ビット分とすることが一般的です。

UART で単一データを送信する際のタイミングチャート。

ここまで説明したことを上のタイミングチャートにまとめました。送受信器の回路は、通信速度より十分速い任意の周期のクロック (CLK) 信号で駆動して構いません。図ではクロック周期を Tclk としています。 送信器が送信したいデータを受け取った (書き込み有効 WE が’1’になった) ら、まずはスタートビットの送出を一定時間続けます。この時間は通信速度の逆数で定められるもので、図では Tser で表しています。例えば、115,200 bit/s で通信を行うならば Tser ≒ 8.68 μs です。スタートビットの送出が出来たら、次はデータを最下位ビットから順に送出し、最後にストップビットを送出します。

タイミングチャートを俯瞰して眺めれば、結局のところ送信器は「1ビットの ‘0’、8ビットのデータ、1ビットの ‘1’」の計10ビットを Tser 時間おきに送出する回路、であることがわかると思います。

文字送信回路の設計

これを踏まえて、他の回路から1文字 (8ビット) のデータを受け取り、シリアル通信により送信する、文字送信回路を設計しましょう。

回路の入出力

まずは回路の外見、つまり入出力から定めていきます。文字送信回路は、受け取ったデータを記憶しておかなければならないので、順序回路です。そのため、まずはクロック入力 (CLK) とリセット入力 (RST) は必要です。データは8ビットで入力 (DATA_IN) し、1ビットで出力 (DATA_OUT) します。入力をいつ受け取るかも必要なので、書き込み有効入力 (WE) も用います。

また、 他の回路からしてみれば、次のデータを送ってもよいかどうか、つまり文字の送信が終わったかどうかを知ることができた方が便利です。そのため、送信が終わってないことを示すビジー出力 (BUSY) も用意しておきましょう。以上をまとめると、入出力は以下の通りとなります。

  • 入力: CLK, RST, DATA_IN (8ビット), WE
  • 出力: DATA_OUT, BUSY

回路の構成要素

回路の中身の設計に移ります。前回、よく使われる順序回路としてカウンタとシフトレジスタを紹介し、シフトレジスタの中には並列 (パラレル) のデータを直列 (シリアル) に変換するバリエーションがあることも確認しました。今回の文字送信回路でデータを受け取って送信する部分は、まさにこのシフトレジスタで実現できます。

また、一定時間待つという機能は、クロックの立上りの回数を数えられれば、つまりカウンタがあれば実現できますし、今何ビット送信が終わったかを数えるのも、やはりカウンタの出番です。最後に、シフトレジスタと2つのカウンタを制御するための順序回路を、状態遷移図を描いて考えていきます。このような状態遷移図で表せる順序回路を、ステートマシン (状態機械) とよびます。

文字送信回路の全体構成。

上図は、これら3種類の4個の構成要素の関係を表したものです。 データの送受信それ自体はシフトレジスタによって行われます。シフトレジスタのサイズは、スタートビットとストップビットを含んだ10ビットとなります。

1ビット分、すなわち Tser の時間だけ待つことを担当するのが、図左側のカウンタです。このカウンタを D 進のカウンタとすると、定数 D は D = Round(Tser / Tclk) で定められます。例えば、通信速度を 115,200 bit/s、クロック周波数を 100 MHz とすれば、Tser ≒ 8.68 μs、Tclk = 0.01 μs ですから、D = 868 となります。一方で、送信したビット数を数えるのが、図右側のカウンタです。このカウンタは10進のカウンタになります。

状態遷移図の設計

文字送信回路のステートマシンの状態遷移図。

ステートマシンは、文字送信回路全体の制御を司る、今回のキモになる部分です。 その状態遷移図を上図に示します。文字送信回路のもつ状態は、外からのデータの受け取りを待っている状態 (IDLE) と、受け取ったデータを送出している状態 (SEND) の2つになります。もちろん、IDLE 状態ならばビジー出力は ‘0’、SEND 状態ならばビジー出力は ‘1’ です。

IDLE 状態では、書き込み有効入力 (WE) が ‘1’ であるかどうかを確認します。もし WE が ‘1’ ならば、シフトレジスタに送信するデータ (1ビットの ‘0’、8ビットのデータ、1ビットの ‘1’ ) をセットして、SEND 状態へと遷移します。

SEND 状態では、まずは待ち時間のカウンタが D – 1 まで数え終わったかどうかを確認します。 もし数え終わっていれば、今度はこれまでに送信したビット数を確認します。送信したビット数が9 (つまりこれが10ビット目) であれば、これでデータの送出は終了となるので、IDLE 状態に戻ります。そうでなければ、次のビットの送出のため、シフトレジスタを1ビットシフトさせ、送信したビット数のカウンタを1つ進めます。

文字送信回路の SystemVerilog 記述

ソースコード全文

以上の設計のもと記述した、文字送信回路の SystemVerilog 記述を以下に掲載します。

module serial_send (
    input  logic       CLK, RST,
    input  logic [7:0] DATA_IN,
    input  logic       WE,
    output logic       DATA_OUT,
    output logic       BUSY);

    parameter  WAIT_DIV = 868; // 100 MHz / 115.2 kbps
    localparam WAIT_LEN = $clog2(WAIT_DIV);

    typedef enum {
        STATE_IDLE,
        STATE_SEND
    } state_type;
    state_type           state, n_state;
    logic          [9:0] data_reg, n_data_reg;
    logic [WAIT_LEN-1:0] wait_cnt, n_wait_cnt;
    logic          [3:0] bit_cnt, n_bit_cnt;

    assign DATA_OUT = data_reg[0];

    always_comb begin
        BUSY       = 1'b0;
        n_state    = state;
        n_wait_cnt = wait_cnt;
        n_bit_cnt  = bit_cnt;
        n_data_reg = data_reg;
        if (state == STATE_IDLE) begin
            if (WE) begin
                n_state    = STATE_SEND;
                n_data_reg = {1'b1, DATA_IN, 1'b0};
            end
        end else if (state == STATE_SEND) begin
            BUSY       = 1'b1;
            if (wait_cnt == WAIT_DIV - 1) begin
                if (bit_cnt == 4'd9) begin
                    n_state    = STATE_IDLE;
                    n_wait_cnt = 0;
                    n_bit_cnt  = 4'd0;
                end else begin
                    n_data_reg = {1'b1, data_reg[9:1]};
                    n_wait_cnt = 0;
                    n_bit_cnt  = bit_cnt + 1'b1;
                end
            end else begin
                n_wait_cnt = wait_cnt + 1'b1;
            end
        end
    end

    always_ff @ (posedge CLK) begin
        if (RST) begin
            state    <= STATE_IDLE;
            wait_cnt <= 0;
            bit_cnt  <= 4'd0;
            data_reg <= 10'h3ff;
        end else begin
            state    <= n_state;
            wait_cnt <= n_wait_cnt;
            bit_cnt  <= n_bit_cnt;
            data_reg <= n_data_reg;
        end
    end
endmodule

解説

基本的な構造は前の記事で説明した通りです。大きく4つ (回路の外見 = 入出力、状態と内部信号、組合せ回路、記憶回路) に分かれています。入出力の定義 (1~6行) については、設計の際に見てきた入出力の信号との対応を確認してください。

8~9行で、1つ新しい文法 parameter 文あるいは localparam 文を使っています。これは、回路の中で定数を定めたい時に使う文です。他の回路の内部でこの回路を使うときに、定数を他の回路から変更してもよい場合には parameter 文を、そうでない時 (回路内のローカルな定数である場合) は localparam 文を、それぞれ用います。今回は、待ち時間のカウンタで使う定数 D に相当する WAIT_DIV を変更可能な定数としています。

D 進のカウンタの値を表現するのに必要な最小のビット数は、2を底とした D の定数を取り、端数を切り上げることによって求められます。SystemVerilog には、これに相当する $clog2 関数が用意されているので、時間待ちカウンタのビット数をローカルな定数 WAIT_LEN として定義しておきます。

状態と内部信号の宣言 (11~18行) では、ステートマシンの状態とそれに対応する内部信号をまず定義します。それに加え、シフトレジスタで記憶する値 (data_reg)、待ち時間のカウンタの値 (wait_cnt)、送信したビット数のカウンタの値 (bit_cnt) もそれぞれ定義しています。

組合せ回路部分 (20~49行) では、ステートマシンの状態遷移図に合わせて、次の状態やカウンタの値、出力を定めます。コメントをつけた3ヶ所が、状態遷移図における3つの矢印にそれぞれ対応しています。シフトレジスタに関しては、前回紹介したものとシフトの方向が左右逆になっていることに注意してください。これは、UART ではデータが最下位ビットから送出するためです。

記憶回路部分 (51~63行) は基本的に決まりきった表現ですが、初期値の定め方には注意が必要です。リセット直後には、送出するデータがないので、状態は IDLE になります。また、このとき ‘1’ を出力する決まりなので、シフトレジスタには全て ‘1’ を入れておきます。

まとめ

シリアル通信 (UART) による文字送信回路の設計と、SystemVerilog 記述について見てきました。ポイントは以下のとおりです。

  • UART で1文字分のデータを送る場合、スタートビット、データ、ストップビットを一定の時間ずつ送出すればよいこと。
  • 文字送信回路はカウンタとシフトレジスタを組合せ、全体を制御するステートマシンを加えることで構築できること。

今回は回路を設計・記述しただけで、この回路が正しく動くかどうかはまだわかりません。次回は、論理シミュレーションを用いて回路を検証する方法について解説していきます。

愛知工業大学 藤枝直輝

タイトルとURLをコピーしました