らんらん技術日記

日々の学習メモに

JenkinsでUVMのテスト結果を集計する

記事の順番で言えば、次はUVMの受信編になるのですが・・・話をすっとばしてJenkinsとの連動を考えてみます! 受信編は気が乗れば書きますw
僕がUVMを学んでいる目的は、HDLのテストケースを効率的に記述することです。となると、テストケースを集計する仕組みがあった方がよく、そこで目星をつけたのがJenkinsです。同様のことを考えている先行例として、Qiitaの以下の記事が参考になります。
qiita.com

上の記事では、UVMのxml_report_serverクラスを継承して、Jenkinsが扱いやすい形式のxmlファイルを作成しているようです(雰囲気、未完で終わっているのかな・・・?)。
ただ個人的には、あんまり好みな方法じゃないです。Jenkins都合のために、System Verilogのコードを書きたくないのでw System Veirlogの記述は最小限に留めて、こういうことはPythonに任せましょう!
Pythonであればコーディングは簡単ですし、解説記事もたくさんあります。UVMがバージョンアップしても柔軟に対応できます。
想定するシステムのイメージは以下になります。

f:id:yukirunrun:20200412000436p:plain

僕が実際に構築したシステムの紹介です。簡単のため一つのパソコンで完結させています。
OS:Windows10
バージョン管理システム:Git、確かGit for Windowsをインストール
JenkinsからGitリポジトリへのアクセス:http、Apacheサーバで実現
HDLシミュレータ:Modelsim Intel FPGA Starter edition
Python:Anaconda3のPython

補足1:Gitリポジトリはローカルに作っています。アクセスは一応httpにしていますが、外部に公開しないならfileアクセスでオッケー。
補足2:AnacondaのPythonWindowsアップデートの影響で、pythonという名前で実行するには絶対パス指定が必要です。僕はシンボリックリンクを張って、python_slという名前で使っています。

UVM側の準備

テスト実行はUVMでやるのですから、テストレポートを出力するまでがUVMの仕事です。レポート用マクロ(uvm_info、uvm_warningなど)の機能を使い、テストに対応したレポートのみを外部ファイルに出力します。コードにするとこんな感じ。

class xxx_scoreboard extends uvm_subscriber #(xxx_item);
    `uvm_component_utils(xxx_scoreboard);

    bit test1_passed;

    function new(string name = "xxx_scoreboard", uvm_component parent);
        super.new(name, parent);
    endfunction: new
    
    extern function void write(xxx_item t);
    extern function void reportTest();
endclass: xxx_scoreboar

function void xxx_scoreboard::write(xxx_item t);
    //省略
endfunction: write

function void xxx_scoreboard::reportTest();
    if(test1_passed)
        `uvm_info("TEST", "Testcase1 Success", UVM_LOW);
    else
        `uvm_warning("TEST","Testcase1 Fail");
    //省略
endfunction: reportTest

class xxx_test extends uvm_test;
    `uvm_component_utils(xxx_test)
    xxx_env env;
    uvm_factory factory;
    int print_file;
    
    function new(string name = "xxx_test", uvm_component parent = null);
       super.new(name, parent);
    endfunction : new
 
    virtual function void build_phase(uvm_phase phase);
         super.build_phase(phase);
         env = xxx_env::type_id::create("env", this);
    endfunction : build_phase

    virtual function void run_phase(uvm_phase phase);
         //テストケース実行、省略
    endfunction : run_phase

    function void end_of_elaboration_phase(uvm_phase phase);
      print_file = $fopen("report/test1.txt");
      set_report_id_file_hier("TEST", print_file);
      set_report_severity_action_hier(UVM_INFO, UVM_DISPLAY | UVM_LOG);
      set_report_severity_action_hier(UVM_WARNING, UVM_DISPLAY | UVM_LOG);
      //dump_report_state();
    endfunction : end_of_elaboration_phase
 endclass : xxx_test

xxx_scoreboad.svhはテスト結果を判定するクラス、xxx_test.svhはテストケースを実行する最上位クラスです。
xxx_test.svhのend_of_elaboration_phaseにおいて、IDが"TEST"のuvm_infoとuvm_warningを外部ファイル"report/test1.txt"に出力するように設定しています。他のテストケースの出力ファイルも、reportフォルダ以下に保存することを想定しています。
xxx_test.svhでは、テストが成功したときはuvm_info、失敗したときはuvm_warningをコールします。引数のIDには"TEST"を指定し、メッセージの一番最初にTestcase名を書いています。
これはあくまで僕が勝手に決めたルールなので、UVMの公式ではないです。Jenkinsには、このルールに従ってテスト結果を集計させます。
シミュレーションの実行後、出力ファイルは下みたいになります。特に驚きもなく、UVMのレポート出力がそのまま入るだけです。

UVM_INFO ..\src\xxx_scoreboard.svh(50) @ 7500: uvm_test_top.env.sb [TEST] Testcase1 Success
UVM_WARNING ..\src\xxx_scoreboard.svh(56) @ 7500: uvm_test_top.env.sb [TEST] Testcase2 Fail


Pythonで変換スクリプト作成

Jenkinsにはデフォルトで、JUnitと呼ばれるテストフォーマットの結果を読み込み、グラフやリストとして表示する機能があります。
僕はJUnitのことはよくわからないです・・・。
とはいえ、UVMのテスト結果をJUnitのフォーマットに変換すれば、JenkinsでUVMのテスト結果を集計できます!以下のスクリプトで、先ほどのファイルを変換します。

import os
import re
from lxml import etree as ET

REPORT_PATH = './report/'
OUTPUT_FILE = './result.xml'

class TestInfo:
    def __init__(self, fail, classname, name, msg):
        self.fail = fail
        self.classname = classname
        self.name = name
        self.msg = msg

class ReportInfo:
    def __init__(self):
        self.name = ''
        self.cnt_test = 0
        self.cnt_fail = 0
        self.test_info = []

def isNewMsg(text):
    patterns = ["UVM_INFO", "UVM_WARNING"]
    for pattern in patterns:
        if pattern in text:
           return True
    return False

def searchId(line):
    match = re.search(r'\[.+\]', line)
    id_bracket = match.group()
    return id_bracket[1:len(id_bracket)-1]

def takeFirstWord(line):
    match = re.search(r'\[.+\]', line)
    end = match.end()
    msg = line[end:]
    return msg.split()[0]

def removeExtension(word):
    return word[:len(word)-4]

def reconstructMsg(line):
    words = line.split()
    msg = ''
    for word in words:
        if msg != '':
            msg += ' ' 
        msg += word
    return msg

def analysisReport(report):
    report_info = ReportInfo()
    report_info.name = removeExtension(report) 
    with open(REPORT_PATH + report) as rpt:
        nxt_line = rpt.readline()
        test_info = TestInfo(False, "dummy", "dummy", "dummy")
        while nxt_line:
            line = nxt_line
            nxt_line = rpt.readline()
            if len(line) < 2:
                #skip a blank line
                continue

            if isNewMsg(line):
                #create a new test class
                report_info.cnt_test += 1
                classname = report_info.name + '.' + searchId(line)
                name = takeFirstWord(line) 
                msg = reconstructMsg(line)
                test_info = TestInfo(False, classname, name, msg)
                if 'UVM_WARNING' in line:
                    test_info.fail = True
                    report_info.cnt_fail += 1
                report_info.test_info.append(test_info)
            else:
                #add more msg to the previous test class
                msg = reconstructMsg(line)
                test_info.msg += "\n" + msg
    return report_info

def makeXml():
    testsuites = ET.Element('testsuites')
    for report in os.listdir(REPORT_PATH):
        report_info = analysisReport(report)
        testsuite = ET.SubElement(testsuites, 'testsuite', {'name' : report_info.name,
                                                'tests': str(report_info.cnt_test),
                                                'failures': str(report_info.cnt_fail) })
        for test_info in report_info.test_info:
            testcase = ET.SubElement(testsuite, 'testcase', {'classname' : test_info.classname,
                                                    'name' : test_info.name}) 
            if test_info.fail == True:
                ET.SubElement(testcase, 'failure', {'message' : test_info.msg})
            system_out = ET.SubElement(testcase, 'system-out')
            system_out.text = test_info.msg
    tree = ET.ElementTree(testsuites) 
    fl = OUTPUT_FILE
    tree.write(fl, pretty_print=True, xml_declaration=True, encoding='utf-8')

if __name__ == '__main__':
      makeXml()                        

変換結果はこんな感じになります。

<?xml version='1.0' encoding='UTF-8'?>
<testsuites>
  <testsuite name="test1" tests="2" failures="1">
    <testcase classname="test1.TEST" name="Testcase1">
      <system-out>UVM_INFO ..\src\xxx_scoreboard.svh(50) @ 0: uvm_test_top.env.sb@@seq [TEST] Testcase1 Success</system-out>
    </testcase>
    <testcase classname="test1.TEST" name="Testcase2">
      <failure message="UVM_WARNING ..\src\xxx_scoreboard.svh(56) @ 0: uvm_test_top.env.sb@@seq [TEST] Testcase2 Fail"/>
      <system-out>UVM_WARNING ..\src\xxx_scoreboard.svh(56) @ 0: uvm_test_top.env.sb@@seq [TEST] Testcase2 Fail</system-out>
    </testcase>
  </testsuite>
</testsuites>

Jenkinsの設定

ここまで完成すれば、後はJenkinsの設定をするだけです。
簡単のため、新規ジョブ作成からフリースタイル・プロジェクトのビルドを選択します。プロジェクト名は適当にJenkins_Testとします。プロジェクト名にはスペースを入れないこと、同様にJENKINS_HOMEのパスにもスペースを入れないことを推奨します。変な文字が入ると、特にModelsimあたりが正常に動きません。僕の環境だとデフォルトのJENKINS_HOMEは「C:\Program Files (x86)\Jenkins」ですが、わざわざ「D:\jenkins」に移しています(CがSSD、DがHDDなのでストレージの節約の目的もある)。
プロジェクトの設定に移ります。ソースコード管理はGitでこんな感じ。httpでlocalhostを参照するくらいなら、file://を使った方が素直な気もしますw
f:id:yukirunrun:20200412145554p:plain

ビルドでは、Windowsバッチコマンドの実行を選択。run.batの中でUVMのシミュレーション、上記pythonスクリプトを実行します。run.batの中身は面倒なので省略。
f:id:yukirunrun:20200412150743p:plain

ビルド後の処理では、JUnitテスト結果の集計を追加。テスト結果XMLに、pythonで変換したxmlへのパスを指定します。
f:id:yukirunrun:20200412151136p:plain

これで設定は完了。ビルドすると、プロジェクトのトップ画面でテスト結果の推移を見ることができます。
f:id:yukirunrun:20200412151302p:plain

テスト結果の詳細は、テスト階層を下に辿ることで見ることができます。
f:id:yukirunrun:20200412152002p:plain
f:id:yukirunrun:20200412152015p:plain


まとめ

UVMでのテスト結果をJenkinisで集計できるようにしました。
集計の方法は、
①UVMレポートへの自分流のルール設定
Pythonの変換スクリプト
によってある程度カスタマイズできます。ここで紹介したのは一例ということで、使用方法に合わせて変更くださいな。

UVM 雑多メモ

UVM1.2 に関する雑多メモです。
気づいた時に適宜更新していきます。

uvm_config_db、uvm_resouce_db、他

UVMには上位コンポーネントが下位コンポーネントへの設定を行うため、データベースのような仕組みがあります。uvm_conifg_dbやuvm_resouce_db、set_config_*など、様々な記述があるようです。で、お前らはどう使い分けたらいいの?

結論:uvm_config_dbを使いましょう

根拠としては、Verification Academyの資料や質問フォーラムを参照しました。
記述の違いについて、簡潔に説明します。
①uvm_config_db #([type])::set(...)、uvm_config_db #([type])::get(...)
 基本的にはこれを使いましょう!

②set_config_*とget_config_*
 OVMやUVM初期の名残なので、移行を推奨。

③uvm_config_[type]::set(...)、uvm_config_[type]::get(...)
 ①と等価です。①の方が汎用的なので、あえて③を使う意味なし。
 経緯としては、②からの移行を想定して追加されたみたいです。

④uvm_resouce_db #([type])::set(...)、uvm_recouce_db #([type])::get(...)
 技術的には、④を使いやすくしたのが①です。UVMのデータベースは3層構造になっていて、下位:uvm_resouce_pool、中位:uvm_resouce_db、上位:uvm_config_dbになります。データベースのデータの"タグ"管理を、uvm_resouce_dbはフラット構造で行うのに対し、uvm_config_dbは階層構造で行います。一見、uvm_resouce_dbを使っても問題ないように見えますが・・・質問フォーラムでは「Never use uvm_resouce_db」と一蹴されていますw 検証環境が複雑になってくると、どこかで"タグ"の衝突が発生する、というのが理由のようです。

uvm_monitor、uvm_scoreboard

UVMのチュートリアルを読んでいると、初めの方にUVMのクラス一覧の図があります。そしてuvm_componentからの派生先に、uvm_moniorやuvm_scoreboardがあるのですが、何に使うのかという話。

結論:現状は不要です。(2020/4/4時点)

一応、uvm_monitorは信号線のモニタのために、uvm_scoreboardはトランザクションの確認のために用意されているクラスです。しかしベンダが公開しているサンプルコードを見ると、派生元のuvm_componentをそのまま使っているケースが多いのです。
何でかなーと思ってソースコードを見ると・・・中身が何も書かれていないじゃないか! そりゃあ使う意味が全然ないわw
こいつらは将来機能拡張があった時の予約用ですね。

UVMチュートリアル(送信編)

UVMの第二回。
しばらくUVMについて勉強してみたのですが、確かにこれはハードルが高い・・・。学習サイトで一番最初に言われることが「UVMは学習コストが非常に大きい、でもそれを乗り越えたら爆発的に開発効率が上がる!」です。ホントかよw とりあえず学習コストの方はマジです。
まぁ、何事も実際に手を動かさないと始まらないものです。騙されたと思って、簡単なプロジェクトを起こしてみたいと思います。参考にしたサイト・本は以下の通り。どれも少し目を通しただけで、まだ途中ですよ。

Udemy Learn to build OVM & UVM Testbenches from scratch
Udemyの無料で受講できる授業。講師の方は、intelIBMのプロジェクト経験者。
Free SystemVerilog Tutorial - Learn to build OVM & UVM Testbenches from scratch | Udemy

Verification Academy
Mentor社が運営している学習サイトです。
Verification Academy - The most comprehensive resource for verification training. | Verification Academy

A Practical Guide to Adopting the Universal Verification Methodology (UVM) Second Edition
Cadenceの検証エンジニアが著した本。

テスト対象

テストというからには、テスト対象がないことには始まりません。シンプルにネーブル信号付きカウンタを検証することにします。Verilog HDLで書くとこんな感じ。

module counter ( input clk, input rst_n, input en_i,  output reg [7:0] cnt_o );

    always @(posedge clk or negedge rst_n) 
        if (!rst_n) 
            cnt_o <= 8'b0;
        else
            if (en_i) 
                cnt_o <= cnt_o + 1'b1;
endmodule

もう見ただけで完璧に動きそうです! これの何を検証するのって感じですw

検証に必要なソースコード

それでは、上のカウンタの検証環境をUVMで作りたいと思います。理想はいろいろあると思うのですが、今回はカウンタを駆動することを目標にします。以下図のような検証環境を作ることにします。

f:id:yukirunrun:20200120224315p:plain:w500

・・・たかがカウンタの検証とは思えないくらい大規模ですね。これを作るのだと気づいた時、すさまじい衝撃を受けましたよ。
ソースコードとして以下を準備することになります。

  • counter.v
  • cnt_wrapper.sv
  • cnt_if.sv
  • top.sv
  • cnt_Item.svh
  • cnt_driver.svh
  • cnt_sequence.svh
  • cnt_env.svh
  • cnt_test.svh

上の図中のSequencer(シーケンサ)はソースコードとして起こさないで、UVMの標準コンポーネントを使うことにします。これでソースコードを1つ減らせます。よかった、よかった。。。

トランザクションを定義する(cnt_item.svh)

そもそも何でUVMを使うのかというと、理由の一つはトランザクションレベルで検証を行いたいからです。私の解釈では、トランザクションとは通信電文や指示書みたいな概念です。ポジティブエッジがどうとか、タイミングがどうとかいった信号レベルの話はしません。カウンタの場合ですと、「10個カウントしてくれ」とか「カウントを2個あげてくれ」といったレベルで話をしたいのです。UVMでは、トランザクションはSystem VerilogのClassを使って定義するのが通例のようです。

class cnt_item extends uvm_sequence_item;
    rand logic [2:0] cnt;
    `uvm_object_utils(cnt_item)
    
    function new(string name = "cnt_item");
      super.new(name);
    endfunction : new
    
    virtual  function string convert2string;
      string s;
      s = super.convert2string();
      $sformat(s, "cnt=%d\n", cnt);
      return s;
   endfunction : convert2string

  endclass : cnt_item

コード中、大切なのは変数cntのみです。カウントしたい数をこの変数に格納すれば、それがトランザクションになります。それ以外はUVMに従ったおまじないですね。クラスをuvm_sequcence_itemから派生していることに注意してください。

シーケンスを作る(cnt_sequence.svh)

トランザクションの型ができたら、それを実体化することを考えます。トランザクションは一度だけ実体化してもいいのですが、検証という観点からは、複数のトランザクションをシーケンスとして実体化した方が都合がいいです。カウンタを例にすると微妙なので・・・例えば、RAMへのWriteをした後、RAMへのReadをするとかね。今回はカウンタなので、単純にカウント命令を複数回発行するシーケンスにします。

class cnt_sequence extends uvm_sequence #(cnt_item);
    `uvm_object_utils(cnt_sequence);
    
    function new(string name = "");
       super.new(name);
    endfunction: new
    
    cnt_item item;
    
    task body;
        repeat (3) begin
        item = new();
        start_item(item);
        assert(item.randomize());
        `uvm_info("SEQ",{"Send transaction:", item.convert2string()},UVM_MEDIUM);
        finish_item(item);
     end
    endtask: body    
 endclass : cnt_sequence

bodyタスクが一番大事です。カウンタのトランザクションを実体化、そのループを3回繰り返しています。後はUVMのおまじないで、クラスをuvm_sequenceから派生していることにやはり注意です。

トランザクションを信号に変換する(cnt_if.svcnt、cnt_driver.svh、cnt_wrapper.sv)

ここまでトランザクションの話をしてきました。しかし残念ながら、肝心の検証対象(カウンタ)はトランザクションを解釈できません。今回は カウンタをVerilog HDLで記述しているので、そもそもクラスの概念がありません(仮にSystem Verilogで記述した場合も、論理合成を考えるとクラスを扱うのは難しいでしょうね)。カウンタを検証するには、トランザクションをデジタル信号に変換する仕組みが必要になります。
そんなわけで、カウンタのインタフェース仕様の話に移ります。作りたいのはイネーブル付きのカウンタなので、クロック図は下になるでしょうか。


f:id:yukirunrun:20200121222758p:plain:w400

こんなのは今考えることじゃないですよね。カウンタのソースコード(counter.v)を作った時点で、この波形が頭にあるわけです。UVMでデジタル信号を扱うには、まずインタフェースを定義します。System VerilogのInterfaceを使います。

interface cnt_if (input clk, input rst_n);
    logic en = 0;
    logic [7:0] cnt;
endinterface : cnt_if

クロックとリセットはインタフェース内で定義する方法もあるのですが、今回は外部入力にしてみました。
インタフェースができたら、それをドライブするためのクラスを用意します。

class cnt_driver extends uvm_driver # (cnt_item);
    virtual cnt_if vif;
    `uvm_component_utils(cnt_driver);

    function new(string name , uvm_component parent);
        super.new(name, parent);
    endfunction: new
    extern virtual function void connect_phase(uvm_phase phase);
    extern virtual task run_phase(uvm_phase phase);
    extern virtual protected task drive_transfer(cnt_item item);
endclass: cnt_driver

function void cnt_driver::connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    if (!uvm_config_db#(virtual cnt_if)::get(this, get_full_name(), "vif", vif))
        `uvm_error("NOVIF", "Failed to getting virtual interface")
endfunction : connect_phase

task cnt_driver::run_phase(uvm_phase phase);
    forever begin
        seq_item_port.get_next_item(req);
        drive_transfer(req);
        seq_item_port.item_done(req);
    end
endtask : run_phase

task cnt_driver::drive_transfer (input cnt_item item);
    wait(vif.rst_n);
    repeat(item.cnt) begin
        repeat(2) @(posedge vif.clk);
        vif.en <= 1;
    end
    vif.en <= 0;
    repeat(2) @(posedge vif.clk);
endtask : drive_transfer 

taskやfunctionの実装をクラス外に出している理由は、 Cadenceの本で推奨していたからです。推奨というか、Cadenceがそういう文化なのだと思います。 ソースコードで重要なのは、run_phasedrive_transferのタスク ぐらいです。これらのタスクで先ほど定義したインタフェースを駆動しています。
最後におまけ。検証対象をSystem Verilogで書いた場合は、interfaceをそのまま使えます。しかしVerilog HDLやVHDLの場合には、intefaceの概念がないので一工夫必要です。今回はカウンタをVerilog HDLで書いているので、下みたいにラッパークラスを定義してあげます。

module cnt_wrapper (
    cnt_if v_if
);
    counter inst_counter(
        .clk    (v_if.clk),
        .rst_n  (v_if.rst_n),
        .en_i   (v_if.en),
        .cnt_o  (v_if.cnt));
endmodule

テスト環境の作成(cnt_env.svh)

ここまでトランザクションの定義に始まり、トランザクションのシーケンス、検証対象のインタフェース、トランザクションをデジタル信号に変換するドライバという流れで見てきました。これらを組み合わせることで、トランザクションを元にして、特定のインタフェースを駆動するための環境を定義できます。以下のようなソースコードになります。

class cnt_env extends uvm_env;
    `uvm_component_utils(cnt_env)
    
    cnt_driver drv;
    uvm_sequencer #(cnt_item) seqr;
    cnt_sequence seq;
    
    function new (string name, uvm_component parent);
       super.new(name,parent);
    endfunction : new;
 
    function void build_phase(uvm_phase phase);
       drv  = cnt_driver::type_id::create("drv",this);
       seqr = new("seqr",this);
       seq  = cnt_sequence::type_id::create("seq",this);
    endfunction
    
    function void connect_phase(uvm_phase phase);
       drv.seq_item_port.connect(seqr.seq_item_export);
    endfunction : connect_phase
    
    task run_phase(uvm_phase phase);
       phase.raise_objection(this);
       seq.start(seqr);
       phase.drop_objection(this);
    endtask : run_phase
 endclass : cnt_env

これにてカウンタをテストするための環境ができました! 本当はインタフェースを駆動するためのエージェントを定義して、テスト環境でエージェントを実体化するのがよいみたいですが・・・今回は他にインタフェースがないので省略です。なにより面倒ですw
ソースコード中にはuvm_sequencerという文字もあります。本記事の最初の方で軽く触れた、シーケンサ(Sequencer)のことです。シーケンスをドライバに転送する役割を担います。扱うシーケンスの数が増えてくると、独自のシーケンサを定義した方がいいみたいです。今回はシーケンスが1つしかないので、標準的なシーケンサを使いました。

テストケースの作成(cnt_test.svh)

最後のひと踏ん張りです! テストケースを作ります。UVMには下位コンポーネントに対するコンフィギュレーションの仕組みがあるのですが、その仕組みを利用して、先ほど作成したテスト環境の条件を振ることができます。一つの環境を作ってしまえば、テストケースを何個も作ることができるのです。今回は特に条件を振るほどのことはないので、一つのテストケースだけ作ります。

class cnt_test extends uvm_test;
    `uvm_component_utils(cnt_test)
    cnt_env env;
    
    function new(string name, uvm_component parent);
       super.new(name, parent);
    endfunction : new
 
    virtual function void build_phase(uvm_phase phase);
       super.build_phase(phase);
       env = cnt_env::type_id::create("env", this);
    endfunction : build_phase
 endclass : cnt_test

やっていることは、先ほど作ったテスト環境の実体化だけですね。他、派生元クラスがuvm_testなことにも注意です。

テストベンチの作成(top.sv)

ようやく見知った形に来ました。いわゆるテストベンチです。検証対象のカウンタとインタフェースの実体化、インタフェースをUVMのテスト環境とつなげてあげます。

import uvm_pkg::*;
import cnt_pkg::*;

module top;
   logic clk = 1'b0;
   logic rst_n;

   cnt_if v_if(
       .clk (clk),
       .rst_n (rst_n));
   cnt_wrapper DUT(v_if);

   always begin
        #10 clk <= ~clk;
   end

   initial begin
     rst_n <= 1'b0;
     repeat(2) @(posedge clk);
     rst_n <= 1'b1;
   end

   initial begin
          uvm_config_db#(virtual cnt_if)::set(null, "*", "vif", v_if);
          run_test("cnt_test");
   end     
      
endmodule // top

上の方にcnt_pkgとあるのは、これまで作ったクラスをパッケージにしているものです。Mentorのお知恵をお借りしています。

package cnt_pkg;
    import uvm_pkg::*;
    
    `include "uvm_macros.svh"
    `include "cnt_item.svh" 
    `include "cnt_sequence.svh" 
    `include "cnt_driver.svh"
    `include "cnt_env.svh"
    `include "cnt_test.svh"   
 
 endpackage // cnt_pkg

シミュレーションの実行

それではシミュレーションを回してみます。使うシミュレータは、XilinxのVivadoに内蔵しているシミュレータです。なぜなら無償かつrandomizeに対応しているから。同じく無償のシミュレータにはModelsim Intel Starter Editionがあるのですが、randomizeが使えません。そこの部分をカットしないとコンパイルは通らないと思います。シミュレーション波形は下になります。

f:id:yukirunrun:20200123235518p:plain

無事、カウンタを駆動できています! 現状では全然UVMのメリットが感じられませんが、とりあえず動いたことが重要です。次回はこの環境もう少し改善してみます。

EDA PlaygroundでUVMを試す

UVMについて

久しぶりの更新になります!前まではBLE Meshを書いていたのですが・・・しばらく中止です。最近はUVMの調査にはまっていますw
UVMとはUniversal Verification Methodologyの略です。私自身、UVMを使って業務をしているわけではないので、以降は私の理解になりますが・・・
UVMは、ASICやFPGA開発におけるHDLの検証用ライブラリです。歴史としては、半導体の微細化が進むにつれて、HDLの検証技術の大切さが増してきたと。で、検証ツールのベンダ(Synopsys、Mentor、Cadenceとか)が各々好きな検証環境を構築していくのですが・・・ユーザとしては、統一感がなくてたまったもんじゃない。紆余曲折あって、いろいろあった検証環境は統合されていき、ようやく生まれたのがUVMです。そんな訳で、UVMは検証ツールベンダの知識の結晶なわけですから、有用なことは間違いないでしょう。ついでに、いらない機能もたくさんあります(政治的な理由で)。私の理解ではだいたいこんな感じ。間違っていたらすいません。

EDA Playgroundとの出会い

さてUVMには一つ大きな問題があります。
UVMの実態はSystem Verilogで書かれたソースコード群であり、オープンソースなので無料で使えます。わーすごい!
しかし、UVMをまともにコンパイルするためのツールは有償になります。しかもツールの値段がべらぼうに高い。1千万というオーダになるんじゃないでしょうか。酷い商売ですね・・・。
私みたいに個人の学習者には、UVMは試すことすらハードルがめちゃくちゃ高いのです。

そんな時に見つけたのが、EDA PlaygroundというWebサービスになります。説明は以下のブログが一番親切かな。
uzusayuu.hatenadiary.jp

このEDA Playgroundの何がありがたいかというと、以下の有償シミュレータが無料で使えちゃうのです。
・Synopsys社 VCS
・Cadence社 Incisive
・Aldec社 Rivera Pro
このうちVCSとIncisiveはプロファイルの認証が必要です。
Rivera Proは特に手続きは不要らしいです。ただしライセンスエラーが何かで、実質的には使えません。海外のフォーラムを覗いてもそんな感じだし・・・どういうことよw

てわけでUVMを試すには、プロファイルの認証を行います。
認証にあたっては、ライセンス条項への同意が必要です。まぁ、たいしたことは書かれていません。EDA Playground上のシミュレータを商用に使うな、シミュレータの性能テストをするな、て感じですかね。UVMの学習をする分には、だまってクリックでOKだと思います。認証が通れば、適当にサンプルプロジェクトを試してみましょう。

f:id:yukirunrun:20200106204844p:plain

サンプルはどれでもいいですが、とりあえずUVM Hello Worldを選びます。
サンプルプロジェクトが開いたら、Testbench + DesignをSystemVerilog/Verilogに。UVM/OVMをUVM1.2に。Tools&SimulatorsにSynopsys VCSを指定。
そのあとRunボタンを押しましょう。

f:id:yukirunrun:20200106205303p:plain

すると、なんかログにいろいろでてきますね。
ログの最後の方、以下みたいなメッセージがでていれば成功です。

--- UVM Report Summary ---

** Report counts by severity
UVM_INFO :   11
UVM_WARNING :    1
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[]     1
[DUT]     8
[RNTST]     1
[TEST_DONE]     1
[UVM/RELNOTES]     1

いや、ほんとすごい! RandomizeやらCoverageは、もはや諦めかけてたもんなー。これで試せるようになります!

Bluetooth meshを試してみたい part5

前回までの話

 BLE meshを試すべく、Laird社のBLEモジュール BL654(USBドングル版)を購入した・・・というのは昔の話。何もすることなく、三ヶ月ほどの時が過ぎていた。さすがにこのままでは不味いということで一年発起、ようやくBLE meshの第一歩 プロビジョニング を完了させたのだった。

エンベデッドシステムスペシャリスト試験 結果報告

 前回の記事で、エンベデッドシステムスペシャリスト試験(以下ES)を受験したことに触れました。なのでBLE meshとはなんの関係もないのですが、結果報告はしておきます。
結果は、
 ・
 ・
 ・
 ・
 合格でした!!ホントに嬉しい!
 点数は午前1:免除、午前2:88、午後1:93、午後2:73でした。午後1の点数が良すぎですね。勉強し過ぎたかもしれません。 過去問数年分解いたのですが、わりと不毛でした。
 ESも取れたことだし、これからは堂々と組み込み系のスペシャリストを名乗っていこうと思います。
 ・・・というのは流石に冗談ですw 仕事では知識不足なことが日常茶飯事で、まだまだ一人前には遠い感じがしています。何ができるようになったら一人前か、と問われると難しいところなんですが。しかし、こうしてES合格という客観的な指標が得られたことは、大きな自信に繋がります。今回の合格で満足するのでなく、これからも精進していきたいですね。

プロビジョニングの詳細

 さて、本題のBLE meshの話になります。今回はプロビジョニングの詳細編、パケットレベルの話を書きたいと思います。
 

スニファ

 パケットレベルの解析といえば、スニファ(Snifferの出番です! スニファとは名前のイメージ通り、空間を飛び交うBLEのパケットを嗅ぎ回して(Sniff)、見つけたパケットをパソコンに流すためのツールです。ポイントは、スニファ自体はBLEのパケットは送信しないし、受信相手でもないことです。要は他所様の通信を盗聴しているだけです。 盗聴というと人聞き悪いですが、別に悪いことをしている訳ではなく、れっきとした開発ツールです。逆に言えば、スニファみたいなことが簡単にできるからこそ、無線通信は暗号化に力を入れているわけですし。
 スニファとして有名なのは、Nordic製のnRF Snifferになります。幸運なことに、千石電商で以下の製品が売っていたので、購入してみました。
 ・Adafruit製 ADA-2269 nRF51822搭載 Bluefruit LE Sniffer
www.sengoku.co.jp

 念のため、nRF SnifferはNordic製ASICおよびFWのことで、Bluefruit LE SnifferはnRF Snifferを搭載した電子基盤のことです。

 製品を購入したらマニュアルに沿ってセットアップしましょう。マニュアルはNordicの公式ページから得られますし、他のブログにも解説記事が載っています。このブログではこれ以上触れるつもりはありません。書くのが面倒というのもありますが、何よりも、


 あまり役に立たなかったからです。


 確かにそこそこは有用だったのですが・・・。肝心のBLE meshに関連したパケットをあまり見ることができませんでした。より具体的には、Advertisingとコネクション開始時のパケットは見れるのに、コネクション確立後のパケットが全然見れないのです。私のセットアップや環境が悪いのかと疑いもしたのですが、ネット上の情報をかき集める感じ、購入した製品に問題がありそうな気がしています。はっきりと言及はされていないのですが・・・。
 てことで、スニファを使えばBLE meshの通信が簡単に見れるじゃん!という目論見は淡くも崩れ落ちました。(もしくは、nRF52832ベースのスニファを買うといいのかも)

追記(2020/6/14)
理由がわかりました。プロビジョナ&BL654はBluetooth5の2M PHY で通信していたせいです。一方で、nRF51822は2M PHY に対応していないため、自ずとスニファも2M PHYのパケットを追えなかったようです。
1M PHYで通信するプロビジョナであれば、スニファで追うことができました。

マニュアルでプロビジョニングに挑戦!

 スニファのあてが外れたので、電文を自分で考えてプロビジョニングに挑戦してみます。ベアラにはGATTを利用することにします。また、簡単のためOOBによる入出力はなしとします。
 プロビジョナにはiPhone8を使用し、アプリはLightBlueというBLEの汎用アプリを使います。BLE meshの機能はないので、BLE meshの仕様に沿った電文はマニュアルで打つ必要があります。
 最初に断っておくと、プロビジョニングの途中までしかやりません。最後までやると暗号化の計算が必要になるので、さすがにやってられませんw 目的がだいたいの雰囲気をつかむことなので、途中までやれば十分というのもあります。

 さて、Mesh Profile Bluetooth Specificationによると、プロビジョニングにおけるプロビジョナとデバイスの通信の流れは以下のようになります。

f:id:yukirunrun:20190623184641p:plain

 「1、Unprovisioned Device Beacon」はAdvertisingで行われます。一般的なBLEと同じですね。GATTベアラでプロビジョニングを行う場合、このUnprovisioned Device Beaconの情報を元に、デバイスとコネクションを張ります。Advertisingベアラでプロビジョニングを行う場合は、コネクションは張りません。以下、GATTベアラの場合で話を進めます。
 iPhone上のアプリ、LightBlueでは次のようにBLE meshデバイスが見えます。タップして、コネクションを張ります。

f:id:yukirunrun:20190623192104j:plain:w300

 コネクション後、キャラクテリスティックの一覧が見えます。
 ・上にある0x2ADBはデータを送信するために使います。
 ・下にある0x2ADCはデータを受信するために使います。

f:id:yukirunrun:20190623192350p:plain:w300

 まずは0x2ADCを開いてNotifyを許可します。Notifyを許可すると、デバイスからデータを受信できるようになります。次に0x2ADBを開いて以下の値を送信します。
 ・0x030005
 するとデバイスから何かしらのデータを受信します。
f:id:yukirunrun:20190623192837j:plain:w300

 実は今送信したデータが「2、Provisioning Invite」、受信したデータが「3、Provisioning Capabilities」に相当します。データの意味を説明します。まず送受信データの先頭は両方とも0x03ですが、これはProxy Protocol(メッシュ仕様の6章に書いてあります)のProxy PDUの内容です。GATTベアラからデータの送受信をするときは、Proxy Protocolを経由するらしいです。次の1byteはProvisioning PDUのデータタイプを意味し、0x00がProvisioning Invite、0x01がProvisioning Capabilitiesとなります。それ以降の意味は仕様書を参照ください。

 次に「4、Provisioning Start」のための電文を送信します。
 ・0x03020000000000
 2byte目の0x02がProvisioning Startを指します。当然のようにデバイスからは反応がないので、続けて「5、Provisioning Public Key」のための電文を送信します。
0x03030A18364B1F3AFFBFE58D000C81CFBA5735C1F324(下に続く)
5F811A0094D369B10486037B511272EACDF6EA1EEEC354(下に続く)
8C6DF8A3B2C90413CCC13F6E7A39459130FDB2118E
 ・・・やたら長いですね。全部で68byteあるのですが、そのうち64byteは暗号化に利用する数値です。送信後、デバイスから同様に長い電文が返ります。「6、Provisioning Public Key」に相当します。2byte目の0x03がProvisioning Public Keyを指します。

f:id:yukirunrun:20190623195132j:plain:w300

 ここから先「7、Provisioning Confirmation」には暗号化の計算が必要になります。めんどうなのでこれ以上はやりませんw

以上、マニュアルでのプロビジョニングでした! 次はプロビジョニングから進んだ段階を試してみたいところですな。




Bluetooth meshを試してみたい part4

前回までの話

 BLE meshを試すべく、Laird社のBLEモジュール BL654(USBドングル版)を購入した。まずはセットアップに取り組むも、これが非常に手間のかかる作業であった。苦労の末にBL654をBLE meshデバイスとして動作させることに成功、ついにBLE meshの世界に踏み入れる時がきた!(part3までBLE meshの話はほとんどなし)

久々の更新

 約3月ぶりの更新になるでしょうか。なんというか、BLE meshなんかやっている場合じゃねーぞ!って状態でした。仕事でBLE meshに携わっている訳でもないし、エンベデッドスペシャリストの受験もあったし、誰かブログを見ている訳でもないし等々、どうやってモチベーション上げればいいんだって感じですw
 特にエンベデッドスペシャリスト。去年は落ちてしまったので、今年こそは絶対に受かりたいんです! 試験が終わって1週間は経つんですけど、結果が気になって、頭から離れんのです。名前を書き忘れていたことに気づいて、受験に落ちたことを悟った夢も見ました。ホントに夢でよかったです。正夢じゃないといいんですが・・・。こういうのを一日千秋の想いというのでしょうか? 他のことを考えている余裕なんてないんです!
 そんな感じで放置していたところ、意外とBLE mesh系の記事がアクセスされていることに気づきました。誰もアクセスしない記事だと思っていたので、嬉しいものですね。 やる気も少し出たことだし、久々に更新してみたいと思います。

プロビジョニング

 前回までの記事で、BL654をBLE meshデバイスとして動作するように設定を行いました。今回はBL654をメッシュネットワークに参加させる作業、プロビジョニング(Provisioning)を行おうと思います。
 簡単にプロビジョニングとは何かを説明しますと、メッシュネットワークを構成するノードを登録する作業のことです。メッシュネットワークに登録されているBLE meshデバイスのことをノードと呼びます。逆に、登録されていないデバイスをUnprovisioned Deviceと呼びます。プロビジョニングを行うことで、BLE meshデバイスは「Unprovisioned Device」から「meshネットワークのノード」へと変換される訳です。
 プロビジョニングを行うデバイスのことを、プロビジョナ(Provisioner)と呼びます。プロビジョナはスマホタブレットが担当することが多いようです。

 詳細な説明はBluetooth SIGが発行している仕様書を参考くださいな。
www.bluetooth.com
 特に Mesh Profile Specification を読むことを推奨します。私はリビジョン1.0.1を落としました。内容はまだ少ししか読んでいませんが・・・これを好んで読もうなんて人は、狂人か相当な暇人だと思いますw とはいえBLE meshは現状ではマイナー技術のようで、あまり解説記事が豊富ではありません。意外と仕様書を読むのが効率的と思います。とりあえず2章 mesh system architecture、5章Provisioningを読んでおけば十分だと思います。一応ですが、BLE meshを理解するには、前提としてBLEそのものへの理解も当然必要です。

実機(BL654)でテスト

実機でプロビジョニングを試したいと思います。環境は以下の通り。

・プロビジョナ:iPhone8
・プロビジョニング用アプリ:nRF Mesh
・BLE meshデバイス:BL654(Server, Client)
・BLE mesh用スニファ:BL654(Sniffer
・BL654用コンソールプログラム:Uw TerminalX

nRF MeshはNordic Semiconductorが出しているスマホ用アプリです。Apple Store等からダウンロードできます。

 まずはBL654のServer, Client、Sniffer全ての電源を入れます。PCとシリアル通信するために(ログを見るだけなので必須ではないですが)、USBポートに挿しておきます。
f:id:yukirunrun:20190505005834j:plain

 電源投入後、ServerとClientはUnprovisioned Deviceであることをアドバタイズし始めます。Uw TerminalXでSnifferのポートを開くと、その様子を確認できます。

f:id:yukirunrun:20190131232624p:plain

 画面内の1行が、1つのパケットに対応しています。
 2列目には、PB-ADVあるいはPB-GATTの文字が見えます。これはプロビジョニングを実現するための方法を示しています。より厳密には、provisioning bearerの種類を示しています。
 PB-ADVではアドバイタジングを利用してプロビジョニングを行います。Bluetooth SIGのHPを見ると、AD Type の0x29にPB-ADVが割り当てられていることが確認できます。一般的なBLE MeshデバイスはPB-ADVに対応していることが望ましいようです。
 PB-GATTGATTを利用してプロビジョニングを行います。専用のAD Typeは用意されていませんが、UUIDの0x1827にMesh Provisioning Serviceが割り当てられています。レガシーなスマホでもプロビジョニングを行えるようにするため、このような仕組みが用意されているようです。
 私のiPhone8はレガシーなスマホだったので、PB-GATTを利用してプロビジョニングを行います。というかPB-ADVに対応したスマホって存在すんの・・・?
 プロビジョニングを進めるために、iPhone8でnRF Meshを起動して、Scannerタブを開きます。

f:id:yukirunrun:20190211191818p:plain:w300

 リストにはPB-GATTに対応したアドバタイジングをしているデバイスの一覧が表示されています。まずはLS P-SERVERを選びます。

f:id:yukirunrun:20190505121421p:plain:w300

 ノードセッティング画面に遷移しました。デバイスのmeshネットワーク内における名称、Unicast AddressAppKeyを設定できるようです。
 名称はアプリの機能でしょうか? メッシュ仕様には該当する記述が見つかりません。
 Unicast Addressは、ここではノード内のprimary elementのUnicast Addressを指します。ノード内のsecondary elementは逐次的にアドレスが割り振られれます。メッシュネットワークの通信はエレメント単位で行われるので、ノードではなく、エレメントにUnicast Addressが割り振られます。
 AppKeyは、エレメント間の通信データを暗号化するための数字列です。
 続いて、右上のIdentifyを押します。

f:id:yukirunrun:20190505121601p:plain:w300

 画面下にIDENTIFICATIONのリストが追加されました。ここで書かれている内容は大雑把に、
 ・ノード内のエレメントの数
 ・暗号化の方式
 ・対応している認証方法(OOB)
になります。OOBはOut Of Bandの略で、通信帯域幅の外、つまりはBluetooth以外の入出力手段という意味です。詳細はMesh Profile SpecificationのProvisioning Capabilitiesに書かれています。
 今回のLS P-SERVERに関してわかることは、エレメント数は1つで、暗号化にはFIPS P-256 Elliptic Curveアルゴリズムを使い、OOBの手段はないということです。OOBの手段は特にないので、右上のProvisionを押すだけで、プロビジョニングが開始されます。

f:id:yukirunrun:20190505121730p:plain:w300

 しばらく時間経過すると、プロビジョニングが完了し、上の画面に遷移します。メッシュネットワークにLS P-SERVERが追加されたことが確認できます。同様の手続きをLS P-CLIENTにも実施した結果が以下になります。

f:id:yukirunrun:20190505121756p:plain:w300

今回はここまで

 以上でプロビジョニングが完了しました。実はプロビジョニングが完了したからといって、何ができる訳でもありませんw メッシュネットワークに2つのノードが登録されただけなのです。
 メッシュネットワーク内のノード同士が通信を始めるには、さらに設定が必要になるのですが、長くなりそうなので一旦区切ります。
 次回はプロビジョニングの補足について書こうと思います。

Bluetooth meshを試してみたい part3

前回までの話

Bluetooth Meshを試すべく、Laird社のBLEモジュール BL654(USBドングル版)を入手した。マニュアル通りにセットアップすればBluetooth Meshデバイスとして動くだろう、そう思っていた矢先、一つの壁にぶち当たる。ハードウエアの壁である。なんとUSBドングル版はLairdの推奨デバイスではなかったのである。 しかしここで諦める訳にはいかない。苦労の末にハードウエアの問題を解決し、Bluetooth meshデバイスとして動作させることに成功したのだった!

スマホアプリ(nRF mesh)の紹介

前回、BL654にBLE mesh用のプログラムをダウンロードしました。これで電源さえ投入すればBLE meshデバイスとして動作します。
次に行うのは、プロビジョニングという操作になります。BLE meshデバイスをmeshネットワークへ参加させるためには、プロビジョニングによってネットワークの構成要素であることを設定しなくてはいけません。プロビジョニングを実施するデバイスプロビジョナーと呼び、たいていの場合はスマホタブレットが担当するようです。
今回はLairdのアプリケーションノートに則り、iPhonenRF Meshインストールし、これをプロビジョナーとして利用します。nRF MeshはNordic社が出しているアプリでApple Storeから入手できます。もちろんAndroid版もあります。ServerとClient担当のBL654の電源を入れると、nRF Meshで以下のような画面が表示されます。
f:id:yukirunrun:20190211191818p:plain:h500
BLE meshデバイスとしてBL654を認識できているようです!
ここからプロビジョニングを進めていきたいのですがそれは次回の話に。というのも、あの問題に再び直面したのです・・・

ハードウエアの壁 再び

プロビジョニング関連でいろいろ試していると、あることに気づきました。

「autorunから抜け出すことができない・・・!」

LairdのSmartBasicには、起動と同時にプログラムを実行するautorunという仕組みがあるのですが、BL654がautorunから抜け出せなくなっていました。おそらくFirmwareアップグレードの影響と思います(「Bluetooth meshを試してみたい part1 」の記事参照)。autorunから抜け出せないと、データリセットやプログラムの更新ができません。私はプロビジョニングを通してBL654にいろいろ変なデータを設定したせいで、リセットの必要に迫られました。しかしautorunから抜け出せないのでリセットができません。つまり、BL654がいつの間にかただのゴミになっていたのです。

もちろん本来使用すべきDevkitでしたら、アプリケーションノートの指示に従ってautorunから抜け出せるでしょう。しかしUSBドングルでは同じ手順でやっても無理です。何十回も試したので断言します。ついでにUwTerminalXの「BL654 USB Dongle - Exit autorun」ボタンを押しても無意味です。これまた何十回も試したので断言します。

ふざけんなよーーーー!!!(自業自得)
お金返せよ・・・(自業自得)

FT232RのBit Bang Mode

やってられねー、とBL654を放置すること1週間。ふとあることを思いつきます。
「USBドングルでdevkitと同じ状態を再現したらよいのでは・・・?」
ソフトウエア的にではなく、ハードウエア(電気回路)的にです。その観点で調べてみると、次のことがわかりました。

 USBドングル:nAutoRun信号はFT232Rと接続している
 Devkit:nAutoRun信号はジャンクションで選択できる

nAutorunはBL654の機能ピンの一つで、0が入力されている時、電源投入と同時にSmartBasicプログラムが動きます。つまり「FT232RからnAutorun信号を制御することで、USBドングルはautorunから抜け出せる」と推測できます。というわけで、そこらへんも含めて回路図を起こしてみたのが下です。

f:id:yukirunrun:20190218204922p:plain


ここで、FT232Rは一般的なCOM通信のモードではなく、Bitbang Modeで動かします。で、Bitbang Modeとは何か? 私はよく知りません。 なんならさっきその存在を知りましたw 調べてみるといろいろ解説ブログが出てくるので、そちらを参考くださいな。

jsdiy.web.fc2.com

FT232RをBitbang Modeで動かすためのプログラムをC#で書くことにします。C#用のdllファイル(FTD2XX_NET.dll)、サンプルプロジェクトはFTDIのサイトから入手できます。個人的にはExample 3が参考になりました。
https://www.ftdichip.com/Support/SoftwareExamples/CodeExamples/CSharp.htm


C#でコンソールアプリケーションのソリョーションを作成します。参照にFTD2XX_NET.dllを加えます。そしたら、以下のソースコードでSmartBasicから抜け出せると思います!

using System;
using System.Threading;
using FTD2XX_NET;

namespace EEPROM
{
    class Program
    {
        static void Main(string[] args)
        {
            UInt32 ftdiDeviceCount = 0;
            FTDI.FT_STATUS ftStatus = FTDI.FT_STATUS.FT_OK;
            FTDI myFtdiDevice = new FTDI();
            ftStatus = myFtdiDevice.GetNumberOfDevices(ref ftdiDeviceCount);
            if (ftStatus != FTDI.FT_STATUS.FT_OK)
                return;
            if (ftdiDeviceCount == 0)
                return;

            FTDI.FT_DEVICE_INFO_NODE[] ftdiDeviceList = new FTDI.FT_DEVICE_INFO_NODE[ftdiDeviceCount];
            ftStatus = myFtdiDevice.GetDeviceList(ftdiDeviceList);
            ftStatus = myFtdiDevice.OpenBySerialNumber(ftdiDeviceList[0].SerialNumber);
            if (ftStatus != FTDI.FT_STATUS.FT_OK)
                return;

            if (ftdiDeviceList[0].Type == FTDI.FT_DEVICE.FT_DEVICE_232R)
            {
                Console.WriteLine("Exiting autorun mode");
                //RI(bit7) & DCD(bit6) & DTR(bit4) & RXD(bit1) as output 
                myFtdiDevice.SetBitMode(0xD2, FTDI.FT_BIT_MODES.FT_BIT_MODE_ASYNC_BITBANG);
                myFtdiDevice.SetBaudRate(1200);
                UInt32 writtenLength = 0;
                byte[] w1 = { 0x52 }; //RI = low, DCD = high, DTR = high, RXD = high
                byte[] w2 = { 0x12 }; //RI = low, DCD = low,  DTR = high, RXD = high
                byte[] w3 = { 0x52 }; //RI = low, DCD = high, DTR = high, RXD = high
                byte[] w4 = { 0x42 }; //RI = low, DCD = high, DTR = low,  RXD = high
                byte[] w5 = { 0x40 }; //RI = low, DCD = high, DTR = low,  RXD = low
                byte[] w6 = { 0x42 }; //RI = low, DCD = high, DTR = low,  RXD = high
                byte[][] wbuf = { w1, w2, w3, w4, w5, w6 };
                foreach(var buf in wbuf)
                {
                    myFtdiDevice.Write(buf, buf.Length, ref writtenLength);
                    Console.WriteLine("length:" + writtenLength.ToString());
                    Thread.Sleep(500);
                }
                myFtdiDevice.SetBitMode(0x00, FTDI.FT_BIT_MODES.FT_BIT_MODE_RESET);
            }
            myFtdiDevice.Close();
 
            Console.WriteLine("Press any key to continue.");
            Console.ReadKey();
            return;
        }
    }
}

このプログラムを実行したら、FT232Rを通常のCOM通信モードで開きましょう。BL654USBドングルはコマンドモードに入っているはずです。めでたし、めでたし。
以上、今回の記事はこれで終わり。次回こそはBLE meshとしての世界に入っていきたいと思います!