Zynq DDR上のデータをPL上で多倍長演算
ハードウェア
図のような構成で、DDR上にある多倍長データをPL上で演算する。
まずAXI-liteで演算対象となるデータのアドレスと、演算結果の書き込みアドレスを指定する。その後AXI-lite上のrunレジスタに書き込むと演算が開始される。
AXI-fullは雑に書くと下のようなステートマシンを持っており、run信号によって待機状態IDLEから実行状態に遷移する。READ0とREAD1はそれぞれread_addr0/read_addr1で指定されるアドレスからバーストリードを行って256bitレジスタに多倍長演算の入力を格納する。その後ステートはEXEに移行し、256ビット加算が1サイクルで実行される。最後にWRITEステートでwrite_addrのアドレスに書き込んでDDRに転送する。
AXI-liteもfullもZilinxのテンプレートを軽く書き直しただけ。
ブロック図は、
こんな感じ、多倍長加算はmy_AXIの中に書いてある。今回petalinux上でアプリケーションを動作させるが、sdカードからlinuxブートさせてからビットストリームを書き込むとちゃんとPL側に反映されることが分かった。ILAも使えるのでPL側のデバッグも楽々。
ソフトウェア
petalinuxで動作させるアプリケーションはこんな感じ。petalinux上で開発してもいいし、vitis上でもいい。WSL上のvitisからでもネットワーク経由で書き込みできる。
#include "stdio.h"
#include "unistd.h"
#include "inttypes.h"
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
typedef unsigned int fp_t[8];
int main(void){
fp_t a = {0x184, 0x11111111, 0x22222222, 0x33333333,
0x444444, 0x55555555, 0x00000000, 0xffffffff};
fp_t b = {0x0101010, 0x99999999, 0xaaaaaaaa, 0xbbbbbbbb,
0xffffddddc, 0xdddd44fd, 0x00000000, 0x1};
fp_t c;
int fd;
//Addition on CPU
for (int i = 0; i < 8; i++){
c[i] = a[i] + b[i];
printf("c[%d] = %08x\n", i, c[i]);
}
/* メモリアクセス用のデバイスファイルを開く */
if ((fd = open("/dev/mem", O_RDWR | O_SYNC)) < 0) {
perror("open");
return -1;
}
// vaddrに物理アドレス0x0e000000をマッピング
unsigned int* vaddr = (int)mmap(NULL, 0x60, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0e000000);
if (vaddr == MAP_FAILED) {
perror("mmap");
close(fd);
return -1;
}
//物理アドレス0x0e000000に配列a, bをコピー
memcpy(vaddr, a, 32);
memcpy(vaddr+8, b, 32);
// vaddrに物理アドレス0x43c00000 (AXI-lite) をマッピング
unsigned int* vaddr_reg = (int)mmap(NULL, 0x60, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x43c00000);
if (vaddr == MAP_FAILED) {
perror("mmap");
close(fd);
return -1;
}
// AXI-liteへ書き込み
vaddr_reg[0] = 0x0e000000;
vaddr_reg[1] = 0x0e000020;
vaddr_reg[2] = 0x0e000040;
vaddr_reg[3] = 0x00000001;
//結果出力
for(int i = 16; i < 24; i++){
printf("vaddr[%d] = %08x\n", i, vaddr[i]);
}
return 0;
}
これは配列aとbに256ビットの値を格納し、それを多倍長演算するプログラムである。AXI-liteへのアクセスは/dev/mem経由で行うが、配列a, bのアドレスとして物理アドレスを渡さなければならない。配列a, bの物理アドレスを得る方法をいくつか試したがうまくいかなかったので、物理アドレス0x0e000000
を決め打ちでAXI-full転送用の一時的なバッファとして利用している。もちろん、この領域を何らかのプロセスで使ってたりすると壊れるが、ここではケアしない。
AXI-liteのレジスタは0x43c00000
にマッピングされているため、mmapでこの領域を仮想アドレスに変換して、その領域に0x0e000000
(配列aのアドレス)、0x0e000020
(配列bのアドレス)、0x0e000040
(結果を書き込むアドレス)、を指定している。最後に vaddr_reg[3] = 0x00000001;
でrun
シグナルが発行される。
実行結果
$ sudo ./a.out
c[0] = 00101194
c[1] = aaaaaaaa
c[2] = cccccccc
c[3] = eeeeeeee
c[4] = 00422220
c[5] = 33329a52
c[6] = 00000000
c[7] = 00000000
vaddr[16] = 00101194
vaddr[17] = aaaaaaaa
vaddr[18] = cccccccc
vaddr[19] = eeeeeeee
vaddr[20] = 00422220
vaddr[21] = 33329a53
vaddr[22] = 00000001
vaddr[23] = 00000000
CPU上で計算した結果はキャリーが捨てられるので、0xffffffff+0x1= c[6] = 00000000
となっている一方、PL側は多倍長演算なのでvaddr[22] = 00000001
となっており、うまく計算できている。
Discussion