はじめに
Xillybus入門記事2本目です.前回はPCから送信されてきたデータをそのままループバックしてPCに戻す,といったチュートリアルをやってみました.
今回は,Vivado HLSで高位合成した回路をXillybusに接続してPCから送信したデータにFPGAで処理を加えてPCに戻す回路を作成してみます.具体的にはPCからグレースケール画像を送信し,FPGA側で白黒反転して,ネガ画像を作成してPCに戻します.
HLSで作成した回路をXillybusに接続し,使用する方法については,公式ドキュメント,"The guide to Xillybus Block Design Flow for non-HDL users"を参考にしています.Xillybusのサイトで公開されています.
前回の記事はこちらです.
PCIe版Xillybus入門(1):デモを動かしてPCと通信してみた
環境
使用するFPGAボードはZynqではなく,Kintex7搭載のPCIe接続のものです.
- FPGA: Kintex7評価ボードKC705, PCIe接続
- Hostマシン:Ubuntu 19.04
- Vivado 2018.3
- Vivado HLS 2018.3
全体像
PCからグレースケール画像を入力すると白黒反転したネガ画像を返してくれる回路を作成しました.図にすると以下のような感じです.
構成する主なパーツは以下のとおりです.
- xillybus IP core: xillybusの本体
- xillybus_wrapper: HLSで自作した回路
- host側のプログラム
図に示す,xillybus_wrapperなるモジュールがHLSで自作した回路です.xillybus coreとはAXI-Streamで接続されます.
FPGA部分の設計と作製
まずは,FPGA部分の回路設計~FPGAへの回路書き込みまでを説明します.
概要
今回作製するのは,グレースケール画像を入れると白黒つまりポジネガ反転した画像を出力するものです.
回路としてはPCからピクセル値が一つずつシリアルに送られてくるのでそれを順番に計算してネガポジ反転します.
具体的にどうするかというと,画像のピクセル値が8bit階調ですので,入力値をx,出力値をyとするとただ以下の計算をするだけです.
y = 255 - x
これで,黒=0は255に,白=255は0になりますね.
このモジュールに求められる機能は,
- xillybusからデータを取得する
- 上記の式を計算する
- 計算結果をxillybusに送信する
の3つです.xillybusからのデータ取得と送信はxillybusのドキュメントを参考にしています.
HLSによる回路のプログラミング
作成したHLSプログラムは以下のとおりです.至ってシンプルです.
vivado HLSで新規にプロジェクトを作成し,以下のcppファイルをプロジェクトに追加します.
#include "ap_int.h"
// 実際にネガポジ反転の計算をしている関数
// ap_uint<8>として引数や戻り値を8bit符号なし整数とした
// これはxillybusの8bitのポートを使用するのと画像値が8bit階調であることによる
ap_uint<8> wb_conv(ap_uint<8> in){
ap_uint<8> result = 255 - in;
return result;
}
// 高位合成のTop Function.
// *data_inと*data_outがそれぞれxillybus Coreの入出力ポートに接続される
// 各pragmaでブロックレベルとポートレベルのI/Fを指定,データポートはAXI-Streamである
void xillybus_wrapper(volatile ap_uint<8> *data_in, volatile ap_uint<8> *data_out){
#pragma HLS INTERFACE ap_ctrl_none port=return
#pragma HLS INTERFACE axis register both port=data_in
#pragma HLS INTERFACE axis register both port=data_out
// I/F read
ap_uint<8> x = *data_in++;
// My process
ap_uint<8> result = wb_conv(x);
// I/F write
*data_out++ = result;
}
プログラムを記述したあとのVivado HLSのスクショです.
構成としては,wb_conv関数が実際に実行したい処理を記述した関数です.
一方,xillybus_wrapperはxillybusとの通信を担当し,xillybusからデータを受け取りそれをデータ処理を担当する関数(この場合wb_conv)に与えます.その後,関数から返ってきた値をxillybusに書き込む処理を行っています.
xillybusの読み込みと書き出しは,ポインタの移動によって表現されています.
これはC++的な意味合いのポインタの移動ではなく,xillybus Coreの受信/送信バッファへのデータの入出力を表しています.FIFOのようになっており,一つ読み込んだら次のデータがやってくるようなかんじです.また送信では,データを書き込むと直ちにPCへの転送処理が実行されます.
xillybus_wrapperが論理合成対象の最上位関数になりますので,これを合成対象に設定します.
Project -> Project Settings -> Synthesis
を開き,Top Functionにxillybus_wrapper.cppを選択してOKします.
その後,
- Run C Synthesis
- Export RTL
を実行して,高位合成し,Vivadoで読み込めるようIPとして固めます.
VivadoでXillybus Coreと接続~書き込み
Vivado HLSはC++の関数をRTLに高位合成するだけですので,ここからはVivadoを用いて,通常のFPGA開発と同様の手順をたどります.
続いて,Vivadoを立ち上げ,前回使用したxillybusのデータがループするだけのデモ用プロジェクトに今回作った回路を追加して機能を拡張してみます.
まずは,HLSで合成したIPをVivadoが読み込めるように,IP Catalogに追加します.
ブロックデザインの適当な空白で右クリックし,
"IP Settings"
を開きましょう.続いて,Project Settings -> IP -> Repositoryの設定を開きます.
Repositoriesの欄の"+"ボタンをクリックして,HLSプロジェクトで高位合成されたIPのフォルダを追加します.場所は,
/solution1/impl/ip
とします.これでselectを押すと,検出されたIP一覧が出てきますので,OKしましょう.
続いて,ブロックデザインに戻り,適当な空白を右クリックし,"Add IP"を選択.xillybus_wrapperを選択し,ブロックデザインに追加します.
最後に,xillybus_wrapperを接続します.
- xillybus IP Coreのfrom_host_write_8とto_host_read_8の接続を切断
- from_host_write_8をxillybus_wrapperのデータ入力端子に接続
- xillybus_wrapperのデータ出力端子をto_host_read_8端子に接続
- ap_rst_nをfrom_host_write_8_open端子に接続
- ap_clkをclk_out1端子に接続
xillybusの各端子の説明は以下のとおりです.
端子 | 説明 | プロトコル |
---|---|---|
from_host_write_x | ホストPCからのデータ受け取り端子,32bitと8bit | AXI-Stream |
to_host_read_x | ホストPCへのデータ送信端子,32bitと8bit | AXI-Stream |
ap_clk | クロック入力 | |
to_host_read_x_open | ホストPCで/dev/xillybus_read_xがオープンされるとアサートされる | |
to_host_write_x_open | ホストPCで/dev/xillybus_write_xがオープンされるとアサートされる |
ここまでできたら,前回のデモ回路のときと同様に,Generate Bitstreamボタンを押して,論理合成~bitstream生成をして,FPGAに書き込みます.
FPGAに書き込み終わったら,FPGAボードの電源は入れたまま,ホストマシンを再起動します.これをやらないと,FPGAボードがPCIeデバイスとして正しく認識されません(当たり前なんですが,これに気づくのに時間がかかって数日溶かしました).
以上でハードウェア側の準備はOKです.
Host側ソフトウェアの作成
これはC++で記述し,OpenCVを用いて画像ファイルを開き,xillybusでデータを送受信し,受け取ったデータを再度画像化して保存するものです.
ブラッシュアップしきれていないため,冗長な記述などあるかもしれません・・・
ホストマシンからXillybus経由でデータを送ったり受け取ったりするには,Linuxの場合,
/dev/xillybus_write_x
/dev/xillybus_read_x
のデバイスファイルをオープンし,そこに書き込み/読み込みをすることでデータのやりとりができる仕組みになっています.
今回書いたコードは以下のようなものです.
このプログラムでは画像1行分のデータを送信したあと,1行分,受信する,というのを全画像にわたって繰り返しています.1ピクセルずつ送信と受信を繰り返すと非常に動作が遅くなったためです.また,FPGA側にフレームバッファなどは設けていないため,xillybus coreのバッファのみとなり,画像1枚分まとめて送信すると,バッファが埋まって書き込みが停止してしまう可能性がありますので,細切れに送受信するようにしています.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termio.h>
#include <signal.h>
#include <opencv2/opencv.hpp>
#include <chrono>
#include "xillybus.hpp"
int main(){
printf("test start\n");
// Setup xyllybus
const char dev_file_write[] = "/dev/xillybus_write_8";
const char dev_file_read[] = "/dev/xillybus_read_8";
xillybus8 xillybus(dev_file_write, dev_file_read);
// Load an image
cv::Mat img_src = cv::imread("Lenna.bmp", CV_LOAD_IMAGE_GRAYSCALE);
if(img_src.empty()) return -1;
// Transport and receive
// 1行ずつ送信,受信を繰り返す
// 1ピクセルずつ送受信すると非常に遅くなる.
cv::Mat img_dst(img_src.rows, img_src.cols, CV_8UC1);
for(int y=0; y<img_src.rows; y++){
// 1行一気に送信する
for(int x=0; x<img_src.cols; x++){
unsigned char num = img_src.at<unsigned char>(y, x);
xillybus.xillybus_write(num);
}
// 1行分受信する
for(int x=0; x<img_src.cols; x++){
img_dst.at<unsigned char>(y, x) = xillybus.xillybus_read();
}
std::cout << "y: " << y << std::endl;
}
cv::imwrite("output.jpg", img_dst);
std::cout << "終了" << std::endl;
}
xillybusの書き込みや読み込みをわかりやすくするためにラッパークラスを作成しています.それがxillybus8です(命名がいまひとつですが・・・).コンストラクタの引数にdevファイルのパスを与えます.
xillybus8のコードは以下のようになっています.
#pragma once
class xillybus8{
public:
xillybus8(const char dev_file_write[], const char dev_file_read[]);
void xillybus_write(unsigned char tx);
unsigned char xillybus_read();
private:
int fd_w;
int fd_r;
void allwrite(int fd, unsigned char *buf, int len);
};
#include "xillybus.hpp"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termio.h>
#include <signal.h>
xillybus8::xillybus8(const char dev_file_write[], const char dev_file_read[]){
// Open the device files.
fd_w = open(dev_file_write, O_WRONLY);
fd_r = open(dev_file_read, O_RDONLY);
// Validate results of open functions.
if(fd_w < 0){
printf("xillybus: fd_w open error. Please check the path and the permission.");
exit(1);
}
if(fd_r < 0){
printf("xillybus: fd_r open error. Please check the path and the permission.");
exit(1);
}
}
void xillybus8::xillybus_write(unsigned char tx){
allwrite(fd_w, &tx, sizeof(tx));
}
unsigned char xillybus8::xillybus_read(){
int rc;
unsigned char buf;
rc = read(fd_r, &buf, sizeof(buf));
if(rc<0){
printf("xillybus read error.");
exit(1);
}
return buf;
}
/*
xillybusのデモ用プログラムとして配布されている関数を利用している.
データの書き込み用関数.
xillybusサイトで配布されているstreamwrite.cの関数を使用
*/
void xillybus8::allwrite(int fd, unsigned char *buf, int len) {
int sent = 0;
int rc;
while (sent < len) {
rc = write(fd, buf + sent, len - sent);
if ((rc < 0) && (errno == EINTR))
continue;
if (rc < 0) {
perror("allwrite() failed to write");
exit(1);
}
if (rc == 0) {
fprintf(stderr, "Reached write EOF (?!)\n");
exit(1);
}
sent += rc;
}
}
また,実際にデータを送信している(つまり,xillybus_write_8ファイルに書き込みを行なっている),"allwrite"関数はxillybusのデモ用プログラムから引用しています.
xillybusのサイトの"Code bundle for the host"の"Click here to download the Linux driver"をクリックしてダウンロードされたファイルの"streamwrite.c"を参照してください.
このプログラムを実行すると,Lennaがグレースケール化されて読み込まれ,FPGA側でネガポジ反転され,ネガ画像が保存されます.
終わりに
Xillybusのデモを拡張し,自作の回路を挿入してみました.自作の回路はI/FをAXI-Streamにすれば好きな処理をHLSで作成し,FPGAに実装できそうです.重たい処理のアクセラレートや研究用途など色々と使い道がありそうです.何よりPCIeを自作しないですぐにFPGAボードとPCの通信が確立できるのは素晴らしいですね.
まだまだ使い始めたばかりなので,これから活用して勉強していきたいと思います.