1.記事一覧
記事は複数回に分けて投稿する予定です。AMD/XilinxのARM SoC(ZynqMP Kria K26 SOM)で動作する、AMD/Xilinx公式認定Ubuntuやベアメタル開発を勉強していく覚書きです。
ZynqMP(APU:Cortex-A53,Arm64) 組込みLinux入門編
- 【ZynqMP】1. 認定UbuntuでPLのGPIOを使う
- 【ZynqMP】2. 認定Ubuntuのカーネルビルド
- 【ZynqMP】3. 認定UbuntuのDeviceTree変更
- 【ZynqMP】4. 認定UbuntuでOpenAMPを試す【CR5Lockstep】
- 【ZynqMP】5. UARTでAMP【コア間通信】←本記事
- 【Linux】6. crash toolでカーネルメモリを見る
ZynqMP(RPU:Cortex-R5,Arm32) RPU入門編
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のテストファームウェア
開発PC | Ubuntu20.04 LTS |
開発ツール | AMD/Xilinx社 Vivado v2024.1.1 Vitis Classic v2024.1 Vitis Unified IDE v2024.1.1 |
ターゲットOS | Xilinx認定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-AMP | https://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-0 | BaseAddr : 0x3ED0 0000 Size : 0x4 0000 (256KB) |
CR5-1 | BaseAddr : 0x3EF0 0000 Size : 0x4 0000 (256KB) |
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プロパティを変更することでできます。
# 無効化の例
&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セットアップできます。
テストデータを送信するスレッドとループバックで受信したデータを検証するスレッドを分けてあります。ほぼ通信帯域いっぱいまで使い切ってサブコアに負荷を与えるようにできます。
実行例
#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のベースアドレスを確認できます。
$ cat /sys/class/tty/ttyUL0/iomem_base
0x80010000
外にも方法はあるかもしれませんが、今の所ベースアドレスで確認して判別するのが確実かなと思います。
6.CR5およびMicroblazeのテストファームウェア
6-1. ファームウェアについて
ループバック用のファームウェアをGithubに公開しています。
ファームウェア作成の過程でベアメタルドライバに割と癖があったので、参考になる情報を挙げておきます。
- FPGAのソフトコアCPU (MicroBlaze)とFreeRTOSでシリアル通信ソフトをつくる:Qiita
- AXI UART Lite standalone driver
- LogiCORE™ IP AXI UART Lite
開発最初は全く動作せず困っていたところ、この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
$ 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ロードを行うと以下のメッセージが出ます。
$ 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メモリアロケータの初期化に失敗していることがあります。
$ 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-0 | BaseAddr : 0x3ED0 0000 Size : (CR5の実行バイナリ=0x4 0000) + (RPMsgのリングバッファ=0x4000) + (RPMsgのリングバッファ=0x4000) + (トレースログバッファ=0x10 0000) =0x148000 ≒1.34MB 専有領域 : 0x3ED0 0000 – 0x3EE4 8000 |
CR5-1 | BaseAddr : 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”プロパティを変更します。
&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記事には感謝ですね。