組込み

【ZynqMP】5. UARTでAMP【コア間通信】

1.記事一覧

記事は複数回に分けて投稿する予定です。AMD/XilinxのARM SoC(ZynqMP Kria K26 SOM)で動作する、AMD/Xilinx公式認定Ubuntuやベアメタル開発を勉強していく覚書きです。

Zynq7000(armv7)版はこちら

2.UARTはOpenAMPの代わりとなるのか

以前、OpenAMPでCR5のコア間通信のサンプルを試してみました。通信は成功したのですが、一度通信に使用するとRPMsgドライバのVirtIOがbusy状態から復帰せず、2度目以降の再接続が不可能になる現象を解決できませんでした。

RemoteprocドライバでCR5コアを一度停止し、再起動すると使えるようになるのですが、再接続するたびにコアごと再起動するのは将来的に不便に感じるので、解決するまでの間、代わりとなる方法を検討してみました。

あくまで私は趣味で活動しているので、そこまで躍起になる必要も無いのですが、UARTでAMPをやってみたら思いの外使い勝手も良かったので記事として残すことにしました。

2-1. UARTを代替OpenAMPとするとどうなるか

OpenAMPの代わりとなる方法を検討するにあたり、以下の目標を概ねクリアできれば良しとします。

代替AMPの目標要件

  • 通信停止してからCR5を停止せず、再接続できること。(重要)
  • 双方向通信が出来ること(半2重、全2重は問わない)
  • コマンドを送受信できる程度の最小限の通信速度
  • ELFロード、コア起動停止はRemoteprocを利用する

上記の条件を満たす中でUARTで実現できるか検討します。仮名”UART-AMP”と呼ぶことにします。

UART-AMPは以下の方法で実装できそうです。

  • PL(FPGA)上にAXI-UARTLite IPをCA53-CR5間に2つ設置し、TX、RX線をクロス接続
  • 片方のAXI-UARTLite IPをLinux、他方をCR5に割り当てる(Devicetreeで無効化)
  • AXI-UARTLite IPはLinuxドライバとベアメタルドライバが提供されてるので実装が楽

2-2. UART-AMPのメリット・デメリット

OpenAMPと比較してメリデメを挙げてみます。

メリット

  • UARTのLinuxドライバとベアメタルドライバが成熟している
  • ハードウェアFIFOがリングバッファの代わりとなるため、ソフト側の複雑性が低下
  • LinuxのUART I/Fがシンプルで、C言語以外の環境でも使いやすい
  • ベアメタルドライバの情報が充実しているため、CR5側の開発難易度が低い
  • Linux-CR5のAMPに限らず、Linux-MicroblazeやCR5-Microblazeの様な組み合わせ自由度がある
  • データグラムではなく、バイトストリームとして利用できる

デメリット

  • PLのフットプリントを圧迫する。1箇所の通信ペアにAXi-UARTLite IPが2つ必要
  • OpenAMPのRemoteprocに依存するため、Devicetree、Remoteprocドライバ、PLBitstream、PL Devicetree Overlay、と準備するものが多くなる
  • PLを搭載するZynq、ZynqMPやFPGAでのみ使用可
  • 通信速度があまり速くない
  • バッファオーバーフローやデータ破損検知するにはプロトコルを組む必要がある

個人的にはLinuxドライバとベアメタルドライバが成熟(枯れているとも言う)していて、ドキュメントやサンプルコードが整備されているところが推しです。OpenAMPはAPIリファレンスや仕様を把握するハードルが個人的に高いと感じるので、お手軽なのは大きなアドバンテージだと思います。

“LinuxのUART I/FがC言語以外でも使いやすい”という点についてですが、デバイスファイルやSysFsにアクセスするだけなので、RPMsgでもC言語以外で扱うことは可能です。しかしライブラリが充実していて容易に扱えるという点ではUARTシリアルに軍配が上がると思います。それよりも、LinuxのRPMsgドライバのリファレンスのようなものがどこにあるのかわからん…

2-3. UART-AMPのデモ環境

今回作成していくUART-AMPの構成です。CR5はSplit(2コア)で使用します。せっかくなのでMicroblazeも1コア追加します。

デモの内容

  • UART-AMPのPLデザイン with Mciroblaze
  • DevicetreeとPL Devicetree Overlayコード
  • Linuxアプリ: Echo test (Python)
  • CR5およびMicroblazeのテストファームウェア
開発PCUbuntu20.04 LTS
開発ツールAMD/Xilinx社
Vivado v2024.1.1
Vitis Classic v2024.1
Vitis Unified IDE v2024.1.1
ターゲットOSXilinx認定Ubuntu22.04
Linux Kernel : 5.15.0-1031-xilinx-zynqmp
ターゲットボードXilinx社製 Kria KR260 ロボティクススターターキット(SK-KR260-G)
SOM : Xilinx社 Kria K26 SOM (Zynq UltraScale+ MPSoCベース)
   APU : Arm CA53x4 1333MHz arm64
   RPU : Arm CR5x2 533MHz armv7-R
SDRAM : DDR4 4GB
Github : UART-AMPhttps://github.com/kern-gt/ZynqMP-UART-AMP-KR260-Ubuntu

チュートリアルとしてとりあえず動かしてみたい方へ

さくっと試せるようにビルド済みのエコーバックテストのチュートリアルを用意しています。詳細はGithubへ。

https://github.com/kern-gt/ZynqMP-UART-AMP-KR260-Ubuntu/tree/main/linux_uart_amp_echo_test

3.PLの作成

VivadoのPLを作成します。ブロックデザインのTCLを前述のGithubに挙げています。

AXI-UARTLite IP

  • Linux-CR5-0間:2個、(115200bps)
  • Linux-CR5-1間:2個、(115200bps)
  • Linux-Microblaze間:2個、(9600bps)

AXI-GPIO IP

  • CR5-0:1個、UF1 LED用
  • CR5-1:1個、UF2 LED用
  • Microblaze:1個、SFP LED1用

Microblaze IP

  • Microblaze(Micro controller)
  • local memory(128KB)
  • mdm debug module
  • microblaze axi interrupt controller
  • AXI Timer

その他

  • FAN制御信号

AXI-UARTLiteはコンパイル前にUARTボーレートを決定します。ソフトウェアから動的にボーレート可変することは出来ません。可変したい場合はAXI UART16550 IPを使用します。CR5とMicroblazeでボーレートを変えてある理由ですが、Mciroblazeでパケロスが発生したので、低速に変更しています。本当はMicroblazeも115200bpsにする予定だったのですが、割り込みハンドラで読み出しても、受信FIFOのオーバーフローが発生したため、処理性能限界と判断し、遅くしました。

Microblazeは今回FreeRTOSを使用するので、local memoryを128KB、AXI-Timerを用意します。

4.DevicetreeとPL Devicetree Overlay

4-1. OpenAMP用Devicetree

CR5のELFローダおよび、コア起動停止管理用にOpenAMPのRemoteprocドライバを使用します。そのために、Xilinx認定UbuntuのデフォルトのデバイスツリーをOpenAMP用に変更する必要があります。

OpenAMP用のDevicetreeに関しては前回の記事”【ZynqMP】4. 1. 認定UbuntuでOpenAMPを試す【CR5Split】“を参照してください。また、Githubにコードを置いてあります。

Githubに置いてあるOpenAMP用のDevicetreeはKR260ボードのUSB-JTAGに共有されている、PS-UART1を無効化してあります。理由はCR5やMicroblazeのデバッグ出力用に利用できるようにするためです。その代わり、Linuxのコンソールは使用できなくなります。

CR5ファームウェア用の予約メモリ

CR5-0コア、CR5-1コアが使用するDRAMの領域はLinux管理外にしなければなりません。Devicetreeで予約領域を設定します。

コアDRAM
CR5-0BaseAddr : 0x3ED0 0000
Size : 0x4 0000 (256KB)
CR5-1BaseAddr : 0x3EF0 0000
Size : 0x4 0000 (256KB)
その他の領域はOpenAMPのRPMsg(メッセージ通信)とトレースで使用する領域で今回は使いませんが残してあります(どこまで削除してもよいかの検証をしていないからです)。

4-2. PL Devicetree Overlay

“3.PLの作成”でVivadoコンパイルした後、ハードウェアエクスポートして、Devicetree Overlayを生成します。Githubのgen_pl_dts.shを実行すると、同階層にpl_dtsoフォルダが作成され、DTOverlayコードを取り出せます(ただし、VitisのXSCTが使える必要あり)。

AXI-UARTLite IPを使用するため、基本的にパラメータを設定する必要はありません(RPMsgと違ってめちゃ楽)。ただし、今回はCR5側で使用するIPをLinux管理外にする必要があります。

Linux管理外にするIPの修正

CR5側で使用するAXI-UARTLite IPやAXI-GPIO IPをLinux管理外にする必要があります。管理外にする記述はstatusプロパティを変更することでできます。

YAML
# 無効化の例
&axi_gpio_0 {
    status = "disabled";
};

&axi_uartlite_3 {
    status = "disabled";
};

UART-AMPデモの設定はGithubを参照してください。

使い方ですが、Xilinx認定Ubuntu22.04にプリインストールされている、xmutilsツールを使用して、PLコンフィグレーションを行います。

/lib/firmware/xilinx/以下にアプリケーション用フォルダを作成し、以下のファイルを配置します。

  • bitstream.bit.bin
  • PL Devicetree Overlay(dtbo)
  • shell.json

5.Linuxアプリ: Echo test (Python)

ループバックテスト用のLinuxアプリを作成しました。

仕様

  • テストデータを送信し、ループバックデータが正しいか検証する
  • Python実装
  • テストデータはテキストファイルに定義し、読み込む
  • サブコアにはループバックファームウェアを動作させておく
  • 第1引数にテストデータファイル
  • “-p”でUARTポート指定
  • “-b”でボーレート指定(デフォルト115200bps)

コードは”uart_amp_echo_test.py”です。”-h”でhelp見れます。ライブラリにpyserialが必要なため、venvで仮想環境を作成してください。Githubにrequirements.txtを用意してあるので、そのままpipセットアップできます。

テストデータを送信するスレッドとループバックで受信したデータを検証するスレッドを分けてあります。ほぼ通信帯域いっぱいまで使い切ってサブコアに負荷を与えるようにできます。

実行例

Bash
#UARTポート確認
$ ls /dev/ttyUL*
/dev/ttyUL0  <--CR5-0
/dev/ttyUL1  <--CR5-1
/dev/ttyUL2  <--Microblaze


#Ptython仮想環境設定
$ python -V
Python 3.10.12

$ sudo apt install python3.10-venv

$ python3 -m venv uart-amp
$ source ./uart-amp/bin/activate
(uart-amp)$ python -m pip install -r requirements.txt
(uart-amp)$ python -m pip list
Package    Version
---------- -------
pip        22.0.2
pyserial   3.5
setuptools 59.6.0


#テスト実行(CR5-0コアを選択)
(uart-amp)$ python uart_amp_echo_test.py test_data.txt -p /dev/ttyUL0

#仮想環境を抜ける
(uart-amp)$ deactivate
$

/dev/ttyULxとAXI-UARTLite IPの対応関係について

UARTのデバイスファイルは”/dev/ttyULx”のファイルになります。番号の割当ての仕組みは把握できてません。もしかしたらデバイスツリーかVivadoのIPブロックの設定でコントロール可能かもしれません。

認定Ubuntu上から確認するには、SysfsでAXI-UARTLite IPのベースアドレスを確認できます。

Bash
$ cat /sys/class/tty/ttyUL0/iomem_base 
0x80010000

外にも方法はあるかもしれませんが、今の所ベースアドレスで確認して判別するのが確実かなと思います。

6.CR5およびMicroblazeのテストファームウェア

6-1. ファームウェアについて

ループバック用のファームウェアをGithubに公開しています。

ファームウェア作成の過程でベアメタルドライバに割と癖があったので、参考になる情報を挙げておきます。

開発最初は全く動作せず困っていたところ、このQiita記事がとても参考になりました。ベアメタルドライバをRTOSと組み合わせて非同期の処理を実装するのはそこそこ難易度が高いので、トラブルが出るとそこそこ面倒でした。特にパケロスの原因調査は結構骨が折れました。この場を借りて先駆者の方に感謝申し上げたいと思います。

また、理由は後述しますが、CR5用ファームウェアは新Vitis(Vitis Unified IDE)でMicroblaze用ファームウェアは旧Vitis(Vitis Classic)で作成しています。ベアメタルドライバの仕様が前者はSDT対応版で後者が非対応版になっているため、その点の実装方法が異なっています。

  • FreeRTOSを使用(CR5-0はTTC3、CR5-1はTTC2をカーネルタイマに割当)
  • UART送信及び受信のI/FはFreeRTOSのStreamBuffer
  • 各コアでハートビートLEDを1つ搭載

タスク構成

  • ループバックテストタスク
  • UARTドライバタスク(送信)
  • UARTドライバタスク(受信)
  • ハートビートLEDタスク

テストタスクとUARTドライバタスク

点線矢印の部分がStreamBufferのIFになっています。Buffer名は以下としています。

  • UART 受信 : uart_recv_buf
  • UART 送信 : uart_send_buf

StreamBufferはQueueと違い、スレッドセーフ出ないため多対多のタスクで通信は出来ません。その代わり、軽量動作かつ、バイトストリームのI/Fとして使うことが可能です。

リンカスクリプト

CR5の2コアはTCMとDRAMにファームウェアを展開するため、Devicetreeで指定した予約メモリ領域を使用する必要があります。MicroblazeはLocal memoryに収める設計なので、考慮は不要です。

GIthubでリンカスクリプトを公開しています。”_DDR_START”の値を変更します。

DRAMに配置されるセクションは以下のとおりです。(CR5-0の場合アドレス0x3EDxxxxxに配置)

  • .text
  • .bss
  • .resource_table
Bash
$ readelf -S app_echo_uart_r5_0.elf 
There are 22 section headers, starting at offset 0x3211c:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .vectors          PROGBITS        00000000 010000 000660 00  AX  0   0  8
  [ 2] .text             PROGBITS        3ed00000 020000 00ba34 00  AX  0   0 16
  [ 3] .init             PROGBITS        00000660 010660 00000c 00  AX  0   0  4
  [ 4] .fini             PROGBITS        0000066c 01066c 00000c 00  AX  0   0  4
  [ 5] .rodata           PROGBITS        00000678 010678 001bb9 00   A  0   0  8
  [ 6] .data             PROGBITS        00002238 012238 000480 00  WA  0   0  8
  [ 7] .drvcfg_sec       PROGBITS        000026b8 0126b8 000dc4 00  WA  0   0  4
  [ 8] .bootdata         PROGBITS        00003480 013480 000180 00  WA  0   0  8
  [ 9] .eh_frame         PROGBITS        00003600 013600 000004 00   A  0   0  4
  [10] .ARM.exidx        ARM_EXIDX       00003604 013604 000008 00  AL  2   0  4
  [11] .init_array       INIT_ARRAY      0000360c 01360c 000008 04  WA  0   0  4
  [12] .fini_array       FINI_ARRAY      00003614 013614 000004 04  WA  0   0  4
  [13] .ARM.attributes   ARM_ATTRIBUTES  00003618 02ba34 00002f 00      0   0  1
  [14] .bss              NOBITS          3ed20100 030100 0191f0 00  WA  0   0  8
  [15] .heap             NOBITS          00003618 013618 001408 00  WA  0   0  1
  [16] .stack            NOBITS          00020000 020000 003800 00  WA  0   0  1
  [17] .resource_table   PROGBITS        3ed20000 02ba63 000000 00   W  0   0  1
  [18] .comment          PROGBITS        00000000 02ba63 000012 01  MS  0   0  1
  [19] .symtab           SYMTAB          00000000 02ba78 003890 10     20 476  4
  [20] .strtab           STRTAB          00000000 02f308 002d50 00      0   0  1
  [21] .shstrtab         STRTAB          00000000 032058 0000c2 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), y (purecode), p (processor specific)

“.resource_table”はRPMesgで利用するもののため、今回はサイズ0になっています。RemoteprocがELF解析する際にこのセクションの情報を読み取るらしいため、一応セクションは存在するようにしています。このセクションを削除した場合は試したことが無いのでわかりません。

ちなみに”.resource_table”セクションがサイズ0の場合、RemoteprocでELFロードを行うと以下のメッセージが出ます。

Bash
$ sudo dmesg
(...)
[ 3833.693914] remoteproc remoteproc0: header-less resource table
[ 3833.699759] remoteproc remoteproc0: no resource table found.
(...)

6-2. CR5のELFの扱い方

CR5はRemoteprocドライバでELF実行ファイルをロードします。

起動手順(CR5-0の場合、ELFのファイル名を”filename.elf”とした場合)

  • PLのコンフィグレーションを実行する
  • ELFファイルを”/lib/firmware”以下に配置する
  • shell でELFをロードする
    sudo sh -c “echo filename.elf > /sys/class/remoteproc/remoteproc0/firmware”
  • shell でコアを起動する
    echo start > /sys/class/remoteproc/remoteproc0/state
  • shell でコアを停止する
    echo stop > /sys/class/remoteproc/remoteproc0/state

CR5-1の場合は /sys/class/remoteproc/remoteproc1以下を操作します。また、ELFの再ロードを行う場合はコアを停止させないといけません。何かエラーが発生した場合はdmesgにヒントがあるかもしれません。

6-3. MicroblazeのELFの扱い方

Microblazeは今回Local memoryのみを使うので、PLコンフィグレーション後にLocal memoryが使用できるようになってからロードする必要があります。しかし、Local memoryはPLのBlockRAMを使用して構成するため、CA53側のアドレス空間に存在しません。

Webで少し調べるとFPGAの部屋のmarseeさんが記事を書いてくださっていました。

VivadoでELFファイルの関連付けを設定すると、Bitstream生成時に結合してくれるようです。もちろんbin出力の場合も出来ます。

ELFを結合すると、PLのコンフィグレーションと同時にLocal memoryへのロードとMicroblazeの起動ができます。

“Tools>Associate ELF Files…”からELFファイルとMicroblazeを紐づけて”Generate Bitstream”を実行すると結合できます。

7. UART-AMP開発の注意点

デバッグモジュールの標準出力の利用

CR5の場合はARM Coresight、Microblazeの場合はMDMのデバッグモジュールを利用できます。標準出力をUARTを用意せずにJTAG経由で出力できます。

Vitis IDEでプラットフォームプロジェクトの設定からstdoutの出力を切り替えることで使うことができます。

ただし、ROM化する際など、JTAGを接続せずにコアを起動すると、標準出力のストリームバッファが詰まるのかわかりませんが、ブロッキングすることがあります。そのため、デバッグモジュールを無効化する必要があります。

PLコンフィグレーションに失敗する

xmutilsコマンドでPLのコンフィグレーションに失敗することがあります。dmesgを確認すると、CMAメモリアロケータの初期化に失敗していることがあります。

Bash
$ sudo dmesg | grep cma
[    0.000000] cma: Failed to reserve 1000 MiB

この場合はbootargsのCMAのメモリプールサイズを縮小(例768MiB)に変更するか、CR5の予約領域の位置を変える必要があります。

bootargsの変更方法は以前の記事で説明しています(【ZynqMP】3.認定UbuntuのDeviceTree変更)。

理由について

PLのコンフィグレーション時に何らかの形でCMAメモリアロケータを利用していると考えられます。そのため、CMAメモリプールの初期化に失敗すると、PLにbitstreamをロードできません。

KriaのDRAMは4GB搭載されていますが、この内2GBはアドレス空間の先頭から32bitの範囲内にあり、残り2GBはその範囲外にあります。

私も詳しくは知らないので、ここから先は話半分に聞いてください。

CMAはDMA用に連続した物理アドレスの領域のメモリを管理するアロケータのため、32bitの範囲内でメモリプールを確保すると都合が良いと思われます。DMAのデフォルトが32bitアドレスモードで64bitモードはオプションが多い…はず。

その場合、CMAはデフォルトで1000MiBをbootargsで確保するように指定しているため、2GBのDRAMのうち、1GBの連続した領域を確保しなければなりません。

今回、DevicetreeでCR5が使用する領域を以下のように指定しています。

コアDRAM
CR5-0BaseAddr : 0x3ED0 0000
Size :
(CR5の実行バイナリ=0x4 0000) +
(RPMsgのリングバッファ=0x4000) +
(RPMsgのリングバッファ=0x4000) +
(トレースログバッファ=0x10 0000)
=0x148000
≒1.34MB
専有領域 : 0x3ED0 0000 – 0x3EE4 8000
CR5-1BaseAddr : 0x3EF0 0000
Size :
(CR5の実行バイナリ=0x4 0000) +
(RPMsgのリングバッファ=0x4000) +
(RPMsgのリングバッファ=0x4000) +
(トレースログバッファ=0x10 0000)
=0x14 8000
≒1.34MB
専有領域 : 0x3EF0 0000 – 0x3F04 8000

この領域はVitisのOpenAMPのサンプルプロジェクトの値をそのまま使っています。つまり、0x3ED0 0000から0x3F048000の領域はCMAと被ってはいけません。

この領域はDRAM先頭から約1GBあたりの位置にあります。つまり2GBの領域のほぼど真ん中になります。そのため、1GBを丸々確保できず失敗すると考えられます。

今回は楽なのでbootargsでCMAを768MiBに減らして対応しましたが、実際はCR5の予約領域を変更するほうが良いと思います。

UARTボーレートずれ

今回開発中に起こった出来事ですが、AXI-UARTLite IPのボーレートがずれて通信データが文字化けしたことがありました。

当時はLinux側(CA53)とCR5でUARTを直結せず、デバッグ用にPmodコネクタからRX、TX信号を外部出力して、USB-UARTで母艦PCと通信させていました。Linux側とCR5で直結する場合はこの問題は起こりません。

AXI-UARTLite IPのクロック源をPL FCLKから取り出していたのですが、PL DTOverlay時にFCLKのクロックジェネレータの分周器の値がずれて、想定から10%の周波数ずれが起こりました。

周波数ずれの理由

  • FCLKの分周器の分解能がそもそも荒い(100MHz出力付近で9MHz刻み)
  • PL DTOverlayコードの設定値を超えない最大周波数を設定しようとする
  • デバイスツリー自動生成のPL DTOverlayコードの値だと、想定より低くなる

解決方法

PL DTOverlayコードの値を変更します。”assigned-clock-rates”プロパティを変更します。

YAML
&fpga_full {
  clocking0: clocking0 {
    assigned-clock-rates = <100000000>;
  };
};

また、UART-AMPでLinux側(CA53)とCR5、MicroblazeとUARTを直結する場合は、全てのAXI-UARTLiteIPを同じクロック源のFCLKを接続すれば、すべて同じ分だけ周波数がずれるので、ボーレートずれは起こりません。

MicroblazeをVitis Classicで作成した理由

今回CR5とMicroblazeの開発でIDEを変更しました。本当は新しいVitis Unified IDEですべて開発する予定だったのですが、MicroblazeでJTAGデバッグが出来ないトラブルが出たため、Vitis Classicを使うことにしました。

トラブルというのは、ブレークポイントを指定すると明らかに違うところのソース行に貼られて、デバッグにならないという現象でした。コンパイラ最適化は切れているのは確認済みです。IDEを使わずにXSCTでXSDBコマンドをポチポチ打ってステップ実行してみると、同様の結果でした。どうやらELFのデバッグシンボル情報が明らかにおかしく、アドレスがずれている?様子でした。コンパイラまわりで何か原因がありそうですが、わかりませんでした。結局Vitis Classicを使うということにしました。

ただし、新VitisとVitis Classicではベアメタルドライバに互換性がありません。割込みまわりの実装で若干の面倒が生じます。新Vitisでは割込みハンドラテーブルがFreeRTOSとベアメタルドライバで同一のテーブルを参照するように割込みまわりのAPIが新しく提供されています。Vitis Classicではそれがないので、FreeRTOSのXilinxポーティングとして実装されている、APIを使用する必要があります。このAPIのドキュメントが無くて最初は存在も知りませんでした。前述のQiita記事には感謝ですね。

ABOUT ME
sh-goto
低レイヤで遊んでいます
関連記事