Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ZephyrRTOSのLongan Nanoへの移植

soburi
June 24, 2022

ZephyrRTOSのLongan Nanoへの移植

800円で買えるRISC-V開発ボードのLongan NanoへZephyrRTOSを移植します。

soburi

June 24, 2022
Tweet

More Decks by soburi

Other Decks in Technology

Transcript

  1. 自己紹介 ▪ 常田 裕士 ▪ 某F社勤務 昔はガラケー、今はカーナビ関連(主にLinuxを動くようにするお仕事) ▪ KiCadで雑に基板を作るチュートリアル (v6.0予習版)

    https://speakerdeck.com/tokitahiroshi/kicaddeza-niji-ban-wozuo- rutiyutoriaru-ver6-dot-x-yu-xi-ban ▪ インターフェース誌 2022年6月号 第2特集 C/C++でPython拡張 第1部 ハードウェア効率化…C/C++で拡張モジュール作り https://interface.cqpub.co.jp/magazine/202106/ ▪ 技術書典とかコミケとか。来年はNT金沢出しに行きたい。
  2. ZephyrRTOS ▪ LinuxFoundationがやっている組み込み向けのRTOS もともとはWindRiverが作った。割と素性は良い。 ▪ LinuxからKconfigとDeviceTreeの仕組みを借りてきている。 他にもいろいろLinuxにAPIなどは似せようとしている。 ▪ POSIX互換レイヤーもある。 ▪

    「組み込み用のLinux」と言っても当たらずとも遠からじ、みた いな雰囲気。 ▪ 最近はNordicSemiが自社の開発環境に組み込んで、 使い倒している。(メインの開発環境にしようとしている) 画像は https://ja.wikipedia.org/wiki/Zephy r_(オペレーティングシステム) より
  3. Longan Nano ▪ 小さなカラーLCDが付いて800円。秋葉原でも入手可能。 ▪ RISC-Vコア搭載のSoC, GigaDevice GD32Vを使用 ▪ もともとGigaDeviceにGD32というARMのマイコンのラインがあった。

    GD32は某S社のマイコンとピンコンパチ ▪ このGD32のARMのプロセッサ部をRISC-Vに差し替えたのが GD32V。RISC-VのコアはNuclei社(武漢)のIPを使用。 画像はhttps://www.switch- science.com/catalog/5946/ より
  4. Longan Nano の ZephyrRTOS対応の経緯 ▪ 2020年3月、技術書典 応援祭の出し物にすべくLongan Nanoへの ZephyrRTOS移植を実施、この時点で初期版はほぼ完成していた。 あわせて出し物として作成した(怪)文書をgithubにアップロード

    ▪ 2021年5月、フランス人の開発者@martoni氏 から問い合わせを受ける。上記 githubに置いた文書を仏語に翻訳して参照してもらっていた。需要が確認でき、まだ 誰もパッチ投稿していなかったので、昨年作成したコードを整理してPRを提出。 ▪ 2021年12月、upstreamへのマージ完了。
  5. archディレクトリの役割 ▪ archはx86, arm, riscvなどCPUアーキテクチャ毎の共通ソース。 ▪ 主に基本的な割り込みの定義と処理 ▪ 割り込みに関連して、コンテクストスイッチの処理もここで実装 ▪

    リセットの処理、CPU例外の処理、アイドル処理 ▪ コード量はそこまで多くない。ほとんどアセンブラ ▪ 命令セットを作らないと追加する必要がないので滅多に増えない。 ▪ 最近mipsが出来た。
  6. main()までの道 ▪ mainまで動けば、基本動作部分の 移植完了と言える。 ▪ OSとしての共通動作は既に実装され ている。SoC固有のブート処理、初期 化処理を作るのが移植のメインの作業。 ▪ 面倒だが、難しいというわけではない。

    ▪ この勉強会の高橋浩和さんの「RISC- V OSを作ろう」の1,2回あたりの内容 を参考にすると追いかけやすいはず。 ▪ mainが動いたら、SoCが持っているペ リフェラルのドライバを作っていく。 EntryPoint 割り込みハンドラの設定 __initialize スタックの初期化、マルチコアの処理 _PrepC BSS初期化, dataセクションをRAMにコピー z_cstart SoC初期化処理(主にclockの設定) ドライバ初期化 割込コントローラー タイマー mainスレッドの作成 main() archレイヤーで実装され ている共通処理(作らない) socレイヤーで実装する処 理
  7. Longan Nano (GD32V)対応で行ったこと。 ▪ 最低限動作させるために必要だったこと ▪ ブート部分の移植 ▪ 変則的なmcauseを正しくハンドリングする ▪

    Machine Timerのドライバ修正 ▪ 割込コントローラを動かす ▪ やらなくてよいこと(Zephyrですでに実装済みのもの) ▪ context switchやタスクなど、スケジューラに関する処理は実装済み、対応不要 ▪ BSS初期化などSoC固有でない初期化処理 ▪ 割り込みハンドラ共通部の実装 ▪ 素直なRISC-Vプロセッサならこの辺は不要となる場合もある。 (SiFive FreedomやQEMU環境ではほとんどやっていない。) ⇔QEMUと違うなら対応が必要。
  8. ブートの処理 #1 ▪ ブートコードを移植する。 ▪ RISC-V共通のブート実装はあるが、 SoC固有の「オマジナイ」的な処理があ ればファイルを分けて自前で実装する必 要がある。 /*

    Disable Global Interrupt */ csrc mstatus, MSTATUS_MIE /* Jump to logical address first to ensure correct operation of RAM region */ la a0, __nuclei_start li a1, 1 slli a1, a1, 29 bleu a1, a0, _start0800 srli a1, a1, 2 bleu a1, a0, _start0800 la a0, _start0800 add a0, a0, a1 jr a0 EntryPointのアドレスを見てRAM/FLASH実行の判定 (オマジナイ) soc/riscv/riscv-privilege/gd32vf103/entry.S
  9. ブートの処理 #2 ▪ 標準にないCSRの設定などをやる。 ▪ Nuclei社の拡張CSRのMMISC_CTLを設定 (0x200=NMIハンドラのアドレスにmtvecの値を共 有する) ▪ mtvecに割り込みハンドラを設定

    ▪ mtvecの下位2ビットを3に設定して、Nuclei社の ECLIC割り込みコントローラを使う設定を行う。(ISA では0, 1しか定義していない。) ▪ mcountinhibitを無効にする。(電力消費を抑える) ▪ 最後に__resetに飛ぶ。(__resetはそのまま __initialize)を呼ぶ。 /* Set the the NMI base to share with mtvec by setting CSR_MMISC_CTL */ li t0, 0x200 csrs CSR_MMISC_CTL, t0 /* Initial the CSR MTVEC for the Trap ane NMI base addr */ la t0, trap_entry csrw mtvec, t0 /* Direct Mode: All exceptions set pc to BASE. */ csrc mtvec, 0x3 /* Disable performance counter */ csrsi mcountinhibit, 0x5 /* Jump to __reset */ tail __reset soc/riscv/riscv-privilege/gd32vf103/entry.S
  10. ブートの処理 #3 ▪ archの共通部分で実装している __irq_wrapperが 64bit alignになっていないので、簡単なラッパを作る。 ▪ mtvecに渡せるアドレスが64bit alignを要求している

    ので、レジスタ定義もMODEのビット数を増やしている。 (ただし実質的には使っていない) soc/riscv/riscv-privilege/gd32vf103/entry.S .align 6 trap_entry: tail __irq_wrapper The RISC-V Instruction Set Manual Volume II: Privileged Architecture より Nuclei ISA Specより Field Bit Description ADDR 31:6 mtvec address MODE 5:0 •MODE field determine interrupt mode: • 000011: CLIC interrupt mode • Others: CLINT interrupt mode
  11. MCAUSEを正しくハンドリングする ▪ RISC-VのInstruction Set Manualの定義 では上位1ビット除いた残りのビットすべてが ExceptionCodeとして使われる。 が、Nucleiのコアだと11ビット目までが ExceptionCodeで、 上位ビットは別の用途で使われている。

    ▪ MCAUSEのマスク値を defineできるようにソース修正した。 The RISC-V Instruction Set Manual Volume II: Privileged Architecture より Nuclei ISA Specより Field Bit Description INTERRUPT 31 Current trap type: •0: Exception or NMI •1: Interrupt MINHV 30 Indicate processer is reading interrupt vector table MPP 29:28 privilege mode before interrupt MPIE 27 Interrupt enable before interrupt Reserved 26:24 Reserved 0 MPIL 23:16 Previous interrupt level Reserved 15:12 Reserved 0 EXCCODE 11:0 Exception/Interrup t Encoding
  12. 影響箇所 ▪ SOC_MCAUSE_EXP_MASKの値を configurableにした ▪ 割り込みハンドラの__irq_wrapperの中 で、mcauseの値を見て、処理の分岐を 行っている。 ▪ ECALL割り込みが正しく処理できなかった

    ので、コンテキストスイッチができなかった。 /* Get IRQ causing interrupt */ csrr a0, mcause li t0, SOC_MCAUSE_EXP_MASK and a0, a0, t0 /* * Clear pending IRQ generating the interrupt at SOC level * Pass IRQ number to __soc_handle_irq via register a0 */ jal ra, __soc_handle_irq /* * If the exception is the result of an ECALL, check whether to * perform a context-switch or an IRQ offload. Otherwise call _Fault * to report the exception. */ csrr t0, mcause li t2, SOC_MCAUSE_EXP_MASK and t0, t0, t2 /* * If mcause == SOC_MCAUSE_ECALL_EXP, handle system call from * kernel thread. */ li t1, SOC_MCAUSE_ECALL_EXP beq t0, t1, is_kernel_syscall arch/riscv/core/isr.S
  13. SoCの初期化処理 ▪ C言語で書ける初期化処理を登録する。 ▪ クロックの設定などは、これで行う場合が多い。 ▪ SYS_INITマクロで呼び出す関数を優先度を指定して登録する。 ▪ リンカで初期化処理専用のセクションにまとめられて、実行される。 ▪

    SoCベンダー提供のコードのクロック初期化処理の関数を呼ぶようにした。 SYS_INIT(gigadevice_gd32v_soc_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); soc/riscv/riscv-privilege/gd32vf103/soc.c
  14. 割り込み処理の呼び出し ▪ arch_irq_(enable|disable|is_enabled|prior ity_set) として割り込みの共通I/Fが定義されてい るので、この実装を入れ替える。 ▪ PLICの代替になるので、PLICと排他的にマクロで切 り分け。 ▪

    抽象化が何もされてないので、ドライバというよりかは 初期化処理の実装の単なるファイル分割。 ▪ SOCの初期化処理と同様、 SYS_INITマクロで登 録する。 void nuclei_eclic_irq_enable(uint32_t irq) { ECLIC_CTRL[irq].INTIE.b.IE = 1; } SYS_INIT(nuclei_eclic_init, PRE_KERNEL_1, CONFIG_INTC_INIT_PRIORITY); drivers/interrupt_controller/intc_nuclei_eclic.c arch/riscv/core/isr.S void arch_irq_enable(unsigned int irq) { uint32_t mie; #if defined(CONFIG_RISCV_HAS_PLIC) unsigned int level = irq_get_level(irq); if (level == 2) { irq = irq_from_level_2(irq); riscv_plic_irq_enable(irq); return; } #endif #if defined(CONFIG_NUCLEI_ECLIC) nuclei_eclic_irq_enable(irq); return; #endif
  15. Machine Timerを動かす ▪ スケジューラを動かすのにタイマー割り込みを使う。 ▪ RISC-VではCPUのクロックを使ったタイマー(Machine Timer)を持っている。 Zephyrにもドライバがある。 ▪ GD32Vでは、CPUクロックを4分周したクロックがこのタイマーに入力されている。

    ▪ ZephyrRTOSのRISC-V Machine Timerのドライバは、CPUの周波数と TickCount数から時間を設定している。動くには動くが、1/4倍速になってしまうので、 入力を1/4、出力をx4して帳尻を合わせる改造を行った。
  16. ドライバを作る ▪ 初期移植だと右図の 「動作確認に必要」あたりまで欲 しい。 ▪ 「Arduino程度」になると結構使 える。 贅沢品 CAN,

    Watchdog, QSPI, etc… Arduino程度 SPI, I2C, PWM, GPIO(入力) 動作確認に必要 UART, GPIO(出力) OSを動かすのに必要 Machine Timer SoCの一部 割り込みコントローラ
  17. DeviceTreeとソースコードとの連携 ▪ DeviceTreeはもともとLinuxの仕組み。 ▪ ARMの無秩序なパッチが多くて、Linus氏がブチ切れて導入された…と記憶 ▪ https://srad.jp/story/11/06/22/0911201/ ▪ デバイスの依存関係を専用の言語で記述して、実行時に解決する仕組み。 ▪

    依存性注入の手法。結果、ドライバとデバイスが依存しなくなる。 ▪ ZephyrRTOSでは、DeviceTreeはビルド時にC言語のマクロとして展開される。 ▪ RTOSの場合、メモリはstaticに確保するのが原則。(mallocが無いかもしれない) マクロに展開されたDeviceTreeの情報を使って、デバイス毎のデータ領域を静的に 確保するのがZephyrのドライバのお作法。 ▪ ここだけ抑えれば、ドライバはただの関数の集まり。
  18. DeviceTreeからマクロが生成される ▪ Zephyrのビルドの過程でDeviceTree のファイルからdevicetree_unfixed.h のヘッダファイルを生成する ▪ 専用のマクロ群で簡単に扱えるように なっている。 ▪ マクロ自体は複雑。

    エラー出るとデバッグは面倒。 dts/riscv/gigadevice/gd32vf103.dtsi から抜粋、編集 / { soc { usart0: serial@40013800 { compatible = "gd,gd32-usart"; reg = <0x40013800 0x400>; interrupts = <56 0>; interrupt-parent = <&eclic>; rcu-periph-clock = <0x60e>; status = “okay"; label = "UART_0"; }; usart1: serial@40004400 { compatible = "gd,gd32-usart"; … 生成されたdevicetree_unfixed.h #define DT_N_S_soc_S_serial_40013800_PATH "/soc/serial@40013800" #define DT_N_S_soc_S_serial_40013800_FULL_NAME "serial@40013800" #define DT_N_S_soc_S_serial_40013800_PARENT DT_N_S_soc #define DT_N_S_soc_S_serial_40013800_CHILD_IDX 3 …
  19. DeviceTreeの情報を使って、デバイス毎のデータを宣言する ▪ ドライバの実装側からは DT_DRV_COMPAT の宣言で対応する 種別を指定する ▪ デバイス毎に必要なデータを宣言するマクロを定義する。 ▪ 引数を使って、名前が被らないようにする

    ▪ 定数値はconfig, 可変値はdata。 ▪ DEVICE_DT_INST_DEFINEでデバイスを登録する。起動時 に初期化処理が行われる。 ▪ 初期化関数とAPIの関数ポインタテーブルを登録する ▪ DT_INST_FOREACH_STATUS_OKAY は、DeviceTreeで 有効なデバイスのマクロを展開して変数宣言を行う ▪ DT_INST_PROP系のマクロで、デバイスツリーの値を取得できる。 ▪ これで全部staticに処理できる。 drivers/serial/usart_gd32.c #define DT_DRV_COMPAT gd_gd32_usart #define GD32_USART_INIT(n) static struct gd32_usart_data usart_gd32_data_##n = { .baud_rate = DT_INST_PROP(n, current_speed), }; static const struct gd32_usart_config usart_gd32_config_##n = { .reg = DT_INST_REG_ADDR(n), }; DEVICE_DT_INST_DEFINE(n, &usart_gd32_init, NULL, &usart_gd32_data_##n, &usart_gd32_config_##n, PRE_KERNEL_1, CONFIG_SERIAL_INIT_PRIORITY, &usart_gd32_driver_api); DT_INST_FOREACH_STATUS_OKAY(GD32_USART_INIT)
  20. ドライバを使う ▪ ドライバで登録したデバイスは DEVICE_DT_GET(node_id)のマクロで取得でき る。 ▪ ZephyrのDevice Driver APIの各関数を 取得したデバイスをして実行する

    ▪ デバイスの種別に合ったAPIを使うこと。 static const struct device *uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE); void main(void) { if (!device_is_ready(uart_dev)) { printk("UART device not found!"); return; } /* configure interrupt and callback to receive data */ uart_irq_callback_user_data_set(uart_dev, serial_cb, NULL); uart_irq_rx_enable(uart_dev); } samples/drivers/uart/echo_bot/src/main.c から抜粋
  21. ARM/RISC-Vの命名規約が合わない ▪ 歴史的な経緯でRISC-VとARMのディレクトリ構成のルールが違っている。 ディレクトリ構成はCMakeの定義名に反映されるので、命名規約が合わない。 (ARMはSoCメーカー名ベースでディレクトリを掘っている, RISC-Vにはriscv-privilegeという中 間ディレクトリがあって名前がぐちゃぐちゃになっている。 riscv-privilegeって分類?) ▪ ARMのGD32シリーズとRISCVのGD32Vシリーズでドライバを共通化しているため、

    SoCの識別ルールが統一されていないとやりづらい。 ▪ SOC_FAMILY > SOC_SERIES > SOC 例) nordic_nrf > nrf51 > nrf51822 GD32Vでは) riscv-privilege > gd32vf103 > gd32vf103 (同じにする必要はない…) ▪ 決めの問題なので、意思決定できる人が決めないと決まらない。 寝てる間にDiscordで電話会議が行われていて、いつの間にか方針が決まってた。 (連絡は来てたが見逃した。結論は「近い将来ディレクトリ構成を直そう。」) ▪ 名前は地味に紛糾する。
  22. クローンチップであることによる足枷 ▪ クローン元のドライバには相乗りできない。 ▪ クローン元の会社さんも開発リソースを投入してZephyrにContributeしている。 ▪ ほぼ同じソースで動くのはわかっているが、仁義としてダメ。 ▪ 実際、RISC-VのIPコアを開発しているNuclei社の開発者はこの移植のdiscussionに 参加しているが、SoCベンダさんの開発者は一切タッチしていない。

    ▪ 故に、部外者の私が移植を行う余地があったとも言える。 ▪ GD32にアンタッチャブル感があったので、ひょっとしたら派手に横紙破りをやった格好? ▪ したがって、GD32Vの開発は「コミュニティドリブン」で進んでいる、という建て付け。 ▪ アクティブな開発者が新しい実装を試す実験場にしてる?! Please, do not mention STM32 at Zephyr. Everybody knows that there are GigaDevices that can replace STM32. というアドバイスをメールで頂戴した。
  23. RISC-V IPのSDKが別個に提供されている ▪ IPベンダーのNuclei社からRISC-V部のSDKが提供が 提供されている。 ▪ SoCベンダーのGigaDevice社はNuclei社から提供され たSDKを取り込んで、 SoCのSDKを作成しているのだが、版数が古い。 ▪

    現状は独自にレジスタの定義ファイルを作成するなどして、 Nuclei社のSDKに依存しないようにしている。 ▪ Nuclei社のコアを使った別のSoCの移植が行われるまで は問題にはならないはず。 Nuclei GigaDevice Nuclei社が最 新版を提供し ている…
  24. 参考文献 ▪ Zephyr Project Documentation ▪ https://docs.zephyrproject.org/3.1.0/ ▪ The RISC-V

    Instruction Set Manual Volume II: Privileged Architecture ▪ https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv- privileged-20211203.pdf ▪ Nuclei ISA Spec ▪ https://doc.nucleisys.com/nuclei_spec/ ▪ Nuclei Bumblebee Datasheet ▪ https://raw.githubusercontent.com/nucleisys/Bumblebee_Core_Doc/master/Bumblebe e%20Core%20Brief%20Manual.pdf ▪ Longan Nano のZephyr 対応事例紹介 増補版 ▪ https://techbookfest.org/product/6326080979861504?productVariantID=5338792099 577856&utm_campaign=share&utm_medium=social&utm_source=twitter