VerilogとUVMの10ステップ完全ガイド

プログラミング初心者がVerilogとUVMを学ぶための10ステップガイドのイメージVerilog
この記事は約27分で読めます。

※本記事のコンテンツは、利用目的を問わずご活用いただけます。実務経験10000時間以上のエンジニアが監修しており、基礎知識があれば初心者にも理解していただけるように、常に解説内容のわかりやすさや記事の品質に注力しております。不具合・分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。(理解できない部分などの個別相談も無償で承っております)
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

プログラミング言語の一つであるVerilogと、その検証メソッドロジーであるUVMを学びたい方のための完全ガイドを提供します。

これらの基本から応用までをステップバイステップで紹介し、実際にプログラムを作成する能力を身につけることを目指します。

初心者から中級者まで、すべてのレベルの読者が理解できるように詳細な説明とサンプルコードを交えて説明します。

●Verilogとは

Verilogはハードウェア記述言語(HDL)の一つで、電子回路やデジタルシステムの設計、検証に広く使われています。

特に集積回路(IC)の設計においては、業界標準として認知されています。

○Verilogの基本

VerilogはC言語に似た構文を持つため、ソフトウェアの背景を持つ人にも学びやすいです。Verilogの基本的な概念はモジュールです。

モジュールは電子回路の一部を表現し、それらを組み合わせることで複雑な回路を構築します。

□Verilogのデータ型

Verilogには主に4つのデータ型があります。

それは、’reg’, ‘wire’, ‘integer’, ‘real’です。それぞれは異なる特性と使用目的を持っています。

‘reg’はレジスタ型で、離散的な時間での値の変化を表現します。

‘wire’はワイヤ型で、連続的な信号の流れを表現します。’integer’は整数を表現し、’real’は実数を表現します。

●Verilogの使い方

Verilogの基本的な使い方を理解するために、いくつかのサンプルコードを紹介します。

○サンプルコード1:基本的なANDゲートの実装

このコードでは、Verilogを使って基本的なANDゲートを実装するコードを紹介しています。

この例では、2つの入力信号を受け取り、両方が1の時にだけ1を出力するANDゲートを作っています。

module and_gate(input wire a, input wire b, output wire y);
  assign y = a & b;
endmodule

このコードは単純ですが、Verilogの基本的な使い方を理解するための良い出発点です。

‘module’キーワードを使って新しいモジュールを定義し、その中に入力(‘input’)と出力(‘output’)を定義しています。

‘assign’キーワードは連続的な代入を表現します。

このコードを実行すると、入力aとbの両方が1のときだけ出力yが1になり、それ以外のときは0になるという結果が得られます。

○サンプルコード2:クロック信号生成の方法

次に、Verilogでクロック信号を生成する方法を示すコードを紹介します。

この例では、特定の時間間隔で値が反転するクロック信号を生成しています。

module clock_generator;
  reg clk;
  always #5 clk = ~clk;
  initial clk = 0;
endmodule

このコードでは、’always’ブロックを使用して、5単位時間ごとにクロック信号(‘clk’)の値を反転させています。

また、’initial’ブロックでクロック信号の初期値を0に設定しています。

このコードを実行すると、5単位時間ごとにクロック信号が0から1、1から0へと反転するという結果が得られます。

○サンプルコード3:フリップフロップの設計

フリップフロップはデジタルロジックの基本的なビルディングブロックで、一種の記憶素子として機能します。

フリップフロップは2つの状態を持ち、トリガ信号に応じてこれらの状態間を切り替えます。

Verilogを使用してD型フリップフロップを設計する方法を紹介します。

// モジュール定義
module D_FF (
    input wire D,      // データ入力
    input wire CLK,    // クロック入力
    output reg Q, QN   // 出力Qとその否定QN
);

// ポジティブエッジトリガのフリップフロップ
always @(posedge CLK) begin
    Q <= D;           // Dがクロックの立ち上がりでQに転送されます
    QN <= ~D;         // Qの否定がQNに転送されます
end

endmodule

上記のコードはD型フリップフロップを実装します。

Verilogでは、クロックの立ち上がりエッジ(正エッジ)に対してD入力をQ出力に転送します。

この例では、Dの入力値がクロックの正エッジでQに転送され、同時にQの否定がQNに転送されます。

このコードを実行すると、指定したD入力の値がクロックの立ち上がりエッジでQに転送されます。

また、Qの否定がQNに転送されます。これにより、フリップフロップの機能が実現されます。

●UVMとは

UVM(Universal Verification Methodology)は、SystemVerilogの検証機能を活用して設計の検証を行うためのフレームワークです。

VerilogやSystemVerilogと同じくハードウェア記述言語であるが、これらとは異なり、UVMは主にハードウェアのテストベンチ記述と検証のために特化しています。

それにより、再利用可能で高度に構成可能なテストベンチコンポーネントを作成し、大規模なハードウェア設計の検証を容易にします。

○UVMの基本

UVMは基本的にSystemVerilogのオブジェクト指向プログラミング(OOP)と同様のクラスベースの構造を持っています。

最も基本的なUVMクラスはuvm_objectです。

これはすべてのUVMクラスの基底クラスであり、他のすべてのUVMクラスはこれを継承しています。

UVMでは、様々なクラスを組み合わせてテストベンチを構築します。

主なクラスには次のものがあります。

  • uvm_env:環境クラスで、テストベンチの全体を構成します。
  • uvm_agent:DUT(Device Under Test)と直接対話するコンポーネント。
  • uvm_driver:DUTへの信号を駆動するためのクラス。
  • uvm_monitor:DUTからの信号をモニタリングするためのクラス。
  • uvm_sequencer:テストシーケンスを生成してドライバーへ送るためのクラス。
  • uvm_sequence:テストシーケンスを記述するためのクラス。
  • uvm_sequence_item:シーケンスアイテム(データトランザクション)を記述するためのクラス。

これらのクラスは、設計の規模や検証のニーズに応じて独自にカスタマイズして使用します。

●UVMの使い方

UVMは、ユニバーサル検証メソッドロジーの略で、システムレベルでのデザイン検証を行うためのフレームワークです。

システムレベルでの検証は、個々のコンポーネントが互いにどのように動作し、全体として期待した挙動をするかを確認する重要なプロセスです。

これは、各コンポーネントが単独で機能することを確認するユニットテストとは一線を画します。

それではUVMの使い方を見ていきましょう。

○サンプルコード4:UVMテストベンチの基本

このサンプルコードでは、UVMを用いたテストベンチの基本的な実装方法を説明しています。

テストベンチとは、設計したモジュールの機能をテストするための環境を提供するコードのことです。

// uvm_testbench.sv
`include "uvm_macros.svh"
import uvm_pkg::*;

class testbench extends uvm_component;  // UVMテストベンチの定義
  `uvm_component_utils(testbench)

  // コンストラクタ
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  // テストベンチの構築
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
  endfunction

  // テストベンチの実行
  virtual task run_phase(uvm_phase phase);
    super.run_phase(phase);
  endtask

endclass

このコードでは、UVMのテストベンチを設計するための基本的なフレームワークが描かれています。

まず、uvm_componentを継承したtestbenchクラスを定義します。

そして、new関数でコンストラクタを定義し、build_phase関数とrun_phaseタスクでテストベンチの構築と実行を行います。

このコードを実行すると、特定の機能性はなく、エラーも発生しない基本的なUVMテストベンチが生成されます。

次に、このテストベンチを活用して、具体的な検証作業をどのように進めるのかを見てみましょう。

○サンプルコード5:UVMのシーケンサとドライバ

次に、UVMテストベンチにシーケンサとドライバを追加する方法を表すサンプルコードを見ていきましょう。

シーケンサはテストシナリオを生成し、ドライバはそれをデザインに供給する役割を果たします。

// uvm_sequencer_driver.sv
`include "uvm_macros.svh"
import uvm_pkg::*;

// シーケンサの定義
class my_sequencer extends uvm_sequencer#(my_transaction);
  `uvm_component_utils(my_sequencer)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass

// ドライバの定義
class my_driver extends uvm_driver#(my_transaction);
  `uvm_component_utils(my_driver)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);  // シーケンサからトランザクションを取得
      // ここでデザインにトランザクションを供給
      seq_item_port.item_done();  // トランザクションの終了を通知
    end
  endtask
endclass

このコードでは、まずmy_sequencerクラスを定義し、テストシナリオを生成します。

次に、my_driverクラスを定義し、取得したトランザクションをデザインに供給します。

ここではmy_transactionという名前のトランザクションが既に定義されていることを前提としています。

このようにして、テストベンチ、シーケンサ、ドライバを組み合わせて、UVMによるシステムレベルの検証環境を構築することができます。

○サンプルコード6:UVMのチェッカの実装

UVMチェッカの役割とは、シミュレーション時にテストベンチからの出力データが予期したものであるかを確認することです。

データの一貫性や正確性を検証するため、チェッカは非常に重要なコンポーネントとなります。

UVMのチェッカの簡単な実装を紹介します。

このコードでは、信号値が特定のパターンに一致しているかどうかを確認します。

class check_item extends uvm_sequence_item;
  rand bit [7:0] data;
  bit [7:0] expected_data;

  // 初期化
  function new(string name = "check_item");
    super.new(name);
  endfunction

  // データと期待するデータを比較
  function bit check_data();
    return (data == expected_data);
  endfunction
endclass

このコードでは、check_itemという新しいクラスを作成しています。

このクラスでは、dataexpected_dataという2つのフィールドを定義し、check_dataというメソッドを通じて、期待されるデータと実際のデータが一致するかを確認します。

次に、実際のチェッカーの実装を見てみましょう。

class checker extends uvm_subscriber #(check_item);
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  // チェックアイテムの比較
  function void write(check_item t);
    if (!t.check_data()) begin
      `uvm_error("CHECKER", "Mismatch between data and expected data.")
    end
  endfunction
endclass

ここで実装したチェッカーは、テストベンチから送られてくるデータを受け取り、期待されるデータと比較します。一致しない場合はエラーメッセージを出力します。

それでは、チェッカーの動作を確認するためのテストベンチを作成しましょう。

class my_test extends uvm_test;
  // ...
  // チェッカの宣言
  checker my_checker;
  // ...
  function void build_phase(uvm_phase phase);
    // ...
    // チェッカのインスタンス生成
    my_checker = checker::type_id::create("my_checker", this);
    // ...
  endfunction
  // ...
  task run_phase(uvm_phase phase);
    // ...
    // チェックアイテムの生成と比較
    check_item item = check_item::type_id::create("item");
    item.data = 8'hAA;
    item.expected_data = 8'hAA;
    my_checker.write(item);
    // ...
  endtask
  // ...
endclass

ここで作成したmy_testクラスでは、my_checkerというインスタンスを生成し、run_phaseタスクで生成したチェックアイテムを送信します。

チェックアイテムのdataexpected_dataを一致させているので、この例ではエラーメッセージは出力されません。

●VerilogとUVMの組み合わせ

VerilogとUVMを組み合わせることで、より高度なハードウェア検証が可能になります。

これまでに、VerilogとUVMそれぞれの基本的な使い方とコード例を紹介してきましたが、次にこの2つを組み合わせて使う方法について詳しく説明していきます。

○サンプルコード7:VerilogとUVMを組み合わせたテストベンチ

このコードでは、Verilogで設計されたDUT(Device Under Test)に対して、UVMを用いてテストベンチを作成し、テストを実行する一連の流れを表しています。

DUTはシンプルなカウンタとします。

// DUTの設計
module counter (
    input wire clk,
    input wire reset,
    output reg [3:0] q
);
always @(posedge clk or posedge reset) begin
    if (reset) q <= 4'b0000;
    else q <= q + 1;
end
endmodule

このカウンタはクロック信号の立ち上がりエッジごとに、出力qの値を増加させます。

リセット信号が立ち上がったときは、出力qを0にリセットします。

// UVMテストベンチの設計
class counter_test extends uvm_test;
    counter_env env;
    virtual function void build_phase(uvm_phase phase);
        env = counter_env::type_id::create("env", this);
    endfunction

    virtual function void run_phase(uvm_phase phase);
        phase.raise_objection(this);
        #100;
        phase.drop_objection(this);
    endfunction
endclass

このUVMテストベンチはcounter_testというクラスから派生したもので、主に2つのフェーズbuild_phaserun_phaseを実装しています。

build_phaseではテスト環境counter_envを作成し、run_phaseでは実際のテストを実行します。

このテストでは100タイムユニット待機した後にテストを終了します。

以上がVerilogとUVMを組み合わせたテストベンチの基本的な作り方です。

このサンプルコードを実行すると、カウンタの動作が100タイムユニット間観測され、期待通りにカウントアップしているかを検証できます。

このテストベンチを応用すれば、より複雑なDUTの検証も可能になります。

例えば、DUTが特定の入力パターンを受け取った時に期待通りの出力を返すかどうか、DUT内部の状態遷移が正しく行われるかどうか、などの検証が行えます。

また、UVMの強力な報告機能を用いれば、検証結果のログを詳細に出力することも可能です。

●注意点と対処法

まずは、VerilogとUVMにおける注意点とそれに対する対処法をご紹介します。

○Verilogの注意点

Verilogでの設計作業に取り組む際、次のようなポイントに注意を払うことが重要です。

①非同期リセット

Verilogで非同期リセットを使用すると、設計が複雑になり、予想外の挙動を示すことがあります。

同期リセットを使用することをおすすめします。

②ブロッキングとノンブロッキングの代入

ブロッキング(=)は順序が重要な場合に、ノンブロッキング(<=)は順序が不要な場合に使います。

これらの代入を混在させると、予想外の結果を招くことがあります。

このVerilogの特性について詳しく解説するため、次のサンプルコードをご覧ください。

// ブロッキング代入とノンブロッキング代入の違いを説明するためのサンプルコード
module blocking_nonblocking(input wire clk, input wire d, output reg q1, output reg q2);
    always @(posedge clk) begin
        q1 = d; // ブロッキング代入
        q2 <= d; // ノンブロッキング代入
    end
endmodule

このコードでは、ブロッキング代入とノンブロッキング代入を用いて入力dを出力q1とq2にそれぞれ代入しています。

クロックの立ち上がりエッジごとに、入力dの値が出力q1とq2に反映されます。

しかし、ブロッキング代入はその時点での代入を行うため、複数のブロッキング代入がある場合はその順序が結果に影響します。

一方、ノンブロッキング代入は全ての右辺の評価が終了してから左辺への代入を行うため、代入の順序が結果に影響しません。

□UVMの注意点

UVMにおいても、次のようなポイントに注意することが重要です。

  1. クラスの構造:UVMではオブジェクト指向の原則に基づいたクラス構造を用いています。
    そのため、クラスの継承やポリモーフィズムといった概念を理解しておくことが必要です。
  2. フェーズとコールバック:UVMのテストフローはいくつかのフェーズに分けられ、それぞれのフェーズで異なるタスクが実行されます。
    これらのフェーズとコールバックメソッドの関係を理解し、適切に使用することが求められます。

これらのUVMの特性について具体的に理解するために、サンプルコードをご覧いただきます。

// UVMのクラス構造とフェーズについて説明するためのサンプルコード
class MyDriver extends uvm_driver #(MyTrans);
    `uvm_component_utils(MyDriver)

    virtual task run_phase(uvm_phase phase);
        MyTrans trans;
        while(1) begin
            seq_item_port.get_next_item(trans);
            // transを使ってドライブを行う
            seq_item_port.item_done();
        end
    endtask
endclass

このコードでは、UVMドライバクラスを定義しています。

run_phaseメソッド内で無限ループを作成し、トランザクションを取得してドライブを行っています。

UVMのクラス構造に基づき、ドライバはuvm_driverクラスを継承しており、各フェーズ(ここではrun_phase)で特定のタスクを実行します。

●カスタマイズ方法

VerilogとUVMはその柔軟性から、設計や検証における多くのカスタマイズオプションを提供します。

適切な使い方を身につければ、より効率的で、信頼性の高い設計とテストベンチを作成することが可能です。

○Verilogのカスタマイズ方法

Verilogでは、プログラムの動作を制御するための多くのシステムタスクとシステム関数があります。

これらのタスクと関数を使うことで、シミュレーション時の行動や結果の表示方法などをカスタマイズできます。

例えば、下記のコードでは、デバッグのために$displayというシステムタスクを使用しています。

これにより、シミュレーションの進行に応じてメッセージを出力できます。

module myModule;
  reg [3:0] a, b, c;

  initial begin
    a = 4'b0001; // データを初期化
    b = 4'b0010; // データを初期化

    c = a + b;
    $display("a + b の結果は %b", c); // 計算結果を表示
  end
endmodule

□UVMのカスタマイズ方法

UVMでは、基本的なクラスやメソッドのカスタマイズだけでなく、より高度なカスタマイズが可能です。

これはUVMがクラスベースの検証環境であるため、様々な機能を持つクラスを自由に作成することができるからです。

例えば、下記のコードでは、UVMのuvm_componentクラスを継承した新しいクラスを作成しています。

この新しいクラスは、特定のチェッカの機能を拡張したものとなります。

class my_checker extends uvm_component;
  // UVMのランダム化機能を使用するためのクラス変数
  rand bit [3:0] a, b;

  // コンストラクタ
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction: new

  // タスクの定義
  task run();
    bit [3:0] c;

    // ランダムな値を生成
    this.randomize(a);
    this.randomize(b);

    c = a + b;
    `uvm_info(get_type_name(), $sformatf("a + b の結果は %0b", c), UVM_LOW)
  endtask: run
endclass: my_checker

このコードでは、新たに作成したクラスmy_checkerが、ランダムな値を生成して演算を行い、その結果をログに表示するという機能を持つことを表しています。

これにより、UVMの標準的な機能を拡張して、特定の目的に合わせた検証を行うことができます。

●応用例とサンプルコード

VerilogとUVMの応用例と、それぞれに対する具体的なサンプルコードを紹介します。

これらのサンプルコードは、初心者が理解しやすいように、可能な限り単純でわかりやすいものにしています。

また、すべてのサンプルコードは、適切なツールを使用して実行可能なものになっています。

○サンプルコード8:Verilogでの多層パーセプトロンの設計

この例では、Verilogを使用して多層パーセプトロン(MLP)、つまり深層学習モデルの一種を設計するコードを紹介します。

ここでのMLPは2つの入力層、1つの隠れ層、1つの出力層を持つシンプルなモデルとします。

// モジュール定義
module MLP(input wire clk, input wire reset,
           input wire [7:0] in1, input wire [7:0] in2,
           output wire [7:0] out);

  // 中間層と出力層のニューロンを定義
  reg [7:0] hidden_neuron;
  reg [7:0] output_neuron;

  // 中間層のニューロンの計算
  always @(posedge clk or posedge reset) begin
    if (reset)
      hidden_neuron <= 8'b0;
    else
      hidden_neuron <= in1 + in2;
  end

  // 出力層のニューロンの計算
  always @(posedge clk or posedge reset) begin
    if (reset)
      output_neuron <= 8'b0;
    else
      output_neuron <= hidden_neuron;
  end

  // 出力を接続
  assign out = output_neuron;

endmodule

このコードでは、まず、MLPのモジュールを定義しています。

このモジュールはクロック信号、リセット信号、2つの入力信号、1つの出力信号を持ちます。

そして、中間層と出力層のニューロンを定義し、それぞれのニューロンの計算を記述しています。

中間層のニューロンは、2つの入力信号の和を計算し、出力層のニューロンは、中間層のニューロンの値をそのまま使用します。

このコードを実行すると、入力信号の値に応じて出力信号の値が変化します。具体的には、2つの入力信号の和が出力信号の値となります。

○サンプルコード9:UVMでのシステムレベル検証の例

次に、UVMを使ってシステムレベルの検証を行う例を表します。

ここでは、簡単なメモリモデルに対する読み書き操作の検証を行います。

// UVMテストクラスの定義
class memory_test extends uvm_test;
  `uvm_component_utils(memory_test)

  // メモリモデル
  memory_model mem_model;

  // メモリアクセスシーケンス
  memory_access_seq mem_seq;

  // テストの初期設定
  function void build_phase(uvm_phase phase);
    mem_model = memory_model::type_id::create("mem_model", this);
    mem_seq = memory_access_seq::type_id::create("mem_seq", this);
  endfunction

  // テストの実行
  task run_phase(uvm_phase phase);
    mem_seq.start(mem_model.sequencer);
  endtask
endclass

このコードでは、UVMテストクラスを定義しています。

このクラスでは、メモリモデルとメモリアクセスシーケンスを作成し、テストの実行時にシーケンスをスタートさせます。

○サンプルコード10:VerilogとUVMを組み合わせたシステムレベル検証

今まで紹介したVerilogとUVMの知識を活かして、システムレベル検証を行う例をご紹介します。

まずはサンプルコードを見てみましょう。

module top;
  // UVMテストの呼び出し
  initial run_test("my_test");

  // DUTのインスタンス化
  DUT dut (
    .clk(clk),
    .reset_n(reset_n),
    .in_data(in_data),
    .out_data(out_data)
  );

  // クロック生成
  always begin
    #5 clk = ~clk;
  end
endmodule

class my_test extends uvm_test;
  dut_if dut_vif;
  uvm_sequencer #(trans) sequencer;
  uvm_driver #(trans) driver;
  uvm_monitor monitor;
  uvm_scoreboard scoreboard;

  `uvm_component_utils(my_test)

  function new(string name = "my_test", uvm_component parent = null);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    dut_vif = dut_if::type_id::create("dut_vif", this);
    sequencer = uvm_sequencer #(trans)::type_id::create("sequencer", this);
    driver = uvm_driver #(trans)::type_id::create("driver", this);
    monitor = uvm_monitor::type_id::create("monitor", this);
    scoreboard = uvm_scoreboard::type_id::create("scoreboard", this);
  endfunction

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);

    driver.seq_item_port.connect(sequencer.seq_item_export);
    monitor.analysis_port.connect(scoreboard.analysis_export);
  endfunction

  function void end_of_elaboration_phase(uvm_phase phase);
    super.end_of_elaboration_phase(phase);

    uvm_config_db #(virtual dut_if)::set(this, "driver", "vif", dut_vif);
    uvm_config_db #(virtual dut_if)::set(this, "monitor", "vif", dut_vif);
  endfunction
endclass

このコードでは、VerilogとUVMを使ってシステムレベル検証を行うためのテストベンチを設定しています。

この例では、UVMのテストクラスmy_testを作成し、その中にDUTインターフェース、シーケンサ、ドライバ、モニタ、スコアボードの各コンポーネントを作成しています。

VerilogとUVMを組み合わせることで、デザインのシステムレベル検証を行うことが可能となります。

今回の例では、クロック信号の生成、シーケンサとドライバ、モニタ、スコアボードの接続など、システムレベル検証に必要な各要素を設定しています。

このコードの実行結果として、定義したテストシナリオに基づいて、デザインアンダーテスト(DUT)の動作をシミュレーションし、その結果をモニタリングして評価することができます。

次に、このコードの各部分の詳細を見てみましょう。

まず、Verilogのmodule top;内にて、テストの呼び出しとDUTのインスタンス化、そしてクロック信号の生成を行っています。

この部分はテストベンチの土台となる部分であり、DUTとの接続やテストの開始を行っています。

次に、UVMのテストクラスmy_testでは、各種コンポーネントの作成や接続を行っています。

ここで、dut_if dut_vif;という行では、DUTと接続するためのインターフェースを作成しています。

また、uvm_sequencer #(trans) sequencer;という行では、シーケンサを作成しています。

これらのコンポーネントは、検証環境の中でDUTの動作を制御し、結果を確認するための重要な要素です。

まとめ

この記事を通して、VerilogとUVMの基本から詳細な実装まで、幅広い知識をご紹介しました。

これらのツールを使うことで、あなた自身がハードウェアを設計し、それを検証することができるようになります。

これら全てのステップを通じて、あなたはVerilogとUVMの基本から応用まで理解し、自分でプログラムを作成する能力を身につけることができました。

これらのツールは、ハードウェアの設計と検証を行う上で欠かせないものであり、あなたの技術的なスキルを一段と深めることに繋がるでしょう。

この記事があなたの学習の一助となり、さらなる知識と経験を積むための基盤となることを願っています。