VivadoのTclでTCPサーバーを利用する

Pocket

VivadoはTclスクリプトで操作できるというのは,Vivadoを普段から利用している人はよく知っていると思います.プロジェクト作成やビルドをスクリプト化している人も多いことでしょう.Tcl経由でFPGAにビットファイルを書き込んだり,ILAやVIOを叩くという使い方もできます.

そんな便利なVivadoのTclインタプリタですが,スクリプトの実行のたびにVivadoを起動すると起動時間の長さが気になってきます.というわけで,VivadoのTclインタプリタでコマンド実行用の簡単なTCPサーバーを作ってみました.

とりあえずの最終版は command_server.tcl です.

$ vivado -mode tcl -source ./command_server.tcl

として実行すると,16384番ポートでTCPサーバーが立ち上がって,Tclコマンドを受け付けます.

ちなみに,ここで紹介する方法では,TCP経由でなんでもできてしまいます.使用する場合には,自己責任で十分に注意して実行してください.

TclのTCPサーバーを使ってみる

TclにはTCPによるソケット通信を行なうライブラリが含まれています.このライブラリは,Vivadoに同梱されているTclインタプリタ(たとえば,2020.1と2023.1ではTclのバージョンは8.5だった)でも利用できます.

https://www.tcl.tk/about/netserver.html には,Tclのサンプルコードが公開されています.公開されている https://www.tcl.tk/about/netserver.txt を保存します.

VivadoをTclモードで起動して,ダウンロードしたTclスクリプトを source コマンドで,ロードしてみましょう.

$ vivado -mode tcl

****** Vivado v2023.1.1 (64-bit)
  **** SW Build 3900603 on Fri Jun 16 19:30:25 MDT 2023
  **** IP Build 3900379 on Sat Jun 17 05:28:05 MDT 2023
  **** SharedData Build 3899622 on Fri Jun 16 03:34:24 MDT 2023
    ** Copyright 1986-2022 Xilinx, Inc. All Rights Reserved.
    ** Copyright 2022-2023 Advanced Micro Devices, Inc. All Rights Reserved.

Vivado% source ./netserver.txt
...(ロードしたスクリプトが表示される)...
Vivado% 

これで準備は完了です.このロードしたスクリプトで定義してある Echo_Server プロシージャを呼び出してTCPサーバーを起動します.

Vivado% Echo_Server 16384

これで,16384番ポートでTCPサーバーが起動します.telnetコマンドを使ってメッセージを送ってみます.以下は,telnetで接続した後,Hello とメッセージを送ったところです.1つ目の Hello が送信したメッセージ,2つ目はサーバーから送り返されてきたメッセージです.

$ telnet localhost 16384 
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello
Hello

TclインタプリタにはTCP接続を受け付けた旨のメッセージが出力されます.

	Accept sock8 from 127.0.0.1 port 56316

telnetを切断すると,Tclインタプリタには切断のメッセージが出力され,次のコネクションを待ちます.

Close 127.0.0.1 56316

簡単ですね.

Tclインタプリタは,無限ループに入っていて簡単には終了させられません.killなどをつかって強制終了させてください.

TCPサーバーのコードを見てみる

TCPでVivadoのコマンドを受け付けられるように,TCPサーバーがどう記述されているか見てみましょう.サンプルスクリプトのTCPサーバーは,

proc Echo_Server {port} { ... }
proc EchoAccept {sock addr port} { ... }
proc Echo {sock} { ... }

の3つのプロシージャから構成されています.Echo_Server は,EchoAccept をソケットにバインドして無限ループに入ります.EchoAccept が,TCP接続を待ち受けるプロシージャで接続がくると,Echo プロシージャを呼び出します.実際にTCPでやりとりするデータのハンドリングは呼び出された,Echo で行われます.

Echo プロシージャは

proc Echo {sock} {
    global echo

    # Check end of file or abnormal connection drop,
    # then echo data back to the client.

    if {[eof $sock] || [catch {gets $sock line}]} {
	close $sock
	puts "Close $echo(addr,$sock)"
	unset echo(addr,$sock)
    } else {
	puts $sock $line
    }
}

となっています.送られてきた文字列を$lineに保存して,それを$sockに出力するという動作をしていることが分かります.

というわけで,この$lineをうまく使えば,TCP経由でVivadoにコマンド実行させることができそうです.

やってみる,前に

さて,サンプルのままでもいいのですが,サーバーを終了させるたびにVivadoを強制終了する,というのは,若干使い勝手が悪いです.まずは,特定の文字列がきたらサーバーが終了するようにしましょう.

変更したコードは terminate_example.tcl です.

Echoプロシージャを変更して,terminateという文字列が送られてきたらEcho_Serverを終了するようにしています.これで,Tclインタプリタに次のコマンドを送ることができるようになります.たとえば,exitコマンドでTclインタプリタを終了させることができます.

もちろん,vwait terminate の次の行に,exitコマンドを記述しておけば,サーバーを終了するとともにtclインタプリタを終了することができます.

シンプルなコマンドサーバー

送られてきた文字列をそのままコマンドとして実行してみることにしましょう.文字列をコマンドとして実行するには,evalを使うことができます.

実装したコードは,simple_server.tclです.スクリプトの最後のEcho_Server 16384で,コードがロードされるとすぐに16384番ポートでTCPサーバーが立ち上がります.

サーバーの起動

VivadoをTclモードで起動します.起動時の引数でTclスクリプトをロードしてしまいましょう.スクリプトのロードが終了するとサーバーを起動するプロシージャが呼び出されます.

$ vivado -mode tcl -source ./simple_server.tcl

****** Vivado v2023.1.1 (64-bit)
  **** SW Build 3900603 on Fri Jun 16 19:30:25 MDT 2023
  **** IP Build 3900379 on Sat Jun 17 05:28:05 MDT 2023
  **** SharedData Build 3899622 on Fri Jun 16 03:34:24 MDT 2023
    ** Copyright 1986-2022 Xilinx, Inc. All Rights Reserved.
    ** Copyright 2022-2023 Advanced Micro Devices, Inc. All Rights Reserved.

source ./simple_server.tcl
# proc Echo_Server {port} {
#     global terminate
...(ロードしたスクリプトが表示される)...
#     unset echo(addr,$sock)
# }
# Echo_Server 16384

クライアントの接続とコマンド実行

telnetを使ってVivadoのコマンドを実行してみます.たとえば,以下のように,telnetで接続して,tclversionの問い合わせをしてみることができます.

$ telnet localhost 16384
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
info tclversion
info tclversion
8.5

引数を持つコマンドも,Tclインタプリタでコマンドを実行する通り,空白をデリミタとした文字列をサーバーに送って実行することができます.

Vivadoの操作をしてみる

以下はArty A7-100が接続された状態でHW Manager関連のコマンドを実行してみたところです.

$ telnet localhost 16384 
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
open_hw_manager
open_hw_manager

connect_hw_server -allow_non_jtag
connect_hw_server -allow_non_jtag
localhost:3121
open_hw_target
open_hw_target

get_hw_devices
get_hw_devices
xc7a100t_0
close_hw_target
close_hw_target

disconnect_hw_server localhost:3121
disconnect_hw_server localhost:3121

close_hw_manager
close_hw_manager

ここで,同じ文字列が二行並んでいるのは,一行目が入力した文字列,二行目がサーバーからエコーバックされてきた文字列です.入力文字列のエコーバックに続いてコマンドの実行結果が出力されます.たとえば,get_hw_devicesの実行結果のxc7a100t_0が出力されています.

Vivado側には以下のように出力されます.

Vivado% Echo_Server 16384
Accept sock8 from 127.0.0.1 port 54810
open_hw_manager
connect_hw_server -allow_non_jtag
INFO: [Labtools 27-2285] Connecting to hw_server url TCP:localhost:3121
INFO: [Labtools 27-2222] Launching hw_server...
INFO: [Labtools 27-2221] Launch Output:

****** Xilinx hw_server v2023.1.1
  **** Build date : Jun 16 2023 at 19:39:31
    ** Copyright 1986-2022 Xilinx, Inc. All Rights Reserved.


INFO: [Labtools 27-3415] Connecting to cs_server url TCP:localhost:0
INFO: [Labtools 27-3417] Launching cs_server...
INFO: [Labtools 27-2221] Launch Output:


******** Xilinx cs_server v2023.1.0
  ****** Build date   : Jun 01 2023-03:31:41
    **** Build number : 2023.1.1685557901
      ** Copyright 2017-2022 Xilinx, Inc. All Rights Reserved.
      ** Copyright 2022-2024 Advanced Micro Devices, Inc. All Rights Reserved.



open_hw_target
INFO: [Labtoolstcl 44-466] Opening hw_target localhost:3121/xilinx_tcf/Digilent/210319AFEB96A
get_hw_devices
close_hw_target
INFO: [Labtoolstcl 44-464] Closing hw_target localhost:3121/xilinx_tcf/Digilent/210319AFEB96A
disconnect_hw_server localhost:3121
close_hw_manager

エラーハンドリングを付ける

このままでも使えないことはないのですが,コマンドが受理されないなどによりエラーが発生すると,そのTCPコネクションでは次のメッセージが受け付けられない,という問題があります.

ある程度機械的に使うのであれば問題ありませんが不便は不便です.というわけで,実用化に向けては,簡易なエラーハンドリング機構を追加する必要があるでしょう.

tclでは,catchコマンドでは,与えられたリストのコマンドをTclスクリプトで実行しエラーの有無を返すコマンドです.エラーがあれば1を,なければ0を返します.

というわけで,evalでのエラーハンドリングを行なうバージョンのコードは,command_server.tcl のように実装できます.

Echoプロシージャの,[ catch { set result [ eval $line ] } ]の部分がコマンド実行部分です.実行時にエラーがなければ,ifが成立せずに,コマンドの結果$result$sock すなわちクライアントに送り返されます.一方で,エラーがあれば,eval errorという文字列が返されます.

proc Echo {sock} {
    global echo
    global terminate
    
    # Check end of file or abnormal connection drop,
    # then echo data back to the client.

    if {[eof $sock] || [catch {gets $sock line}]} {
	    CloseSocket $sock
    } else {
        puts $sock $line
        if {$line eq "terminate"} {
	        CloseSocket $sock
            set terminate 1
        } else {
	        puts $line
	        if { [ catch { set result [ eval $line ] } ] } {
	            puts $sock "eval error"
            } else {
	            puts $sock $result
            }
	    }
    }
}

たとえば,こんな感じです.

$ telnet localhost 16384
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
info tcl_version
info tcl_version
eval error
info tclversion
info tclversion
8.5
1 + 3
1 + 3
eval error
expr 1 + 3
expr 1 + 3
4

最初の info tcl_version はエラーですが,次のinfo tclversion では正しくTclインタプリタのバージョンが返ってきています.また,足し算も expr がないときにはエラーで,expr があるときには計算結果が返されます.

まとめ

というわけで,Tclのsocketを使ったVivadoのコマンドサーバーを作ってみました.Vivadoの外からVivadoを操作する,特に別の言語でVivadoを操作する場合には便利かもしれません.

ところで,このスクリプトは,evalでなんでもかんでも実行してしまいます.そのため,大変危険なこともできてしまいます.利用する際には十分に気をつける必要があること,注意が必要です.