Ryuz's tech blog

FPGAなどの技術ブログ

uioやudmabufにアクセスするC++のクラスを作ってみる(復刻記事)

おしらせ

以前Qrunchで書いていた記事の復刻です。

はじめに

Xilinx社の Zynq や ZynqMP などのPL(プログラマブルロジック)付きのSoCでプログラミングする場合、PL部分にある自分で作った回路のレジスタにアクセスしたり、メモリ領域を回路からのメモリアクセス用に割り当てたりしたいといったことが発生します。

そういったときに Linux標準の uio (User space I/O) や、iwkzm氏のudmabufが以上に便利です。

これらはオープンしてmmapしてしまえば、論理アドレス空間にマップされ、MMUの無いスタンドアアロン環境のマイコンのようにプログラムできるので、簡単にロジックとマイコンソフトを協調動作させる環境が開発できとても便利です。

一方で、普段、マイコンプログラムを行っている人は、Linuxの作法に慣れていないので、一番最初の 「オープンしてmmapしてしまう」という部分に手を焼いたりもします。

今回はまだ洗練できていませんが、この部分を簡単に行うC++のクラスを書いてみたのでブログネタにしてみようと思います。

前提

マイコンプログラマは、メモリマップドレジスタといって、メモリにマップされたレジスタにアクセスすることが多いです。 その際、どのアドレスからマップされているのか、何番目のレジスタなのか、レジスタのbit幅はいくつかのかなどを意識しながら volatile 属性などを付けてアクセスします。 (厳密には volatile はシングルスレッドでのメモリバリアしか保証してくれませんが、マイコン制御の世界では多くの場合足りてしまったりもします。組み込みの場合、言語やOSの支援が得られないケースも多いので、volatileで足りないバリアが必要な場合は自分で埋め込みましょう)。

また、同じIPコアやLSIでも接続されるバスによって、レジスタがバイト単位でアサインされたたり 32bit 単位でアサインされたりするケースがあったりもします。そのため、バイト単位のアドレスで指定したい場合や、「32bit単位でn番目」といった指定が指定ケースがあったりします。

作成したもの

作成したのはこちらのソースとなります。

クラスの説明

作成してみたクラスの概要です。

  • MemAccessorクラス 確保済みのメモリのベースアドレスとサイズを渡して生成し、物理アドレスレジスタ番号でアクセスするクラス
  • MmapAccessorクラス(MemAccessorクラスの派生クラス) オープン済みのファイルディスクリプターを渡して生成し、mmapしてアクセスできる。
  • UioAccessクラス(MmapAccessorクラスの派生クラス) uioの名前を渡すと uio0~uio255 までの間で検索してオープンしてmmap してアクセスできる。
  • UdmabufMmapクラス(MmapAccessorクラスの派生クラス) udmabufの名前を渡すとオープンしてmmap してアクセスできる。割り当てられた物理アドレスやサイズも取得できる

なお各クラスには MemAccessor8 ~ MemAccessor64 まで、アクセス単位ごとにクラスを用意しており、何もついていない MemAccess はコンパイル環境のデフォルトのポインタサイズになります(32bit環境用コンパイラなら32bit、64bit環境用コンパイラなら64bit)。

特徴

  • とりあえずLinuxの作法を隠蔽できる
  • アドレスの範囲内を小分けにして、さらにMemAccessクラスが生成(親子関係が作れる)できる
  • すべてのインスタンスが消えたときに自動でクローズされる

といったところでしょうか。自動クローズがらくちんに書けるのがC++的に良いですね。

使い方

サンプルコード

これを使ったサンプルが3つあります。

ソース読んでいただければ何となく使い方わかるのではないかと思います。

UIOの開きかた

UioAccessor uio_acc("uio_name", size);
if ( !uio_acc.IsMapped() ) {
    std::cout << "uio open error" << std::endl;
    exit(1);
}

のような感じでオープンできます。

udmabufの開きかた

UioAccessor udmabuf_acc("udmabuf_name");
if ( !udmabuf_acc.IsMapped() ) {
    std::cout << "udmabuf open error" << std::endl;
    exit(1);
}

std::cout << "phys addr : " << udmabuf_acc.GetPhysAddr() << std::endl;
std::cout << "size      : " << udmabuf_acc.GetSize()     << std::endl;

同様に名前だけでオープンできます。サイズは自動で取得されます。 GetPhysAddr() や GetSize() で確保された物理アドレスやサイズを知ることができます。

メモリアクセス方法

バイトアドレスでメモリアクセスするには WriteMem, ReadMem系のメソッドを使い、レジスタ番号でアクセスするWriteReg, ReadReg 系のメソッドでアクセスできます。

mem_acc.WriteReg32(3, 0x12);    // 3番目のレジスタに32bitで0x12を書き込む
a = mem_acc.ReadMem64(0x100);   // 0x100番地のを64bitで読む

mem_acc.WriteReg(5, 0x33);   // 3番目のレジスタにそのクラスの持つ幅で0x33を書く

インスタンスの作り方

先のサンプルの中で

// UIOの中をさらにコアごとに割り当て
auto dma0_acc = uio_acc.GetAccessor(0x0000);
auto dma1_acc = uio_acc.GetAccessor(0x0800);
auto led_acc  = uio_acc.GetAccessor(0x8000);

などがあったが、一つの領域をさらに細かく分けて再割り当てできるようにしています。 再割り当てすると、新しく生成されたインスタンスからはその先頭からの相対アドレスでアクセスが可能です。

またこの時、もし範囲内にレジスタ配置の単位が異なるコアが混在していた場合

auto dma1_acc = uio_acc.GetAccess32(0x0800);
auto dma1_acc = uio_acc.GetAccess64(0x0800);
auto led_acc  = uio_acc.GetAccess8(0x8000);

のように、アクセス単位の異なるクラスを派生させることもできます。 子インスタンスを作ると、最後のインスタンスが削除された段階でオープンしていたデバイスはクローズされます。

隠蔽しているLinuxのお作法について

uioについて

uioは割り当てられた順に /dev/uio0 から順に番号が振られていくようです。ですので自分が DeviceTreeなどに記載した uio が実際には何番目なのか調べる必要があります。 この時、例えば /dev/uio0 のデバイス名は /sys/class/uio/uio0/name から取得できます。

cat /sys/class/uio/uio0/name

などとすれば簡単に閲覧できます。 実際には0から順に開いていって、目的のものを探す必要があります。今回のクラスではここを自動で行うようにしていますので、コンストラクタに名前だけ指定すればオープンして mmap まで行います。

udmabufについて

こちらも同様に - 物理アドレス : /sys/class/udmabuf/ <name>/phys_addr - メモリサイズ : /sys/class/udmabuf/<name>/size

から取得できます。 当然いちいちオープンして確認するのは面倒なので、クラスの中に隠蔽しています。