米国 Efinix社製 FPGAスタータキット
“Xyloni”入門[RISC-V編]

ソースコード完全公開!オープンソース CPUの組み込みからソフトウェア開発まで


前編の「導入編」では,新興FPGAメーカEfinix社と,その製品,開発ツール,およびマッチ箱サイズの超小型ボードXyloniを使った設計事例について,詳細に解説しました.

本稿「RISC-V」編では,同じくXyloni超小型ボードを使って,FPGAにRISC-VコアのSoC(System on a Chip)を実装してプログラム開発を楽しむ方法について解説します.なお本稿では,「導入編」で説明したFPGA統合開発環境Efinityのインストールが完了し,その使い方を概略理解されていることを前提にして説明します.

本稿に記載されている社名および製品名は,一般に開発メーカーの登録商標または商標です.本文中では TM,®,©の各表示を明記していません.

インデックス

  1. 米国 Efinix社製 FPGAスタータキット“Xyloni”入門[導入編]
  2. Efinix社のFPGA
  3. オープンソースな命令セットRISC-V
  4. Efinix社のRISC-V IPコア Sapphire SoCの概要
  5. Efinity RISC-V Embedded Software IDEをインストールする
  6. 超小型Xyloniボードで動かすRISC-V Sapphire SoCの仕様
  7. 超小型XyloniボードにRISC-V Sapphire SoCを実装する
  8. コンピュータ対戦型Tic-Tac-Toeゲームを作成しよう
  9. Xyloniボード上の全リソースを活用するプログラムを開発しよう
  10. FPGAとRISC-Vを大いに楽しもう
  11. 参考文献

2.Efinix社のFPGA

最初に,Efinix社のFPGAについておさらいしておきます.米国Efinix社は,2012年に誕生した比較的新しい新興FPGAメーカです.Efinix社の汎用FPGA製品は現在,Trion FPGAシリーズとTitanium FPGAシリーズが展開されています.

Trion FPGAシリーズ

Trion FPGAシリーズはモバイルやIoT向けの小~中規模なFPGAで,40nmプロセスで製造されています.

Trion FPGAの内部構造を図1に示します.プログラマブル・ロジックとルーティング配線はQuantumテクノロジをベースにファブリックとして構成され,内部に内蔵メモリや乗算ブロックを含み,その回りにI/Oインターフェースが配置されています.

Trion製品シリーズのロジック・エレメントの規模は,3,888個から112,128個まで用意されています.I/Oインターフェースは一般的なLVTTLインターフェースに加え,MIPI,LVDS,DDRをサポートします.Trionシリーズのうち小規模側の製品では,MPM(Mask Program Memory)をサポートしています.

FPGAのコンフィグレーション・データは通常,外部のシリアル・フラッシュ・メモリに記憶されますが,MPMではFPGA内部のオンチップ・マスクROMに記憶します.FPGAのコストが低減し,かつ部品点数を削減できるので,低コスト化が要求される大量生産用途に活用できます.Trionシリーズのパッケージは,FBGAとLQFPで供給されています.

図1 Trion FPGAのブロック(Efinix社ホームページから引用)

Titanium製品シリーズ

Titanium製品シリーズは,機械学習を含む高度なコンピューティング応用や高性能産業機器・通信機器向けの中規模~大規模なFPGAで16nmプロセスで製造されています.

Titanium FPGAの内部構造を図2に示します.Titanium FPGAも,プログラマブル・ロジックとルーティング配線はQuantumテクノロジをベースにしています.最大100万規模のLEと,大容量メモリ,およびAI処理に最適化したDSPブロックを内蔵した高性能ファブリックを搭載し,その周囲にI/Oインターフェースが配置されています.さらにハード化されたRISC-V CPUを4コア内蔵した製品も展開しています.

Titanium製品シリーズのロジック・エレメントの規模は,36,176個から969,408個という大規模な数まで用意されています.I/OインターフェースはTrionシリーズに対し大幅に強化されており,DDR4/LPDDR4,高速SerDes,PCI Expressなどもサポートしています.Titaniumシリーズのパッケージは,FBGAとWLCSPで供給されています.

図2 Titanium FPGAのブロック(Efinix社ホームページから引用)

FPGA基本技術:Quantumファブリック

Efinix社のFPGAの基本構造は図3に示す独自技術Quantumファブリックをベースにしています.

従来のFPGAは,ロジック・セルとインターコネクト(配線スイッチ)が独立していました.このため,ゲート量と配線量のバランスによってFPGA全体のリソース利用効率が悪くなるケースがありました.特に論理規模が大きくなるほど,配線の割合が増えてしまい,ロジック・セルが余っているにもかかわらず,配線が混雑して配置配線が閉じなくなることがあり,FPGA活用時の悩みの種でした.

これに対し,Efinix社のQuantumファブリックは,XLR(eXchangeable Logic and Routing )セルから構成されており,LUT(LookUp Table)ベースのロジックセルおよび配線スイッチのいずれとしても機能します.この構造により,ゲート過多の設計に対しても,配線過多の設計に対しても,ロジックと配線を柔軟に再構成し,インターコネクトの接続性を高めることができます.

また,XLRセルのロジック・セルは,複数の機能に変化(FPGA製品シリーズにも依存するが,例えば,単一のLUTを複数のLUTに分割したり,加算器にしたり,シフト・レジスタに設定するなど)できるので,より効率的に論理回路構造を構築することができます.

結果として,Quantumファブリックは,次のメリットがあります.

  1. 面積を1/2~1/4に削減可能
  2. 消費電力を1/2以下に削減可能
  3. 100 万LE(Logic Element)以上まで拡張可能
  4. 幅広なデータバスを構成可能
  5. データのパイプライン化が容易
  6. 全てCMOSパス・ゲートで実現しており,シリコン・プロセスに依存しない
図3 Efinix社FPGAの回路構造:Quantumファブリック(Efinix社ホームページから引用)

Efinix社は,自社のFPGAブロックをIPコア(Intellectual Property Core:回路部品)としてSoCベンダに供給するビジネスも展開しています.SoCに,ハードで固定化した機能だけでなく,FPGAによるフレキシビリティも搭載することができ,応用の幅を従来以上に拡大できることになります.

ここで特徴的なのが,Efinix社は,他社とは異なり,製造するファウンドリのプロセスをカスタマイズすることなく標準CMOSプロセスを使っている点です.FPGAブロックは,他のファウンドリのプロセスへの移植が容易と考えられるので,SoC設計者にとってファウンドリが固定されない点もメリットになっていくでしょう.また,Efinix社にとっても,コストメリットがある生産工場に移りゆくことができると思われます.

ちなみに,半導体不足だった折,Efinix社の製品は,ファウンドリのカスタム工程を使わずに済んだので,生産が滞ることがなかったそうです.

RISC-V Sapphire SoCの提供

RISC-Vは命令セットがオープン化されたCPUアーキテクチャであり,誰もが自由に設計できます.

そのため,従来のARMのように1社にCPUアーキテクチャが独占されることがなくなり,現在では多くの実装がカスタマイズ含め存在し,加速度的に普及が拡大しています.Efinix社も,RISC-V CPUコアと周辺機能を含むSapphire RISC-V SoCをIPとして提供しています.

ブロック図を図4に示します.このIPをFPGAに組み込むことで,好きな仕様のマイコンやシステムLSIを実現することができます.本稿では,この具体的な設計事例を解説します.

図4 RISC-V Sapphire SoCのブロック図(Efinix社ホームページから引用)

Efinix社のFPGA開発環境

Efinix社のFPGAでシステム開発する際に最も重要かつ基本となるツールが統合化開発環境Efinity IDEです.

無償で提供されており,論理シミュレーション,論理合成,配置配線,タイミング解析,コンフィグレーションまでのフローを一貫して実行します.論理シミュレーションは外部ツールを使いますが,デフォルトで無償のVerilogシミュレータIcarus Verilogをサポートしています.これ以外に,独Siemens社のModelSimや米Cadence社のNCSimもサポートしています.

Efinity IDEの画面の例を図5に示します.

図5 FPGA統合化開発環境Efinityの画面例

Efinix社のFPGAにRISC-V Sapphire SoCを組み込んだときのソフトウェア統合化開発環境として,Efinity RISC-V Embedded Software IDEが無償提供されています.

画面の例を図6に示します.C/C++言語のソース・コード編集から,プログラムのビルド,エミュレータによるシミュレーション,実機へのダウンロードとデバッグを行なえます.内容としてはオープンなEclipseベースの統合開発環境に,GNUツール・チェーンとOpenOCDを統合したものであり,RISC-V向けとしてはとても標準的で実績があるものです.

図6 RISC-V統合化開発環境Efinity RISC-V Embedded Software IDE

超小型開発ボードXyloni

FPGAを手軽に使いたいときは,既成の評価用ボードを活用するのが便利です.Efinix社もTrion FPGAとTitanium FPGAそれぞれが搭載された評価用ボードやI/O拡張ボードなどを多く準備しています.

その中で異彩を放つのがマッチ箱サイズのXyloniです.ボード本体の写真を図7に示します.ボード・サイズはわずか50mm×33mmと小さく手軽に扱うことができます.

Xyloni ボードは,Trion FPGAシリーズのうち小規模なT8を搭載しています.パッケージは5mm□のFBGAで端子数は81ボール (0.5mmピッチ)です.ロジック・エレメントは7,384 個あり,77kbのメモリ,8個の乗算器(18x18),5個の PLLを内蔵しています.これでも趣味の電子工作や簡単な実験には十分な構成であり,またRISC-V Sapphire SoCも搭載できるので,応用の幅はとても広いといえるでしょう.

本稿では,Xyloniボードを使ったRISC-V SoCの設計事例を紹介します.

図7 超小型開発ボードXyloniの外形

3.オープンソースな命令セットRISC-V

RISC-Vは,カリフォルニア大学バークレイ校(UCB)計算機科学科のKrste Asanovi$\acute{c}$氏が提唱したオープンで自由に使える命令セット・アーキテクチャ(ISA: Instruction Set Architecture)です.現在は,「RISC-V International」がRISC-Vの仕様を管理・公開しており,誰もがRISC-VのISAをベースにしたソフトウェアおよびハードウェアを自由に開発できます.

「RISC-V International」には,RISC(Reduced Instruction Set Computer)アーキテクチャの教科書 [1] [2] [4] で有名な David Patterson氏も参加しており,まさにRISC研究を集大成したアーキテクチャがRISC-Vです.

RISCアーキテクチャは1980年代から研究が始まり,RISC-Vはカリフォルニア大学バークレイ校が発表したRISC ISAの5番目のバージョンであることを表しています.

RISC-Vの本質

RISC-Vが登場するまでは,図8左のように,ARMアーキテクチャが非常に普及していて,スマホ,パソコン,家電機器,車載機器などのCPUコアとして大量に使われてきました.

皆さんの身の回りには,ARMコアで動いている機器がたくさんあるばずです.ARMアーキテクチャは実績が豊富で,CPUコアのハードウェアIPはもちろん,ソフトウェア開発環境,OS(Operating System),ミドルウェアなどのエコ・システム(生態系)が充実した一大文化を築いています.

しかし,アーキテクチャはARM社が独占しており,使用するにはARM社からCPUコアIP(RTL:Register Transfer Levelの論理記述)のライセンスを購入し,搭載チップの生産数に応じてロイヤルティを支払い続ける必要があります.ARMアーキテクチャのCPUを第三者が独自に開発することは可能ですが,非常に高価なライセンス費用が必要になります.

こうした中でRISC-V ISAアーキテクチャが登場しました.

RISC-VはISAすなわち命令セットを明確に規定した「仕様」そのものであることに注意してください.図8中央のように,CPUコアというハードウェアとしての実装は定義されていません.そして,RISC-VはそのISAがオープンであり,誰もがそれにもとづいたCPUコアを開発し,製品化できることが大きなインパクトを生みました.

CPUコアというハードウェアの実体はなくとも,ISAをインターフェースとして,多くのエコ・システムを開発していくことが可能です.すなわち,CPUアーキテクチャが自由化されたのです.この図8中央がRISC-Vの本質なのです.

結果として,図8右側のように,充実したエコ・システムを背景にして,自由な発想でさまざまなCPUコアが開発され,多くのSoCに搭載されるようになってきました.ローエンドからハイエンド,さらにはAI処理性能を強化したCPUまで,たくさんの実装が生まれています.

CPUベンダが工夫をこらしたプロプライエタリな有償コアが世に出てきていますが,無償提供されるオープンなIPもたくさん誕生しています.実績も増え,高品質で機能安全が要求される車載向けRISC-V CPUコアも登場しています.

図8 RISC-Vの本質

CPU命令のあるべき姿とRISC-V

ここでは,CPU命令のあるべき姿とRISC-V命令の関係を説明します[4]

低コストであること

ISAは単純なほうが論理回路がシンプルになりダイ面積が小さくなります.

RISC-Vは過去のアーキテクチャとの互換性を気にせず,シンプルで最適化されたアーキテクチャとして定義されています.実際に,同程度のコア同士で比較すると過去のしがらみがあるARM系よりも新しいRISC-Vの方がコア面積は小さくなる傾向があります.

またISAが単純化だと,設計工数や検証工数も削減できます.ARMv7には以下のような複雑な機能てんこもりな命令が存在します.

  1. 命令:ldmiaeq SP!,{R4-R7, PC} (Load Multiple Increment Address on Equal)
  2. 動作:Equal条件が成立していれば,5個のデータをメモリからロード(リード)して,6つのCPUレジスタにライトして,さらにリードした値の1つをPC(プログラム・カウンタ)にライトして分岐する.

こうした命令を互換性維持のために実装して検証するのは工数がかかります.

高性能であること

プログラムの実行性能は以下の3要因で決まります.小さい値の方が高性能です.

\begin{align} \frac{\text{命令数}}{\text{プログラム}} \times \frac{\text{平均クロック・サイクル数}}{\text{命令}} \times \frac{\text{時間}}{\text{クロック・サイクル}} = \frac{\text{時間}}{\text{プログラム}} \end{align}

RISC-VのようにISAが単純だと,複雑な処理には複数命令を組み合わせるため,プログラムあたりの命令数が増える可能性があります.

しかし,単純なISAは,命令あたりのクロック・サイクル数を減らせ,かつ動作周波数を向上させることができるため,結果として実行性能を向上できることが多いです.

実装方式と独立していること

かつてのISAは,チップの論理実装方式(マイクロ・アーキテクチャ)に依存して決めることが多かったです.

例えば,分岐時のロスを補うための遅延分岐命令がありました.しかし,これはパイプライン段数に依存した命令動作であり,CPUの実装方法によっては命令動作仕様を変更しないといけません.あるいは,スーパスカラ方式に適さない複雑な命令動作の実装などがされていた例もあります.

このようなISAはその世代の特定のチップにしか役立たないものです.ISAはその実装方式(マイクロ・アーキテクチャ)とは独立させて定義しておかないと,今後の実装方式の改善設計や命令機能の拡張性に影響を及ぼします.RISC-Vは,ハードウェアの実装方法に依存しないようにISAを定義しています.

ISA拡張の余地が多くあること

システム性能を向上させたいとき,特定の専用処理命令を追加したくなります.

ディープラーニング関係の命令,動画や音声の圧縮伸長関係の命令,グラフィック処理関連の命令,大規模行列演算命令などです.

ISA体系としての命令コード(機械語)のフォーマットには,拡張の余地をたくさん用意しておくべきであり,RISC-Vの命令フォーマットもそのようになっています.実際,多くの拡張命令が議論され定義され続けています.

コード・サイズが小さいこと

あるプログラムにおいて,その命令コード・サイズが小さいほど,システムに必要なメモリ・サイズが小さくなりコスト面で有利になります.また,コード・サイズが小さいほど,一定の処理あたりの命令メモリのアクセス数が平均的に減るので性能が向上する可能性もあります.

RISC-Vの基本命令は,32ビット長(4バイト)ですが,拡張命令として16ビット長(2バイト)命令「C」が定義されており,そららを効率的に混在させることでコード・サイズを小さくすることができます.

コンパイラ/リンカとの親和性が高いこと

コンパイラは人間が理解しやすいC言語などのソース・コードをCPUが取り込める機械語(命令コード)に変換するツールです.

関数の引数(int func(int a, int b)などのaやbが引数)をメイン・ルーチンからメモリを介して引き渡すと,メモリ・アクセスは一般に遅いので引数が多い場合に性能低下を招きます.このためできるだけCPU内のレジスタを介して引き渡すべきであり,ISA定義においてCPU内部レジスタを多くしたほうが有利です.

RISC-Vの基本命令は32本の汎用レジスタを用意しています.さらに,各命令は基本的に1サイクルで実行できるようにした方が,コンパイラとして性能の最適化(見積もり)がやりやすく,RISC-VはシンプルなRISCアーキテクチャでもあり,原則として1命令は1サイクルで実行されます.

コンパイラが出力した機械語列は,まだ実際の命令メモリのアドレスにアサインされていません.リンカというツールが,単一または複数のソース・コードからコンパイラが生成した複数組の機械語列を合体して実際の命令メモリのアドレスにアサインします.

そこで,コンパイラが生成した機械語列は,任意の命令アドレスにアサインできるように,リロケータブルになっている必要があります.これには,例えば分岐命令の分岐先を絶対アドレスではなく,分岐距離(相対アドレス)で指定できるようなISAが必要です.もちろん,RISC-Vはこの特徴をしっかり具備しています.

RISC-V命令セット概要

RISC-V命令セットの種類

RISC-V CPUの命令セットは,表1に示すように,基本命令と拡張命令の組み合わせで構成されます.基本命令は,32ビット・アーキテクチャとしてRV32IまたはRV32E,64ビット・アーキテクチャとしてRV64I,128ビット・アーキテクチャとしてRV128Iが定義されています.

基本命令RV32Eは,命令の種類はRV32Iと同じですが,汎用レジスタ本数を削減した縮小版アーキテクチャです.RV32Eはハードウェアを削減する必要があるローエンド・マイコンなどに適したアーキテクチャです.

性能向上やコード・サイズ削減のために,拡張命令が多数定義されています.代表的な拡張命令として,高速な乗算や除算・剰余を演算する拡張命令「M」,単精度浮動小数点(32ビット)演算を実行する拡張命令「F」,コード・サイズ削減効果が高い16ビット長の拡張命令「C」などがあります.

例えば,基本命令RV32Iに,拡張命令「M」「C」を追加したISAを「RV32IMC」と表記します.この表記を見れば,どういう命令が含まれているか一目でわかるようになっています.現時点も,命令セットの拡張に関する議論が継続しています.

表1 RISC-Vの命令セット種類 [5]

RISC-Vの命令長

RISC-Vの命令コードは,現時点では32ビット長と16ビット長の2種類が規定されています.

  1. RV32C:16ビット長
  2. RV32Iなどそれ以外:32ビット長

図9に示すように,RISC-Vの命令長は将来の拡張のため,LSB側のaaやbbbなどで命令長を規定し16ビット単位で増やすことが可能です.長いビット長の命令コードでも,リトルエンディアン配置の場合,CPUはメモリに配置された命令コードをLSB側からフェッチするので,先に命令長を知ることができます.

図9 RISC-Vの命令長 [5]

RISC-VのCPUリソース

32ビット基本命令RV32Iが持つ汎用レジスタを図10に示します.

32ビット幅の汎用レジスタx0~x31とプログラム・カウンタPCがあります.各レジスタの右側に,C言語でプログラムを組んだ場合のそのレジスタの一般的な用途を記しました.このうちx0だけは特殊で,ハード的にゼロに固定されておりライトしてもゼロのままですが,このレジスタをうまく使うと命令種類を増やすことができます.

縮小版アーキテクチャの基本命令RV32Eでは,汎用レジスタはx0~x15の16本だけ使います.

本稿では説明を省略しますが,RISC-VのCPUリソースは,汎用レジスタx0~x31とプログラム・カウンタPCに加えて,CPUの状態を示したり,例外処理や割込みなどを制御する専用レジスタCSR(Control Status Register)が複数定義されています.専用命令(CSRRW命令など)を使って12ビット・アドレスで指定してアクセスします.

図10 RISC-V RV32Iの汎用レジスタ [5]

RISC-Vの特権モード

ユーザ・プログラムが,システム内の全てのリソースをアクセスできるとシステムを破損したり,悪意のあるプログラムによるアタックを許してしまいます.これを防ぐため,一般のCPUには複数の特権モードがあり,それぞれできることに制限を与えています.RISC-Vにも表2に示す特権モードが定義されています.

ローエンドな組み込み向けプロセサは,ROMにファーム・ウェアが固定され,ユーザが任意のプログラムをロードする仕組みがないため,一般的に特権モードとしては制限のない1種類(RISC-Vの場合,マシン・モード)しかないことが多いです.

標準RISC-V仕様では表2に加え,デバッグ・モードも定義されています.デバッガからの要求や,ブレーク条件発生により,CPU命令実行が一時的に停止しデバッグ・モードに入ります.その状態で,デバッガからCPU内リソースやメモリ・周辺レジスタをアクセスすることができます.コアの実装方法によっては,バッグ・モードに入らなくても,デバッガから各種リソースのアクセス可能なものもあります.

表2 RISC-Vの特権モード [5]

RISC-VのRV32I基本命令

基本命令RV32Iの命令フォーマットを図11に,命令種を図12に示します.この基本命令だけで完全なコンピュータとして成立しており,C言語でプログラムを組むことができます.

基本的なアーキテクチャがRISCなので,メモリ・アクセス命令と演算命令は明確に分離されています.すなわち,メモリ・アクセスと同時に演算実行はしない,ロード・ストア・アーキテクチャを採用しています.メモリ・アクセスする際のアドレッシングは,オフセット(符号拡張12ビット)付きレジスタ間接だけをサポートしています.

汎用レジスタへの即値(定数)ロードについては,32ビット固定長命令の中に32ビット定数を入れて,同時にロード先のレジスタ指定や命令コードを押し込むことはできないので,次の方法が取られます.

  • 汎用レジスタの上位20ビット(bit31~bit12)への即値ロード命令
  • 符号拡張12ビット(bit11~bit0)の即値を汎用レジスタに加算する命令
  • PCの上位20ビット(bit31~bit12)に即値を加算した値を汎用レジスタにロードする命令

一般のCPUには条件分岐のためのフラグ(演算結果により反映されるキャリー,ゼロ,オーバフロー,負数,などのフラグ・ビット)がありましたが,RISC-Vにはそうしたフラグはありません.条件分岐は,汎用レジスタの数値比較結果(大・小・一致・不一致)で分岐するかどうかを決めています.

サブルーチン・コール命令は,戻り先をスタック・メモリに格納せずに,汎用レジスタ(x1を推奨)に格納するようになっています.

図11 RISC-VのRV32I命令フォーマット [5]
図12 RISC-VのRV32I基本命令 [5]

4.Efinix社のRISC-V IPコア Sapphire SoCの概要

Efinix社は,RISC-VアーキテクチャCPUをコアに持つSapphire SoCをIPとして提供しています.Sapphire SoCは,32ビットRISC-V CPUコアと,内部メモリ,外部拡張バス,内蔵周辺機能,ユーザ機能拡張インターフェースなどを搭載しており,それぞれユーザの要求仕様にもとづいてコンフィグレーションが可能です.Efinix社のTrion FPGAおよびTitanium FPGAに簡単に実装できるように,開発環境も整備されています.

Sapphire SoCの機能仕様

Sapphire SoCはユーザー側で機能設定が可能な高性能・高機能なSoC IPです.ブロック図を図13に示します.開発ツールEfinityのIP Editorにより,必要な機能構成やペリフェラルを選択し,SoCをカスタマイズすることができます.

図13 Sapphire SoCのブロック・ダイアグラム(Efinix社ホームページから引用)

Sapphire SoCの各機能仕様を以下に示します.

  1. VexRiscvコア:6段パイプライン・ステージ(フェッチ,インジェクタ,デコード,実行,メモリ,ライトバック),割込処理,例外処理,特権モードはマシン・モードのみ,コア個数は1~4
  2. システム・クロック:20M~400MHz
  3. オンチップRAM:1K~512KB(SPI FLASH用のブート・ローダを含む)
  4. DDR3,LPDDR4xまたはHyperRAMメモリ・コントローラ
    • メモリ・サイズ・サポート:4MB~3.5GB
    • ユーザ設定可能な外部メモリ・バス周波数
    • 外部メモリ・インターフェースのための,半二重AXI3インターフェース(最大512ビット)×1,または全二重AXI4インターフェース(最大512ビット)
    • DDR3クロック周波数:400MHz,800Mbps
    • LPDDR4xクロック周波数:1,089MHz,2,178Mbps
    • HyperRAMクロック周波数:200MHz,400Mbps
  5. ユーザ・ロジック用のAXIマスタ・チャネル(最大2系統,データ幅32ビット~512ビット)
  6. ユーザ・ロジック用のAXIスレーブ・チャネル(1系統)
  7. 最大8-Wayの命令キャッシュとデータ・キャッシュ
  8. 浮動小数点ユニット(FPU)
  9. Linux用のMMU(メモリ・マネジメント・ユニット)
  10. カスタム命令セット1024ID(様々な機能を実現)
  11. RISC-V拡張命令(オプション):アトミック命令(A),16ビット長短縮命令(C)
  12. APB3ペリフェラル機能
    • GPIO:最大32本
    • I$^2$Cマスタ:最大3個
    • CLINT(Core Local Interrupt)タイマ
    • PLIC(Platform Level Interrupt Controller)
    • SPIマスタ:最大3個(最大25MHz)
    • タイマ:最大3個
    • UART:最大3個(115,200bps)
    • APB3ユーザ・ペリフェラル:最大5個
    • ユーザ割込み:最大8本

Sapphire SoCの開発フロー

Sapphire SoCを開発するための環境全体を図14に示します.Efinity社は,Sapphire SoCのハードウェアとソフトウェアをシームレスに開発できる環境を無償提供しています.

図14 Sapphire SoCの開発環境(Efinix社ホームページから引用)

Sapphire SoCの大まかな開発フローを以下に示します.

  1. FPGA統合開発環境EfinityのIP Editor上でSapphire SoCの機能を選択してコンフィグレーションすると,対応するRTL記述と,ソフトウェア開発のためのBSP(Board Support Package)とサンプル・プログラムが自動生成される.
  2. FPGA統合開発環境EfinityのInterface Designerで,I/Oポート,PLL,JTAGなどFPGAの外部インターフェース関係を設計する.
  3. FPGA統合開発環境Efinityで,Sapphire SoC IPとユーザ機能を含むトップ階層のRTL記述,およびタイミング制約ファイルを作成する.
  4. FPGA統合開発環境Efinityで論理合成,配置配線,ビット・ストリーム・データ生成を行なう.
  5. FPGA統合開発環境EfinityのProgrammerでビット・ストリーム・データをFPGAボードにダウンロードする.
  6. RISC-V統合化開発環境 Efinity RISC-V Embedded Software IDEの上で,アプリケーション・プログラムを作成し,デバッガ経由でSapphire SoCの命令メモリ(SRAM)へダウンロードし,実行,デバッグを行なう.
  7. 開発したRISC-Vプログラムを,FPGAの電源投入時にすぐに実行させることもできる.作成したRISC-Vプログラムのバイナリ・ファイルをFPGAのビット・ストリーム・データと合体してFPGAボードにダウンロードしておくことで,ブート・ローダがFPGA起動時に自動的に命令メモリ(SRAM)にプログラムのバイナリをダウンロードして起動してくれる.

5.Efinity RISC-V Embedded Software IDEをインストールする

ここで,RISC-Vソフトウェア統合化開発環境Efinity RISC-V Embedded Softwae IDE(以下,RISC-V IDEと記載)をインストールしておきましょう.まずは,Efinix社のホームページの Support Centerをアクセスしてください(図15).すでにFPGA統合化開発環境Efinityをインストール済みだと思うので,ユーザ登録はされていると思います.

次に,中央のEfinity Softwareの列のDownload Software / Current Version:...をクリックして,図16に示すダウンロード・サイトに移動してください.RISC-V IDE本体のインストーラ:Efinity RISC-V Embedded Software IDEのWindows版efinity-riscv-ide-2023.1.3-windows-x64.msiをダウンロードしてください.

図15 Efinix社Support Center(一部加工)
図16 Efinix社IDEダウンロード・サイト(一部加工)

ダウンロードしたインストーラを起動して画面の指示に従ってインストールしてください(図17).これでRISC-V IDEのインストールが完了しました.

図17 Efinity RISC-V Embedded Softwae IDEをインストール

6.超小型Xyloniボードで動かすRISC-V Sapphire SoCの仕様

超小型Xyloniボードの概要

Xyloniボードの外形

マッチ箱サイズの超小型FPGAボード,Xyloniボードの部品配置について,表面を図18に,裏面を図19に示します.ユーザ・インターフェースとして,表面にUSBコネクタ,コンフィグレーション用プッシュ・ボタン,ユーザ用プッシュ・ボタン(2個),ユーザ用LED(黄色×4個),裏面にSDカード・スロットが実装されています.

図18 Xyloniボードの表面(Xyloni Development Kit User Guideから引用)
図19 Xyloniボードの裏面(Xyloni Development Kit User Guideから引用)

Xyloniボードの機能

Xyloniボードのブロック図を図20に示します.

USBコネクタには,英FTDI社のFT4232H-56Q(Hi-Speed Quad USB UART IC)が接続されており,下記4つのインターフェースを構成しています.

  1. FTDI interface 0 = SPI:ボード上のSPI NOR FLASHメモリへの書き込み
  2. FTDI interface 1 = JTAG:FPGAのコンフィグレーション,搭載RISC-Vのデバッグ
  3. FTDI interface 2 = UART:ユーザ用シリアル通信
  4. FTDI interface 3 = VCCIO設定:FPGAのBANK2A,BANK2Bの電源電圧設定(3.3V,2.5V,1.8V)

これらにより,PC上の複数の開発ツールとスムーズにインターフェースを取れます.FPGAのコンフィグレーションはもちろん,RISC-Vコアを実装したときのプログラムのダウンロードやソース・レベル・デバッグ,UART(Universal Asynchronous Receiver/Transmitter)によるシリアル通信が可能です.本稿ではこれらの動作を実際に試してみます.なお,USB経由でFPGAの一部のバンクのI/O電源電圧を設定することができますが,本稿では使いません.FPGAのI/Oバンクの電源電圧は全て3.3Vとします.

Xyloniボード上には33.33MHzのクロック発振器が搭載されています.そのクロックはTrion T8 FPGAのPLLIN端子(GPIOL_20)に接続されています.FPGAの内蔵PLLを使って好きな周波数のクロックを生成して内部論理で使うことができます.

Xyloniボードに搭載されているユーザ用プッシュ・ボタン(2個),ユーザ用LED(黄色×4個)の制御信号,SDカードとの通信用SPI信号,拡張端子の信号は,それぞれTrion FPGA T8に接続されており,ユーザ論理で制御可能です.

本稿では,FPGAにRISC-VコアのSapphire SoC IPを実装し,Xyloniボードに載っているリソースを全て使えるようにしてみます.

図20 Xyloniボードのブロック図

XyloniボードにおけるFPGAのコンフィグレーション

Xyloniボードの上でTrion T8 FPGAをコンフィグレーションする方法はいくつかありますが,デフォルトでは次の方法が取られます.

まず,PCからUSBを介してビット・ストリーム・データを送り,Xyloniボード上のSPI NOR FLASHメモリに書き込みます.Trion T8 FPGAは,アクティブ・コンフィグレーション・モードに設定され,その起動時にNOR FLASHメモリを読み出して自分自身をコンフィグレーションします.

Trion T8 FPGAのコンフィグレーションはCRESET信号をLowレベルからHighレベルに立ち上げると開始します.Xyloniボード上ではCRESET信号はプルアップ抵抗とコンデンサによる簡単なパワーオン・リセット回路から生成されているので,SPI NOR FLASHメモリにコンフィグレーション・データが書かれた状態でXyloniボードのUSBケーブルから電源を印加すると,自動的にコンフィグレーションが始まります.あるいは,ボード上のコンフィグレーション用プッシュ・ボタンCRSTを押すとCRESET信号がLowレベルになるので,Trion T8 FPGAをコンフィグレーション含めてリセットしたいときはCRSTボタンを押します.

コンフィグレーションが完了するとCDONE端子(プルアップされたオープン・ドレイン端子)がHighレベルになり,ボード上のLED CDN(緑色)が点灯して,FPGA内部のユーザ回路が動作開始します.

ただし,CDONEがHighレベルになってから実際のユーザ回路が動作可能になるまでは$tUSER$(15μs+CDONE端子がLowレベルからHighレベルになるまでの立上り時間)以上待つ必要があります.すなわち,この期間はユーザ回路をリセット状態にしておかなくてはなりません.外部からリセット信号を入力する場合は,CDONE信号がHighになった後も$tUSER$の期間はリセットをアサートし続けるようにしてください.

内部でリセット信号を生成する場合は,PLLのロック信号を使えますが,PLLがロックしてから$tUSER$の間は内部リセット信号をアサートし続ける回路が必要です.本稿の設計事例ではこのための回路も組み込んであります.

Trion FPGA T8に実装するSapphire SoCの仕様

超小型ボードXyloniに搭載されたFPGAはTrionシリーズのT8であり,内蔵するロジック・エレメントは7,384個,メモリは77kbです.Sapphire SoCのコンフィグレーションとしてはかなり絞る必要がありますが,マイコンとして必要な最小機能は実装できます.本稿でXyloniボードのT8に実装するSapphire SoCの仕様を表3に示します.

表3 本稿でXyloniボードのT8に実装したSapphier SoCの仕様

RISC-VコアはRV32IMC

Sapphire SoCのRISC-V CPUコアは,32ビット基本命令RV32Iに対して,乗算・除算命令(M)はデフォルトで拡張されています.ここに16ビット長縮小命令(C)を拡張したRV32IMCを命令セットとします.マルチ・コアではないので,アトミック命令(A)は実装しません.

デバッガはRISC-V標準

RISC-Vコアのデバッグ・インターフェースは,RISC-V標準仕様デバッガを搭載します.

JTAG端子経由でOpenOCDから操作して,プログラムのロードやCソースレベルのデバッグを行なうことができます.なお,命令メモリがRAMであり,ブレークはブレーク箇所をブレーク命令で書き換えるソフトウェア・ブレークが使えるので,ハードウェア・ブレークは実装しません.

内蔵メモリは4KB

FPGAの内蔵RAMは77kb(≒9KB)あるので,RISC-Vの命令メモリとデータ・メモリとしては合計8KB程度実装できそうですが,実際にはCPUの汎用レジスタやその他の論理リソースとして内蔵メモリがアサインされてしまうので,RISC-Vのプログラム用メモリ(命令とデータ兼用)は4KBしか実装できません.しかし,RISC-Vは縮小命令のおかげでコード効率が良いため,RISC-Vのプログラミング・ワールドを十二分に楽しめると思います.

UARTとSPIを搭載

通信インターフェースとしては非同期通信用のUARTを1チャネル,同期通信用のSPI(Serial Periheral Interface)を2チャネル搭載します.

UARTはユーザ用で,PC上のターミナルなどと通信ができます.SPIは2チャネルともマスタ通信機能を持たせます.SPIの1チャネルはプログラムをXyloni基板上のシリアルNOR FLASHメモリから内蔵RAMにブートするのに使います.SPIの残りの1チャネルはユーザ用でXyloni基板に挿し込めるSDカードのアクセスに使います.

GPIOは32本

汎用入出力GPIO(General Purpose Input/Output)は,Sapphire SoCとして最大の16本×2chを用意します.Xyoni基板上のプッシュ・ボタン,LEDおよび,外部拡張端子に接続します.

Sapphire SoCから省いた機能

2線式シリアル・バスI$^2$C(Inter Integrated Circuit)機能はSapphire SoCに内蔵できますが,Xyloni基板上にI$^2$C通信するデバイスが搭載されていないので本稿では無しとしました.

ユーザ用タイマも内蔵できますが,本稿では搭載していません.タイミグ生成等に使う周期割込みは,CPUコアに内蔵しているCLINT(Core Local Interrupt)から発生させることができます.

Sapphire SoCに搭載した周辺機能(UART,SPI,GPIO)からは割込みを発生させることができます.一方,ユーザ用の割込み要求信号をさらに追加できますが,本稿では無しとしました.

Sapphire SoCは,内蔵している周辺機能では不足する場合に備え,SoC IP外部にユーザ周辺機能を追加するためのバス(APB3,AXI4)を出すことができます.本稿では,Sapphire SoC内蔵された周辺機能だけを使うので,バス拡張はしません.

Sapphire SoCを大規模FPGAに搭載する場合は,今回省いた機能に加え,高速外部メモリ・インターフェース,キャッシュ・メモリ,浮動小数点演算命令など,高性能な機能をたくさん追加できます.

XyloniボードへのSapphire SoCの実装

XyloniボードのTrion T8 FPGAへのSapphire SoCの実装方法を図21に示します.

Efinix社のFPGAは,外部とインターフェースするデバイスI/Fと内部のユーザ論理を明確に分けて設計します.

外部とインターフェースするデバイスI/Fは,FPGA統合開発環境EfinityのInterface Designerで定義します.ここには,外部端子,PLL,JTAG(Joint European Test Action Group)インターフェースなどが入ります.

デバイスI/F内のPLL部は,専用の入力クロック端子がアサインされています.出力クロックは,その周波数をInterface Designerで指定すれば,クロックの逓倍率や分周比を計算して自動的にコンフィグレーションしてくれます.PLLから,ロックしたことを知らせる信号PLL_LOCKEDを出力でき,これをユーザ論理内の内部リセット生成部で,FPGAのコンフィグレーション待ち時間$tUSER$以上引き延ばしてユーザ論理用パワー・オン・リセット信号resorgを生成しています.

デバイスI/F内のJTAGは,FPGAをコンフィグレーションした後,ユーザ側で使うことができます.JTAG信号TCK,TMS,TDI,TDOをそのままスルーさせるだけでなく,TAP(Test Access Port)のステートマシンの状態を出力することができるので,JTAGインターフェースを使うユーザ論理をシンプルに作ることができます.Sapphire SoCでは,RISC-Vのデバッグ用インターフェースしてJTAGを使います.

今回のFPGA内のユーザ論理は,前述の内部リセット生成部とSapphire SoCのIPブロックCORE.vをインスタンス(U_CORE)化してそのまま搭載したシンプルな構成になっています.なお,JTAGデバッガから制御できるリセット信号ressysをSapphire SoC IPから出力していますが,本設計事例ではIP外に機能モジュールがないので使っていません.

図21 Sapphire SoCを実装するFPGA内のブロック図

コラム:Sapphire SoCのSPI端子

Sapphire SoCのSPIはマスタ通信用で,2線全二重通信,2線半二重通信,4線半二重通信をサポートしています.

2線通信の場合,マスタ機能であればデータ端子の向きは,MOSI(Master Output/Slave Input)は出力方向で,MISO(Master Input/Slave Output)は入力方向になります.しかし4線半二重通信もサポートしているので,データ端子4本は双方向になります.Sapphire SoCのSPIのデータ線は,io_data_0~io_data_3の4本あり,2線通信の場合,MOSIはio_data_0,MISOはio_data_1を使います.

本稿では,Sapphire SoCのSPIは2線通信機能として使うので,io_data_0を出力専用として,io_data_1を入力専用として使いますが,Sapphire SoC IPのSPIデータ信号は双方向制御信号も出力されているため,デバイスI/F内のSPIデータ端子は全て入出力端子として定義しています.

また,SPI関係の入出力信号には全てRegister属性を付与して,デバイスI/F内でフリップ・フロップで叩くように指示されています.こうすることで,シリアル同期通信における,クロック変化時刻に対するデータ変化タイミングのマージンが改善し高速転送が可能になる効果もあります.

Sapphire SoCのメモリ・マップ

Sapphire SoCをフル・スペックでコンフィグレーションしたときのメモリ・マップを図22(a)に示します.広大な外部メモリ空間とAXI4バスやAPB3バスによる拡張スレーブ空間があります.

本稿ではSapphire SoCを最小構成で実装します.そのメモリ・マップを図22(b)に示します.内蔵RAM空間と内蔵周辺空間のみになります.内蔵RAMは4KBで,ここにユーザ・プログラムとブート・ローダが置かれます.ブートの仕組みは次項で説明します.

APB3周辺機能空間のメモリ・マップを図23に示します.

この空間のアドレスはデフォルトでは0xF800_0000から始まる16MBを占めます.先頭からオフセット0x0010_0000の間は,Sapphire SoC内部に設定した周辺機能がアサインされます.図23に記載したUARTからGPIOまでに各周辺機能のアドレスは,筆者がEfinityでSapphire SoCをコンフィグレーションしたときに自動的にアサインされたものです.各アドレスは設定内容やツールのバージョンによって変わってきますが,IPの論理記述と同時に自動生成されるCのアドレス・シンボルを定義するヘダー・ファイル(ソフトウェアを組むためのBSP:Board Support Packageの中)に記載されますので,プログラム開発時に絶対アドレスの数字を気にする必要はありません.

図22 Sapphire SoCのアドレス・マップ全体(Sapphire RISC-V SoC Data Sheet から引用)[7]
図23 Sapphire SoCのAPB3周辺機能空間(Sapphire RISC-V SoC Data Sheet から引用)[7]

Sapphire SoCのRISC-Vプログラム・ブート方法

Sapphire SoC内のプログラム・メモリはSRAMなので,RISC-Vデバッガからダウンロードすれば実行できます.

ただし,パワー・オン時にすぐにユーザ・プログラムを実行させるためには,ブート・シーケンスが必要です.本稿で使用するSapphire SoCの最小構成時のRISC-Vのプログラム・ブート方法を図24に示します.ブート時の動作シーケンスは以下のようになります.

  1. 0xF900_0000番地からのSRAM(4KB)はSapphire SoCのRTL内で,ブート・プログラムのバイナリにより初期化されるように記述されている.そのためFPGAのコンフィグレーションが完了して起動した時点で,SRAMの内容はブート・プログラムが書き込まれている状態になる.
  2. Sapphire SoCがリセットされると,RISC-VのPC(Program Counter)は0xF900_0000番地から初期化されたSRAM内のプログラムを実行開始し,すぐにブート・ローダ本体がある0xF900_0C00に分岐する.
  3. ブート・ローダは,SPI FLASHメモリの0x0038_0000番地から格納されているユーザ・プログラムをリードして,SRAMの0xF900_0000番地から順に転送する.
  4. ブート・ローダがユーザ・プログラムを転送し終わったら,0xF900_0000番地から格納されているユーザ・プログラムに分岐して実行開始する.

このブート・シーケンスを成立させるために,次の点に注意する必要があります.

  1. ユーザ・プログラムが3KBを越えていると,ブート中にブート・プログラムを壊してしまいます.ユーザ・プログラムの実行コード,定数データ,変数の初期値データを合わせた領域が3KB以内になるようにしてください.ただし,ブートが完了したあとは,ブート・プログラム領域を壊してもいいので,ユーザ・プログラムが実行中に書き換えるデータ領域は3KBを越えた領域にあっても問題ありません.
  2. Xyloniボード上のSPI FLASHメモリには,FPGAコンフィグレーション用のビット・ストリーム・データを0x0000_0000番地から,またRISC-Vのブート用ユーザ・プログラムを0x0038_0000番地から書き込んでおく必要があります. FPGA統合化開発環境EfinityからSPI FLASHメモリを書き込む時は,FPGAコンフィグレーション用のビット・ストリーム・データとRISC-Vのブート用ユーザ・プログラムを合体します.この手順は,EfinityのツールProgrammerがサポートしています.具体的な方法は後述します.
図24 Sapphire SoCのRISC-Vプログラム・ブート方法(Sapphire RISC-V SoC Hardware and Softwar User Guideから引用)[8]

作成するRISC-Vアプリケーション・プログラム

本稿では,Xyloniボード上のRISC-Vで動作するプログラムを以下の2本紹介します.

  1. コンピュータと対戦する3目並べ(Tic-Tac-Toe)AIゲーム
  2. 割込み制御によりXyloniボード上の全リソースを活用するプログラム

具体的な開発方法を次項以降で詳しく説明します.

7.超小型XyloniボードにRISC-V Sapphire SoCを実装する

Efinityを起動する

ここからは,実際にFPGA統合化開発環境Efinityを使って,Sapphire SoCをXyloniボード上のFPGA Trion T8に実装してみましょう.Efinityを起動してください.図25のようにメイン・ウィンドウが起動します.

図25 FPGA統合開発環境Efinityを起動

プロジェクトXyloniRISCVを作成する

メニューFile→Create Project...を選択してください.

図26のボックスが現れます.NameフィールドにXyloniRISCVと入力してください.Locationフィールドはこのままにしておきます.デフォルトでXyloniRISCVプロジェクトは,Efinityがインストールされたディレクトリの下のprojectディレクトリ以下に作成されます.Efinityをデフォルト・インストールした場合は,C:\Efinity\2023.1\project\XyloniRISCVの下にプロジェクト関連ファイルが格納されます.

画面下の部分のFamilyフィールドは,Xyloniボートに搭載されたFPGAデバイスに対応させて,TrionDeviceにはT8F81Timing ModelにはC2を設定します.

続いて,Designタブを押してください.図27の画面になります.Top Module/Entityには,FPGAのユーザ論理部の最上位階層のモジュール名を記載してください.今回はTOPと入力します.本設計事例のVerilog HDL記述はVerilog 2001ベースなので,ここではプルダウン・リストVerilogからverilog_2kを選んでおきます.OKボタンを押して閉じてください.

プロジェクトのファイル名は,XyloniRISCV.xmlになります.作業終了後,再度開くときは,このファイル名を指定してください.

図26 プロジェクトの新規作成
図27 プロジェクト設定の「Design」タブ

Interface DesignerでFPGAのデバイス・インターフェースを設計しよう

Efinityの基本思想として,FPGAのデバイス・インターフェース部と,FPGA内部のユーザ論理部は,明確に切り分けて設計します.

FPGAのデバイス・インターフェース部は,EfinityのInterface Designerを使います.メイン・ウィンドウのメニューTools→Open Interface Designerを選択して,図28の画面を出しましょう.

図28 Interface Designerを開く

図18のデバイスI/Fの内部にあるリソースを作成します.以下に示す要素を,対応する表の内容に従って作成してそれぞれ設定してください(表4表18).

GPIO端子(表4,表5

表4 入出力バッファGPIO0[15:0]の作成
表5 入出力バッファGPIO1[15:0]の作成

PLLへのクロック入力端子(表6

表6 入力バッファPLLCLKINの作成

SDカード・アクセス用SPI端子(表7~表10

表7 出力バッファSD_CS_Nの作成
表8 入出力バッファSD_DIの作成
表9 入出力バッファSD_DOの作成
表10 出力バッファSD_SCLKの作成

NOR FLASHブート用SPI端子(表11~表14

表11 入出力バッファSPI_MISOの作成
表12 入出力バッファSPI_MOSIの作成
表13 出力バッファSPI_SCLKの作成
表14 出力バッファSPI_SS_Nの作成

UART端子(表15,表16

表15 入力バッファUART_RXDの作成
表16 出力バッファUART_TXDの作成

PLLブロック(表17

表17 PLLの作成:目標出力周波数を20MHzに設定(結果は20.1369MHzとなる)

ユーザ用JTAGインターフェース(表18

表18 ユーザ用 JTAG インターフェースの作成

Interface Designerで入出力信号のボール位置をアサインする

Interface Designerでの作業はあと少しです.

残りは,作成した各入出力端子がFPGAのどのピンにアサインされるかを定義します.メニューDesign→Show/Hide GPIO Resource Assignerを選択してInstance Viewを表示してください.この画面の中で,表19表20に示した内容で,入力信号,出力信号,入出力信号の端子位置を指定します.画面Instance View内のInstanceの列に,対応するFPGAの端子名GPIOx_nnを入力してください.JTAG関係は固定されているのでここで指定する必要はありません.

ちなみに,Xyloni基板上の全ての拡張端子をGPIO端子に接続できていません.これは,プッシュ・スイッチ,LED,基板上の拡張端子の合計よりもGPIO端子の合計32本の方が少ないことと,基板の拡張端子の一部がFPGAのコンフィグレーション用の信号と兼用しているため,それを避けたためです.基板上の拡張端子は,PMOD端子も含めて,お好みで繋ぎ変えてもかまいません.

表19 各入出力信号のFPGA端子へのアサイン:GPIO
表20 各入出力信号のFPGA端子へのアサイン:クロック,SPI,UART

この状態で,メニューFile→Saveを選択してセーブしてください.図29のようになります.念のため,Interface DesignerのメニューDesign→Check Designを選択して,設定内容を自動チェックさせてください.エラーが報告されたら適宜修正します.Interface Designerの定義ファイルはディレクトリXyloniRISCVの下にXyloniRISCV.peri.xmlという名前で保存されます.終わったら,Interface Designerをクローズしてください.

図29 Interface Designerの入力完了

Sapphire SoC IPを生成する

さて,ここがEfinityツールの真骨頂,Sapphire SoC IPを自分の好きなようにコンフィグレーションして自動生成する方法を説明します.

IPマネージャを起動する

Efinityのメイン・ウィンドウの左側Projectタブを開いて,図30のようにプロジェクト名XyloniRISCVの下のIPの行を右クリックして,New IPを選択してください.図31の画面が出るので,階層を手繰ってSapphire SoCを選択して,Next>ボタンを押します.

図30 IPの新規作成
図31 Sapphire SoCを選択

IP名とSoC基本事項を設定

ここから,Sapphire SoC IPの仕様をコンフィグレーションしていきます.まず,画面一番上部のModule Nameを,ここではCOREにしておいてください.

次にSOCタブの中身を図32のように設定します.動作周波数Frequency(MHz)を20にしてください.後ほど説明するタイミング制約を掛けた合成結果を見ると,Xyloniボードに載っているTrion FPGA T8ではこの値が丁度良いようです.Peripheral ClockCacheCustom InstructionはいずれもOFFにします.CacheをONにすると,Linuxシステム用のメモリ管理ユニットや浮動小数点演算命令など,大規模高性能システム向けのオプションを選択できるようになりますが,本稿ではFPGAが小規模なので使いません.

Compressed Extensionは,RISC-Vの16ビット長短縮命令を追加するオプションで,これはコード・サイズ削減のために重要なのでONにします.

図32 Sapphire SoCの設定:SOCタブ

メモリ関係を設定

Cache/Memoryタブを開いて,図33のように設定してください.

外部メモリ拡張はしないので,External Memory InterfaceOFFにします.On-Chip RAM Size4KBを選択します.本FPGAの内蔵メモリは77kbなので,8KBにしたいところですが,内部ロジック内でもメモリを消費するので,4KBが限界でした.デフォルトのブート・ローダを使うので,Custom On-Chip RAM ApplicationOFFにします.

図33 Sapphire SoCの設定:Cache/Memoryタブ

デバッグ機能を設定

Debugタブを開いて,図34図35のように設定してください.

RISC-Vの標準デバッグ・インターフェースを使うので,Risc-V Standard DebugONにします.プログラム・メモリがRAMでありブレーク箇所はブレーク命令に置き換えればいいのでハード・ブレークは不要です.よってHardware Breakpoint0にします.Soft Debug Tapは,デバイスI/F内のユーザJTAGを使うのでOFFにします.FPGA Tap Port1にします.

この画面をスクロール・ダウンして図35のように設定します.IDE SelectionEfinity RISC-V IDE (OpenOCD v0.11)Target Board/Cable/ModuleXyloniApplication Debug ModeTurn On by defaultをそれぞれ選択します.これで,RISC-V IDEでデバッガ操作のフル機能を使えます.

図34 Sapphire SoCの設定:Debugタブ(1)
図35 Sapphire SoCの設定:Debugタブ(2)

UART機能を設定

UARTタブを開いて,図36のように設定してください.

1チャネルは必須なので,UART 0 (Required)ONです.UART 0 Interrupt IDはデフォルトにして,UART 1UART 2OFFにします.

図36 Sapphire SoCの設定:UARTタブ

SPI機能を設定

SPIタブを開いて,図37のように設定してください.

SPIのうち1チャネルはボード上のNOR FLASHメモリからプログラムをブートするために必須であり,SPI 0 (Required)ONです.本稿ではSDカードのアクセスにSPIを使うので,SPI 1ONにします.Interrupt IDはいずれもデフォルトのままにします.

図37 Sapphire SoCの設定:SPIタブ

I$^2$C機能を設定

I2Cタブを開いて,図38のように設定します.I2Cは使わないので,I2C 0I2C 2OFFにします.

図38 Sapphire SoCの設定:I2Cタブ

GPIO機能を設定

GPIOタブを開いて,図39のように設定します.

本設計事例ではGPIOはフル機能の16ビット×2チャネルを設定します.GPIO 0ONにし,GPIO 0 Bit Width16にします.GPIO 1ONにし,GPIO 1 Bit Width16にします.Interrupt IDはいずれもデフォルト値のままにしてください.

図39 Sapphire SoCの設定:GPIOタブ

APB3拡張バスを設定

APB3タブを開いて,図40のように設定します.APB3拡張バスは使わないので,APB Slave 0APB Slave 4OFFにします.

図40 Sapphire SoCの設定:APB3タブ

AXI4拡張バスを設定

AXI4タブを開いて,図41のように設定します.AXI4拡張バスは使わないので,AXI SlaveOFFにします.

図41 Sapphire SoCの設定:AXI4タブ

ユーザ割込みを設定

User Interruptタブを開いて,図42のように設定します.ユーザ割込み入力は使わないので,User A InteruptUser H InteruptOFFにします.

図42 Sapphire SoCの設定:User Interruptタブ

ユーザ・タイマを設定

User Timerタブを開いて,図43のように設定します.本稿ではユーザ・タイマを使わないので,User Tiimer 0User Time 2OFFにします.

図43 Sapphire SoCの設定:User Timerタブ

各周辺機能のベース・アドレスを確認

Base Addressタブを開いて,図44のように設定します.

Address Assignment MethodAutoにして,各機能のベース・アドレスを確認しておきます.これらの値は,IPマネージャにより自動生成されるCのアドレス・シンボルの定義ヘダー・ファイル(ソフトウェアを組むためのBSP:Board Support Packageの中)に記載されます.

図44 Sapphire SoCの設定:Base Addressタブ

コンフィグレーションしたSapphire SoC IPの出力ファイルを確認

Deliverablesタブを開いて,図45の内容を確認してください.全てが出力されるように設定しておきます.

図45 Sapphire SoCの設定:Deliverablesタブ

コンフィグレーションしたSapphire SoC IPのサマリを確認

Summaryタブを開いて,図46の内容を確認してください.問題なければ,右下のGenerateボタンを押してください.

図46 Sapphire SoCの設定:Summaryタブ

Sapphire SoC IPを自動生成

最終確認として,図47が表示されます.

問題なければ,Generateボタンを押してください.自動生成に成功すれば,図48が表示されるので,OKボタンを押して,図49の画面をCloseボタンで閉じてください.

図47 Sapphire SoC IPの生成:内容確認
図48 Sapphire SoC IPの生成:成功
図49 IP Catalogをクローズ

生成されたSapphire SoC IPを確認する

Sapphire SoC IPが生成されると,そのモジュール名COREが,図50に示すように,統合開発環境EfinityのProjectの中のIPの下に登録されます.

図50 Efinityメイン画面に生成したIPが登録された

IP:COREの部分をダブル・クリックすると,図51のように,自動生成されたSapphire SoC IPのRTLコードCORE.vが表示されます.CPUコアや周辺機能全てがこの1本のRTLコードに含まれています.メイン・ウィンドウ内の編集領域が狭い場合は,各領域の境界部分をマウスでドラッグすれば拡大できます.

図51 生成されたSapphire SoC IPのRTLコードを見る

生成したSapphire SoC IPのもモジュール定義部分,すなわちIPコアの入出力信号の定義を以下に示します.

信号の順序は若干変わる可能性があるかもしれません.この各入出力信号の意味と,FPGA内の接続先を表21に示します.これにもとづいて,FPGAのTOP階層のRTL記述を作成します(リスト1).

module CORE(
    input         io_systemClk,
    input         jtagCtrl_enable,
    input         jtagCtrl_tdi,
    input         jtagCtrl_capture,
    input         jtagCtrl_shift,
    input         jtagCtrl_update,
    input         jtagCtrl_reset,
    output        jtagCtrl_tdo,
    input         jtagCtrl_tck,
    input         system_spi_0_io_data_0_read,
    output        system_spi_0_io_data_0_write,
    output        system_spi_0_io_data_0_writeEnable,
    input         system_spi_0_io_data_1_read,
    output        system_spi_0_io_data_1_write,
    output        system_spi_0_io_data_1_writeEnable,
    input         system_spi_0_io_data_2_read,
    output        system_spi_0_io_data_2_write,
    output        system_spi_0_io_data_2_writeEnable,
    input         system_spi_0_io_data_3_read,
    output        system_spi_0_io_data_3_write,
    output        system_spi_0_io_data_3_writeEnable,
    output        system_spi_0_io_sclk_write,
    input         system_spi_1_io_data_0_read,
    output        system_spi_1_io_data_0_write,
    output        system_spi_1_io_data_0_writeEnable,
    input         system_spi_1_io_data_1_read,
    output        system_spi_1_io_data_1_write,
    output        system_spi_1_io_data_1_writeEnable,
    input         system_spi_1_io_data_2_read,
    output        system_spi_1_io_data_2_write,
    output        system_spi_1_io_data_2_writeEnable,
    input         system_spi_1_io_data_3_read,
    output        system_spi_1_io_data_3_write,
    output        system_spi_1_io_data_3_writeEnable,
    output        system_spi_1_io_sclk_write,
    output [ 0:0] system_spi_1_io_ss,
    input         io_asyncReset,
    output        io_systemReset,
    output        system_uart_0_io_txd,
    input         system_uart_0_io_rxd,
    output [15:0] system_gpio_0_io_writeEnable,
    output [15:0] system_gpio_0_io_write,
    output [15:0] system_gpio_1_io_write,
    input  [15:0] system_gpio_1_io_read,
    output [15:0] system_gpio_1_io_writeEnable,
    input  [15:0] system_gpio_0_io_read,
    output [ 0:0] system_spi_0_io_ss
);

リスト1 FPGAのTOP 階層のRTL 記述(TOP.v)
表21 Sapphire SoCの入出力信号の接続先

トップ階層のRTL記述を作成する

デバイス・インターフェースの設計とSapphire SoC IPの生成が終わったら,FPGA内部のトップ階層のVerilog RTL記述ファイルを新規作成しましょう.

Efinityのメイン・ウィンドウの左側Projectタブを開いて,図52のようにプロジェクト名XyloniRISCVの下のDesignの行を右クリックして,Createを選択してください.図53の画面が出るので,ファイル名を指定します.まず,File TypeVerilog Design File (*.v)になっていることを確認してください.ここではTOP.vというファイルを作成します.File Nameには拡張子.vがないTOPだけを入力してOKボタンを押してファイルを新規作成してください.

Efinityのメイン・ウィンドウの左側Projectタブの中のDesignの中に新規作成したファイルTOP.vができているので,図54のように,この行をダブル・クリックして開いてください.空っぽのTOP.vが開きます.

図52 トップ階層のRTL記述を作成
図53 トップ階層のRTL記述のファイル名を指定
図54 トップ階層のRTL記述TOP.vを開く

TOP階層のRTL記述TOP.vを次のように入力してください.まず,TOP階層のモジュール定義を記述します(リスト2).各入出力信号は,FPGAのデバイス・インターフェースと接続されます.

//----------------------------------
// Module Definition
//----------------------------------
module TOP
(
    input  wire PLL_LOCKED, // PLL Lock
    input  wire CLK,        // 20MHz (from PLL)
    //
    input  wire [15:0] GPIO0_IN,  // GPIO0
    output wire [15:0] GPIO0_OUT, // GPIO0
    output wire [15:0] GPIO0_OE,  // GPIO0
    input  wire [15:0] GPIO1_IN,  // GPIO1
    output wire [15:0] GPIO1_OUT, // GPIO1
    output wire [15:0] GPIO1_OE,  // GPIO1
    //
    output wire UART_TXD,   // UART TXD
    input  wire UART_RXD,   // UART RXD
    //
    output wire SD_CS_N,    // SD Card SPI Chip Selct
    output wire SD_SCLK,    // SD Card SPI Clock
    input  wire SD_DO_IN,   // SD Card SPI Data Out
    output wire SD_DO_OUT,  // SD Card SPI Data Out
    output wire SD_DO_OE,   // SD Card SPI Data Out
    input  wire SD_DI_IN,   // SD Card SPI Data In
    output wire SD_DI_OUT,  // SD Card SPI Data In
    output wire SD_DI_OE,   // SD Card SPI Data In
    //
    output wire SPI_SS_N,     // NOR FLASH SPI Chip Select
    output wire SPI_SCLK,     // NOR FLASH SPI Clock
    input  wire SPI_MOSI_IN,  // NOR FLASH SPI Data Out
    output wire SPI_MOSI_OUT, // NOR FLASH SPI Data Out
    output wire SPI_MOSI_OE,  // NOR FLASH SPI Data Out
    input  wire SPI_MISO_IN,  // NOR FLASH SPI Data In
    output wire SPI_MISO_OUT, // NOR FLASH SPI Data In
    output wire SPI_MISO_OE,  // NOR FLASH SPI Data In
    //
    input  wire JTAG_TCK,     // JTAG TCK
    input  wire JTAG_TMS,     // JTAG TMS
    input  wire JTAG_TDI,     // JTAG TDI
    output wire JTAG_TDO,     // JTAG TDO
    input  wire JTAG_DRCK,    // JTAG Gated Test Clock
    input  wire JTAG_SEL,     // JTAG Instruction Active
    input  wire JTAG_CAPTURE, // JTAG Tap State CAPTURE
    input  wire JTAG_SHIFT,   // JTAG Tap State SHIFT
    input  wire JTAG_UPDATE,  // JTAG Tap State UPDATE
    input  wire JTAG_RUNTEST, // JTAG Tap State RUNTEST
    input  wire JTAG_RESET    // JTAG Reset
);

リスト2 FPGA の TOP 階層のモジュール定義

以下は,パワー・オン・リセット回路です(リスト3).

電源投入時は,PLLから出力されるロック信号PLL_LOCKEDが一時的にLOWレベルになっているので,これを受け取って,FPGAのコンフィグレーション待ち時間$tUSER$以上(>15μs)の間,リセット信号resorgを引き延ばします.

//------------------------------------
// Generate Internal Power-On-Reset
//------------------------------------
reg  [15:0] count_reset;
wire        count_reset_stop;
wire        resorg; // power-on-reset
wire        ressys; // reset from debugger
//
always @(posedge CLK, negedge PLL_LOCKED)
begin
    if (~PLL_LOCKED)
        count_reset <= 16'h0000;
    else if (~count_reset_stop)
        count_reset <= count_reset + 16'h0001;
end
//
assign count_reset_stop = (count_reset == 16'd1000);
assign resorg = ~count_reset_stop;

リスト3 パワー・オン・リセット回路

次に,Sapphire SoC IPのCORE.vをインスタンス化します.TOP階層の入出力信号を接続します(リスト4).

最後は,endmoduleでRTL記述を終了します(リスト5).

//----------------------------------
// Sapphire SoC IP Core
//----------------------------------
CORE U_CORE
(
    .io_systemClk     (CLK),
    //
    .jtagCtrl_enable  (JTAG_SEL    ),
    .jtagCtrl_tdi     (JTAG_TDI    ),
    .jtagCtrl_capture (JTAG_CAPTURE),
    .jtagCtrl_shift   (JTAG_SHIFT  ),
    .jtagCtrl_update  (JTAG_UPDATE ),
    .jtagCtrl_reset   (JTAG_RESET  ),
    .jtagCtrl_tdo     (JTAG_TDO    ),
    .jtagCtrl_tck     (JTAG_TCK    ),
    //
    .system_spi_0_io_data_0_read        (SPI_MOSI_IN ),
    .system_spi_0_io_data_0_write       (SPI_MOSI_OUT),
    .system_spi_0_io_data_0_writeEnable (SPI_MOSI_OE ),
    .system_spi_0_io_data_1_read        (SPI_MISO_IN ),
    .system_spi_0_io_data_1_write       (SPI_MISO_OUT),
    .system_spi_0_io_data_1_writeEnable (SPI_MISO_OE ),
    .system_spi_0_io_data_2_read        (1'b0),
    .system_spi_0_io_data_2_write       (),
    .system_spi_0_io_data_2_writeEnable (),
    .system_spi_0_io_data_3_read        (1'b0),
    .system_spi_0_io_data_3_write       (),
    .system_spi_0_io_data_3_writeEnable (),
    .system_spi_0_io_sclk_write         (SPI_SCLK ),
    .system_spi_0_io_ss                 (SPI_SS_N ),
    //
    .system_spi_1_io_data_0_read        (SD_DI_IN ),
    .system_spi_1_io_data_0_write       (SD_DI_OUT),
    .system_spi_1_io_data_0_writeEnable (SD_DI_OE ),
    .system_spi_1_io_data_1_read        (SD_DO_IN ),
    .system_spi_1_io_data_1_write       (SD_DO_OUT),
    .system_spi_1_io_data_1_writeEnable (SD_DO_OE ),
    .system_spi_1_io_data_2_read        (1'b0),
    .system_spi_1_io_data_2_write       (),
    .system_spi_1_io_data_2_writeEnable (),
    .system_spi_1_io_data_3_read        (1'b0),
    .system_spi_1_io_data_3_write       (),
    .system_spi_1_io_data_3_writeEnable (),
    .system_spi_1_io_sclk_write         (SD_SCLK  ),
    .system_spi_1_io_ss                 (SD_CS_N  ),
    //
    .io_asyncReset  (resorg),
    .io_systemReset (ressys),
    //
    .system_uart_0_io_txd (UART_TXD),
    .system_uart_0_io_rxd (UART_RXD),
    //
    .system_gpio_0_io_read        (GPIO0_IN ),
    .system_gpio_0_io_writeEnable (GPIO0_OE ),
    .system_gpio_0_io_write       (GPIO0_OUT),
    .system_gpio_1_io_read        (GPIO1_IN ),
    .system_gpio_1_io_write       (GPIO1_OUT),
    .system_gpio_1_io_writeEnable (GPIO1_OE )
);

リスト4 TOP階層の入出力信号を接続
//------------------------
// End of Module
//------------------------
endmodule

リスト5 RTL記述を終了

最後にTOP.vのセーブを忘れないようにしてください.図55のようになります.

図55 トップ階層のRTL記述TOP.vを編集してセーブ

タイミング制約ファイルを作成する

論理回路は,その機能設計だけでなく,タイミング設計も重要です.

クロックの立上りエッジでデータを取り込むD-フリップ・フロップを基本として動作しますので,回路内の全てのフリップ・フロップのセット・アップ時間とホールド時間を満足させる必要があります.FPGAの開発ツールでは,簡単なタイミング制約スクリプトを書けば,その制約が満たされるように,論理合成や配置配線を頑張ってくれて,設計結果に対してどのくらいタイミング制約が満足できているのか,あるいはできていないのかをレポートしてくれます.

そのためのタイミング制約ファイルSDCを作成しましょう.SDCは,Synopsys Design Constraintsの略であり,SoCの世界でもFPGAの世界でも,タイミング制約の標準フォーマットになっています.

Efinityのメイン・ウィンドウの左側Projectタブを開いて,図56のようにプロジェクト名XyloniRISCVの下のConstraintの行を右クリックして,Createを選択してください.図57の画面が出るので,ファイル名を指定します.ここでは,File TypeSDC File (*.sdc)になっています.File Nameには,拡張子無しでconstraintsを入力してください.OKボタンを押してファイルconstraints.sdcを新規作成します.

Efinityのメイン・ウィンドウの左側Projectタブの中のConstraintの中に新規作成したファイルconstraints.sdcができているので,図58のように,その行をダブル・クリックして開いてください.

図56 タイミング制約ファイルを作成
図57 タイミング制約ファイルのファイル名を指定
図58 タイミング制約ファイルを開く

空っぽのconstraints.sdcが開くので,下記の記述を入力してください(リスト6).

#################
# PLL Constraints
#################
create_clock -period 50 -waveform {25 50} [get_ports {CLK}]
set_clock_groups -exclusive  -group {CLK} -group {JTAG_TCK}

####################
# GPIO Constraints
####################
set_input_delay  -clock CLK 5 [get_ports {GPIO*_IN[*]}]
set_output_delay -clock CLK 5 [get_ports {GPIO*_OUT[*]}]
set_output_delay -clock CLK 5 [get_ports {GPIO*_OE[*]}]
#
set_output_delay -clock CLK 5 [get_ports {UART_TXD}]
set_input_delay  -clock CLK 5 [get_ports {UART_RXD}]
#
set_output_delay -clock CLK 5 [get_ports {SD_CS_N}]
set_output_delay -clock CLK 5 [get_ports {SD_SCLK}]
set_input_delay  -clock CLK 5 [get_ports {SD_DO_IN}]
set_output_delay -clock CLK 5 [get_ports {SD_DO_OUT}]
set_output_delay -clock CLK 5 [get_ports {SD_DO_OE}]
set_input_delay  -clock CLK 5 [get_ports {SD_DI_IN}]
set_output_delay -clock CLK 5 [get_ports {SD_DI_OUT}]
set_output_delay -clock CLK 5 [get_ports {SD_DI_OE}]
#
set_output_delay -clock CLK 5 [get_ports {SPI_SS_N}]
set_output_delay -clock CLK 5 [get_ports {SPI_SCLK}]
set_input_delay  -clock CLK 5 [get_ports {SPI_MOSI_IN}]
set_output_delay -clock CLK 5 [get_ports {SPI_MOSI_OUT}]
set_output_delay -clock CLK 5 [get_ports {SPI_MOSI_OE}]
set_input_delay  -clock CLK 5 [get_ports {SPI_MISO_IN}]
set_output_delay -clock CLK 5 [get_ports {SPI_MISO_OUT}]
set_output_delay -clock CLK 5 [get_ports {SPI_MISO_OE}]

####################
# JTAG Constraints
####################
create_clock -period 100 [get_ports {JTAG_TCK}]
create_clock -period 100 [get_ports {JTAG_DRCK}]
set_input_delay  -clock JTAG_TCK 3 [get_ports {JTAG_TMS}]
set_input_delay  -clock JTAG_TCK 3 [get_ports {JTAG_TDI}]
set_output_delay -clock JTAG_TCK 3 [get_ports {JTAG_TDO}]
set_input_delay  -clock_fall -clock JTAG_TCK 3 [get_ports {JTAG_SEL}]
set_input_delay  -clock_fall -clock JTAG_TCK 3 [get_ports {JTAG_CAPTURE}]
set_input_delay  -clock_fall -clock JTAG_TCK 3 [get_ports {JTAG_SHIFT}]
set_input_delay  -clock_fall -clock JTAG_TCK 3 [get_ports {JTAG_UPDATE}]
set_input_delay  -clock_fall -clock JTAG_TCK 3 [get_ports {JTAG_RUNTEST}]
set_input_delay  -clock_fall -clock JTAG_TCK 3 [get_ports {JTAG_RESET}]

リスト6 PLLやGPIOの要件

タイミング制約SDCファイルconstraints.sdcの中身を少し説明します.

create_clockでシステム・クロックCLK,およびJTAGクロックJTAG_TCKの周期を定義しています.また,CLKJTAG_TCKは非同期なので,排他的関係であることをset_clock_groups -exclusiveで指定します.

set_input_delayは入力信号経路のタイミングを定義します.簡単に表現すると,外部に仮想的に内部回路と同じクロックで動くフリップ・フロップがあると仮定して,そのフリップ・フロップの出力からFPGAデバイスの入力端子までの伝搬時間Aをset_input_delayで指定します.FPGAデバイスの入力端子から,FPGA内部のフリップ・フロップ入力までの経路の伝搬時時間Bは,タイミング解析ツールが自動計算してくれます.結果として,A+BにFPGA内部のフリップ・フロップ入力端子のセットアップ時間を加えた値がクロック周期より短ければセットアップ・タイミングが満たされているといえます.

set_output_delayは出力信号経路のタイミングを定義します.簡単に表現すると,外部に仮想的に内部回路と同じクロックで動くフリップ・フロップがあると仮定して,FPGAデバイスの出力端子からそのフリップ・フロップの入力端子までの伝搬時間Aをset_output_delayで指定します.FPGA内部のフリップ・フロップ出力からFPGAデバイスの出力端子までの伝搬時間Bは,タイミング解析ツールが自動計算してくれます.結果として,A+BにFPGA外部の仮想フリップ・フロップ入力端子のセットアップ時間を加えた値がクロック周期より短ければセットアップ・タイミングが満たされているといえます.

実際のタイミング計算は,フリップ・フロップ内部の遅延時間や,クロック分配回路のスキュー,クロックのジッタ,なども加味して,かなり複雑な計算がなされます.また,フリップ・フリップのセットアップ時間が満たされるかだけでなく,ホールド時間も満たされるかも含めて解析が行なわれます.

他にも多くのタイミング制約があります.詳細は,Efinity Timing Closure User Guide[23]を参照してください.

入力し終わったら,忘れずにセーブしてください(図59).

図59 タイミング制約ファイルを編集してセーブ

FPGAの合成・配置・配線のフローを実行しよう

ここまできたら,あとはFPGAの合成・配置・配線の一連のフローをEfinityに自動実行してもらいましょう.

Efinityメイン・ウィンドウのメニューFlow→Single Step Flow RunOFFにしておきます.ちなみに,これがONの場合は,設計フローが1ステップずつ実行されます.

図60に示すように,メイン・ウィンドウのdashboardの中の左側4個のボタンがあります.左から,論理合成,配置,配線,コンフィグレーション用ビット・ストリーム・データ生成に対応しており,それぞれ押すことでその工程から実行可能です.今回は,最初から論理合成を実行するので,一番左のボタンを押してください.または,メニューFlow→Synthesizeを選択しても同じです.

この操作により,合成・配置・配線,さらにタイミング解析が実行され,コンフィグレーション用のビット・ストリーム・データが生成されます.メイン・ウィンドウ中央のConsole内のメッセージにエラーがないことを確認してください.エラーがあれば,これまでの編集や設定を見直してください.うまくいけば,図61のように,メイン・ウィンドウのdashboardの中の左側4個のボタン全部の右上角に小さいグリーンのチェック・マークが付きます.ちなみに,各フローの実行中は,Efinityのメイン・ウィンドウ左下外枠近くに,実行内容がひっそりと表示されていきますので,フロー処理の状況を把握することができます.

Efinityのメイン・ウィンドウの左側Resultタブを開いて結果をスクロールしてチェックしてください.FPGAのリソースの利用率やタイミング解析の結果などが表示されています.

図60 FPGAの論理合成と配置配線を開始
図61 FPGAの論理合成と配置配線が完了

FPGAをコンフィグレーションしよう

FPGAへの実装設計が無事完了したので,いよいよコンフィギュレーション・データをXyloniボードにダウンロードしましょう.

まずは,Xyloniボードを付属のUSBケーブルでPCに接続してください.次に,Efinityのメイン・ウィンドウのメニューTools→Open Programmerを選択して図62に示すEfinity Programmerを開いてください.USB TargetXyloniになっていることを確認しておきましょう.

EfinityがFPGAコンフィグレーション用のビット・ストリーム・データを生成すると,プロジェクトのディレクトリXyloniRISCVの下のoutflow内にXyloniRISCV.bitXyloniRISCV.hexの2種類のファイルが格納されます.Xyloniボード上のTrion FPGA T8はその起動時にアクティブ・コンフィグレーション・モードで基板上のSPI NOR FLASHメモリからビット・ストリーム・データを読みだしてコンフィグレーションします.そのため,Efinity Programmerは基板上のSPI NOR FLASHメモリへの書き込みを行ないますが,この場合に使うファイルがXyloniRISCV.hexです.テキスト・フィールドBitstream Dataの右側にある01が書かれた小さいボタンを押して,ディレクトリoutflowの中のXyloniRISCV.hexを選択してください.

Programming ModeSPI Activeに設定して,その右側にある$\blacktriangleright$か書かれたボタンを押すと,書き込みが始まります.書き込みが終わると図63の画面になります.

なお,この時点では,RISC-V用のプログラムをメモリに入れていないので,何も動作をしません.次項から,Sapphire SoC内のRISC-Vのプログラムの開発方法を説明します.

Efinityをクローズしたあと,またプロジェクトXyloniRISCVを開く場合は,Efinityのメイン・ウィンドウのメニューFile→Open Project...から,XyloniRISCVディレクトリの下のXyloniRISCV.xmlを開いてください.

図62 ProgrammerでXyloniボードに書き込む
図63 Xyloniボードに書き込み完了

Sapphire SoCの論理シミュレーションについて

Sapphire SoC IPそのものは,ベンダ側でしっかり論理検証されていると思ってよく,あらためてIP本体の論議シミュレーションを実行する必要は少ないでしょう.

Sapphire SoCからAPB3バスやAXI4バスを拡張し,独自の周辺機能を接続する程度なら,CPUのバス・アクセスをエミュレートするバス・ファンクション・モデルをテストベンチ上に構築し,独自の周辺機能を検証すれば,Sapphire SoC IPを含む論理シミュレーションは不要となります.しかし,大規模FPGAを使って複数コアを入れて協調動作させるなど,システムが複雑になると,きちんとSapphire SoC IPも入れて論理シミュレーションをしたくなります.

Sapphire SoC IPの論理シミュレーションは,FPGA統合化開発環境Efinityがサポートしてくれてはいます.ただし,現時点でサポートする論理シミュレータ・ツールは,米Siemens社(旧米Mentor社)のModelSimまたはQuestaSimのみになっています.フリーの論理シミュレータIcarus Verilogで簡単に論理シミュレーションする環境(スクリプトなど)は提供されていません.ただし,Sapphire SoC IPはRTLが公開されていますので,少し頑張れば,自分で論理シミュレーション環境を構築することはできるでしょう.

以下,Sapphire SoC IPをQuestaSimで論理シミュレーションした例を示します.ただし,筆者が使ったツールは米Intel社のMAX 10やCycloneなどのFPGA開発用無償環境Quartusに付属していたQuestaSimなので,ここはIntel社に感謝させていただきます.ModelSimまたはQuestaSimが正しくインストールされた状態で,コマンド・プロンプトを開いてください.

以下のコマンドで,Sapphire SoC IPのディレクトリ内のTestbenchに移動します(リスト7).ここで,ディレクトリ名などはEfinityツールをデフォルト・インストールしている場合で示しています.

cd C:\Efinity\2023.1\project\XyloniRISCV\ip\CORE\Testbench

リスト7 Sapphire SoC IPのディレクトリ内のTestbenchに移動する

Efinity環境をセットアップします.以下のようにバッチ・コマンドを実行してください(リスト8).

C:\Efinity\2023.1\bin\setup.bat

リスト8 Efinity環境をセットアップ

デフォルトのRISC-Vアプリケーションを使って論理シミュレーションを実行する場合は以下のコマンドを入力してください(リスト9).

Python3 run.py

リスト9 論理シミュレーションを実行

ModelSimまたはQuestaSimのウィンドウが図64のように開き,以下のメッセージが出れば成功です(リスト10).

# 0 -----------------------------------------
# 0 [EFX_INFO]: Start executing helloWorld TEST
# 0 -----------------------------------------
# 51315 -----------------------------------------
# 51315 [EFX_INFO]: Receiving uart data from soc
# 51315 -----------------------------------------
# 2121065 -----------------------------------------
# 2121065 [EFX_INFO]: TEST PASSED
# 2121065 [EFX_INFO]: Hello World from Efinix!
# 2121065 -----------------------------------------

リスト10 成功したときのModelSimまたはQuestaSimのメッセージ
図64 QuestaSimによるSapphire SoC IPの論理シミュレーション

デフォルトのプログラムではなく,独自のカスタム・プログラムをRISC-Vに実行させるシミュレーションを実行する場合は以下のようにします(リスト11).

Python3 run.py -b <path to application>/app.bin

リスト11 独自のカスタム・プログラムをRISC-Vに実行させるシミュレーションを実行

ただし,カスタム・プログラムを実行した場合は,テストベンチがデフォルト・ドライバやモニタ・シーケンスをバイパスするので,以下のワーニング・メッセージが出力されます(リスト12).これを防ぐには,独自のシーケンスを開発する必要があるとのことです.

# 0 -----------------------------------------
# 0 [EFX_INFO]: Executing custom binary file...
# 0 [EFX_WARN]: Skipped testbench default driver and monitor sequences.
# 0 [EFX_INFO]: Running simulation...
# 0 -----------------------------------------

リスト12 カスタム・プログラムを実行したときに出るワーニング・メッセージ

Sapphire SoC IPの論理シミュレーション環境は,APB3やAXI4のバス・ファンクション・モデルも含めて,今後整備されていくことを期待します.

8.コンピュータ対戦型Tic-Tac-Toeゲームを作成しよう

超小型Xyloniボードで動く,コンピュータ対戦型AI3目並べ(Tic-Tac-Toe)ゲームを作りましょう.

UART経由でPC上のターミナル・ソフトで通信しながら人間がRISC-Vと対戦します.3目並べは,チェス,将棋,囲碁,リバーシなどど同じ二人零和有限確定完全情報ゲームですが,両対戦者が最適な手を打てば必ず引き分けになるゲームです.シンプルなので,コンピュータ側はMiniMax法で短時間でゲーム木を全検索できます.そのため人間は引き分けるか負けるしかありません.こんなプログラムでもRISC-Vの短縮命令のおかげで3KB未満のサイズに収まってしまいます.

このプロジェクトは,ライブラリとして用意されている簡単なAPI(Application Program Interface)を使う程度で作成できるので,Sapphire SoCのレジスタ仕様や細かいAPIの理解は不要です.必要あれば「Sapphire RISC-V SoC Data Sheet [7]」と「Sapphire RISC-V SoC Hardware and Software User Guide[8]」も参照してください.

RISC-V IDEを立ち上げてWorkspaceを作成する

インストールしたEfinity RISC-V Embedded Software ICE(RISC-V IDEと略記)を起動してください.

図65のバナーが表示されたあと,Workspaceの場所を指定する図66が表示されます.このツールは,多くのプログラミング言語をサポートする統合化開発環境EclipseのC/C++プログラム開発版をカスタマイズしたものです.このEclipseでは,ある共通したプラット・フォーム向けの複数のプロジェクトや設定情報を置くためのディレクトリをWorkspaceと言います.

図65 RISC-V IDEを起動
図66 Workspaceの場所を指定

前項のFPGA統合化開発環境EfinityXyloniRISCVプロジェクトを作り,その中でSapphire SoC IPCOREという名称としてカスタマイズして作成しました.その出力として,SoC IP全体のRTL記述と,ソフトウェア開発のためのライブラリや各種設定ファイルを含むBSP(Board Support Package)とサンプル・プログラムが自動生成されています.

Sapphire SoC IPにおいては,Workspaceの場所は特に明確に規定されてはいませんが,本稿では次の場所を指定することを前提に説明していきます.図66の画面で,IPと一緒に生成されたソフトウェア関係の下記ディレクトリをWorkspaceとして指定してLaunchボタンを押してください(リスト13) .

C:\Efinity\2023.1\project\XyloniRISCV\embedded_sw\CORE

リスト13 Workspaceとして指定するディレクトリ

プロジェクト Xyloni_TicTacToeを作成する

すると,図67の画面が現れます.プロジェクトを新規作成するため,画面上部のメニューから{File→New→Project...}を選択すると,図68が現れます.Efinix Project→Efinix Makefile Projectを選択してNext>ボタンを押してください.

図67 RISC-V IDEの初期画面
図68 プロジェクトを新規作成

図69の画面になるので,図の内容になるように,プロジェクトを設定してください.

図69 プロジェクトを設定

まず,今回は組込み用リアルタイムOS RTOS(Realtime Operating System)は使わないのでProject typeStandaloneを選択し,Project nameには今回のプロジェクト名であるXyloni_TicTacToeを入力します.BSP locationBrowse...ボタンを押して以下のディレクトリを指定してください(リスト14).

C:\Efinity\2023.1\project\XyloniRISCV\embedded_sw\CORE\bsp

リスト14 BSP locationのディレクトリ指定

Create launch configurationsONにしてください.Project locationはBSPの場所を指定した時点で以下のように自動入力されています(リスト15).最後にFinishボタンを押してください.

C:\Efinity\2023.1\project\XyloniRISCV\embedded_sw\CORE\software\standalone

リスト15 Project locationのディレクトリ

すると,図70の画面になると思います.画面左側のProject Explorerの中にプロジェクトのディレクトリ構造と中のファイルが表示されていますので,階層を展開して内容をチェックしておいてください.

図70 プロジェクト生成を確認

メイン・プログラムXyloni_TicTacToe.cをコーディングする

画面左側のProject Explorerの中で,Xyloni_TicTacToe→src→Xyloni_TicTacToe.cをダブル・クリックするとメイン・プログラムのソース・コードが開きます.

最初は,簡単なコードが書かれていますが,それは消去して,以下に順に説明する内容でソース・コードを編集してセーブしてください(図71).本プロジェクトXyloni_TicTacToeのソース・コードはこのXyloni_TicTacToe.cの1本だけです.ソース・コード編集画面の左端を右クリックしてShow Line Numbersを選択すると行番号を表示できます.

図71 ソース・コードを編集してセーブ

ライブラリのヘダー・ファイルをインクルード

本プログラムが使うSapphire SoC IPの周辺機能はUARTのみです.

UART関係ライブラリのヘダーを下記コードのようにしてインクルードします(リスト16) .bsp.hはUART通信ターミナルに文字列を出力するための関数などが定義されています.uart.hはUART機能をアクセスする低レベルなアクセス関数などが定義されています.これらの定義内容は,画面左側のProject Explorerの中でXyloni_TicTacToe→Includes→...の下を手繰っていくと見つけることができます.

#include <stdint.h>
#include "bsp.h"
#include "uart.h"

リスト16 UART関係ライブラリのヘダーをインクルード

Tic-Tac-Toeの盤面状態を格納する配列変数

ここで,Tic-Tac-Toeの盤面状態を格納するデータを説明しておきます.

盤面の状態は,図72に示す8ビット符号付整数の配列変数board[9]に格納します.盤面は3×3の2次元ですが,要素が9個の1次元配列を使い,盤のマス目の座標位置と配列のインデクスの対応は,盤の左上角が0,右上角が2,左下角が6,右下角が8になるようにアサインしました.人間の石が置かれているマス目の位置には+1を,コンピュータの石が置かれている位置には-1,何も置かれていない位置には0が格納されます.初期状態は,どこにも石が置かれていないので,配列の要素は全て0です.

なお,本プログラムではメモリを削減するために,変数はできるだけ8ビット長を使うようにしました.

図72 Tic-Tac-Toeの盤面状態を格納する配列変数board[9]

Tic-Tac-Toeの盤面を描画する関数

Tic-Tac-Toeの盤面状態をUART通信ターミナルに表示する関数Draw_Board()のコードを以下に示します(リスト17) .

盤面状態を格納する配列変数のポインタboardを受け取って,図69の定義に従って表示します.Cの標準入出力ライブラリstdio.hで定義されているprintf()の簡易版の関数bsp_printf()bsp.hに定義されており,そちらを使います.bsp_printf()はコンパクトでXyloniボードのようなメモリが少ないシステムではとても便利な関数です.bsp_printf()がサポートするフォーマットを以下に示します.

  1. Character(%c)
  2. String(%s)
  3. Decimal(%d)
  4. Hexadecimal(%x)

通信ターミナルへの盤面表示は,人間の石を``O'',コンピュータの石を``X'',何もない箇所は``-''を使います.盤面表示の右側には,人間が石を置くときに参照するため,マス目に対応する番号を表示します.

UART通信フォーマットは8N1(8ビット長,パリティ無,STOPビット×1),ボーレートは115,200bpsです.

//----------------
// Draw Board
//----------------
void Draw_Board(int8_t *board)
{
    uint8_t i;
    uint8_t ch;
    //
    for (i = 0; i < 9; i++)
    {
        // Print current Board
        if ((i % 3) == 0) bsp_printf("  ");
        ch = (board[i] == +1)? 'O' : (board[i] == -1)? 'X' : '.';
        bsp_printf("%c", ch);
        //
        // Print Map Help
        if (i == 2) bsp_printf("  (012)\r\n");
        if (i == 5) bsp_printf("  (345)\r\n");
        if (i == 8) bsp_printf("  (678)\r\n");
    }
}

リスト17 Tic-Tac-Toeの盤面をUART通信ターミナルに表示する関数 Draw_Board()

Tic-Tac-Toeの盤面から勝者を判断する関数

Tic-Tac-Toeの盤面状態から,人間が勝ったか,コンピュータが勝ったかを判定する関数Winner_Check()のコードを以下に示します(リスト18).盤面状態を格納する配列変数のポインタboardを受け取って,人間が勝っている場合は+1を,コンピュータが勝っている場合は-1を,まだ勝敗が決していない場合は0を返します.

//--------------------
// Winner Check
//--------------------
int8_t Winner_Check(int8_t *board)
{
    if ((board[0]!=0)&&(board[0]==board[1])&&(board[1]==board[2])) return board[0];
    if ((board[3]!=0)&&(board[3]==board[4])&&(board[4]==board[5])) return board[3];
    if ((board[6]!=0)&&(board[6]==board[7])&&(board[7]==board[8])) return board[6];
    if ((board[0]!=0)&&(board[0]==board[3])&&(board[3]==board[6])) return board[0];
    if ((board[1]!=0)&&(board[1]==board[4])&&(board[4]==board[7])) return board[1];
    if ((board[2]!=0)&&(board[2]==board[5])&&(board[5]==board[8])) return board[2];
    if ((board[0]!=0)&&(board[0]==board[4])&&(board[4]==board[8])) return board[0];
    if ((board[2]!=0)&&(board[2]==board[4])&&(board[4]==board[6])) return board[2];
    return 0;
}

リスト18 人間が勝ったか,コンピュータが勝ったかを判定する関数 Winner_Check()

コンピュータ思考ルーチンのコア部:ゲーム木探索MiniMax評価関数

コンピュータ(CPU)側の思考ルーチンのコア部を説明します.本プログラムではMiniMax法を使いました.Tic-Tac-Toeは,対戦者が交互に石を打ちますので,お互いの石の打ち方を場合分けしていくと,図73のようなツリー状になります.これをゲーム木といいます.

CPUはゲーム木を再帰的に検索して,自分の手番で打てる手の評価値(符号付)を全て計算し,正の方向で最も大きい評価値のマス目に石を打ちます.この評価値を求める方法としてMiniMax法を使います.CPUの手番と人間の手番を再帰的に全て検索して,双方にとって最善の評価値の手を選び,最終的なCPUの手を選択します.

以下,盤面のマス目xに石を打つことをmove=xと表現することにします.図73のゲーム木において,枝はその手番の側が打てる手を表し,添えられているx:nxmove=xの意味で,nは,その手の評価値で,正の方向で大きいほうが良い手を表します.打つ手の評価値の算出方法は以下で説明します.ゲーム木のノードに対応する楕円の中の数字は,その手番で打てる手の中のベストな評価値です.変数playerは,人間が手番なら+1,CPUが手番なら-1とします.

図73 MiniMax法によるゲーム木の検索

MiniMax法によるゲーム木の検索と評価値の求め方を説明します.

  1. 図73の一番上,CPUに手番(player=-1)がある状態から考えます.盤面で打てる手がmove=amove=bmove=cの3箇所あるとし,この各枝の評価値を求めます.まずmove=aの枝をたどります.
  2. 次の人間の手番(player=+1)で,打てる手がmove=dmove=eの2箇所あるとし,move=dの枝をたどります.
  3. 次のCPUの手番(player=-1)で打てる手がmove=jだけあるとし,その枝をたどります.
  4. 次の人間の手番(player=+1)になった時点で人間が負けているとします.人間にとっては悪夢なので人間としての評価値を-100にします.ただし,検索の深さdepthが深いところで負けたなら,まだ長続きした方だ,ということで,depthの分だけ評価値を良い側に持っていって-98とします.
  5. ここまで,move=amove=dmove=jの枝をたどってきたので,今度は逆にたどり,打ってきた手に評価値を与えていきます.
  6. CPUによるmove=jの手の評価値は,人間の評価値と逆の立場になるので,符号反転した+98とします.さらに,その前の人間によるmove=dの評価値を符号反転して-98とします.
  7. 人間がmove=dの手を打った手番では,もうひとつ打てる手move=eがありました.再帰的に今度はこの枝をたどります.次のCPUの手番では,打てる手がmove=kだけあるとし,その枝をたどります.
  8. 次の人間の手番で,打てる場所がなく引き分けになったとします.この人間としての評価値は0です.これを逆にたどって,CPUが打ったmove=kの評価値は符号反転しますが0です.さらに,その前の人間によるmove=eの評価値も同様に0です.
  9. ここで,人間の手番でのmove=dの評価値-98とmove=eの評価値0を比較すると,move=eの方が大きいので,この時点の人間の手番の評価値は0となります.
  10. 結局,この前のCPUの手番で打ったmove=aの評価値は,符号反転して0になります.これでCPUによるmove=aの手の評価は終わりました.
  11. 次にCPUの手番のmove=bの手とmove=cの手の評価値を同様に再帰的にたどっていって求めていきます.ここでは,move=bの評価値が-99,move=cの評価値が+98とします.よって,CPUの手番における,move=amove=bmove=cの3つの手からは,評価値が最大のmove=cを選択します.ゲーム木を眺めればわかりますが,move=cの先の展開は必ず人間が負けるので,CPUにとってはmove=cがベストなわけです.

ちなみに,図70のゲーム木の末端の勝ち負け判定の箇所では,負けるか引き分けるかのいずれかのみ表現しています.これは,Tic-Tac-Toeゲームの場合,自分が石(''○''か''×'')を打った時点で相手が勝ちになることがないためです.

以上のゲーム木検索と評価値の算出方法,すなわちMiniMax法を実装した関数MiniMax()のコードを以下に示します(リスト19).図73のゲーム木内のあるノード(楕円)における評価値,すなわちそこで打てる手の枝の中のベスト評価値を求めて返します.入力は盤面状態を格納する配列変数のポインタboard,プレーヤ変数player,検索の深さdepthです.

関数の冒頭で,盤面状態から勝敗を確認します.前に説明したとおり,この関数をコールされるのは,前の手番が打った状態の後になるので,この状態で自分が勝っていることはなく,勝ちを判定することはありません.なので,if (winner * player > 0)}は成立せず,負けが確定してelse if (...)}が成立するか,または,まだゲームが続いていて次のステートメントに進むかのどちらかです.ここで,else if (...)}が成立したノードは,図73loseと記したノードに対応します.

その後,盤面内で打てる箇所に自分の石を仮に置いて,再度MiniMax()を再帰的にコールします.ただし,立場を反転する必要があるので,プレーヤ変数playerを符号反転し,またこの関数からの戻り値,すなわち評価値も符号反転します.

再帰的なMiniMax()から戻ってきたら,仮に置いた石はどかして,次に打てる箇所に石を置いて,同様にMiniMax()をコールします.これを繰り返し,ベストな評価値とその手を求めます.置き場所がなかったら評価値0を返します.なお,関数MiniMax()は,ゲーム木を再帰的にたどる際,仮に盤面に石を置きながら評価しますが,最終的には盤面状態は変化させず,評価値だけを求めています.

//-------------------------------
// MiniMax Method
//-------------------------------
int8_t MiniMax(int8_t *board, int8_t player, int8_t depth)
{
    // Evaluation at Leaf
    int8_t winner = Winner_Check(board);
    if (winner * player > 0) // Player's best
        return +100 - depth;
    else if (winner * player < 0) // Player's worst
        return -100 + depth;
    //
    // Evaluation before Leaf
    int8_t move  = -1;
    int8_t value = -128;
    int8_t value_try;
    int8_t i;
    for(i = 0; i < 9; i = i + 1)
    {
        if(board[i] == 0)
        {
            board[i] = player; // Try
            value_try = -1 * MiniMax(board, player * -1, depth + 1);
            if(value_try > value)
            {
                value = value_try;
                move = i;
            }
            board[i] = 0; // Revert
        }
    }
    value = (move == -1)? 0 : value;
    return value;
}

リスト19 打てる箇所に自分の石を仮に置いて,再度MiniMax()を再帰的にコール

コンピュータ側が手を思考する関数

コンピュータ(CPU)側思考のメイン関数Move_by_CPU()のコードを以下に示します(リスト20).

CPUの手番になったら,この関数が呼ばれます.入力は盤面状態を格納する配列変数のポインタboardとプレーヤ変数playerです.プレーヤはCPUなので関数呼び出し側はplayerに-1を指定します.

内容は関数MiniMax()に似ていますが,評価値の戻り値はなく,冒頭で勝敗判定はしません.現在の盤面状態の中から打てる手を探して,その手の評価値を関数MiniMax()で求め,ベストな評価値を持つ手で盤面状態を実際に更新します.最後に,打った手を表示します.

//----------------------
// Move by CPU
//----------------------
void Move_by_CPU(int8_t *board, int8_t player)
{
    int8_t value = -128;
    int8_t value_try;
    int8_t move = -1;
    int8_t i;
    //
    for (i = 0; i < 9; i++)
    {
        if (board[i] == 0) // vacant
        {
            board[i] = player; // Try
            value_try = -1 * MiniMax(board, player * -1, 0);
            board[i] =  0; // Revert
            if (value_try > value)
            {
                value = value_try;
                move = i;
            }
        }
    }
    board[move] = player;
    bsp_printf("[CPU] Move to %d\r\n", move);
}

リスト20 CPU側思考のメイン関数Move_by_CPU()

人間側の手を入力するインターフェース関数

人間側が手番のときに呼び出される関数Move_by_YOU()のコードを以下に示します(リスト21).人間が打つ場所をターミナルからUART経由で受け取り,打てる場所であれば,盤面を更新して,打った手を表示します.

//----------------------
// Move by You
//----------------------
void Move_by_YOU(int8_t *board, int8_t player)
{
    uint8_t ch;
    //
    // Ask Move?
    while(1)
    {
        bsp_printf("[You] Move to [0-8] ? ");
        ch = uart_read(BSP_UART_TERMINAL);
        bsp_printf("%c\r\n", ch);
        if ((ch >= '0') && (ch <= '8'))
        {
            if (board[ch - '0'] == 0) break;
        }
    }
    //
    // Move your selection
    board[ch - '0'] = player;
}

リスト21 人間側が手番のときに呼び出される関数 Move_by_YOU()

Tic-Tac-Toeゲームのメイン関数

Tic-Tac-Toeゲームのメイン関数main()のコードを以下に示します(リスト22).メッセージ表示のあと,人間が先攻か後攻のどちらにするかを聞き,盤面を初期化します.

盤上の9マスに対して,交互にゲームを進めます.各々の手番の前に盤面の勝敗状態を確認し,勝者がいれば終了します.ゲーム中は手番に応じて,Move_by_YOU()Move_by_CPU()をコールし,盤面を表示して,プレーヤを交代します.勝敗が決まるか,打つ場所がなくなったら,ゲームの結果を表示して,また先攻・後攻を入力する場所に戻ります.

//-------------------
// Main Routine
//-------------------
int main(int argc, char **argv)
{
    uint8_t i;
    uint8_t ch;
    uint8_t turn;
    int8_t  player;
    int8_t  winner;
    int8_t  board[9]; // You=+1, AI=-1, Vacant=0
    //
    // Repeat Games
    while(1)
    {
        // Message
        bsp_printf("\r\n=== TictacToe Game ===\r\n");
        //
        // Ask 1st or 2nd?
        while(1)
        {
            bsp_printf("1st or 2nd [1-2] ? ");
            ch = uart_read(BSP_UART_TERMINAL);
            if ((ch == '1') || (ch == '2'))
            {
                bsp_printf("%c\r\n", ch);
                break;
            }
        }
        //
        // Initialize Board
        for (i = 0; i < 9; i = i + 1) board[i] = 0;
        Draw_Board(board);
        //
        // Play a Game!
        player = (ch == '1')? +1 : -1;
        for (turn = 0; turn < 9; turn++)
        {
            // Check Winner
            winner = Winner_Check(board);
            if (winner != 0) break;
            //
            // Do Move
            if (player == +1) Move_by_YOU(board, player);
            if (player == -1) Move_by_CPU(board, player);
            Draw_Board(board);
            player = -player;
        }
        //
        // Print Result
        bsp_printf("-----------------\r\n");
        if (winner == 1)
            bsp_printf("--- You  won! ---\r\n");
        else if (winner == -1)
            bsp_printf("--- You lose! ---\r\n");
        else
            bsp_printf("---   Draw!   ---\r\n");
        bsp_printf("-----------------\r\n");
    }
    return 0;
}

リスト22 Tic-Tac-Toeゲームのメイン関数 main()

プロジェクトXyloni_TicTacToeをビルドする

ソース・コードXyloni_TicTacToe.cの編集とセーブが終わったら,このプロジェクトをビルドしましょう.

メニューProject→BuildProjectを選択します.エラーがあればソース・コードを修正してビルドを繰り返してください.エラーなく成功すれば図74のようになり,RAMの使用量が表示されます.ここでは,2,656Byteと表示されており,今回のSapphire SoC IPが持っているRAMサイズに収まることがわかります.

なお,本プログラムのMiniMax()関数は再帰的にコールされるため,スタックを掘っていきますが,ネスティングはそれほど深くなく,ブート・ローダ領域もスタック・エリアとして使えるので,メモリ使用量は問題ありませんでした.

図74 プロジェクトをビルドする

デバッグ・パースペクティブを開く

ビルドしたプログラムをXyloniボードで動かすには,デバッガを使ってRAMにダウンロードする必要があります.

メニューWindow→Perspective→OpenPerspective→Debugを選択して図75のようにデバッグ・パースペクティブを開いてください.

図75 デバッグ・パースペクティブをオープン

UART通信用ターミナルを表示する

Tic-Tac-Toeゲームで遊ぶため,UART通信用ターミナルを表示します.

メニューWindow→Show View→Terminalを選択します.画面内のどこかのタブに追加されます.操作しやすくするために,Terminalのタブをドラッグしてメイン・ウィンドウの右端に表示されるように移動するといいでしょう.慣れないと言うことをききませんが,メイン・ウィンドウの右下辺りに移動するようにすると図76のようになります.

デバッグを開始すると,メモリ内容を表示し編集するためのMemoryタブが自動的に現れてTerminalタブに重なって入力操作の邪魔をすることがあります.メニューWindow→Show View→Memoryを選択し,Memoryタブを表示させ,そのタブを画面下側に移動しておくと便利でしょう.

図76 UART通信用ターミナルを表示

XyloniボードをPCに接続して通信セッションをオープンする

表示したターミナル画面とXyloniボードが通信できるように設定しましょう.

Xyloni_RISCVプロジェクトのビット・ストリームを書き込んだXyloniボードをPCのUSBポートに接続してください.ここでWindowsのデバイス・マネージャを開いてポート(COMとLPT)の項目からXyloniボードのCOMポート番号を確認してください.本稿ではCOM5にアサインされたとして説明します.図77のポインタが指しているOpen a Terminalボタンを押します.すると図78が表示されるので,以下のように通信パラメータを設定してください.

  1. Choose teminalSerial Terminal
  2. Serial portCOM5(デバイス・マネージャで確認したCOMポートを選択)
  3. Baud rate115200
  4. Data size8
  5. ParityNone
  6. Stop bits1
  7. EncodingDefault (ISO-8859-1)
図77 Xyloni基板を接続して,通信セッションをオープン
図78 UART通信パラメータを設定

デバッグ・セッションを開始する

デバッグ・セッションを開始します.メニューRun→Debug Configurations...を選択してください.

図79の画面が出るので,左側の画面からGDB OpenOCD Debugging→Xyloni_TicTacToe_trionを選択します.すでにデバッグ関連のパラメータが設定されてあります.右下のDebugボタンを押すと,Xyloniボードとの間でデバッグ・セッションを開始します.

デバッグ・セッションがはじまると,自動的にプログラムがXyloniボードの中のSapphire SoC IPのRAMにダウンロードされ,プログラム実行を開始します.デバッグ・セッションのデフォルト設定では,main()関数の先頭でブレークして一旦実行停止します.その様子を図80に示します.

図79 デバッグ・セッションを開始
図80 main()関数の先頭でブレーク

デバッガの操作方法

デバッガの操作方法の概略を図81と以下に示します.

  1. Debugボタン:前項でデバッグ・セッションXyloni_TicTacToe_trionを1回実行したあとは,このボタンを押せば同じセッションを開始できる.
  2. ブレークの設定と解除:ソース・コード内でブレーク(一時停止)させたい行の左端をダブル・クリックすることで,ブレークの設定と解除ができる.
  3. Resumeボタン:プログラムをブレークした状態から実行再開する.
  4. Suspendボタン:プログラムを強制的に一時停止させる.
  5. Step Intoボタン:ステップ実行する.ただし,ブレークした位置が関数ならその内部に入る.
  6. Step Overボタン:ステップ実行する.ただし,関数の中に入らず,次の行に進む.
  7. Step Returnボタン:ステップ実行する.ただし,関数の中から出る.
  8. CPU命令ステップ・モードONにすると,Cコードの行単位ではなく,CPU命令単位でステップ実行する.
  9. リセット&実行再開ボタン:リセットしてプログラムを最初から再開する.
  10. Terminateボタン:デバッグ・セッションを終了する.
  11. デバッガ終了ボタン:デバッガ関係で動いていサポート・プログラム類を終了させる.
  12. Runボタン:プログラムをデバッガ・セッションを使わずに(ブレークなどせずに)実行する.
図81 デバッガの操作方法

UART通信ターミナルをアクティブにしてプログラムの実行を開始

UART通信ターミナル上でキーボード入力操作をするため,Terminalタブ画面内をクリックして前面に出しておいてください.ここで,ブレークしていたプログラムの実行を再開します(図82).図81に示すResumeボタンを押してください.

図82 通信用ターミナル画面を前に出してResumeボタンでプログラム実行

Tic-Tac-Toeゲームを遊ぶ

プログラムが実行されると図83のようにターミナルに表示されゲームが始まります.

図83 Tic-Tac-Toeゲームを遊ぶ

まず,人間が先攻か後攻かを聞かれます.先攻なら1,後攻なら2を入力します.

人間の手番になったら,自分の石を置く場所を0~8で指定します.場所と数字の関係は盤面の右側に表示されています.最後まで戦ったら結果が表示されます.その次は,また人間が先攻か後攻かを聞かれるところに戻り,無限にゲームが続きます.ターミナルへの表示例を以下に示します(リスト23).

コンピュータに勝てましたか?

なお,コンピュータが先手番の場合,最初の一手を打つのに時間がかかっていると思います.今回のMiniMax法は,読む必要がないゲーム木検索を途中打ち切る処理(α-β法など)を入れておらず,単純にしらみつぶしに全検索しているためです.

=== TictacToe Game ===
1st or 2nd [1-2] ? 1
  ...  (012)
  ...  (345)
  ...  (678)
[You] Move to [0-8] ? 4
  ...  (012)
  .O.  (345)
  ...  (678)
[CPU] Move to 0
  X..  (012)
  .O.  (345)
  ...  (678)

 ... ...

[You] Move to [0-8] ? 8
  XOX  (012)
  OOX  (345)
  OXO  (678)
-----------------
---   Draw!   ---
-----------------

リスト23 ターミナルの表示例

デバッガ・セッションの終了方法

デバッガ・セッションを終了させるには,図81に示した赤色のTerminateボタン ■を押してください.さらに画面左側の<terminated>...と表示されている行を選択し,全部が消えるまで同図の黒いXデバッガ終了ボタンを押してください(図84).

図84 やめるときは■ボタンとXボタン

デバッガ・セッションを使わずにプログラムを実行する方法

デバッガ・セッションを使わずに,すなわちブレークなどさせずにプログラムを実行するには,メニューRun→Run Configurations...を選択してください.

図85が表示されるので,右下のRunボタンを押してください.一旦この操作をした後は,図81に示すRunボタンを押せばプログラム実行可能です.

図85 デバッグ操作なしで実行させるだけなら

プログラムを少しいじってみよう

人間が勝てないと思うので,少しCPUをアホにしてみましょう.

MiniMax()関数の中で,またMiniMax()関数を再帰的にコールしていますが,このMiniMax()関数の返り値に-1を掛けない(符号を反転しない)ように修正して実行してみてください.

CPU同士で対戦させることも可能です.main()の中のMove_by_YOU()Move_by_CPU()に変更してください.

RISC-Vのプログラムをデバッガからダウンロードすることなく,電源投入時に最初からRAMに自動的にロードして実行させる方法は次項の設計事例で説明します.

9.Xyloniボード上の全リソースを活用するプログラムを開発しよう

Xyloniボード上のプッシュ・ボタン,LED,SDカードを全て活用するプログラムXyloni_BoardTestを作ります.Xyloniボードのハードウェア・チェックにも使えます.このプログラムではRISC-Vの割込みを使っているので,その使い方を理解することもできます.RTOS(Realtime Operating System)を使わないスタンドアロン・プログラムとして作成します.

このプロジェクトは,Sapphire SoC IPの周辺機能APIを活用するので,「Sapphire RISC-V SoC Data Sheet [7]」と「Sapphire RISC-V SoC Hardware and Software User Guide [8]」も参照しながら理解を深めてください.

本プログラムの動作仕様は以下のとおりです.

導入編」の中でVerilog HDLだけで作成したプロジェクトBlinkLEDの動作をソフトウェアで実現します.Xyloniボードに載っているプッシュ・スイッチでLEDの点滅パターンを図86のように変化させます.4個のLEDがチカチカするパターンは4種類で,2進数としてインクリメントするパターン,1個だけ点灯して左方向に移動するパターン,1個だけ点灯して右方向に移動するパターン,2個ずつ交互に点滅するパターンがあります.これらのパターンはプッシュ・ボタンBTN1を押すと順に切り替わります.点滅スピードはBTN2をプッシュするたびに4段階で切り替えることができます.初期値は最速です.

なお,UART通信ターミナルを接続していれば,1を入力するとプッシュ・ボタンBTN1を押した動作を,2を入力するとプッシュ・ボタンBTN2を押した動作をします.

また,XyloniボードにはSDカード・スロットがあるので,SDカードを挿した状態で,UART通信ターミナル上で改行(エンター・キー)を入力すると,SDカード情報がUART通信ターミナルに表示されます.

図86 プログラムXyloni_BoardTestのプッシュ・スイッチとLEDの動作仕様

RISC-V IDEを立ち上げてWorkspaceを開く

Efinity RISC-V Embedded Software ICE(RISC-V IDEと略記)を起動して,前項のXyloni_TicTacToeプロジェクトを作成したときと同じWorkspace図81リスト24)を選択してください.メイン・ウィンドウが図87のように開きます.

C:\Efinity\2023.1\project\XyloniRISCV\embedded_sw\CORE

リスト24 前項のプロジェクトを作成したときと同じWorkspaceを選択
図87 RISC-V IDEを起動してWorkspace「CORE」を開く

プロジェクトを新規作成する

左側のProject Explorerの空白領域を右クリックして,図88のようにメニューNew→Project...を選択してください.図89の画面が出るので,Efinix Project→Efinix Makefile Projectを選択して下部のNext>ボタンを押してください.

図88 プロジェクトを新規作成
図89 Makefileプロジェクトとして作成

次に,プロジェクトの各種パラメータを図90のように設定してください.Project typeStandalonを選択し,Project nameにはXyloni_BoardTestを入力します.BSP locationは,Browse...ボタンを押して以下のディレクトリを指定してください(リスト25).

C:\Efinity\2023.1\project\XyloniRISCV\embedded_sw\CORE\bsp

リスト25 BSP locationの指定ディレクトリ

Create launch configurationsONにしてください.Project locationはBSPの場所を指定した時点で以下のように自動入力されています(リスト26).最後にFinishボタンを押してください.

C:\Efinity\2023.1\project\XyloniRISCV\embedded_sw\CORE\software\standalone

リスト26 Project locationのディレクトリ
図90 プロジェクトのパラメータを設定

図91の画面になると思います.画面左側のProject Explorerの中にプロジェクトのディレクトリ構造と中のファイルが生成されていますので,階層を展開して内容をチェックしておいてください.

図91 作成したプロジェクトを確認

Efinix社のサンプル・プログラムから3つのソースを拝借する

ここで,Efinix社のサンプル・プログラムから3つのソース・コードを拝借します(リスト27).

trap.Sは,割込み発生時にコールされるエントリ・ルーチンです.sd_ctrl.csd_ctrl.hはXyloniボードのSDカード・スロットに挿したSDカードの内部情報を表示するためのプログラムです.それぞれのソース・ファイルのありかを以下に示します.trap.SはSapphire SoC IPを自動生成したときにできるサンプル・プログラムの中に,sd_ctrl.csd_ctrl.hはEfinity社のGithubサイトにあります.

(1) trap.S
...C:\Efinity\2023.1\project\XyloniRISCV\embedded_sw\CORE\software\standalone\common

(2) sd_ctrl.c
(3) sd_ctrl.h
...https://github.com/Efinix-Inc/xyloni/tree/master/design/soc_Opal_t8
          /soc_Opal_sw_t8/software/standalone/Xyloni_SelfTest/src

リスト27 Efinix社のサンプル・プログラムから3つのソース・コードをGET

これらの3つのファイルを,図92のように,

C:\Efinity\2023.1\project\XyloniRISCV\embedded_sw\CORE\software
  \standalone\Xyloni_BoardTest\src

リスト28 3つのファイルを指定のディレクトリにコピー

の下にコピーしてください(リスト28).

図92 サンプル・プログラムから3つのソースを拝借

拝借した3つのソース・ファイルをプロジェクトに登録する

3つのソースファイルtrap.Ssd_ctrl.csd_ctrl.hをコピーしたら,プロジェクトに登録します.

図93のように,画面左側のProject Explorer内のプロジェクト名Xyloni_BoardTestを右クリックして現れるメニューRefreshを選択してください.図94のように,コピーしたファイル3個がプロジェクト内のsrcの下に登録されます.

図93 プロジェクトをリフレッシュ
図94 コピーしたソースがプロジェクトに登録されたことを確認

メイン・プログラムXyloni_BoardTest.cをコーディングする

画面左側のProject Explorerの中で,Xyloni_BoardTest→src→Xyloni_BoardTest.cをダブル・クリックするとメイン・プログラムのソース・コードが開きます.以下に順に説明する内容でソース・コードを編集してセーブしてください(図95).

図95 ソース・コードを編集してセーブ

ライブラリのヘダー・ファイルをインクルード

本プログラムが使うライブラリのヘダーを下記コードのようにしてインクルードします(リスト29).

#include <stdint.h>
#include "bsp.h"
#include "clint.h"
#include "gpio.h"
#include "plic.h"
#include "riscv.h"
#include "sd_ctrl.h"
#include "uart.h"

リスト29 ライブラリのヘダー・ファイルをインクルード

タイミング関係のパラメータを定義

タイミング関係のパラメータを下記コードのように定義します(リスト30) .

本プログラムの基本タイミングは,Sapphire SoC IPに実装されているタイマCLINT(Core Local Interrupt)が出力する周期割込みで生成します.周期CLINT_PERIODは10,000サイクルとします.動作周波数が20MHzなので,CLINTは0.5msごとに割込みを発生します.4通りあるLED点滅周期の定数SPEED0SPEED3は,CLINTの割込み周期を基準にして設定します.SPEED0は周期0.0625secにするため125,SPEED3は0.5secにするため1,000と定義しておきます.

//----------------------
// Speed Parameters
//----------------------#define CLINT_PERIOD 10000
#define SPEED0  125 //0000 // 0.0625sec
#define SPEED1  250 //0000 // 0.125sec
#define SPEED2  500 //0000 // 0.25sec
#define SPEED3 1000 //0000 // 0.5sec

リスト30 タイミング関係のパラメータ

Sapphire SoC IPでのRISC-V割込み制御の仕組み

RISC-Vアーキテクチャにおける割込みは,マシン・タイマ割込み,外部割込み,ソフトウェア割込みの3種類が定義されています.

Sapphire SoC IPでは,タイマCLINTの割込みはマシン・タイマ割込みに,ボタン(GPIO)など周辺機能からの割込みは外部割込みにアサインされています.

割込みは,CPU内部に複数種類あるCSR(Control and Status Register)と,PLIC(Platform Level Interrupt Controller)で制御されます.CSRのアクセスはCPUのCSRアクセス専用命令,PLICのアクセスはメモリ・アクセス命令で行ないます.

割込みを受付可能にするには,CSRMIEレジスタの中のマシン・タイマ割込みイネーブルMTIEビットと外部割込みイネーブルMEIEビットをセットし,さらにCSRMSTATUSレジスタの中のグローバル割込みイネーブルMIEビットをセットする必要があります.CPUに割込み要求が入力され受け付けると,MSTATUSレジスタのMIEビットの値が同じレジスタ内のMPIEビットにコピーされ,同時にそのMIEビットがクリアされ全割込みがマスクされます.処理が中断された実行前命令のアドレスPCの値はCSRMEPCに退避されます.そしてCSRMTVECにセットされているアドレスに分岐し,割込みサービス・ルーチンを開始します.

割込みサービス・ルーチンから元のルーチンに戻るにはMRET命令を実行します.この命令では,MSTATUSレジスタのMPIEビットの値を同じレジスタのMIEビットに戻し,CSRMEPCに格納してある値をプログラム・カウンタPCに格納し,割込み前のルーチンに戻ります.

RISC-Vの割込み処理については,「The RISC-V Instruction Set Manual, Volume II」[6]も参照してください.

複数の周辺機能からの割込み要求については,まずPLIC(Platform Level Interrupt Controller)が受け取ります.各割込み要求には,優先レベル(0~3)を設定でき,PLICが内部的に持つ閾値(Threshold)より高い優先レベルの割込み要求のうち,最優先の割込みが選択されCPUに割込み要求を出します.

本プログラムの割込みの種類と,グローバル変数で定義する割込み発生フラグ

本プログラムで発生させる割込みは,以下の3種類です.

  1. タイマCLINTが発生する周期割込み
  2. ボタンBTN1をプッシュしたときに発生する割込み(GPIO0[0]の立下りエッジ)
  3. ボタンBTN2をプッシュしたときに発生する割込み(GPIO0[1]の立下りエッジ)

割込みサービス・ルーチンはできるだけ短時間で抜けるべきなので,本プログラムで上記割込みが発生したら,割込みフラグとしてのグローバル変数gTickgBTN1gBTN2にそれぞれ1をセットするだけにします.各割込みに対応した実処理は,メイン・ルーチン側でこれらのグローバル変数の変化を見て実行します.このグローバル変数を以下のように宣言します(リスト31).

//--------------------
// Globals
//--------------------
volatile uint8_t gTick = 0;
volatile uint8_t gBTN1 = 0;
volatile uint8_t gBTN2 = 0;

リスト31 グローバル変数の宣言

関数のプロトタイプ宣言

本プログラムで使う関数のプロトタイプを以下のように宣言しておきます(リスト32) .

//---------------------
// Define Prototypes
//---------------------
void trap_entry(); // A routine in ./software/standalone/common/trap.S.
void trap(void);   // A routine called by trap_entry().
void INT_Handler_CoreTimer(void);
void INT_Handler_External(void);
void Change_Pattern(uint8_t *pPattern, uint32_t *pLed);
void Change_Speed(uint8_t *pSpeed);

リスト32 関数のプロトタイプ宣言

割込み発生時のエントリ・ルーチンと割込みサービスのエントリ・ルーチン

このうち,trap_entry()は,割込み発生時の最初の飛び先で,拝借したコードtrap.Sの中にあり,中身はRISC-Vのアセンブラで記述されています.

このtrap_entry()の中でCPUの汎用レジスタをスタックに退避し,trap()をコールし,CPUの汎用レジスタをスタックから復帰させ,元のルーチンに戻る処理を行ないます.そのtrap()が,割込みサービスのエントリ・ルーチンになります.

割込みサービスのエントリ・ルーチン

以下のコードが割込みサービスのエントリ・ルーチンtrap()です(リスト33) .

CLINTによる割込みの場合は,マシン・タイマ割込みなのでINT_Handler_CoreTimer()をコールし,プッシュ・ボタンによる割込みの場合は,外部割込みなのでINT_Handler_External()をコールします.

//------------------------------
// Exceptions and Interrupts
//------------------------------
void trap(void)
{
    int32_t mcause = csr_read(mcause);
    int32_t interrupt = (mcause < 0); //Interrupt if NEG, exception if POS
    int32_t cause     = mcause & 0xF;
    if (interrupt)
    {
        switch(cause)
        {
            case CAUSE_MACHINE_TIMER   : INT_Handler_CoreTimer(); break;
            case CAUSE_MACHINE_EXTERNAL: INT_Handler_External(); break;
            default: break;
        }
    }
}

リスト33 割込みサービスのエントリ・ルーチン

CLINTによる割込みサービス

CLINTによる割込みサービス・ルーチンを以下に示します(リスト34) .

グローバル変数のgTickを1にセットし,現在のCLINTタイマの現在のカウンタ値に割込み周期を加えた値を,コンペア・レジスタに格納します.カウンタは,基本クロック(20MHz)の1サイクルごとにインクリメントしており,カウンタとコンペア・レジスタが一致したら次の割込みが発生します.なお,カウンタとコンペア・マッチの各レジスタの幅は64ビットです.

//----------------------------------------
// Interrupt Handler for Core Timer
//----------------------------------------
void INT_Handler_CoreTimer(void)
{
    uint64_t cycle;
    //
    // Set Flag
    gTick = 1;
    //
    // Update Compare Register
    cycle = clint_getTime(BSP_CLINT);
    cycle = cycle + CLINT_PERIOD;
    clint_setCmp(BSP_CLINT, cycle, 0);
}

リスト34 CLINTによる割込みサービス

プッシュ・ボタン入力による割込みサービス

プッシュ・ボタンBTN1またはBTN2をプッシュしたときに発生する外部割込みのサービス・ルーチンを以下に示します(リスト35).

外部割込みはPLIC(Platform Level Interrupt Controller)で制御します.PLICからどの周辺モジュールが割込み要求しているかの情報を受け取り,プッシュ・ボタンBTN1に対応したGPIOからならグローバル変数gBTN1に1をセットし,プッシュ・ボタンBTN2に対応したGPIOからならグローバル変数gBTN2に1をセットします.最後に,今回処理した割込みが終わったことをPLICに知らせ,別の割込みがあればそれをCPUに出力させます.

//-----------------------------------
// Interrupt Handler for External
//-----------------------------------
void INT_Handler_External(void)
{
    uint32_t claim;
    uint64_t cycle;
    //
    //While there ares pending interrupts
    while(claim = plic_claim(BSP_PLIC, BSP_PLIC_CPU_0))
    {
        switch(claim)
        {
            case SYSTEM_PLIC_SYSTEM_GPIO_0_IO_INTERRUPTS_0:
            {
                // Set Flag
                gBTN1 = 1;
                break;
            }
            case SYSTEM_PLIC_SYSTEM_GPIO_0_IO_INTERRUPTS_1:
            {
                // Set Flag
                gBTN2 = 1;
                break;
            }
            default: break;
        }
        // Un-mask the claimed interrupt
        plic_release(BSP_PLIC, BSP_PLIC_CPU_0, claim);
    }
}

リスト35 プッシュ・ボタン入力による割込みサービス

メイン・ルーチン

以下にメイン・ルーチンをいくつかに区切って説明します(リスト36) .

まず,BSP(Board Support Package)を初期化し,UARTターミナルにメッセージを表示します.ここでは,前項で説明したXyloni_TicTacToeプロジェクトで使ったbsp_printf()ではなくbsp_putString()を使って文字列を出力しています.

今回のプログラムのほうがRAMを消費するので,コード・サイズを節約できる文字列専用のbsp_putString()を使います.その後,GPIOを初期化します.BTN1GPIO0[0]入力,BTN2GPIO0[1]入力LED1~LED2GPIO0[2]~GPIO0[5]出力に対応しますので,これに従って端子の入出力方向を設定し,最後にLED4個を消灯しておきます.

//----------------------
// Main Routine
//----------------------
void main(void)
{
    uint8_t  ch;
    uint8_t  pattern = 0;
    uint8_t  speed   = 0;
    uint32_t led     = 0;
    uint32_t count   = 0;
    uint64_t cycle;
    //
    // Initialize BSP
    bsp_init();
    //
    // Print Message
    bsp_putString("\n\r=== Xyloni_TestProgram ===\r\n");
    //
    // Initialize GPIO (BTN1, BTN2, LEDx4)
    gpio_setOutput(SYSTEM_GPIO_0_IO_CTRL, 0);
    gpio_setOutputEnable(SYSTEM_GPIO_0_IO_CTRL, (0x0f << 2));
    gpio_setOutput(SYSTEM_GPIO_0_IO_CTRL,(0x00 << 2));

リスト36 メイン・ルーチン

下記のコードで割込み関係の初期化を行ないます(リスト37).

周辺機能からの割込みを受け取るPLICを設定します.割込み優先レベルの閾値を0にし,GPIOからの割込み受付をイネーブルにし,それらの優先レベルをいずれも1に設定します.

GPIOを設定して,GPIO0[0]GPIO0[1]の立下りエッジでそれぞれ割込みを発生させます.

タイマCLINTの割込み周期を設定します.現在のCLINTタイマの現在のカウンタ値に割込み周期を加えた値を,コンペア・レジスタに格納します.

割込みが発生したときの分岐先trap_entry()CSRMTVECレジスタにセットします.

割込みをイネーブルにするため,CSRMIEレジスタの中のマシン・タイマ割込みイネーブルMTIEビットと外部割込みイネーブルMEIEビットをセットし,さらにCSRMSTATUSレジスタの中のグローバル割込みイネーブルMIEビットをセットします.MSTATUSレジスタのMPPフィールドもセットし,マシン・モードで実行中であることを指定しています.

    //
    // Configure PLIC : CPU accept all interrupts with priority above 0
    plic_set_threshold(BSP_PLIC, BSP_PLIC_CPU_0, 0);
    plic_set_enable(BSP_PLIC, BSP_PLIC_CPU_0, 
                    SYSTEM_PLIC_SYSTEM_GPIO_0_IO_INTERRUPTS_0, 1);
    plic_set_enable(BSP_PLIC, BSP_PLIC_CPU_0, 
                    SYSTEM_PLIC_SYSTEM_GPIO_0_IO_INTERRUPTS_1, 1);
    plic_set_priority(BSP_PLIC, SYSTEM_PLIC_SYSTEM_GPIO_0_IO_INTERRUPTS_0, 1);
    plic_set_priority(BSP_PLIC, SYSTEM_PLIC_SYSTEM_GPIO_0_IO_INTERRUPTS_1, 1);
    //
    //Enable Falling edge interrupts from BTN1 and BTN2
    gpio_setInterruptFallEnable(SYSTEM_GPIO_0_IO_CTRL, 3);
    //
    // Initialize Core Timer
    cycle = clint_getTime(BSP_CLINT);
    cycle = cycle + CLINT_PERIOD;
    clint_setCmp(BSP_CLINT, cycle, 0);
    //
    //Set the machine trap vector (../common/trap.S)
    csr_write(mtvec, trap_entry);
    //
    //Enable interrupts
    csr_set(mie, MIE_MEIE | MIE_MTIE);
    csr_write(mstatus, MSTATUS_MPP | MSTATUS_MIE);

リスト37 割込み関係の初期化

下記のコードからは,大きな無限ループになっています(リスト38).

UARTに受信データがあれば取り出し,改行ならSpi_Read_SDCard_Info()をコールし,SDカードが挿さっていればその情報を表示します.このルーチンの詳細説明は省略します.sd_ctrl.cを参照してください.

受信データが1なら,Change_Pattern()をコールしてLEDの点滅パターンを示す変数patternとLEDの初期パターン出力変数ledを更新します.受信データが2なら,Change_Speeed()をコールしてLEDの点滅パターンを示す変数speedを更新します.

    //-----------------------
    // Forever Loop
    //-----------------------
    while(1)
    {
        //--------------------------
        // Check Commands from UART
        //--------------------------
        while(uart_readOccupancy(BSP_UART_TERMINAL))
        {
            ch = uart_read(BSP_UART_TERMINAL);
            if ((ch == '\n') || (ch == '\r'))
            {
                bsp_putString("\n\r");
                Spi_Read_SDCard_Info();
            }
            else if (ch == '1')
            {
                Change_Pattern(&pattern, &led);
                bsp_putChar(ch);
            }
            else if (ch == '2')
            {
                Change_Speed(&speed);
                bsp_putChar(ch);
            }
        }

リスト38 UART受信データによる処理

UARTに受信データがなければ下記のコードに進みます(リスト39).ここでは,割込みフラグとしてのグローバル変数gBTN1またはgBTN2がセットされていたら,それぞれLEDの点滅パターンまたはLEDの点滅スピードを更新します.各フラグ変数はクリアして次に備えます.

        //------------------------
        // Check Buttons
        //------------------------
        if (gBTN1)
        {
            gBTN1 = 0; // Clear Flag
            Change_Pattern(&pattern, &led);
        }
        if (gBTN2)
        {
            gBTN2 = 0; // Clear Flag
            Change_Speed(&speed);
        }

リスト39 LEDの点滅パターンまたはスピードの更新

大ループの中の最後でLEDを点滅させます(リスト40) .タイマCLINTの周期割込みが発生していたら,フラグ変数gTickがセットされているので,LED点滅のための処理に入ります.

まず,gTickはクリアして次に備えます.LED点滅スピードを作るため,カウンタ変数countをインクリメントします.countspeedに対応した点滅タイミングになったら,点滅パターンpatternに応じてLED出力変数ledを変化させます.最後に変数ledGPIO0[5:2]に出力します.そしてまたループの最初に戻ります.

        //------------------------
        // Blink LED
        //------------------------
        if (gTick)
        {
            // Clear Flag
            gTick = 0;
            //
            // Blink Speed Control
            count = count + 1;
            if (speed == 0) count = (count >= (SPEED0 -1))? 0 : count;
            if (speed == 1) count = (count >= (SPEED1 -1))? 0 : count;
            if (speed == 2) count = (count >= (SPEED2 -1))? 0 : count;
            if (speed == 3) count = (count >= (SPEED3 -1))? 0 : count;
            //
            // Initialize LED Pattern
            if (count == 0)
            {
                if (pattern == 0) led = led + 1;
                if (pattern == 1) led = (led << 1) | ((led & 0x08) >> 3);
                if (pattern == 2) led = (led >> 1) | ((led & 0x01) << 3);
                if (pattern == 3) led = led ^ 0x0f;
                led = led & 0x0f;
            }
            //
            // Set LED
            gpio_setOutput(SYSTEM_GPIO_0_IO_CTRL, led << 2);
        }
    }
}

リスト40 LEDを点滅させる

下記のルーチンは,main()からコールされる関数で,Change_Pattern()がLEDの点滅パターンを示す変数patternとLEDの初期パターン出力変数ledを更新し,Change_Speeed()がLEDの点滅パターンを示す変数speedを更新します(リスト41) .

//----------------------
// Change Pattern
//----------------------
void Change_Pattern(uint8_t *pPattern, uint32_t *pLed)
{
    *pPattern = (*pPattern + 1) & 0x03;
    *pLed = (*pPattern == 0)? 0x00
          : (*pPattern == 1)? 0x01
          : (*pPattern == 2)? 0x08
          : (*pPattern == 3)? 0x05 : 0x00;
}

//----------------------
// Change Speed
//----------------------
void Change_Speed(uint8_t *pSpeed)
{
    *pSpeed = (*pSpeed + 1) & 0x03;
}

リスト41 点滅パターン変数の更新

プロジェクトXyloni_BoardTestをビルドする

ソース・コードXyloni_BoardTest.cの編集とセーブが終わったら,このプロジェクトをビルドしましょう.

メニューProject→BuildProjectを選択します.エラーがあればソース・コードを修正してビルドを繰り返してください.エラーなく成功すれば図96のようになり,RAMの使用量が表示されます.ここでは,3,648Byteと表示されています.

このサイズは,RAMサイズ4KBからブート・プログラム1KBを引いた3KBを越えており,一見するとRAMに収まらないように見えます.しかし,プログラム本体とデータ領域を合わせたサイズであり,プログラム領域は3KB以下になっているので,ブート中にブート・プログラムを壊すことなく正常動作します.

ビルド結果を見ると,spi.hの中でbsp_uDelay()が暗黙的に使われていることを示すワーニングが出ています.最終的にリンクは通るので問題はないですが,このワーニングを消したければ,画面左のProject Exproler内,Xyloni_BoardTest→Includes→...XyloniRISCV/embedded_sw/CORE/sortware/standalone/driverの下のspi.hを開き,先頭に#include ``bsp.h''を追加して,再ビルドしてください.

図96 プロジェクトをビルドする

デバッグ・パースペクティブを開き,UART通信ターミナルを準備する

ビルドしたプログラムをデバッガからXyloniボードにダウンロードして動かすために,メニューWindow→Perspective→OpenPerspective→Debugを選択してください.さらに,メニューWindow→Show View→Terminalを選択します.画面内のどこかのタブに追加されます.

操作しやすくするために,図97のようにTerminalを適当な位置にずらしておきます.また,メモリ内容を表示し編集するためのMemoryタブも出してTerminalのタブとかぶらない位置に移動しておきましょう.

図97 デバッグ・パースペクティブを開き,Terminalを準備

XyloniボードをPCに接続して通信セッションをオープンする

XyloniボードをPCのUSBポートに接続してください.前項と同様に,Terminal内のOpen a Terminalボタンを押して,通信パラメータを図98のように設定してください.COMポート番号は,Windowsのデバイス・マネージャを開いて確認して選択してください.

図98 Xyloniボードを接続してターミナル通信を接続

デバッグ・セッションを開始する

デバッグ・セッションを開始します.メニューRun→Debug Configurations...を選択してください.

図99の画面が出るので,左側の画面からGDB OpenOCD Debugging→Xyloni_BoardTest_trionを選択します.すでにデバッグ関連のパラメータが設定されてあります.右下のDebugボタンを押すと,Xyloniボードとの間でデバッグ・セッションを開始します.

デバッグ・セッションがはじまると,自動的にプログラムがXyloniボードの中のSapphire SoC IPのRAMにダウンロードされ,プログラム実行を開始し,main()関数の先頭でブレークして一旦実行停止します.その様子を図100に示します.

図99 デバッグ用パラメータを設定
図100 main()関数の先頭でブレーク

UART通信ターミナルをアクティブにしてプログラムの実行を開始

UART通信ターミナル上でキーボード入力操作をするため,Terminalタブ画面内をクリックして前面に出しておいてください.ここで,ブレークしていたプログラムの実行を再開するため(図101),Resumeボタン図81)を押してください.

図101 プログラムをResumeしてプログラム動作を確認

プログラムの動作を確認する

プログラムの動作を確認しましょう.

Xyloniボード上の2つのプッシュ・ボタンを押して,LEDの点滅パターンやスピードが変わることを確認してください.また,通信ターミナルで12を入力してプッシュ・ボタンを押すのと同様な効果があることを確認してください.

SDカードを挿さない状態でターミナルで改行を入力すると,SDカードからのレスポンスがない旨のエラー・メッセージが表示されます.SDカードを挿してから,再度改行を入力すると,SDカードの簡単な情報が表示されます.図96の例では,16GBのmicro SDHCカードを挿したときの反応を示しています.

プログラムをNOR FLASHメモリからブートする

これまでの方法だと,ビルドしたプログラムはデバッガ経由でRAMにダウンロードして実行していました.

図24のように,Xyloniボードの電源を印加したらすぐにプログラムをNOR FLASHメモリからブートして実行させる方法を説明します.

図102のように,FPGA統合化開発環境Efinityを起動して,プロジェクトXyloniRISCV.xmlを開いてください.

図102 EfinityでXyloniRISCVプロジェクトを開く

メニューTools→Open Programmerを選択してProgrammerを起動してください.図103のように,ファイルを合体するための小さいボタンCombine Multiple Image Filesを押してください.

図103 Efinity Programmerを開く

図104の画面になります.

ModeGeneric Image Combinationを選択し,Output Fileにはcombine.hexを入力してください.Output Directoryはデフォルトで...\XyloniRISCV\outflowになっていることを確認してください.

次に右側の小さい「+」が書いてあるボタンAdd Imageを押して,ディレクトリoutflowの下のXyloniRISCV.hexを選択して登録してください.拡張子が.hexなので注意してください.

次に,RISC-V IDEでビルドしたバイナリを登録します.同様にして,C:...\XyloniRISCV\embedded_sw\CORE\software\standalone\Xyloni_BoardTest\buildの下にあるXyloni_BoardTest.binを選択して登録してください.こちらは拡張子が.binです.

以上の2つのファイルを合体しますが,それぞれの開始番地を指定する必要があります.下記のように入力してください(リスト42).図99に示すように設定できたら,ボタンApplyを押します.2つのファイルが合体されたcombined.hexが生成されます.

0x00000000 XyloniRISCV.hex
0x00380000 Xyloni_BoardTest.bin

リスト42 FPGAビット・ストリームとプログラム・バイナリを合体
図104 Efinity ProgrammerでFPGAビット・ストリームとプログラム・バイナリを合体

図105の画面に戻りますので,Programming ModeSPI Activeにして,右側のStart Programを押してください.Xyloniボード上のNOR FLASHメモリへの書き込みが始まります.少し時間がかかりますが,図105の一番下に示すようにベリファイまで成功したメッセージが出ればOKです.

図105 合体したバイナリをXyloniボードに書き込み,プログラムの実行を確認

PCからボードを抜いて,再度接続しよう

PCからXyloniボードを抜いて,再度接続すれば,すぐに動作を開始すると思います.このように,電源投入すれば自分のプログラムをRISC-Vの上ですぐに動かすこともできます.

10.FPGAとRISC-Vを大いに楽しもう

本稿の「導入編」と「RISC-V編」で,FPGAの究極のコンフィギャビリティとRISC-Vの充実したエコ・システムを体験していただきました.

Xyloniボードに載っているFPGAは小規模ですが,CPUを搭載して自分でプログラミングを楽しむことができる実力があります.FPGAの論理規模にはまだ余裕があるので,もう少しSapphire SoCの機能を拡張したり,自分のロジックを実装してみたり,まだまだやれることは多いと思います.

コンパクトで手軽なXyloniボードをキャンパスとして,思いついたアイディアでいろいろ遊んでみましょう.その過程で,必ずご自身の技術力も自然に向上していきます.たっぷりと楽しんでください.

11.参考文献

  1. John L. Hennessy, David A. Patterson, 2019年, コンピュータアーキテクチャ 定量的アプローチ 第6版, エスアイビー・アクセス
  2. John L. Hennessy, David A. Patterson, 2020年, Computer Organization and Design -THe hardwre/software interface- RISC-V Edition, 2nd Edition, Morgan Kaufmann
  3. Sarah L. Harris, David Harris, 2022年, Digital Design and Computer Architecture RISC-V Edition, Morgan Kaufmann
  4. David A. Patterson, Andrew Watermann, 2018年, RISC-V原典 オープン・アーキテクチャのススメ 第1版, 日経BP
  5. Andrew Waterman, Krste Asanovi$\acute{c}$, The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA, Document Version 20190608-Base-Ratified, June 8, 2019
  6. Andrew Waterman, Krste Asanovi$\acute{c}$, The RISC-V Instruction Set Manual, Volume II: Priviledged Architecture, Document Version 20190608-Priv-MSU-Ratified, June 8,2019
  7. Efinix Inc., Sapphire RISC-V SoC Data Sheet, DS-SAPPHIRE-v3.3, Jul.2023
  8. Efinix Inc., Sapphire RISC-V SoC Hardware and Software User Guide, UG-RISCV-SAPPHIRE-v5.3, Aug.2023
  9. Efinix Inc., Efinix Trion FPGA Overview, TRION-OVERVIEW-3.1, 2023
  10. Efinix Inc., Trion FPGA Selector Guide, TRION-SELECTOR-3.2, 2023
  11. Efinix Inc., T8 Data Sheet, DST8-v5.0, Oct.2023
  12. Efinix Inc., t8_pinout-v3.3.xlsx, Rev3.3, Sep.2023
  13. Efinix Inc., AN006: Configuring Trion FPGAs, AN006-v5.9, Sep.2023
  14. Efinix Inc., AN023: Using the Trion Power Estimator, AN023-v1.3, Sep.2022
  15. Efinix Inc., AN042: Working with PLLs, AN042-v1.0, Mar.2022
  16. Efinix Inc., AN046: Reset Guidelines for Efinix FPGAs, AN046-v1.1, Nov.2022
  17. Efinix Inc., Efinity Software Installation User Guide, UG-EFN-INSTALL-v2.9, Ma.r2023
  18. Efinix Inc., Efinity Software User Guide, UG-EFN-SOFTWARE-v12.1, Aug.2023
  19. Efinix Inc., Efinity Trion Tutorial, UG-EFN-TUTORIAL-v7.0, Aug.2022
  20. Efinix Inc., Trion Interfaces User Guide, UG-TiINTF-v3.2, Oct.2023
  21. Efinix Inc., Quantum Trion Primitives User Guide, UG-EFN-PRIMITIVES-v4.5, Jun.2023
  22. Efinix Inc., Efinity Synthesis User Guide, UG-EFN-SYNTH-v3.7, Jun.2023
  23. Efinix Inc., Efinity Timing Closure User Guide, UG-EFN-TIMING-v4.0, Jun.2023
  24. Efinix Inc., Efinity Python API, UG-EFN-PYAPI-v6.1, October 2023
  25. Efinix Inc., Efinity Programmer User Guide, UG-EFN-PGM-v2.9, Jun.2023
  26. Efinix Inc., AN050: Managing Windows Drivers, AN050-v1.1, Jul.2023
  27. Efinix Inc., Xyloni Development Kit Overview, XYLONI-DK-OVERVIEW-1.0, 2020
  28. Efinix Inc., Xyloni Development Kit User Guide, XYLONI-DK-UG-v1.4, Nov.2022
  29. Efinix Inc., Xyloni Development Board Schematics and BOM, XYLONI-SCHE, V1.06, Mar.2018

本稿の執筆には,本ページに記載のEfinix社のFPGAに関する技術資料を参照しました.いずれも同社ホームページのSupport Centerから入手できます.各資料のバージョンは2023年10月時点のものであり,頻繁に更新されているので,最新版を参照するようにしてください.


©2023 Munetomo Maruyama All Right Reserved.