0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自作PCを作る6 ~キーボード入力を受け取ろう~

Posted at

前回までのあらすじ

  • 自作PCを作ろう!
  • まずメモリを作ったよ!
  • ISAを作ったよ!(コンパイラはまだ)
  • アセンブリ言語を作ったよ!(コンパイラはまだ)
  • CPUを作ったよ!
  • 任意のプログラムを実行できるようになったよ!

前回からの変更

いっぱいあるので,ここでまとめて紹介はしないことにします.
これ以降で少しずつ説明します.

今回の目標

キーボードで何か入力し,その入力された文字をそのまま出力することをひとまず目標とします.
といっても,自作PCに直接キーボードや画面を繋いで…とやるのは難易度が高いので,一旦以下のような方法で妥協したいと思います.

キーボード入力はPYNQ-Z2搭載のCPUから行い,コマンドラインツールはPYNQ-Z2に搭載されているものを使用する.
PYNQ-Z2のCPUはキーボード入力をそのまま自作PCに送信し,自作PCは読み取った値をそのままPYNQ-Z2のCPUに返す.
PYNQ-Z2のCPUは受け取った値を画面出力する.

どう実現するのか

PYNQ-Z2のCPUと自作CPUのデータ通信はDMAを使用し,バッファにはAXI4-Stream Data FIFOを使用します.

標準入出力用のIOをCPUに増設します.
ついでに既存のレジスタも含めて記載すると以下の通りです.

アドレス 割り当て
6'h006'h0f CPU内のレジスタ.
6'h10 SP.スタックポインタ.下記戻り先のうち,現在使用しているもの.
6'h0f6'h1a 呼び出し関数の戻り先を保存.
6'h1c FLG.フラグ.書き込み禁止.
6'h1d RSI.引数が格納されているレジスタの一つ目の番地
6'h1e RAX.演算結果.
6'h1f PC.プログラムカウンタ.書き込み禁止.
6'h20 タクトスイッチ.上位4ビットは0固定.書き込み禁止.
6'h21 DIPスイッチ.上位6ビットは0固定.書き込み禁止.
6'h22 LED.上位4ビットは0固定.
6'h23 RGB LED.上位2ビットは0固定.
6'h24 Pmod A.
6'h25 Pmod B.
6'h26 AR8~AR13.上位2ビットは0固定.
6'h27 A,AR_SDA,AR_SCL.上位5ビットは0固定.
6'h28 AR0~AR7.
6'h29 AR_RST.上位7ビットは0固定.書き込み禁止.
6'h2A AR_MISO,AR_SCK,AR_MOSI,AR_SS.上位4ビットは0固定.
6'h2B アナログピンはいったんオミット
6'h2C XADCもいったんオミット
6'h2D GPIO0~GPIO7.
6'h2E GPIO8~GPIO15.
6'h2F GPIO16~GPIO23.
6'h30 GPIO24~GPI27.上位4ビットは0固定.
6'h31 標準入力データ
6'h32 標準入力シグナル.{29'h0, tlast, tvalid, tready}
6'h33 標準出力データ
6'h34 標準出力シグナル.{29'h0, tlast, tvalid, tready}

回路

必要なモジュールは以下です.

  • キーボードの入力を受け取るデバイスドライバ.ただし,実際にはPYNQ-Z2のCPUからの通信を受け取る仕事をする
  • CPUからの標準出力を送信するデバイスドライバ

デバイスドライバと言っても,DMAとのインターフェースとしてFIFOを置くだけなのですが.

ブロックデザインにDMAとFIFOを追加し,マザーボードにそれらとのインターフェースを追加します.回路側の準備としてはこのくらいですね.

image.png
※右下のやつはデバッグ用に追加したものなので気にしないでください.

あとは,これらにシグナルを送る部分を作ります.alu_sv.svを修正.

   // 組み合わせ回路
   always_comb begin
       // 省略

       // 標準入出力
       stdout_tkeep = 4'hf;
       stdout_tlast = 1'b1;
   end

   // 順序回路
   always_ff @(posedge clk) begin
       // リセット
       if (!resetn || force_reset) begin
           // 省略

           // レジスタ
           for (logic [5:0] i = 0; i <= 6'h30; i++) begin  // 標準入出力以外を初期化
               register[i] <= 0;
           end
       end
       // 命令実行
       else begin
           // IOからレジスタに値を格納する
           register[6'h20] <= {4'b0, btn};
           register[6'h21] <= {6'b0, sw};
           register[6'h31] <= stdin_tdata;
           register[6'h32][2] <= stdin_tlast;
           register[6'h32][1] <= stdin_tvalid;
           register[6'h32][0] <= stdin_tready;
           register[6'h33] <= stdout_tdata;
           register[6'h34][2] <= stdout_tlast;
           register[6'h34][1] <= stdout_tvalid;
           register[6'h34][0] <= stdout_tready;

           // 関数タイプごとに実行
           unique case (command.m_type)
               // 省略
           endcase
       end
   end

それでですね,標準入出力用の命令ももう作っちゃいます.
レジスタへの値コピーとかで標準入出力を代替するの面倒くさいし.
そもそも関数とか命令ってこういう時のためにあるやつですよね.そうですよね?
標準入力はバッファを一文字読み込み,標準出力はバッファに一文字出力で行きたいと思います.

標準入出力は新たにIO系として作ります.本当は今までの形式に倣ってアルファベット一文字にしたかったけど入力と出力がある時点で一文字は無理.

標準入出力命令の機械語への翻訳はこんな感じ.

   // 標準入出力系(IO系)
   function machine_t scan(
       input addr_t rd,
   );
       scan = {3'h7, SCAN, 4'h0, 6'h00, 6'h00, rd, 33'h000000000};
   endfunction
   function machine_t print(
       input addr_t rs1,
       input imm_t imm,
   );
       print = {3'h7, PRINT, 4'h0, rs1, 6'h00, 6'h00, imm};
   endfunction

標準入出力の実際の回路はこんな感じ.

            // 関数タイプごとに実行
           unique case (command.m_type)
               // 省略

               // 標準入出力系
               IO_TYPE: begin
                   unique case (command.func)
                       // 標準入力
                       SCAN: begin
                           unique case (stdin_state)
                               // 待機
                               IDLE: begin
                                   // rdが書き込み可能であることを確認する
                                   if (is_writable(command.rd)) begin
                                       // 実行を指示
                                       stdin_state <= EXECUTE;
                                       stdin_tready <= 1'b1;
                                   end
                                   else begin
                                       force_reset <= 1'b1;
                                   end
                               end

                               // 実行
                               EXECUTE: begin
                                   // データが送られてきているなら
                                   if (stdin_tvalid) begin
                                       register[command.rd] <= stdin_tdata;

                                       // 一文字ずつ読み込むため,これだけ読みこんだら終了する
                                       stdin_state <= IDLE;
                                       stdin_tready <= 1'b0;

                                       // プログラムカウンタをインクリメント
                                       register[PC_ADDR] <= register[PC_ADDR] + 1;
                                   end
                                   else begin
                                       // 読み取り準備が整っていることを送る
                                       stdin_tready <= 1'b1;
                                   end
                               end

                               // その他
                               default: begin
                                   force_reset <= 1'b1;
                               end
                           endcase
                       end

                       // 標準出力
                       PRINT: begin
                           unique case (stdout_state)
                               // 待機
                               IDLE: begin
                                   // イミディエイトデータを使用するなら
                                   if (command.imm[32]) begin
                                       // データを送る
                                       stdout_tdata <= command.imm[31:0];
                                       stdout_tvalid <= 1'b1;
                                       stdout_state <= EXECUTE;
                                   end
                                   // rs1のデータを使用するなら
                                   else begin
                                       // rs1が読み込み可能なら
                                       if (is_readable(command.rs1)) begin
                                           stdout_tdata <= register[command.rs1];
                                           stdout_tvalid <= 1'b1;
                                           stdout_state <= EXECUTE;
                                       end
                                       else begin
                                           force_reset <= 1'b1;
                                       end
                                   end
                               end

                               // 実行
                               EXECUTE: begin
                                   // データの送信に成功したなら
                                   if (stdout_tready) begin
                                       // 一文字ずつ書き込むため,これだけ書き込んだら終了する
                                       stdout_state <= IDLE;
                                       stdout_tvalid <= 1'b0;

                                       // プログラムカウンタをインクリメント
                                       register[PC_ADDR] <= register[PC_ADDR] + 1;
                                   end
                                   else begin
                                       // 書き込み準備が終わっていることを送る
                                       stdout_tvalid <= 1'b1;
                                   end
                               end

                               // その他
                               default: begin
                                   force_reset <= 1'b1;
                               end
                           endcase
                       end

                       default: begin
                           force_reset <= 1'b1;
                       end
                   endcase
               end

               default: begin
                   force_reset <= 1'b1;
               end
           endcase

プログラム

PL側

標準入力を読み込んで標準出力に書きだす簡単なプログラムを書きました.
ちょくちょく使っていない定数がありますが,気にしないでください.

module rom_sv (
    // メモリデータ読み出し
    rom_read_if.slave rom_read
    );
    // import文
    import machine_p::*;

    localparam integer ROM_SIZE = 8;

    localparam integer STATE_ADDR = 6'h00;
    localparam integer TDATA_ADDR = 6'h01;
    localparam integer TVALID_ON_ADDR = 6'h02;
    localparam integer TREADY_ON_ADDR = 6'h03;
    localparam integer STDIN_TVALID = 6'h04;
    localparam integer PARAM1 = 6'h04;
    localparam integer PARAM2 = 6'h05;
    localparam integer PARAM3 = 6'h06;
    localparam integer STDIN_DATA_ADDR = 6'h31;
    localparam integer STDIN_SIGNAL_ADDR = 6'h32;
    localparam integer STDOUT_DATA_ADDR = 6'h33;
    localparam integer STDOUT_SIGNAL_ADDR = 6'h34;

    // キーボード読み取りプログラム
    machine_t machines[0:ROM_SIZE - 1] = {
        // 現在の状態を保存する
        mov(4'hf, 0, STATE_ADDR, {1'b1, 32'b0}),

        // 各信号を保存しておく
        // tvalidがonの状態
        mov(4'hf, 0, TVALID_ON_ADDR, {1'b1, 32'h2}),
        // treadyがonの状態  // 使ってない
        mov(4'hf, 0, TREADY_ON_ADDR, {1'b1, 32'h1}),

        // 標準入力のtvalidを抽出
        and_(STDIN_SIGNAL_ADDR, TVALID_ON_ADDR, STDIN_TVALID),

        // キーボード入力の割り込みが行われていないなら,最後の命令まで飛ぶ
        ne(TVALID_ON_ADDR, STDIN_TVALID, {1'b1, 32'h3}),

        // 標準入力から一文字読み込み,一時領域へ書き込む
        scan(TDATA_ADDR),

        // 標準出力へ出力
        print(TDATA_ADDR, {1'b0, 32'h0}),

        // pcをレジスタ登録後に戻す
        jmp(0, {1'b1, 32'h3})
    };

    // メモリデータの読み出し
    always_comb begin
        if (rom_read.pc >= ROM_SIZE) begin
            rom_read.machine = nop();
        end else begin
            rom_read.machine = machines[rom_read.pc];
        end
    end

endmodule

PS側

PS側は打ち込まれたキーの文字コードを一文字ずつ送信するだけです.簡単ですね.

別に一文字ずつの送受信じゃなくても行けるはずなんですが,なぜか偶数番目の文字だけ無視される怪現象が発生したため一文字ずつ送るようにしています.

#include <iostream>
#include <string>
extern "C" {
#include <pynq_api.h>
}

int main(void) {
    char bit_path[] = "./bit/top_wrapper.bit";
    const int ADDR = 0x40400000;
    const int BURST_SIZE = 100;   // 一度に送れるのは100文字まで
    std::string line = "";

    // ビットファイルの準備
    PYNQ_loadBitstream(bit_path);

    // メモリの準備
    PYNQ_SHARED_MEMORY write_memory,read_memory;
    PYNQ_allocatedSharedMemory(&write_memory, sizeof(int) * BURST_SIZE, 1);
    PYNQ_allocatedSharedMemory(&read_memory, sizeof(int) * BURST_SIZE, 1);
    int *write_data = (int *)write_memory.pointer;
    int *read_data = (int *)read_memory.pointer;
    for (int i = 0; i < BURST_SIZE; i++) {
        write_data[i] = 0;
        read_data[i] = 0;
    }

    // DMAの準備
    PYNQ_AXI_DMA dma;
    PYNQ_openDMA(&dma, ADDR);

    // メイン部分
    while (line != "exit") {
        // 一行分の入力を受け付ける
        std::cout << ">>> ";
        std::cin >> line;

        // 標準入出力から読み込んだデータを送る
        for (int i = 0; i < line.size(); i++) {
            // データコピー
            write_data[0] = (int)line[i];

            // 文字列を出力する
            PYNQ_writeDMA(&dma, &write_memory, 0, sizeof(int) * 1);
            PYNQ_waitForDMAComplete(&dma, AXI_DMA_WRITE);
        }

        int line_size = line.size();
        for (int i = 0; i < line_size; i++) {
            // 文字列を受け取る
            PYNQ_readDMA(&dma, &read_memory, 0, sizeof(int) * 1);
            PYNQ_waitForDMAComplete(&dma, AXI_DMA_READ);

            // 受け取った文字列を出力する
            std::cout << (char)read_data[0];
        }
        std::cout << std::endl;
    }

    // DMAを閉じる
    PYNQ_closeDMA(&dma);
    PYNQ_freeSharedMemory(&write_memory);
    PYNQ_freeSharedMemory(&read_memory);

    return 0;
}

実行結果

>>> a
a
>>> abcd
abcd
>>> hello,world!!
hello,world!!
>>> exit
exit

次回の目標

次はromに書き込むプログラムを変更して,足し算を行えるようにしていきましょう!!!

おまけ

久々にPYNQ-Z2のjupyter notebookにアクセスして,忘れていることも多かったので備忘録.

PYNQ-Z2とSSH接続する方法

ssh xilinx@(IPアドレス) -o ServerAliveIntrerval=15
# パスワードはxilinx

jupyter notebooksのパス

/home/xilinx/jupyter_notebooks

エクスプローラーからpynqのディレクトリにアクセスするときのパス

\\pynq

名前とパスワードは両方xilinx

LAN内の機器を検索する方法

arp -a

pynq_api.hのインストール方法

git clone https://github.com/mesham/pynq_api.git
cd pynq_api
make
sudo make install

公式サイト

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?