Zenn
🔥

KV260/KR260のLEDチカで学ぶシミュレーションのやり方入門

2025/02/11に公開

はじめに

当ブログでは過去に Kria(KV260/KR260) について、下記のような LED チカチカ記事をいくつか書いてきました。

今回は、これらの延長として、シミュレーションを行う方法を書きたいと思います。

なお、ちゃんとした検証を行うものではなく、シミュレーションを動かすための環境を説明することを主眼に、なにかしらシミュレーションを動かすまでをなるべくシンプルに説明できればと思います。

本記事は、PC でのシミュレーションのみなので実機ボードを持っている必要はありません。

KV260 と KR260 の2つ説明しますが、それぞれで少しだけ趣向を変えますので、ぜひ両方読んでみてください。

本記事のベースになっている拙作のサンプルコードを下記にリンクしておきます。

なお、本記事では Linux 版について説明を行います。

  • Ubuntu 20.04.6 LTS (Windows11 WSL2)
  • Vivado 2023.2

を利用しています。

シミュレーションに先だってLチカのソースにリセットをつける

実はここまでの LEDチカチカ の例では、リセットを実装していませんでした。
リセットとは、リセット信号によって起動時や障害時などに強制的に回路の状態を初期状態にする機能です。

SystemVerilog などの言語において、レジスタに割り当てられる初期化を行っていない変数の初期値は不定値(X)となります。

LEDチカチカにおいては、これはあまり問題にならず、仮にどんな値から始まっても、カウントアップを繰り返して一定の値になった時点でゼロに戻りますので、実機動作としては問題なくLEDの点滅を繰り返します。

しかしながら、シミュレーションの場合は、不定値に 1 を足しても不定値になってしまうため、永久にLEDへつながる信号は不定値のままとなり、シミュレーションが進行しません。

テストベンチから強制的に初期化することも可能ではあるのですが、折角ですので、今回は過去のサンプルにリセットを追加する改造をしてから話を進めたいと思います。

なお、FPGA に関して言えば、リセットを用意しなくても、bitstream ダウンロード時に反映される初期値を指定することもできるのですが、以下の理由で、リセットを書いておくことをお勧めします。

  • ASIC や CPLD などでは、リセット以外の初期化方法がなく、パワーオンリセットなどが必須
  • FPGA であっても、bitstream ダウンロード以外の強制初期化方法があると便利
  • 本当にリセットが不要なら、リセット信号を非リセット状態で固定値にすれば論理合成時に消える

などの理由です。なるべく FPGA 以外にも利用可能なリセットの書かれたソースコードにしておく方が行儀のよいプログラムと言えるでしょう。

KV260 のLEDチカにリセットを実装する

KV260 での前提として、こちら のLEDチカチカ記事や こちら の ILA を使う記事に加えて、こちら のファン制御の追加などを適用した前提としておきます。

変数名やポート名など、過去記事から少し変更している部分もありますが、基本を理解していれば問題にならない差分の筈ですので、ご自身のコードと読み替えながら見ていただければと思います。

ブロックデザインで同期リセット生成

まず、ブロックデザインが下記のように PS のある Zynqブロックから、クロックとfan制御のみ引き出されているものとします。

KV260リセット追加前

ここで、例えば ZYNQブロックにある pl_resetn0 ポートが、PS から制御可能なリセット端子ですのでこれを利用します。

実機においては PS の ARMプロセッサなどから制御することになりますが、今回はシミュレーションのみなので後ほど、テストベンチから制御します。

なお、単にこの信号を引き出してもよいのですが、FPGA ではしばし、クロックに同期してリセットが解除される同期リセットが好まれます(こちらなどで、同期リセットが推奨されています)。

そこでブロックデザイナでの同期クロックの作り方も説明しておきます。

「Add IP」ボタンから、「Processor System Reset」を探して追加してください。

Processor System Reset の追加

追加したら、slowest_sync_clk にクロックをつなぎ、ext_reset_in に pl_resetn0 を接続します。

これで様々なタイミングのリセットを作ってくれますが、詳細は PG164などのマニュアルにあるのでそちらを参照いただくとして、今回は pheripheral_reset を引き出しておきます。

Processor System Reset の接続

これで、clk に同期するリセットを作ることができました。

ソースコードの修正

次に、ソースコードの方を修正します。

`timescale 1ns / 1ps
`default_nettype none

module kv260_blinking_led
        #(
            parameter  int COUNT_LIMIT = 100000000
        )
        (
            output  var logic   [7:0]   pmod    ,
            output  var logic           fan_en
        );
    
    // PS
    logic           reset   ;    // sync reset
    logic           clk     ;    // 100MHz
    design_1
        u_design_1
            (
                .fan_en     (fan_en ),
                .reset      (reset  ),
                .clk        (clk    )
            );
    
    // counter
    (* MARK_DEBUG = "true" *)   logic   [26:0]     counter;
    (* MARK_DEBUG = "true" *)   logic   [7:0]      led    ;
    always_ff @(posedge clk) begin
        if ( reset ) begin
            counter <= '0;
            led     <= '0;
        end
        else begin
            counter <= counter + 1'b1;
            if ( counter >= 27'(COUNT_LIMIT - 1) ) begin // 1秒をカウントする
                counter <= '0;
                led     <= led + 1'b1;
            end
        end
    end

    // PMOD output
    assign pmod = led;
   
endmodule

`default_nettype wire

今までになかった要素がいくつか出てきているので説明します。

まず、モジュール宣言の箇所の

  parameter  int COUNT_LIMIT = 100000000

ですが、これは 100MHz での動作時にカウントをリセットするリミット値を parameter という構文で渡しています。デフォルトで 100000000 としているので、普通に合成すれば 1秒づつ点滅が変わります。

一方で、シミュレーション時は 1秒分シミュレーションするのは大変ですので、もっと短い時間で変化するように値を変えてシミュレーションできるように parameter 化しました。

design_1 から追加した reset 信号を取り出している部分は、説明は不要でしょう。

肝心のリセットの個所ですが、

    always_ff @(posedge clk) begin
        if ( reset ) begin
            counter <= '0;
            led     <= '0;
        end
        else begin
            counter <= counter + 1'b1;
            if ( counter >= 27'(COUNT_LIMIT - 1) ) begin // 1秒をカウントする
                counter <= '0;
                led     <= led + 1'b1;
            end
        end
    end

ある程度一目瞭然かとは思いますが、このように if 文でリセットを書いてしまうのが、Verilog でのリセットの書き方の定石となります。

なお、変数宣言の部分で、例えば下記のように初期化することも可能ではあります(MARK_DEBUG は ILA を使うためのおまじないなのでここでは無視してください)。

  (* MARK_DEBUG = "true" *)   logic   [26:0]     counter = '0;
  (* MARK_DEBUG = "true" *)   logic   [7:0]      led     = '0;

しかしながら、ここでの初期化は FPGA での bitstream ダウンロード時に限り有効で、ASIC や CPLD では通用しなかったり、FPGAでもリセットボタンを押したときにも初期化されてほしかったりなど、いろいろあるので、合成用の記述では(特に慣れないうちは)あまりお勧めできません。

むしろうっかり初期化を書いたせいで、シミュレーションでは動くけど、実機で動かない というバグを作りかねない点で注意が必要です。

KR260 のLEDチカにリセットを実装する

KR260 では LEDチカを行った こちらの記事 の延長で説明を行います。

こちらも、細かいところで一部命名を変えていますが、うまく読み替えてください。

ブロックデザインで非同期リセットを取り出す

KR260 の LEDチカでは KV260 とは異なり、PS からではなくボード上のオシレーターからクロックを使っていました。

これを使って同期リセットを作ることも可能ですが、折角なので KR260 では非同期リセットの使い方を解説するために、下図のように pl_resetn0 を reset_n という名称で取り出します。

KR260 のブロックデザイン

ここで変数名に n がついている点に注意してほしいのですが、リセット信号にはしばし負論理(Active-Low) が用いられる場合があります。これは電子回路が CMOS ではなく TTL だった時代の名残りに始まっていろいろな経緯があるのですが、とにもかくにも「信号が 0 の時にリセットがかかる」という論理極性で利用されることがあります。

そして ZYNQブロックが出力する pl_resetn0 は負論理になっており、しばし信号名にの前や後ろに n や x や b をつけたりして、ハンガリアン記法 的に間違いを防止する慣例があります。

ソースコードの修正

ソースコードにもリセットを加えていきます。

`timescale 1ns / 1ps
`default_nettype none

module kr260_blinking_led
        #(
            parameter   int COUNT_LIMIT = 25000000
        )
        (
            input   var logic           clk     ,
            output  var logic   [1:0]   led     ,
            output  var logic           fan_en
        );
    
    // Block design
    logic   reset_n;
    design_1
        u_design_1
            (
                .fan_en     (fan_en ),
                .reset_n    (reset_n)
            );
    
    // Blinking LED
    logic   [24:0]     counter; // リセットがないので初期値を設定
    always_ff @(posedge clk or negedge reset_n) begin
        if ( ~reset_n ) begin
            counter <= 0;
            led     <= 0;
        end
        else begin
            // 25MHz で 1秒間隔でカウントアップ
            if ( counter >= 25'(COUNT_LIMIT - 1) ) begin
                counter <= 0;
                led     <= led + 1;
            end
            else begin
                counter <= counter + 1;
            end
        end
    end

endmodule

`default_nettype wire

KR260 版では、ボード上にある 25MHz のクロック発信機(オシレータ)からくるクロックを使っています。

ここで非同期リセットを使う上で重要なのは

  always_ff @(posedge clk or negedge reset_n) begin

の部分です。

counter などのレジスタ変数は、内部的にはフリップフロップに割り当てられますが、フリップフロップの非同期リセットはクロックと無関係にリセット信号の入力で値をリセットすることができます。

このフリップフロップの動作と等価になるように ソースコードを書くと、非同期リセットが割り当てられます。

決してなんでも書けるわけではなく、フリップフロップの実際の動作 どおりに書いた Verilog のみが合成可能となるので、注意が必要です。

実際のフリップフロップの持つ非同期リセットと同じ動作になるようにするには reset_n が 1 から 0 になった場合は、クロックに無関係に評価が行われるソースコードにする必要があります。

結果 posedge clk と negedge reset_n という2つのイベントを or 待ちする書き方が合成可能な記述となります。

なお KV260 や KR260 の中にある実際のフリップフリップの仕様はUG574のFlip-Flop Primitivesの項 などにあります。日本語版もあるので詳しく知りたい方はぜひ読んでみてください。

非同期リセットの書き方を知っていれば、そこから同期リセットを生成することも難しくないため、外部のリセットボタンを一度非同期リセットのフリップフロップで受けて、以降は同期リセットを使うといった使い方も可能になります。

テストベンチを用意する

シミュレーションを行うには、テストベンチと呼ばれる、テストの為だけのプログラムを用意する必要があります。

テストベンチのソースコードは合成には利用しませんので、合成可能記述以外の記法も用いてもよく、(利用するシミュレータが対応している範囲で)広く SystemVerilog の機能を使って記述することができます。

また、シミュレーションでは、テスト対象とする部分を DUT(Device Under Test)と呼ぶことがあります。

テストベンチでは DUT にクロックやリセットや、外部の入出力を記述したり、一部、テスト対象以外の議事モデルを書いたり、スタブモジュールを用意したりしてテストを行います。

このあたりは、ソフトウェアにおける、テストドライバやテストスタブの考え方と大きくは異なりません。

通常シミュレーションにおいては、モジュールの単体テストや、結合テストなど、様々な単位を DUT として扱い、多数のテストベンチを書くことになりますので、個人的には CLI でのシミュレーションがお勧めではあります。

詳しい解説に先だって、まず KV260 と KR260 のソースコードを示します。

KV260 のテストベンチ

下記が KV260 用に書いたテストベンチです。

Vivado シミュレータ以外の利用も意図して vcd ファイルのダンプも含みます。

`timescale 1ns / 1ps
`default_nettype none

module tb_top();
    
    initial begin
        $dumpfile("tb_top.vcd");
        $dumpvars(0, tb_top);
        
    #10000000
        $finish;
    end
    
    // ---------------------------------
    //  reset and clock
    // ---------------------------------

    localparam RATE = 1000.0/100.00;

    logic       reset = 1'b1;
    initial #(RATE * 20) reset = 1'b0;

    logic       clk = 1'b1;
    initial forever #(RATE/2.0) clk = ~clk;

    
    // ---------------------------------
    //  DUT
    // ---------------------------------

    logic           fan_en  ;
    logic   [7:0]   pmod    ;
    kv260_blinking_led
            #(
                .COUNT_LIMIT(100000  )   // シミュレーション時は高速にする
            )
        u_kv260_blinking_led
            (
                .pmod       (pmod   ),
                .fan_en     (fan_en )
            );
    
    initial begin
        force u_kv260_blinking_led.u_design_1.reset  = reset;
        force u_kv260_blinking_led.u_design_1.clk    = clk;
    end

endmodule

`default_nettype wire

また、Vivado Simulator 以外の利用のために、ブロックデザインのスタブモジュールとして design_1.v というコードも用意しました。

`timescale 1 ns / 1 ps

module design_1
    (
        fan_en  ,
        reset   ,
        clk     
    );
  
  output fan_en ;
  output reset  ;
  output clk    ;

  wire fan_en ;
  wire reset_n;
  wire clk    ;

  assign fan_en  = 1'b0  ;

endmodule

design_1.v が SystemVerilog ではなく、普通の Verilog になっている理由は、Vivado の 「Create HDL Wrapper」の機能で作ったラッパーを元に改造したためです。

Create HDL Wrapper

ブロックデザインの信号が増えてくると、スタブを書くのも大変なので、このような機能を使うと楽ができます。

KR260 のテストベンチ

次に KR260 のテストベンチです。

`timescale 1ns / 1ps
`default_nettype none

module tb_top();
    
    initial begin
        $dumpfile("tb_top.vcd");
        $dumpvars(0, tb_top);
        
    #10000000
        $finish;
    end
    
    // ---------------------------------
    //  reset and clock
    // ---------------------------------

    localparam RATE25 = 1000.0/25.00;

    logic       reset = 1'b1;
    initial #(RATE25 * 20) reset = 1'b0;

    logic       clk25 = 1'b1;
    initial forever #(RATE25/2.0) clk25 = ~clk25;

    
    // ---------------------------------
    //  DUT
    // ---------------------------------

    logic   [1:0]   led     ;
    logic           fan_en  ;
    kr260_blinking_led
            #(
                .COUNT_LIMIT(25000  )   // シミュレーション時は高速にする
            )
        u_kr260_blinking_led
            (
                .clk        (clk25  ),
                .led        (led    ),
                .fan_en     (fan_en )
            );
    
    initial begin
        force u_kr260_blinking_led.u_design_1.reset_n  = ~reset;
    end

endmodule

`default_nettype wire

ブロックデザインも同様に下記に示します。

`timescale 1 ns / 1 ps

module design_1
    (
        fan_en  ,
        reset_n
    );
  
  output fan_en ;
  output reset_n;

  wire fan_en;
  wire reset_n;

  assign fan_en  = 1'b0  ;

endmodule

テストベンチの解説

KV260 の例で説明を行います(KR260もだいたい同じなので随時読み替えてください)。

時間単位

シミュレーションにおいて時間の単位は重要です。
ソースコードの冒頭で

`timescale 1ns / 1ps

と宣言することで、ソースコード中の時間単位の解釈は 1ns となり、分解能は 1ps 単位となります。

VCDファイルへのダンプ

下記の記述により、シミュレーション開始時に tb_top.vcd というダンプファイルを設定し、tb_top 以下のすべての階層をダンプするように指示しています。

$dumpvars の引数を 0 以外にすると指定した深さの階層までをダンプします。

また #10000000 にて 10000000 ns 後、つまり 10ms 分だけシミュレーションを進めた後に $finish でシミュレーションを終了するようにしています。

    initial begin
        $dumpfile("tb_top.vcd");
        $dumpvars(0, tb_top);
        
    #10000000
        $finish;
    end

リセットの生成

まず

localparam RATE = 1000.0/100.00;

RATE というパラメータにクロック1サイクル分の時間を定義しているので、

下記のような記述で、20サイクル分の期間のリセットを生成できます。

  logic       reset = 1'b1;
  initial #(RATE * 20) reset = 1'b0;

初期値が 1 で、20サイクル後に 0 が代入されてリセットが解除されます。

クロックの生成

例えば下記のような記述でクロックが生成できます。

  logic       clk = 1'b1;
  initial forever #(RATE/2.0) clk = ~clk;

初期値を1として 1/2 周期ごとに反転を指示しています。

ここでは forever を使っていますが、always などで書くこともできます。

Zynq ブロックからくる信号を force で上書きする

KV260 や KR260 では、PS のある Zynq ブロックから信号を受け取るケースがあります。

そこでテストベンチで作った信号で、強制的にスタブの信号を上書きします。

このようなときに便利なのが force という構文で、下記のように書けます。

  initial begin
      force u_kv260_blinking_led.u_design_1.reset  = reset;
      force u_kv260_blinking_led.u_design_1.clk    = clk;
  end

また、合成可能記述と異なり、シミュレーションでは u_kv260_blinking_led.u_design_1 のような . (ドット) で繋ぐ書き方で、階層を超えた変数アクセスができます。

簡単な説明ではありますが、DUT をテストするために必要な、周辺の動作をテストベンチに記述する方法がなんとなくご理解いただければと思います。

実際にシミュレーションしてみる

シミュレータの種類

世の中には SystemVerilog シミュレーションを行うことのできるシミュレータは商用/非商用問わず多数のツールがあり、それぞれで GUI でも実施できたり CLI でのみ実施できたり様々です。

まず、Vivado が対応しているものだけでも、標準で含まれる Vivado Simulator 以外にも ModelSim はじめ、各種あるようです。

シミュレータの種類

一方で無償版 Vivado にも含まれる Vivado Simulator 以外は、基本的には有料ですので、今回の記事では割愛します(一部、制限付きで無償評価版などはありますが)。

そのほかに OSS のシミュレータとして

などがあり、これらについても、本記事でも少し取り扱っていきます。

また、シミュレーション後の波形をみるツールとしては

も利用していきます。

GUI で Vivado Simulator を使ってみる

今度は KR260 版で説明してみます。KV260 の人は随時読み替えてください。

GUI でシミュレーションするには、まずプロジェクトにシミュレーションファイルを足す必要があります。

下記からソースファイルを追加できます。

ソースファイル追加

Add or create simulation source からテストベンチを作成するなり、別に作っておいたファイルを追加するなりしてください。

Add or create simulation source

追加するとおそらくシミュレーショントップになるはずですが、tb_top が太文字にならない場合は、下記のように右クリックメニューから Set as Top を選んでトップに設定してください。

Set as Top

無事にテストベンチのファイルが追加出来たら、Flow Navigator の 「Run Simulation」の「Run Behavioral Simulation」からシミュレーションが開始できます。

Run Simulation

ここでしばし、謎のエラーが出て、シミュレーションが始まらないケースがありますが、その場合の対応は様々なケースがあり、ここでは説明できないため、早めにあきらめて以降で説明する CLI でのシミュレーションに移行することをお勧めします。

シミュレーション画面では、見たい波形を追加してから実行を進めることで、下記のように波形を見ることができます。

シミュレーション画面

リセット解除後にカウントが進んで行きます。

ツールバーにシミュレーションを制御するボタンがあります。

GUIのシミュレーション制御

時刻ゼロに戻したり、シミュレーションを走らせたり、一定時間だけ進めたり、ソースコードのコンパイルからやり直したりできますので、いろいろ弄ってみて理解を深めてみてください。

また、ここでソースコードの方にブレークポイントを張ったりして、止まったところで変数の中身を見たりするようなことも可能です。

picture 29

CLI で Vivado Simulator を使ってみる

CLI での Vivado Simulator (xsim) の使い方を説明します。

説明に先立って、こちら が先に筆者が作った xsim 用の Makefileのリンクです。

CLI での xsim の実行は

  1. xvlog コマンドで SystemVerilog のソースコードをコンパイルする
  2. xelab コマンドでコンパイル済みコードをエラボレートする
  3. xsim コマンドでシミュレーションを実行する

という流れになります。

よく C言語などのプログラムで、コンパイル → リンク → 実行 という手順を踏むのとよく似ています。

例えば

xvlog -sv tb_top.sv

のようにすると tb_top.sv が SystemVerilog モードでコンパイルされ、結果は xsim.dir ディレクトリ以下に生成されます。

実際のパスに合わせて、 tb_top.sv、design_1.v、kr260_blinking_led.sv などシミュレーションに使うソースコードをすべてコンパイルします。

次に

xelab tb_top

のように、トップモジュール名を指定してエラボレートします。

最後に

xsim tb_top --R

のように、トップネットを指定して実行します。 --R オプションをつけることで、起動と同時に ru --all が自動的に実行され、 $finish まで実行が行われて終了します。

無事に実行が完了すると、実行したディレクトリに tb_top.vcd という波形ファイルができます。

これは gtkwave などで見ることができますが、gtkwave のインストールは Ubuntu などでは

sudo apt update
sudo apt install gtkwave

とすることでできます。

gtkwave を下記のように起動し、

gtkwave tb_top.vcd &

起動後に信号を波形表示に追加して、表示フォーマットを変えたりしながら眺めることができます。

gtkwave

Verilator を使ってみる

OSS の Verilator でシミュレーションする方法を説明します。

Verilator については過去にいくつか記事を書いており

などがあります。

不定値が使えないとか、コンパイルが遅いなどはありますが、

  • コンパイル後の実行がとても高速
  • C/C++言語でテストドライバを書いて時間進行を制御できる
  • iverilog より SystemVerilog 対応がしっかりしている

などの特徴もあり、使い方次第では効果的でした。

特に、Ver5 以降は、Verilog だけでテストベンチを書けるようになり使い勝手も向上しています。以降は Ver5 以降を前提に説明します。

説明に先立って、こちら が先に筆者が作った verilator 用の Makefileのリンクです。

Verilator では force の挙動が本来の Verilog と異なります。

現時点の(Ver 5.031)では force, release については、

Verilator instead evaluates the current value of b when the force statement is executed, and forces a to that value, without updating it until a new force or release statement is encountered that applies to a. This non-standard behavior is nevertheless consistent with some other simulators.

との記述があり、強制的に代入する値が変化するたびに代入しなおさないと値が変わらないことがわかります。

そこで、always_comb 文を用いて、

  always_comb force u_kr260_blinking_led.u_design_1.reset_n  = ~reset;

のように書いてやることで、値を伝搬させます。

コンパイルは、

verilator --binary --trace-fst --top tb_top tb_top.sv design_1.v kr260_blinking_led.sv

のように行います。

--binary は ver5 以降でできた機能で、Verilog のみで実行バイナリを生成でき、他の普通のシミュレータのように振舞います。

--trace-fst は波形ファイルを出力させるオプションですが、gtkwave が利用可能な vcd よりも高速な fst 形式の波形ファイルの出力を指定しています。

--top では トップモジュールを指定し、以降にはソースファイルが並びます。

コンパイルに成功すると obj_dir/Vtb_top という実行ファイルが出力されるので

./obj_dir/Vtb_top 

と実行することで、シミュレーションが実行され tb_top.fst が出力されます。

gtkwave tb_top.fst &

と実行すれば、波形ファイルを見ることができます。

iverilog(Icarus Verilog) を使ってみる

OSS の iverilog(Icarus Verilog) でシミュレーションする方法を説明します。

iverilog のセットアップについては こちら の記事を参照ください。バージョンが古いと SystemVerilog の一部の構文でエラーになるようですので注意ください。

説明に先立って、こちら が先に筆者が作った iverilog 用の Makefileのリンクです。

iverilog では iverilog コマンドでソースファイルをコンパイルして .vpp ファイルを作った後、vvp コマンドで実行します。

コンパイルは、

iverilog -g2012 -o tb_top.vpp -s tb_top tb_top.sv design_1.v kr260_blinking_led.sv

のような感じで、-g2012 オプションで SystemVerilog 2012 を指定し、-o オプションで出力ファイル、 -s コマンドで トップモジュール を指定して、あとは入力ソースファイルを並べればいいようです。

コンパイルがうまくいくと tb_top.vpp というファイルができるので、実行は

vvp tb_top.vpp

とすれば良いようです。

tb_top.vcd ができますので

gtkwave tb_top.vcd &

などで、波形を見ます。

おわりに

今回は細かいところで端折った部分もありますが、初めて KV260 や KR260 で FPGA を使おうという方が、シミュレーションを動かしてみるという第一歩目を踏み出す際の助けになれば幸いです。

慣れてくると、回路への入力データをファイルから読み込んだり、逆に計算結果をファイルに保存したり、違反する動作をしていないかアサーションをはったり、いろんなことができるようになります。

今回は SystemVerilog の本には書かれていない、ツール固有の部分にフォーカスして説明しましたが、シミュレーションについては検索すると様々な情報が出てくると思いますので、ぜひいろいろ調べてみてください。

GitHubで編集を提案

Discussion

ログインするとコメントできます