2026/10/27: 不具合修正
panic!()を実行した時にRGB LEDの赤が点灯されないバグを修正しました。原因はsrc/debug.rsで利用するGPIOレジスタアドレス指定の誤りです。
リポジトリのmainブランチには修正済みのコードがコミット済みです。
今後も修正と細かい機能追加を予定しています。変更はソースコードのリポジトリで確認して下さい。
はじめに
ZYNQ-7000のベアメタル環境をRustで実装しました。本記事では動作環境の構築方法について記述しています。
機能概要
ここで紹介する環境では下記機能が実装されています。
- u-bootからの起動を前提とした動作環境
- UARTからのprint!()/println!()出力処理
- 例外発生時のコンテキスト情報収集
- マルチコア動作
ベアメタルシステムの基礎となる部分を実装しています。
実装方針
ZYNQ-7000に関しては既にzynq7000, zynq7000-hal, zynq7000-rtなどベアメタル環境を想定したクレートが公開されています。しかし、これらで作られたExampleを見ると、BootROMから起動される事を前提としています。
実際の開発ではu-bootから起動できた方が便利なので、u-bootからの起動を前提として実装しました。
デバイス定義はzynq7000クレートを利用しています。この他、zynq7000-hal, aarch32-cpu, aarch32-rtを利用しており、ビルド環境に関するファイルに関しては一部流用しています。
ブートストラップコードや例外処理、ドライバは追加で実装しています。
動作環境
Board
Zybo Z7-20
Parts
Pmod USBUART x2
Tools
Vivado 2025.1
Vitis 2025.1
rustc 1.99.0
cargo 1.99.0
Host OS
Ubuntu 24.04 Desktop
Block Design
動作確認に利用するBlock Designは以下の通りです。
Console用にUART Lite x2、LED制御とSwitch/Button入力用にAXI GPIO x2を実装しています。UARTはPmod USBUARTへ接続しますが、これらが無くてもボード上のUARTのみで動作可能です。
Pmod USBUARTの接続は以下の通りです。
Vivadoビルド手順
Bitstream作成
最初にVivadoでBitstreamを生成します。先ずは下記コマンドでスクリプト一式を取得して下さい。
cd $HOME
git clone -b pmod-usbuart-x2 https://github.com/hkato-sssl/zybo-z7-20-example.git
続いて取得したスクリプトをVivadoで実行します。スクリプトはCLIのみで実行可能です。
source /tools/Xilinx/2025.1/Vivado/settings64.sh
cd $HOME/zybo-z7-20-example
vivado -source vivado/script/zybo-z7-20-example.tcl -mode batch
Vivadoオプションの-mode batchを外すとGUIで実行されます。
Vivado projectは$HOME/ws/vivado/zybo-z7-20-example/に作成されます。
スクリプトの実行が成功するとzybo-z7-20-example.xsaがproject配下に作成されます。
Petalinuxビルド手順
先ほど作成したXSAファイルを元にLinuxとu-bootのビルドを行います。本記事でLinuxは使いませんが、動作確認用に作成しておきます。
前提条件
作業手順では下記条件での実行を前提としています。
- petalinux の各種ツールが利用可能な状態
- ホームディレクトリ配下に
zybo-z7-20-example.xsaがコピー済み
準備
最初に環境変数を設定します。ここでは$HOME/petalinux/配下にプロジェクトを作成する様に設定しています。
WORK_DIR=$HOME/petalinux
PROJ_NAME=zybo-z7-20-example
PROJ_DIR=$WORK_DIR/$PROJ_NAME
PROJ_XSA=$HOME/$PROJ_NAME.xsa
プロジェクト作成
次にプロジェクトを作成し、作成した XSA ファイルを取り込みます。
mkdir -p $WORK_DIR
cd $WORK_DIR
# create project
petalinux-create project --template zynq --name $PROJ_NAME
cd $PROJ_DIR
# configure H/W
petalinux-config --get-hw-description $PROJ_XSA --silentconfig
ビルド
petalinux/u-bootをビルドした後、BOOT.BINを作成します。
# build images
petalinux-build
# create BOOT.BIN
petalinux-package --boot --fsbl images/linux/zynq_fsbl.elf --fpga images/linux/system.bit --u-boot --force
Rustビルド手順
準備
Rustのツール一式をインストールします。インストールはユーザー権限で実行可能です。
先ずインストール作業に必要なツール群をUbuntuにインストールします。
sudo apt update
sudo apt install curl git build-essential
次にRustのインストール・スクリプトを取得・実行します。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
インストールしたツールのパスを有効化する為、ログアウトした後に再度ログインして下さい。再ログインしたら追加のツールをインストールします。
rustup target add armv7a-none-eabihf
cargo install cargo-binutils
rustup component add llvm-tools
本記事執筆時点では1.99.0がインストールされました。
$ rustc --version
rustc 1.99.0 (b940084d7 2026-09-28)
ビルド
Rustのインストールが出来たら、ソースの取得&ビルドを行います。
cd $HOME
git clone https://github.com/hkato-sssl/zynq7000-baremetal-rs.git zynq7000-baremetal
cd zynq7000-baremetal
cargo build -r
クレート名をzynq7000-baremetalにしているので、git clone時にフォルダ名をクレート名に合わせています。フォルダ名とクレート名が違っていても、ビルドに支障はありません。
target/armv7a-none-eabihf/release/配下にhelloとttcの2ファイルが生成されたらビルド成功です。最後にこれらをBIN形式に変換します。
cargo objcopy --release --bin hello -- -O binary hello.bin
cargo objcopy --release --bin ttc -- -O binary ttc.bin
コマンドを実行するとhello.binとttc.binがプロジェクト配下に生成されます。
Example実行手順
Boot用SD card作成
Petalinuxビルド手順で作成した下記ファイルをmicroSD MemoryのFATパーティションに書き込みます。
- BOOT.BIN
- boot.scr
- image.ub
image.ubはLinuxイメージですが、システムが起動しない場合のFPGA動作確認用に書き込んでいます。無くても支障はありません。
次にRustで作成したBINファイルを書き込みます。
- hello.bin
- ttc.bin
全て書き込んだら、SD cardをZyboのスロットに挿して電源を入れます。
hello.binの実行
起動手順
ボードへ電源投入後、u-bootから下記コマンドを実行します。
fatload mmc 0:1 0x10000000 hello.bin;dcache flush;go 0x10000000
実行するとボード上のUARTから下記の様にHELLOという文字が出力され続けます。
0:HELLO#3681
1:HELLO#3681
0:HELLO#3682
1:HELLO#3682
0:HELLO#3683
1:HELLO#3683
....
行頭の数値は出力しているコア番号です。文字列の出力はCore#0/Core#1の双方で実行されますので、出力元の区別が付く様にしています。
右端の数値は各コア上でループした回数になります。出力例ではUARTの制御権をコア間で交互に取得できているので、この様な出力になります。場合によってはCore#0だけ出力されたり、逆の場合もあります。
排他制御はcritical-sectionクレートで実現していますが、アーキテクチャーによる実装依存部はaarch32-cpuクレートのcritical_section.rsで実装されています。
armではSpin-Lockの実装にsev/wfe命令の利用が推奨されますが、ここでは利用されていません。これが原因かは不明ですが、片方のコアばかり制御権を取得する現象が見られます。
なるべく均等に制御権を取得させる目的で文字列出力のループ処理側でsev/wfeを実行しています。かなり微妙ですが、お気持ち程度の効果はありそうです。
出力先UARTの切り替え
Cargo.tomlの[features]でprintln!()の出力先が切り替え可能です。
default = ["uart-console-shared"] # 全てボード上のUARTに出力
default = ["uart-console-exclusive"] # コア毎にPmod USBUARTへ出力
uart-console-exclusiveに変更して再ビルドすると、println!()の出力がコア毎に分けられます。
ttc.binの実行
ボードへ電源投入後、u-bootから下記コマンドを実行します。
fatload mmc 0:1 0x10000000 ttc.bin;dcache flush;go 0x10000000
ttc.binはタイマ割り込み処理を実装しています。現状、割り込み要因の判定やハンドラ管理などの機能は実装されておらず、IRQ handlerを直に実装しています。
プログラムを実行すると、各コア上でGICのレジスタ状態をダンプ後、1秒周期で*を出力します。
タイマ割り込みの発生にはPS内蔵のTTC(Triple Timer Counters)を利用しています。割り込みハンドラはCore#0で実行され、割り込み発生毎にAtomicBool型のSIGNALをtrueに設定します。Core#1でSIGNALの変化を検知するとUARTへ文字を出力します。
実装概要
ソース構成
今回はライブラリクレートの形で実装し、src/bin/配下にExampleを置いてます。
/
├── build.rs - Build script
├── memory.x - Linker script
└── src
├── bin
│ ├── hello.rs - UART test example
│ └── ttc.rs - Interrupt test example
├── boot - Bootstrap codes
│ ├── mod.rs
│ ├── secondary.rs
│ └── startup.rs
├── cache
│ ├── l1.rs - L1-$ driver
│ ├── l2.rs - L2-$ driver
│ └── mod.rs
├── cpu
│ ├── mod.rs
│ └── register - System register access library
│ ├── mod.rs
│ ├── ttbcr.rs
│ └── ttbr1.rs
├── debug.rs - Debug library
├── device.rs - Define device instance
├── driver
│ ├── axi_gpio.rs - AXI GPIO driver
│ ├── mod.rs
│ ├── slcr.rs - SLCR driver
│ ├── uartlite.rs - AXI UART Lite driver
│ └── xpsuart.rs - PS UART driver
├── exception
│ ├── abt_handler.rs - Abt mode exception handler
│ ├── mod.rs
│ ├── svc_handler.rs - Svc mode exception handler
│ └── und_handler.rs - Und mode exception handler
├── lib.rs
├── macros.rs - Define macros
├── mmu.rs - Define the MMU description table
└── pl.rs - Define device instances in PL
ブートストラップ
Primary core
FILE: src/boot/startup.rs
u-bootからの起動を想定しており、最初にMMU無効化とキャッシュの初期化を行ってからプロセッサの初期化処理を行う形になっています。
Secondary core
FILE: src/boot/secondary.rs
Secondary coreの起動はLinuxの実装をベースに作成しています。
zynq7000-rtクレートにもSecondary coreの起動処理が実装されていますが、こちらはBootROMからの起動を想定しているらしく、うまく動作しませんでした。
本実装ではSecondary coreがアドレス0000_0000hから起動される為、このアドレスに起動用のベクタ・テーブルを書き込みます。この仕様により、Secondary coreを起動すると0000_0000h以降数バイトのメモリが破壊されます。
Secondary coreが起動されると、スタック・ポインタの初期化後にkmain_secondary()が実行されます。
キャッシュ制御
FILE: src/cache/*.rs
l1.rsはaarch32-cpuクレートで用意されているAPIのラッパーAPIです。aarch32-cpuではキャッシュ関連のAPIがジェネリック関数で実装されており、実行時にジェネリック・パラメータを指定する必要があります。パラメータ指定が少々面倒なのでラッパーを用意しました。
l2.rsは新規実装です。この他、L1/L2双方を操作するAPIとしてcache.rsを用意してあります。
キャッシュ制御はzynq7000-halクレートでも実装されていますが、過去の実装と名称等を合わせたいという個人的な理由で実装しています。
System register制御
FILE: src/cpu/register/*.rs
aarch32-cpuではSystem registerへのアクセスAPIが用意されていますが、一部APIが提供されていないSystem registerがあります。この未サポート分を自前で実装しています。これらのAPIはaarch32-cpuクレートと同じ形で実装しています。
デバッグ機能
FILE: src/debug.rs
panic!()やデバッグ用にLED制御APIとGICのレジスタ・ダンプ機能を提供します。
これらの機能は非常用・デバッグ用という事でデバイス・アクセスの排他制御は考慮していないので、利用時は注意が必要です。debug.rsで提供されるAPIは全てunsafeで定義されています。
デバイス定義
FILE: src/device.rs
システムで利用するデバイスのインスタンス定義がまとめられています。ここで定義されるのはPS上のデバイスのみです。各インスタンス定義はマルチ・コア間での排他制御の為、critical_section::Mutexを利用しています。
各種ドライバ
FILE: src/driver/*.rs
src/driver/配下には各種ドライバの実装が置いてあります。ほとんどのドライバではマルチ・コア間による排他制御は考慮していません。排他制御は利用側で対応する形を想定しています。
現状のドライバで提供している機能は、Exampleを動作させるのに必要な最低限の機能だけ実装されています。
例外処理
FILE: src/exception/*.rs
各種例外ハンドラを実装しています。例外処理はaarch32-rtクレートで実装されていますが、例外発生時のコンテキスト情報をハンドラに渡したかったので実装しました。
ハンドラに渡すコンテキスト情報はsrc/exception/mod.rsに定義してあります。
例外情報の出力例を下記に示します。
#### Undefined instruction exception ####
SPSR: 0x2000015F
PC: 0x100000F4
R0: 0x00000001
R1: 0xFFFFFFFF
R2: 0x00000000
R3: 0x00000001
R4: 0x3EADC414
R5: 0x10000000
R6: 0x00000002
R7: 0x00000000
R8: 0x3FFE8E18
R9: 0x3EAD1EC0
R10(SL): 0x00000002
R11(FP): 0x1FFFBFF8
R12(IP): 0x00007FFF
USR SP: 0x1FFFBFE8 LR: 0x1000441C
ABT SP: 0x1FFEC000 LR: 0x00000000
FIQ SP: 0x1FFF8000 LR: 0x00000000
IRQ SP: 0x1FFF4000 LR: 0x00000000
SVC SP: 0x1FFE4000 LR: 0x3FF013C4
マクロ定義
FILE: src/macros.rs
ここではprint!()とprintln!()を定義しています。コンフィグレーションで全コアで共通のUARTを使うかコア毎に専用のUARTを使うか、出力先が選択可能になっています。
ここの処理を変更する事でprintln!()の出力先が変更可能です。
MMU descriptor定義
FILE: src/mmu.rs
aarch32-cpuクレートをベースに実装しています。マッピングはセクション単位でのみ可能です。aarch32-cpuクレートではマッピング毎に処理を記述する形でした。ここではINITIAL_MAPの定義を変更する形でマッピングを変更可能にしています。
現状、MMU descriptorを静的に生成する機能のみで、システム動作時に動的にマッピングを変更する機能はありません。
PLデバイス定義
FILE: src/pl.rs
これはdevice.rs同様PL上にあるデバイスのインスタンス定義をまとめています。システムが変わればPLの実装も変わる為、PSとは別定義にしています。
改善点
デバイス単位での排他制御
今回の実装ではマルチコア間の排他制御にcritical-sectionクレートのMutexとwith()を利用しています。with()の実行中は排他制御が保証されますが、実装を見る限りGiant lockの形になっています。Mutexの定義単位では無く、critical-sectionクレート全体で排他制御が行われています。これはUARTとEthernetのドライバでcritical-sectionを利用すると、デバイス単位ではなくドライバ間でも排他制御が行われるので実行効率が悪いです。
ドライバの排他制御はデバイス単位が最適な粒度になるので、専用の実装が必要そうです。Atomic types(core::sync::atomic)が使えるので、Spink-Lock形式であれば実装の敷居は低いと思われます。
実行イメージサイズ
BINイメージを見ると2MiB程の大きさになります。この原因はmmu.rsで利用しているL1Table型です。この型はaarch32-cpuクレートで1MiBアライメントが指定されています。
SYS_L1_PAGE_TABLEはbuild_initial_l1_table()で作成しており、これをコンパイルした結果mmu.rsでは2つのL1Table型のデータが生成されます。
- SYS_L1_PAGE_TABLE
- build_initial_l1_table()内のdesc
この2つのデータアライメントを取った結果、.dataセクションのサイズが2MiBになっています。
サイズが気になる場合はSYS_L1_PAGE_TABLEの生成方法を変更する必要があります。
おわりに
とりあえずベアメタル開発に必要な最低限の環境が用意できました。今後はこれをベースに何か作る予定です。

