LoginSignup
10
3

More than 1 year has passed since last update.

DPI-C で C言語と SystemVerilog をインターフェース【Windows無償ツールで実現】

Last updated at Posted at 2021-12-08

目次

1.はじめに

筆者は、Verilog HDL を拡張したSysteVerilogというHDL言語に関心を持ち始めています。
その中でもC言語と連携を行う DPI-C というインターフェースに着目しています。
筆者の場合、Windows環境の方に慣れ親しんでいる為、Linux環境には幾分の敷居の高さ
を感じています。
そのためWindowsの環境で、且つ、費用的にも敷居の低い無償のツールで動かせるか
調査を行い、インテル® 社が提供するFPGA開発ツールQuartus® Primeに同梱されて
いる無償のシミュレーション・ツールだけで実現を試みました。
とりあえず「動かす」ことに重点を置き、稚拙ながら簡単な自作コードを用いて、
言語間でインターフェースが行われている「動き」について投稿してみました。

本稿の執筆にあたり、参考にした情報も文中に掲載しました。

2.環境

ModelSim® インテル® FPGA Starter Edition 10.5b (Windows版)
kom_0.png

  • このシミュレーション・ツールは、Quartus® Prime Lite Edition (無償版)の
    バージョン17.1 ~19.1に同梱されています。
  • Quartus® Prime Standard Edition (有償版)でも同梱されています。

3.サンプル・コード

本稿で使用するサンプル・コードは、次の①~③を目的としています。

  • ① SystemVerilog コードからC言語を呼び出す

    • 応用例:ハード・ソフトの協調検証
      • ハードウェアで受信したデータをホスト側のソフトウェアに渡して
        高度なアルゴリズムでエラー・チェック
  • ② C 言語からSystemVerilog コード(ファンクションを使用)を呼び出す

    • 応用例:ハードウェア・アクセラレーター
      • ホスト側のソフトウェアで処理時間を要する機能をハードウェアで処理
  • ③ C 言語からSystemVerilog コード(タスクを使用)を呼び出す

    • 応用例:ハード・ソフトの協調検証
      • ハードウェアのテスト・プログラムをC言語で記述

上記①~③では、SystemVerilogとC言語の他に Tcl スクリプトも使用します。

3-1. ① SystemVerilog コードからC言語を呼び出す

SystemVerilog コードからC言語を呼び出す場合、SytemVerilogコード内にimport "DPI-C"
を記述し、その後に、C言語の関数名と、その関数名に関連する情報(型や引数)を指定し
ます。
SytemVerilogコード内に記述するので、SystemVerilogの文法に従って関数を指定します。
importの場合、指定する関数は、purecontext に分類されます。
サンプル・コードでは、C言語からSystemVerilogコードに参照するような記述では無いので、
pure の方を使用しています。

3-1-1. SystemVerilog コード

import "DPI-C" 後に、pure function int add(input int value_1, int value_2)
という二つの引数を加算する関数を指定しています。
関数の実体はC言語側にあるので、加算部分はC言語側で記述しています。
引数に値を代入して、initial文の中で20ns後に関数が呼び出され、C言語側で処理した
加算結果が、戻り値としてResultに代入しています。

tb_dpi.sv
`timescale 1 ns/ 1 ps

module tb_dpi;

  import "DPI-C" pure function int add(input int value_1, int value_2);
  int Result_val = 0;
  int val_a = 13;
  int val_b = 21;


  initial
  begin
    $display("--- SystemVerilog to C function using 'DPI-C' ---");
    #20
    $display("Call to C function from System Verilog");
    Result_val = add(val_a, val_b);
    #20
    $display("Get Value from C function: Result_val = %d", Result_val);
    $display("Finish");
    #1000
    $finish;
  end

endmodule

個人的には、importという意味合いから、SystemVerilogコードからC言語に処理を分岐する
とよりも、SystemVerilogコードの中に、C言語の関数を組み込まれているような印象を持って
います。


参考:
・ModelSim DEサンプル・コード


3-1-2. C コード

本コードでは、加算処理を行い、その結果をメッセージに表示後、呼び出し側に戻り値として
返しています。
ヘッダ・ファイルもC言語で標準的に用意されているものをインクルードしています。

CFunc1.c
#include <stdio.h>
#include <stdlib.h>

extern int add(int val_1, int val_2) {
  int accm = 0;
  accm = val_1 + val_2;

  printf(">>> Run C function result = %d\n", accm);
  return accm;
}

3-1-3. Tcl スクリプト

ModelSimで利用可能なコマンドを記述しています。
もっと適切な書き方はあると感じていますが、深追いはせずに、とりあえず「動かす」ことに
重点を置きました。

dpic_sv2c.tcl
vlib work
vlog -sv -dpiheader tb_dpi.sv CFunc1.c
vlog -sv tb_dpi.sv
vsim -gui work.tb_dpi
add wave -r /*

参考:
・ModelSim DEサンプル・コード


3-1-4. 実行手順

3-1-5. Modelsim の起動

1) 半角英数のPATHによる任意のディレクトリに、以下のファイルを格納します。

tb_dpi.sv

CFunc1.c

dpic_sv2c.tcl

本稿に掲載されている該当のソース・コードをコピー&ペーストして、
上記のファイル名(および拡張子)で保存します。

2) ModelSim インテル® FPGA Starter Edition を起動します。

3) Transcript ウィンドウで次のコマンドをタイプして、上記 1) のディレクトリに移動します。

  【例】 cd C:/work/test_sv2c

カレント・ディレクトリを移動後、ls を実行して、上記のファイルがあることを確認します。

kom_1.png

3-1-6. Tclスクリプトの実行

次のコマンドで Tclスクリプトを実行します。

do ./dpic_sv2c.tcl



Tcl スクリプトを実行後、Wave ウィンドウ内にポートや信号の全てが追加されています。
kom_2.png

3-1-7. Modelsim によるシミュレーション

Wave ウィンドウ内にカーソルを置いて、右クリックより Add -> New Divider を選択します。
kom_3.png

次のように、SystemVerilog のモジュールと、C言語の関数を区分けします。
kom_4.png

Simulation メニューから Run -> Run -All を選択します。
kom_5.png

次のような画面が起動したら、[ いいえ(N) ] をクリックします。
kom_6.png

Wave ウィンドウに波形が表示されます。
kom7.png

[ Zoom Full (F) ] アイコンをクリックします。
kom_8.png

波形の全体像が現れてきます。
kom_9.png

3-1-8. シミュレーション結果

変化点をズームアップすると、SystemVerilog 側で用意した、引数 13 と 21 とが、20ns 地点でC の関数に渡された後、即座に加算結果が返される動きを把握できます。
kom_10.png

このときのメッセージでも、C言語側の関数を呼び出して、加算結果 34 が返ってきたことを示しています。
kom_11_up2.png


参考:
・ModelSim DEサンプル・コード
DPI-CでSystemVerilogからC言語の関数呼び出し
ModelSimASE+clangでSystemVerilog DPI-Cをシミュレーションする


3-2. ② C 言語からSystemVerilogコード(ファンクションを使用)を呼び出す

CコードからSystemVerilog コードを呼び出す場合、SystemVerilogコード内に、
export "DPI-C" を記述します。import のようにSystemVerilog の中にC言語を取り込むの
とは逆の方向になるので、SystemVerilogのコードを外部に吐き出す意味合いでexportが使われるようです。
export "DPI-C"の後には、C言語にエクスポートされるSystemVerilogの関数やタスクを指定します。
本サンプル・コードでは、SystemVerilogのファンクションをエクスポートするので、
export "DPI-C" の後ろに、ファンクション名と、それに関連した情報(型や引数)を使用しています。
exportの場合、importとは異なり、purecontextは不要になります。

3-2-1. C コード

本コードは、C言語のmain関数では無く、add_main()というサブ関数になります。
また、その下にはadd_func()というサブ関数を定義しています。
ヘッダ・ファイルには、C言語側で標準的に用意されているライブラリに加えて、dpiheader.h
を明示的にインクルードして、DPI-C経由で処理を分岐できるようにしています。
サブ関数add_func()の引数に値を指定した後、サブ関数add_func()を呼び出して、
SystemVeriligで定義したadd_func というfunctionに加算の処理を委ねています。
SystemVerilog側からの加算結果は、戻り値として変数resultに格納後、printf()を使用
してメッセージに表示できるように記述しています。
ここでは同時に、変数 num の値もメッセージに表示させています。
後術しますが、この値はadd_maion()の引数として外部から指定されます。

add_main.c
#include <stdio.h>
#include <stdlib.h>
#include "dpiheader.h"

int add_func(int, int);

int add_main(int num){

int a = 35;
int b = 11;
int result;

result = add_func(a, b);
printf("************************************************************************\n");
printf("Mesage from C function\n");
printf(">>> Culcurate System Verilog function result = %d <- %d\n", result, num);
printf("************************************************************************\n");

return (0);
}

3-2-2. SystemVerilog コード

本コードでは、sub_moduleというモジュール名を定義しています。実装を想定し、トップ階層よりも下位の階層にインスタンスされるサブ・モジュールを意識し、敢えてポート名を定義しています。
このモジュール内には、SystemVerilog の function としてadd_funcを定義していますが、これがC言語側から呼び出される対象になります。SystemVerilog側から見れば、外部にエクスポートする対象となります。
add_funcでは 入力ポートとしてint 型で宣言されたf_daf_db を仮引数とし、C言語側から与えられた引数と対応しています。

仮引数の型をintから wireに切り替えば、assign 文で記述した接続に従い、
モジュール名の直後に指定した入力ポートda_iとdb_iに各々接続され、
function で記述した加算結果が出力ポートq_oと接続され、トップ階層と
接続する加算器として実装されます。

VerilogやSystemVerilogではfunctionは、組み合わせロジックの回路記述
に該当します。

f_daf_dbを加算した結果が、add_funcの戻り値となり、DPI-Cによって、C言語側に送られます。DPI-Cに処理をエクスポートする為、export "DPI-C" の後ろに function add_func の記述を追加しています。

また、本コードでは、initial文を使用して、シミュレーション波形で時間的な動きを視覚的に把握できるようにしています。
シミュレーション直後、つまり0nsでnum_valに3を代入した後、20ns経過した後に、C言語側のサブ関数add_main()に引数3を与えて実行しています。組込みシステムのように、CPUのリセットを解除して、ROMからCコードをブートする動きに似ているかもしれません。
この処理によって、C言語側のadd_main()が実行後、その中にあるサブ関数add_func()が実行されるフローがシミュレートされます。

このフローは、結果的にSystemVerilogのinitial文からC言語を呼び出す仕組みが無いと実現できないので、本コード内では、import “DPI-C”も併用しています。
SystemVerilogから呼び出されるC言語のadd_main()の中では、サブ関数add_func()からSystemVerilogを参照するので、本コードではimport “DPI-C”の後ろに context を付加して、int型のfunction としてadd_main の呼び出せるように記述しています。

sub_module.sv
`timescale 1ns/1ps

module sub_module(
    input wire  [31:0]  da_i,   // Input Data
    input wire  [31:0]  db_i,   // Input Data
    output wire [31:0]  q_o // Output Data
);

import "DPI-C" context function int add_main(input int num);
export "DPI-C" function add_func;

int num_val = 0;

// Function                                                   
function int add_func;
    input int f_da;
    input int f_db; 

    begin
        add_func = f_da + f_db;
    end
endfunction

// Connect
assign q_o  = add_func(da_i, db_i);

// Monitoring
    initial 
    begin
        num_val = 3;
        #20 
        add_main(num_val);
        #20 $finish;
    end

endmodule

参考:
・ModelSim DEサンプル・コード


3-2-3. Tclスクリプト

ModelSimで利用可能なコマンドを記述しています。
もっと適切な書き方はあると感じていますが、深追いはせずに、とりあえず「動かす」ことに重点を置きました。

dpic_c2sv.tcl
vlib work
vlog -sv -dpiheader dpiheader.h sub_module.sv add_main.c -ccflags "-g"
vsim -c sub_module
add wave -r /*

参考:
・ModelSim DEサンプル・コード


3-2-4. 実行手順

3-2-5. Modelsim の起動

1) 半角英数のPATHによる任意のディレクトリに、以下のファイルを格納します。

sub_module.sv

add_main.c

dpic_c2sv.tcl

本稿に掲載されている該当のソース・コードをコピー&ペーストして、
上記のファイル名(および拡張子)で保存します。

2) ModelSim インテル® FPGA Starter Edition を起動します。

3) Transcript ウィンドウで次のコマンドをタイプして上記 1) のディレクトリに移動します。
  【例】 cd C:/work/test_c2sv

カレント・ディレクトリを移動後、ls を実行して、上記のファイルがあることを確認します。
kom_12.png

3-2-6. Tclスクリプトの実行

次のコマンドで Tclスクリプトを実行します。

do ./dpic_c2sv.tcl

Tcl スクリプトを実行後、Wave ウィンドウ内にポートや信号の全てが追加されています。
kom_13.png

3-2-7. Modelsim によるシミュレーション

Wave ウィンドウ内にカーソルを置いて、右クリックより Add -> New Divider を選択します。
kom_14.png

次のように、SystemVerilog のモジュールと、C言語の関数を区分けします。
その際、信号の順番も適宜入れ替えます。
kom_15.png

Simulation メニューから Run -> Run -All を選択します。
kom_16.png

次のような画面が起動したら、[ いいえ(N) ] をクリックします。
kom_17.png

Wave ウィンドウに波形が表示されます。
kom_18.png

[ Zoom Full (F) ] アイコンをクリックします。
kom_19.png

波形の全体像が現れてきます。
kom_20.png

3-2-8. シミュレーション結果

シミュレーターで時系列の動きを追うため、SystemVerilog コードの initial 文に記述した内容に従い、nmu_val に値3を指定してから20 ns 経過後にnumが0から3に変化していることから、この時点でC言語側のadd_main()が実行開始していることが把握できます。
kom_20_up.png

同時に、add_main()関数内のサブ関数 add_func()が実行され、add_func()の実体であるSystemVerilog コードで定義した関数add_func で即座に加算が行われ、その結果がC言語側に返された後、次のようにprintf()によってModelSimのTranscriptウィンドウ内にメッセージとして表示されます。
kom_21.png

また、add_func はトップ階層では無く、DPI-Cとのインターフェースによって、C言語側と接続していることから、モジュール名の後ろに定義した入力ポートには何も値が表示されず、出力ポートにも加算結果が一切反映されていないこともシミュレーション波形からも把握できます。

3-3. ③ C 言語からSystemVerilogコード(タスクを使用)を呼び出す

CコードからSystemVerilog コードを呼び出す場合、SystemVerilogコード内に
export "DPI-C" を記述します。import とは異なりSystemVerilog の中にC言語を取り込む
のとは逆の方向になるので、SystemVerilogを外部言語に吐き出す意味合いのexportがDPI-C
では使われます。
export "DPI-C"の後には、C言語側にエクスポートされるSystemVerilogの関数(ファンク
ションやタスク)を指定します。
本デザインでは、SystemVerilogのタスクをエクスポートするので、export "DPI-C"
後ろに、タスク名と、それに関連した情報(型や引数)を使用しています。
exportの場合、importfunctionを指定するケースとは異なり、purecontext
不要です。また、importの場合でもtaskを指定する際には必ずcontextを使用します。

3-3-1. C コード

本コードは、C言語のmain関数では無く、add_main()というサブ関数になります。加えて、
その下にはadd_task()というサブ関数を定義しています。
ヘッダ・ファイルには、C言語側で標準的に用意されているライブラリに加えて、
dpiheader.hを明示的にインクルードして、DPI-C経由で処理が分岐されます。
サブ関数add_task()の引数に値を指定した後、add_task()の呼び出しを行い、
SystemVeriligで定義したadd_task というタスクに加算処理を渡しています。
SystemVerilog側からの加算結果は、戻り値として変数resultに格納後、printf()を使用
してメッセージに表示できるように記述しています。
ここでは同時に、変数 num の値もメッセージに表示させています。後術しますが、この値は
add_maion()の引数として外部から指定されます。

add_main_tsk.c
#include <stdio.h>
#include <stdlib.h>
#include "dpiheader.h"

int add_task(int, int, int*);

int add_main(int num){

int a = 35;
int b = 11;
int result;

add_task(a, b, &result);
printf("************************************************************************\n");
printf("Mesage from C function\n");
printf(">>> Culcurate System Verilog task result = %d <- %d\n", result, num);
printf("************************************************************************\n");

return (0);
}

3-3-2. SystemVerilog コード

本コードでは、tb_moduleというモジュール名を定義しています。タスクはテストベンチで
利用するケースが多いので、モジュール名の後ろに定義するポート名は何も定義していません。あくまでC言語と連携した検証システムのようなものをイメージしています。
このモジュール内には、SystemVerilog のタスクadd_taskを定義していますが、これが
C言語側から呼び出される対象になります。
SystemVerilog側から見れば、外部にエクスポートする対象となります。
add_taskでは 入力ポートとしてint 型で宣言された仮引数t_dat_db が、C言語側の
引数に対応します。

t_dat_dbを加算した結果は、出力ポートとしてint型で宣言された仮引数t_q
送られて、add_taskの第三引数に反映されることで、外部から参照できるようになります。
DPI-Cに処理をエクスポートする為、export "DPI-C" の後ろに task add_task の記述を
追加しています。
このタスクには、時間的変化を与えています。対象のadd_taskは、開始して20ns後に
即値99をt_qに出力し、さらに20ns後に加算結果がt_qに出力するように記述しています。


本コードでは、initial文を使用して、シミュレーション波形で時間的な動きを視覚的に
把握できるようにしています。
シミュレーション直後、つまり0nsでnum_valに3を代入した後、20ns経過した後に、
C言語側のサブ関数add_main()に引数3を与えて実行しています。組込みシステムのように、
CPUのリセットを解除して、ROMからCコードをブートする動きに似ているかもしれません。

この処理によって、Cコードadd_main()が実行後、その中にあるサブ関数add_task()
実行されるフローがシミュレートされます。

このフローは、結果的にSystemVerilogのinitial文からC言語を呼び出す仕組みが無いと
実現できないので、本コード内では、import “DPI-C”も併用しています。
SystemVerilogから呼び出されるCコードadd_main()の中では、サブ関数add_func()
SystemVerilogに参照するので、本コードではimport “DPI-C”の後ろに context を付加
して、int型のfunction としてadd_mainを呼び出すように記述しています。

tb_module.sv

`timescale 1ns/1ps

module tb_module();

import "DPI-C" context task add_main(input int num);
export "DPI-C" task add_task;

int num_val = 0;

// Task                                                   
task add_task;
    input int t_da;
    input int t_db; 
    output int t_q; 

    begin
        #20 t_q = 99;
        #20 t_q = t_da + t_db;
    end
endtask


// Monitoring
    initial 
    begin
        num_val = 3;
        #20 
        add_main(num_val);
        #20 $finish;
    end

endmodule

参考:
・ModelSim DEサンプル・コード


3-3-3. Tcl スクリプト

ModelSimで利用可能なコマンドを記述しています。
もっと適切な書き方はあると感じていますが、深追いはせずに、とりあえず「動かす」ことに
重点を置きました。

dpic_c2sv_tsk.tcl
vlib work
vlog -sv -dpiheader dpiheader.h tb_module.sv add_main_tsk.c -ccflags "-g"
vsim -c tb_module
add wave -r /*

参考:
・ModelSim DEサンプル・コード


3-3-4. 実行手順

3-3-5. Modelsim の起動

1) 半角英数のPATHによる任意のディレクトリに、以下のファイルを格納します。

tb_module.sv

add_main_tsk.c

dpic_c2sv_tsk.tcl

本稿に掲載されている該当のソース・コードをコピー&ペーストして、
上記のファイル名(および拡張子)で保存します。

2) ModelSim インテル® FPGA Starter Edition を起動します。

3) Transcript ウィンドウで次のコマンドをタイプして、上記 1) のディレクトリに移動します。
  
【例】 cd C:/work/test_c2sv_tsk

カレント・ディレクトリを移動後、ls を実行して、上記のファイルがあることを確認します。
kom_22.png

3-3-6. Tclスクリプトの実行

次のコマンドで Tclスクリプトを実行します。

do ./dpic_c2sv_tsk.tcl

Tcl スクリプトを実行後、Wave ウィンドウ内にポートや信号の全てが追加されています。
kom_23.png

3-3-7. ModelSim によるシミュレーション

Wave ウィンドウ内にカーソルを置いて、右クリックより Add -> New Divider を選択します。
kom_24.png

次のように、SystemVerilog のモジュールと、C言語の関数を区分けします。
その際、信号の順番も適宜入れ替えます。

kom_25.png

Simulation メニューから Run -> Run -All を選択します。
kom_26.png

次のような画面が起動したら、[ いいえ(N) ] をクリックします。
kom_27.png

Wave ウィンドウに波形が表示されます。
kom_28.png

[ Zoom Full (F) ] アイコンをクリックします。
kom_29.png

波形の全体像が現れてきます。
kom_30.png

3-3-8. シミュレーション結果

シミュレーターで時系列の動きを追うため、SystemVerilog コードの initial 文に記述した
内容に従い、nmu_val に値3を指定してから20 ns 経過後にnumが0から3に変化している
ことから、この時点でCコードadd_main()が実行開始していることが把握できます。
kom_30_up.png

同時に、add_main()関数内のサブ関数 add_task()が参照されて、SystemVerilog コードで
定義したタスクadd_task に加算する引数が渡されていることが確認できます。
add_task内では出力に与える値を時間に変えるように記述しています。
その為、add_taskが呼び出された地点から20ns後に即値99を出力し、さらに20ns後に加算が
行われている動きがシミュレーション波形でも確認できます。
加算結果がCコードに返された後、次のようにprintf()によってModelSimの
Transcriptウィンドウ内にメッセージとして表示されます。
kom_31_up.png

4. 【考察】 DPI-CとCコンパイラとの関係

コンパイル・メッセージからの推察の範囲になりますが、ModelSimにはGCCが内包されているように見受けられます。

どのバージョンからGCCが内包されたかのは未調査

また、コンパイル・オプションでは、次のように -dpiheaderオプションの後ろにdpiheader.hと追記しています。

vlog -sv -dpiheader dpiheader.h (ファイル名).sv (ファイル名).c -ccflags "-g"  

このときツールが自動でdpiheader.hをプロジェクト・ディレクトリに生成していました。

さらに、このdpiheader.hでは、#include "svdpi.h"と記述されており、ファイル名から
推察して、svdpi.hがDPI-Cとの関連付けを行っているように感じています。
また、このdpiheader.hでは、DPI-Cでインポート/エクスポートを行う為の関数プロトタイプ
も再定義されていました。
その他、Load時に幾つか専用のDLLファイルも生成されていることから、シミュレーション時
に、シミュレーターがこれらのDLLを参照しているように思われます。

この辺りの仕組みは、シミュレーション・ツールの作りに依存するので、筆者のような
一般ユーザーでは、ここまでが限界と言えます。


参考:
DPI-CでSystemVerilogからC言語の関数呼び出し


5. まとめ

  • DPI-Cを使用して、SystemVerilog コードからC言語を呼び出す場合、System Verilogコード
    にはimport "DPI-C"を記述し、その後にはSystemVerilogコード内に記述している関数
    (ファンクションもしくはタスク)の名前を記述

    • 指定する関数は、purecontext に分類され、次のルールに基づいて関数名の
      前に追記
    • ファンクションを呼び出す場合:
      - C言語からSystemVerilogコードに参照しない場合、pure を追記
      - C言語からSystemVerilogコードに参照する場合、context を追記
    • タスクを呼び出す場合:常にcontext を追記
  • DPI-Cを使用して、C言語からSystemVerilogコード内に記述している関数(ファンクション
    やタスク)を呼び出す場合、export “DPI-C”を記述し、その後にはSystemVerilogコード
    に記述している関数の名前を記述
    (exportの場合、importとは異なり、purecontextは不要 )

  • DPI-Cを使用する場合、コマンド・オプション -dpiheaderを追加してコンパイル

    • dpiheader.h内にDPI-Cを使用するためのインクルード・ファイルsvdpi.hが定義
    • dpiheader.hは、ModelSimが自動的に生成
  • ModelSimに内包されているGCCを使用

どのバージョンからGCCが内包されたかのは未調査

6. リファレンス

この記事は以下の情報を参考にして執筆しました。
・ SystemVerilog設計スタートアップ Design Wave Magazine 編集部 編(CQ出版社)
DPI-CでSystemVerilogからC言語の関数呼び出し
ModelSimASE+clangでSystemVerilog DPI-Cをシミュレーションする
16.2 DPI
無償ツールで実践する「ハード・ソフト協調検証」(1) ―― SystemVerilogのDPI-C機能
無償ツールで実践する「ハード・ソフト協調検証」(2) ―― テスト・プログラムはC言語で書く(Tech Village)
・ModelSim DEサンプル・コード


10
3
2

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
10
3