↓ メインコンテンツへスキップ

Rustが未対応だったC-SKY MCUでEmbassyを動かすまで

🌐 한국어 아티클 | English Article

本記事では、公開されているC-SKYアーキテクチャ、汎用QEMU環境、そして 市販のHLK-W806-KITで検証した内容だけを扱う。
csky-rust-buildroot: https://github.com/pmnxis/csky-rust-buildroot

EspressifのESP32シリーズはXtensa LXとRISC-Vを、GigaDeviceのGD32シリーズは Arm Cortex-MとRISC-Vを使用している。それらほど知られてはいないが、中国国内向けの 無線・家電用MCUにはC-SKY ISAを採用した製品もある。

簡単な無線・家電製品を調べるFerris

このような簡単な製品なら、8051やC-SKYが入っているのではないだろうか?

RustにはすでにTier 3のcsky-unknown-linux-gnuabiv2ターゲットがあり、LLVMにも C-SKYコードジェネレータがある。しかし、これはLinuxプログラムを作るための経路だ。 スタートアップコード、ベクタテーブル、割り込み、リンカスクリプト、coreを含む MCU bare-metal環境は用意されていなかった。

本記事では、csky-unknown-none-elfabiv2環境を構築し、GCCリンカを使わずに RustとLLVM/LLDでファームウェアをビルドし、QEMUのCK801・CK802・CK803・CK804・ CK805プロファイルと実機のWinnerMicro W806でEmbassy executor、timer、 embassy_sync::Channelを動かすまでの過程をまとめる。

まず結果から
#

2026年9月時点で確認できた範囲は次のとおりだ。

  • パッチを適用したRust/LLVMで、CK801、CK802、CK803、CK804、CK805向けの bare-metal Rustバイナリを生成できることを確認した。
  • パッチを適用したLLDでC-SKY little-endian ELF32をリンクし、最終リンクに GCC toolchainのldを使用しないことを確認した。
  • 汎用C-SKY QEMUで、reset、メモリ初期化、IRQ、compiler builtins、 Embassyのwake/timer/queueを含むworkloadを実行できることを確認した。
  • CK804でqueue容量8の飽和、繰り返しcancel、同時deadline、IRQ priority、 64-bit timebase wrapを確認した。
  • 標準Embassy Channelのping/pong taskがQEMUとHLK-W806-KITで動作した。
  • W806の1 ms TIM0 IRQでEmbassy time driverを駆動し、CPUがbusy-spinせず wait32から起床することを実機で確認した。

ただし、まだrustcにupstreamされた完成済みターゲットではない。公式rustupの rust-lldにも、このC-SKY bare-metal対応は含まれていない。以下の結果は プロジェクトローカルのcompilerとpatch seriesを使用した実験結果である。

なぜRustなのか?
#

単純に、CよりもRustが好きだからだ。

以前、関連する経験を Rust組み込み初回量産開発シリーズ にまとめた。何より、Rust組み込み製品を一人で開発し、実際の量産から市場への リリースまで自分の手で進めた経験がある。

C-SKYとCK80x
#

C-SKYは32-bit RISC ISAである。本作業はABIv2のCK80x MCUファミリを中心に進めた。 CK801/CK802は小規模MCU向けのプロファイルであり、CK803、CK804、CK805へ進むに つれて命令とCPU構成が変わる。ただし、これらの番号をArmv6-MやArmv7-Mへ一対一で 対応させてはいけない。ISA、ABI、exception entry、interrupt controllerのモデルが 異なるためだ。

命令セット、register、exception modelの詳細については、 C-SKY Architecture User Guide を参照した。

MCU runtimeの観点では、特に次の違いが重要だった。

  • 例外への進入と復帰では、PSR/EPSR/EPCおよびCPU/compilerのinterrupt-frame規則に 従う必要がある。ipush/ipopは、このcontext処理に使われるISA命令だ。公開W806 SDKのvectors.S はtrap進入時に汎用registerとEPSR/EPCを保存し、 tspend_handler はtask contextでEPSR/EPCを含む状態を保存・復元する。
  • アーキテクチャの基本exception vector数とSoCが提供する全vector数は区別する必要が ある。実際のMCUは48または64 entryを持つ場合がある。
  • VBR(Vector Base Register)の位置と、ROM vectorをRAMへ移すかどうかは chip/runtimeの方針である。
  • CPUのinterrupt enableと、SoC interrupt controllerのenable、pending、priority、 wake enableは別々の状態である。

公開ボードにはW800 Arduino、W803 Pico、HLK-W800、HLK-W806などがある。ボードごとに pinとflash配置は異なるが、それぞれが別のRust ISA targetというわけではない。 ここでは比較的入手しやすいHLK-W806-KITのW806、つまりXT804/CK804EF系を実機検証用に 選択した。

LLVM backendがあるのに、なぜ追加作業が必要だったのか
#

「LLVMにC-SKY backendがある」と「Rust bare-metalファームウェアを完成できる」は 同じ意味ではない。後者には、次の経路がすべて必要になる。

flowchart TB
    APP["1. アプリケーション / runtime

Cortex-M: Embassy + cortex-m + cortex-m-rt
C-SKY: C-SKY対応Embassy + csky + csky-rt"] TARGET["2. target / 基本ライブラリ

Cortex-M: rustc内蔵target + 配布済みcore
C-SKY: custom target JSON + build-std core / compiler_builtins"] TOOLS["3. codegen / link

Cortex-M: 配布rustc / LLVM Arm + rust-lld
C-SKY: project-local rustc / patched LLVM + patched ld.lld"] APP --> TARGET --> TOOLS
  1. アプリケーションとruntime
    Cortex-MではEmbassyがcortex-mと cortex-m-rtの上で動作する。C-SKYでは同じ役割を C-SKY対応Embassyとcsky/csky-rtが担う。 runtimeはreset entry、メモリ初期化、 vector table、割り込みの進入・復帰をアーキテクチャに合わせて提供する必要がある。

  2. targetと基本ライブラリ
    一般的なCortex-M target情報はrustcに内蔵され、coreはrustupで配布される。 C-SKY bare-metal targetはまだ公式配布物ではないため、custom target JSONでCPUや ABIなどを渡し、build-stdでcoreとcompiler_builtinsをソースからビルドする。

    参考リンク:

  3. codegenとlink
    rustcはClangでRustをコンパイルするのではなく、LLVMライブラリをcode-generation backendとして使う。Cortex-Mでは配布rustcのLLVM Arm backendとrust-lldを使える。 現在のC-SKY構成では、プロジェクトローカルrustcのパッチ済みLLVM C-SKY backendで objectを生成し、パッチ済みld.lldとlink.xでbare-metal ELFへまとめる。Clangは 同じLLVMエコシステムのC/C++ front endであり、このRustビルド経路には参加しない。

この研究では2つのLLVM treeを使用する。

  1. Rust sourceに含まれるLLVMはrustcのC-SKY codegenを担当する。
  2. standalone LLVM treeではld.lld、llvm-readelf、llvm-objdumpなどをビルドする。

custom target JSONはCPU、ISA feature、endian、pointer width、atomic capabilityを rustcへ渡す。rustupはcustom target向けcoreを提供しないため、 -Z build-std=core,compiler_builtinsで一緒にビルドする。no_stdはstdを使わない という意味であり、coreも不要という意味ではない。

LLVM/LLDで補強した部分
#

実際の適用順序とpatch fileは patches/llvmで確認できる。

  • assemblerの.ltorg/.pool literal-pool directive
  • little-endian ELF32 C-SKY LLD backendと初期relocation
  • PC-relative IMM16 relocationの範囲・alignment検査
  • 自然alignmentの8/16/32-bit atomic load/store codegen

最後の項目はEmbassyへ直接つながる。CK80xにcompare-and-swapがないことと、通常の atomic load/storeまで実行できないことは別問題だ。targetはmax-atomic-width = 32、 atomic-cas = falseで能力を表現し、LLVMは可能なatomic load/storeを生成する。 その結果、EmbassyへC-SKY専用queueを追加せず標準embassy_sync::Channelを使えた。

LLDのdynamic linking、TLS/GOT/PLT、ELF attributesのmerge、互換CPU間のe_flags merge、long-branch thunk、big-endian regressionはまだ完成範囲ではない。まずMCUの static bare-metal ELFを検証した。

cskyとcsky-rt
#

Armエコシステムのcortex-mと cortex-m-rtと同様に、2つの層へ分けた。

csky crateはCPU自体の機能を提供する。

  • PSR、VBR、SP、EPSR、EPC、CPUIDなどのarchitectural register API
  • interrupt maskとcritical-section backend
  • compiler barrierとhardware barrier
  • nop、waitなどの低レベル命令

csky-rtは、プログラムがmainへ到達する前の 処理とinterrupt entryを担当する。

  • reset handler、stack、.data copy、.bss初期化
  • #[csky_rt::entry] Rust entry macro
  • 32/48/64-entry vector layoutとdefault handler
  • flash vectorをRAMへ移すVBR policy
  • Rust IRQ handlerへ移行するassembly wrapper

W806のclock、GPIO、UART、TIM、VIC registerはSoC HAL/BSPの責任である。汎用runtimeは vector sizeとVBR policyを選択できるようにするが、特定製品のmemory mapは内蔵しない。

IRQ frameはCPU featureによって異なる
#

C-SKYのipushだけでは、Rust呼び出しに必要なすべてのregisterを保存できない。 wrapperはr15を追加で保存し、high registerを持つCK804E/EFではr18–r31も保存する。 反対に、それらのregisterを持たないCPUで同じframeを無条件に使ってはいけない。 したがってnested IRQ frameはCPU profileごとに分けて検証する必要がある。

※ W806はCK804EFだが、現在のtargetではLLVMのFPU instruction featureを無効にしている。

Embassy executorとidle
#

EmbassyはMCUでasync/awaitを使えるようにするRust embeddedエコシステムだ。 timerやprotocol state machineを複数taskへ分け、await境界を明確に表現する目的に適する。

主な変更はembassy-executorのarchitecture-specific部分だけに置いた。 embassy-time、embassy-time-driver、embassy-time-queue-utils、embassy-syncは 可能な限りそのまま使った。Channelの問題もEmbassyへ一時実装を追加せず、LLVMの atomic対応を修正して解決した。

idle動作は予想以上に深い問題だった。まずspin loopで正しさを確認してから csky::asm::wait()を適用した。interruptをmaskしたcritical section内でwait32を 実行すると起床できず、interrupt enable直後にsleepする境界にはcheck-then-sleep raceも存在する。

現在の実装は次の順序を使用する。

  1. interruptをmaskし、executorのwork flagを再確認する。
  2. workがなければ、隣接したpsrset ie → wait32で待機する。
  3. W806は1 ms recurring IRQを使うため、短い境界raceに入っても次のtickで進む。
flowchart LR
    CHECK["1. interruptをmask
work flagを再確認"] --> WORK{"workあり?"} WORK -- "あり" --> RUN["executorを実行"] WORK -- "なし" --> WAIT["2. psrset ie → wait32
interrupt enable後に待機"] WAIT --> IRQ["3. 今回または次の1 ms IRQ
境界race後もwake"] IRQ --> RUN

QEMUを先に使用した理由
#

QEMU patchの適用順序と役割は patches/qemuにまとめている。

実機だけではinterrupt frame、compiler codegen、peripheral設定の問題を分離しにくい。 QEMUなら小さなELFを作り、register、exception、memory状態を繰り返し検証できる。

初期のC-SKY QEMU 6.xを調べた後、XUANTIE QEMU 9 branchへ移行し、interrupt controllerのpending mask、priority、IRQ lifecycle、EPSR保存を補強した。

公開buildrootのQEMUは特定の市販MCUを複製したboard modelではない。汎用CPU、 interrupt、timer、UART、semihostingでcompiler/runtime/executorを検証する。 特定製品のperipheral、ROM、application protocolは含まない。そのためQEMU通過が 特定実機のclock・IRQ・peripheral互換性を自動的に証明するわけではない。

検証した項目
#

  • reset、stack、.data、.bss、vector base
  • critical section、繰り返しIRQ、callee-saved register canary
  • 256回のIRQ stressとcompiler_builtins
  • Embassy task wake、timer ordering、cancel/re-arm
  • 容量8のqueueを飽和させた後の9番目task完了
  • 同一deadlineのalarmと異なるinterrupt priority
  • counter wrap直前・直後のdeadlineと64-bit Embassy Instant

CK801、CK802、CK803、CK804、CK805の5プロファイルでworkload matrixを実行した。

CK802のE2命令を使う64-bit division/remainderでは、LLVM codeとQEMUの結果が一致しない 問題が残っている。一時的な-e2,+e1回避策をCK802 E2対応完了とは見なしていない。

※ E1とE2は、C-SKY ABIv2 toolchainで基本ISAの上に追加される拡張命令群を区別する 名称である。E1はCK801に対応する最初の拡張命令群、E2はCK802に対応する次の拡張命令群 であり、LLVMではE2がE1を含む。-e2,+e1はCK802 CPUの選択を維持したままE2専用 命令生成を無効にし、E1までの命令だけを使うLLVM target-feature overrideである。 CPUをCK801へ変更したり、ELF ABI v2をv1へ変更したりするoptionではない。

HLK-W806-KIT実機bring-up
#

今回の検証に使用したHLK-W806-KIT

本記事の実機検証に使用したWinnerMicro W806ベースのHLK-W806-KIT。

W806の主要構成は次のとおりだ。

CPU              XT804 / CK804EF family
application      0x0801_0400
vector RAM/VBR   0x2000_0000, 64 entries
UART0            115200 baud
Embassy tick     APB TIM0, IRQ 30, 1 ms recurring
idle             C-SKY wait32

最初はtimerが動かなかったため、CORET、VIC、priority、PSRを一つずつ確認した。 busy-spinでTIM0 IRQとping/pongが動くことを先に証明し、その後wait32を適用すると、 再び最初の待機で停止した。

原因はW806 VICのinterrupt enableとwake enableが別registerであることだった。 IRQ30をISERだけで有効にするとCPU実行中にはIRQが届くが、wait32中のCPUを起こせない。 VIC IWER0(0xe000_e140)にIRQ30 bit 0x4000_0000を設定すると、各tickで起床し executorをpollするようになった。

実機で確認した出力は次のとおりだ。

HLK-W806 Embassy multi-task started
ping: received=0, sent=1, elapsed_ms=250
pong: received=1, sent=2, elapsed_ms=1000
ping: received=2, sent=3, elapsed_ms=250
pong: received=3, sent=4, elapsed_ms=1000

この例ではDummyCounter { value: i32, time: Instant }を標準Embassy Channelで受け渡す。 pingは250 ms、pongは1,000 msをawaitする。この出力は次の各層が一緒に動作した根拠だ。

TIM0 hardware IRQ
  → csky-rt interrupt entry/frame
  → Embassy time driver and timer queue
  → C-SKY executor
  → embassy_sync::Channel
  → ping/pong futures

再現可能なcsky-rust-buildroot
#

この作業をcsky-rust-buildrootとしてまとめた。Linux Buildrootそのものではなく、 Cargo xtaskでcompiler、emulator、examplesを再現するsource buildroot兼example workspaceである。

# distributionごとのdependencyをインストールする。
cargo xtask deps

# pinしたRust/LLVM、standalone LLVM、QEMU、Embassyを取得してpatchする。
cargo xtask fetch

# ローカルrustc、LLVM/LLD tool、QEMUをビルドする。
cargo xtask tools

# 基本timer exampleをQEMUで実行する。
cargo xtask run

# 標準Embassy Channel multi-task exampleを実行する。
EXAMPLE=embassy-multi-task RUN_SECONDS=15 cargo xtask run

# QEMU transportのdefmt frameをリアルタイムでdecodeする。
DEFMT_LOG=trace RUN_SECONDS=15 cargo xtask run-defmt

# HLK-W806-KITへflashし、UART logを表示する。
./scripts/run-w806.sh multi-task ttyUSB0 --manual-reset

cargo run --exampleより長いのは、単一packageだけをビルドする作業ではないためだ。 project-local rustc、custom target、build-std、patched LLD、image generation、QEMU またはflashingを同じ組み合わせで選ぶ必要がある。host Cargoはorchestratorだが、実際の codegenは.local/rust/bin/rustc、linkは.local/bin/ld.lldが担当する。

本作業はLinux hostでのみ開発した。WindowsとmacOSでは個別にテストしていない。

今後の課題
#

  • CK801/CK802などのCPU profile選択とE1/E2 feature overrideの組み合わせが、実際の 対応命令範囲へ与える影響の確認(現時点では情報不足)
  • one-shot IRQにも適用可能なatomic idle/wakeup規則
  • FPUを有効にしたCK804EFのIRQ context save/restore
  • LLD relocation、attributes、thunk、endian対応の拡張
  • rustc/LLVM/LLDのupstream化とrustup配布経路
  • W806以外の公開C-SKY MCUにおけるclock、flash、IRQ、peripheral検証

QEMUとW806での成功を、すべてのC-SKY MCUの成功へ一般化してはいけない。CPU feature、 vector数、interrupt controller、wake register、ROM contract、memory mapはSoCごとに 異なる可能性がある。

おわりに
#

出発点は「LLVMにC-SKY backendがあるなら、target JSON一つで済むのではないか」という 考えだった。実際にはcodegen、linker、runtime、vector/IRQ ABI、atomics、executor、 emulator、SoC wake mechanismそれぞれへの理解が必要であり、それらを一つの連続した 経路へ接続するために多くの作業が必要だった。

現在はGCCリンカを使わず、RustとLLVM/LLDでC-SKY bare-metal ELFを生成し、QEMUの複数 CPU profileでregression testを行い、実機W806でwait32ベースのEmbassy executor、 hardware timer、標準Channelを実行できる。

この基盤によりC-SKYバイナリのテストと解析が可能になり、QEMU仮想環境へ専用のscratch registerを構成して外部状態や入力を注入することで、ファームウェアコードの動作と機能を emulation上で検証することもできる。今後のCI/CD regression testやC-SKYファームウェア 研究にも活用できると期待している。さらに多くの市販C-SKY MCUで検証することが次の目標だ。

関連資料
#