なんとか動き出したので、そのうちGitHubに上げてブログで紹介します
前回の続きでQuartus II 13.1に付属のModel-sim Altera Starter Edition 10.1を64ビット版のUbuntu 16.04で動かしてみました。前回も触れましたが、64bit版Linuxと旧バージョンのQuartusの相性はあまり良くないようです。また、Ubuntuはサポート外となっていますので、私のような事情のない方は最新のQuartusとサポートOSの組み合わせをおすすめします。
AlteraのModelsSim-Altera Editionのチュートリアル通りに進めていくと、Tools -> Run Simulation Tool -> RTL simulation を選んだところでこんなエラーが出ました。
$ ** Fatal: Read failure in vlm process (0,0)
[2]+ Segmentation fault (core dumped) bin/vsim
予想通りの展開です。
どうやらこの原因はFreeTypeのバージョンの食い違いによるもののようです。いろいろなサイトに情報がありますが、こちらのブログが一番まとまっていました
Freetypeのソースコードを手に入れてコンパイルし、modelsim_ase以下にlib32ディレクトリを作って、できたライブラリーをコピー。configureやmakeでエラーがでる場合は必要がライブラリーが足りないので適宜インストールします。このあたりは上記のブログを参照ください。
なお、私の環境では、途中で以下のようなエラーが出ましたが、
$ sudo apt-get build-dep -a i386 libfreetype6
E: You must put some 'source' URIs in your sources.list
sudo apt-get update
sudo apt-get build-dep -a i386 libfreetype
を行うことで実行することができました。
これで、LD_LIBRARY_PATHにこのlib32ディレクトリを登録すれば、modelsim_ase/linux/vsimを呼び出すことができるようになります。("~/altera/13.1/modelsim_ase"はmodelsimのインストールディレクトリに変更する必要あり。)
$ export LD_LIBRARY_PATH=~/altera/13.1/modelsim_ase/lib32
$ ~/altera/13.1/modelsim_ase/linux/vsim
ただ、これではまだQuartus II のメニューから呼び出す事ができません。Quartus IIで行った設定を元にシミュレーションしたい場合は以下の作業が必要です。
dir=`dirname $arg0` #この行はもとからあった
export LD_LIBRARY_PATH=/home/uname/altera/13.1/modelsim_ase/lib32 #追加
さらに、最近の4.0以上のカーネルではディレクトリの指定がうまくいっていないので、以下の部分を、
case $utype in
2.4.[7-9]*) vco="linux" ;;
2.4.[1-9][0-9]*) vco="linux" ;;
2.[5-9]*) vco="linux" ;;
2.[1-9][0-9]*) vco="linux" ;;
3.[0-9]*) vco="linux" ;;
*) vco="linux_rh60" ;;
esac
このように変更します
case $utype in
2.4.[7-9]*) vco="linux" ;;
2.4.[1-9][0-9]*) vco="linux" ;;
2.[5-9]*) vco="linux" ;;
2.[1-9][0-9]*) vco="linux" ;;
3.[0-9]*) vco="linux" ;;
4.[0-9]*) vco="linux" ;; この行追加
*) vco="linux_rh60" ;;
esac
これでどうにかQuartus II 13.1からmodelsimを呼び出すことができるようになりました
Ubuntuは関係ありませんが、Megawizardで作ったモデルを使うときは、Start Simulationの画面で適切なライブラリ(今回の場合はaltera_mf_ver)をLibrariesタブからSearch Libraries Firstのテーブルに入れておく必要があります。また、ROMなどの初期化ファイルはプロジェクトフォルダにおいておきます。(わからなくてしばらくハマりました。)
久しぶりにFPGAボード(Terasis DE0)を動かそうと思ったのですが、最近デスクトップのメインOSをUbuntuに変えたところなので、ついでにQuartus II 13.1をインストールしたところいろいろ大変だったので、その個人的メモです。間違っている可能性もかなりありますので、もし参考にされる方がいらっしゃる場合はご自身の判断を元に自己責任でおねがいします。
最新のQuartusが16.1だというのに、なぜいまさら13.1なのかというと、手元に2個もあるDE0*1がもったいなかったからで、DE0に入っているCyclone IIIをサポートする最新のバージョンが13.1だからです。UbuntuがQuartusのサポートOSリストに入っていないことはインストールしてしばらく経ってから気がついたのですが、いまさら戻るに戻れず、無理やり進めています。
これからインストールするのなら、サポートされているOSで、最新のQuartus IIを使ったほうが無難だと思います。
libxext6:i386 libxtst6:i386 libxi6:i386 libpng12-0 libpng12-0:i386 libxext6:i386 libxi6:i386 libsm6 libsm6:i386
ln -s /lib/x86_64-linux-gnu/libpng12.so.0 /usr/lib64/libpng12.so.0
ln -s /lib/i386-linux-gnu/libpng12.so.0 /usr/lib/libpng12.so.0
export QUARTUS_ROOTDIR="$home/altera/13.1/quartus"
export QUARTUS_ROOTDIR_OVERRIDE=$QUARTUS_ROOTDIR
export QUARTUS_64BIT=1
sudo rm /bin/sh
sudo ln -s /bin/bash /bin/sh
同様に、QsysでSerial Flash Controllerのcontrol portがNIOS2のインストラクションアドレスに加えて、データアドレスに接続されている事を確認。さもないと、registerが見つからないという上と同様のエラーがでる
この記事では結果だけ書いたのでさくさくすすんでいるように見えますが、各項目解決するまで1−2時間悩んだり、検索したり、試行錯誤したりを繰り返しました。大変でしたが、これでどうにか、参考書として使っている「FPGAボードで学ぶ組込みシステム開発入門 Altera編」の第5章までUbuntu16.04-64bitとQuartus II 13.1の組み合わせで進むことができました。
まだModelsimを試していないのでこの後もどんなトラップが待ち受けているのかドキドキです。
*1:配達時のトラブルで一つ注文したはずのものがなぜか2つ手に入りました。ひとつは未使用
先日Githubに公開したラズベリーパイ用Haribote OSについてオリジナルのX86版からの変更箇所解説2回目です。齢のせいか、たったの一月で何をやったのかどんどん忘れて来ているので、なるべく急いで進めて行こうと思います。今回はタスクスイッチです。
ラズベリーパイ用Haribote OS (RPiHaribote)はこちらに公開してあります
github.com
関連記事はこちら
uzusayuu.hatenadiary.jp
uzusayuu.hatenadiary.jp
uzusayuu.hatenadiary.jp
注意事項:あくまで筆者にとってOS開発は趣味の範疇の上に勉強中の身のため、内容については不正確な点もあると思います。そのような点が見つかった場合の指摘、または、さらによい方法の提案などしていただける場合は、喜んで参考にさせていただきます。なお、本記事はあくまで上記プログラムの解説であり、ARMの解説記事ではありません。また、記事の内容の正確性や実機での動作の再現性についてはいずれも保証できませんし、本記事の内容を使って起こるいかなる損害についても責任をもちません。
元々の「30日でできる!OS自作入門」では、タスクスイッチにはTSS(タスク状態セグメント)という仕組みを使っています。
struct TSS32 { int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3; int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi; int es, cs, ss, ds, fs, gs; int ldtr, iomap; };
これはタスクの情報とレジスタの内容を保存した構造体で、これをセグメントとして設定しておき、そこにジャンプすることで対象のタスクにスイッチできます。タスクの情報、レジスターの内容は自動でこの構造体から読み込まれ、セグメントもスタックも切り替えが行われ、CPUのステートも自動で変わります。(便利ですね。)
ラズベリーパイで使われているARMv6でもこういった仕組みがあるのかなと思って探してみたのですが見つかりませんでした。ということは、レジスターの値の待避と読み込みCPUのステートの変更はOSが行う必要があります。OS自作入門の中にセグメント切り替えに関して、
ちなみに、タスク切り替えは最初からTSSを使ってやりましたが、あれもTSSを使わないでやる方法があります。その場合は、これくらいに大変になります。
という記述がありますが、ラズベリーパイでは「これくらい」(セグメント切り替えをソフトウェアでやるくらい)大変になるわけです。(他にもっと楽な方法があるようなら教えていただければ感謝します)
ARM社の「ARMコンパイラツールチェーン」という文書を見ると、「6.17 コンテキストスイッチ」という項があり、ここでPCB(Process Control Block)という物を使ったコンテキストスイッチが紹介されています。ページにでかでかとSupersededと書かれていますが、他に良い方法が見つからなかったのでより良い方法が見つかるまではこちらを参考にして進めます。
まず、以下のような構造体を作りました。名前はTSS32を流用していますが、元のHaribote用のTSS32とはまったく違う物です。
struct TSS32 { uint32_t cpsr, pc, r0, r1; uint32_t r2, r3, r4, r5; uint32_t r6, r7, r8, r9; uint32_t r10, r11, r12, sp; uint32_t lr, sp_svc, sp_sys, sp_usr; uint32_t run_flag, vmem_table; };
意味は以下の通りです
上記でわかるように、RPiHariboteでは各プロセス毎に、システムモード用(task_aとconsoleが使用)と、スーパーバイザー用(スーパーバイザーコール時のみに使用)、ユーザーモード用(アプリケーションが使用)の3つのスタックが割り当てられています。ラズベリーパイのベアメタルプログラミングではスーパーバイザーモードを使う事が多いようですが、その場合タスク切り替え時の条件分けが複雑になるため、RPiHariboteではtask_aとconsoleはシステムモードで動作させています。
「ARMコンパイラツールチェーン」ではレジスタr12に次のPCBのポインタを代入しておくことで切り替え先のプロセスを指定しますが、RPiHariboteでは、r0を使ってPCBを指定しますタイマー割り込み時にタスクスイッチが発生すると、割り込みハンドラーから帰ってくるときに戻りに値に次のPCBの値が代入されているようにしてあります。(戻り値が0の場合はタスクスイッチせず割り込み元のプロセスに帰る)ARMのABI(Application Binary Interface: C言語などの関数との引数と戻り値に関する規約)では戻り値はr0に納められることになっているので、結果的にPCBのアドレスがr0に入ることになります。
実際にタスクを切り替えるアセンブラのルーチンは以下のようになります。なお、この部分は割り込み処理の一環としてIRQモードで処理されます。また、この処理の前にタスク切り替え以外の割り込み処理(キーボード・マウスのチェック、タイマー処理、など)があり、sp以外の汎用レジスタはIRQ突入時にスタックにプッシュしてあります。
_TaskSwitchIRQ: str r0, _next_pcb ldmia sp!, {r0-r12, lr} // restore all registers at IRQ entry from sp_irq str sp, _sp_save // Save SP_irq ldr sp, _cur_pcb // load current pcb adr, sp = pcb->cpsr add sp, #8 // move to PCB.r0 position stmia sp, {r0-lr}^ // store all usr registers (sp_usr, lr_usr) mrs r0, spsr // stmdb sp, {r0, lr} // spsr and return address ldr r0, _next_pcb // r0 = &pcb_b str r0, _cur_pcb // switch virtual memory table ldr r0, [r0, #0x54] // load vmem table address bl switch_vmem // switch virtual memory table // retrieve next registers ldr r0, _next_pcb // r0 = &pcb_b add sp, r0, #8 // move sp to PCB.r0 position ldmdb sp, {r0, lr} msr spsr, r0 ldmia sp, {r0-lr}^ ldr sp, _sp_save // retrieve sp_irq nop subs pc, lr, #4 _sp_save: .word 0
一つ一つ見ていきます。
まず、r0に入っている次のPCBのアドレスを_next_pcbというラベルの位置に保存します。
str r0, _next_pcb
次に、割り込み発生時に保存したSP以外のレジスターをIRQ用のスタックからポップして、割り込み突入時のレジスター状態を復元します。
ldmia sp!, {r0-r12, lr} // restore all registers at IRQ entry from sp_irq
IRQ用SPを一旦_sp_saveというラベルの位置に保存します。
str sp, _sp_save // Save SP_irq
現在のPCBへのポインターを_cur_pcbというラベルの位置から読み出し、+8します。これでspはTSS32.r0のアドレスを示すことになります
ldr sp, _cur_pcb // load current pcb adr, sp = pcb->cpsr add sp, #8 // move to PCB.r0 position
割り込み元の汎用レジスターの値を_cur_pcbが示すPCB領域に書き込みます。ここで^は、ユーザーモード又はシステムモードのspとlrを書き出すことを示します。(sp, lrはIRQモードで独立していますが、システムモードとユーザーモード間では共有されます。)
stmia sp, {r0-lr}^ // store all usr registers (sp_usr, lr_usr)
次にユーザーモードのCPSRの値が保存されている特殊レジスタであるSPSRをr0にコピー
mrs r0, spsr //
割り込み元のプログラムアドレス+4が保存されているlrと、spsrが保存されているr0をPCBに書き込み
stmdb sp, {r0, lr} // spsr and return address
これでタスクスイッチ前のレジスタが待避できました。次はスイッチ先のPCBの読み込みです。
次に、先ほど保存した次のタスクに対応するPCBへのポインターを呼び出します。
ldr r0, _next_pcb // r0 = &pcb_b
現在のPCBを保存しておく_cur_pcbというラベルが示すアドレスに同じ値を保存しておきます(次回のタスクスイッチの時に参照するため)
str r0, _cur_pcb
ここで仮想メモリーのテーブル(TTBR)を変更します。まず、TSS32.vmem_tableをr0に読み込み、仮想メモリーテーブル変換ルーチンを呼び出します。(割り込みルーチンなどが読み込まれるアドレスはどの仮想メモリー空間でも変わらないように設定してあるので、以下の処理に影響はありません。)仮想メモリーについては以前のエントリーを参照ください。
ldr r0, [r0, #0x54] // load vmem table address bl switch_vmem // switch virtual memory table
再びspに次のPCBのアドレス+8を読み込ませ、TSS32.r0のアドレスを参照させます。
ldr r0, _next_pcb // r0 = &pcb_b add sp, r0, #8 // move sp to PCB.r0 position
PCBから次のpc+4、cpsrを、spsrとlrに読み込ませます。割り込みから戻る際にこれらの値はcpsrとpcに書き戻されます。
ldmdb sp, {r0, lr} msr spsr, r0
汎用レジスターを、ユーザーモードのレジスターに読み込みます。ユーザーモードのspとlrはIRQからもどる際に復帰されます
ldmia sp, {r0-lr}^
最初に保存しておいたIRQ用のspを読み込みます。
ldr sp, _sp_save // retrieve sp_irq
おまじない(nop)の後、IRQからユーザーモード(又はシステムモード)への復帰手順を使い、次のプロセスに移ります。
nop subs pc, lr, #4
ユーザーモードに戻る際に、先ほど読み込んだspsrはcpsrにコピーされ、spとlrはユーザーモード用の物に切り替わります。
これで、指定されたPCBの示すプロセスに復帰することができました。
RPiHariboteのタスクスイッチについて解説しました。オリジナルHariboteではTSSとCPUの機能を使ってタスクスイッチしているところを、RPiHariboteではPCBとソフトウェアによって状態の待避と復帰を行っています。
短いアセンブラルーチンですが、安定して動作するようになるまでずいぶん試行錯誤を繰り返しました。現状、PCBのアドレスの保存やSPの一時保存にtextセグメント内のアドレスを使っているのがあまり好みで無いので、そのうちdataセグメントに移しておこうと思います。
先日こちらのブログを拝見しました。
見に行ったときはLLVMについて興味があったのですが、記事中で使われているBrainf**kという言語に興味津々。恥ずかしながらこれまで存在を知りませんでした。Wikipediaによると、この言語で使われる要素は><+-.,[]だけだそうです
Wikipediaにのっていた例によるとHello, World!を表示するコードはこうなります。(実行部分のみ)
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
Wikipediaによると意味は以下の通り
ちょうど、久しぶりにハードウェア関係で何か遊びたいなとか、VerilogHDLでデザイン書いてみたいな(自分はVHDL派でした)、などと思っていたところなので、Brainf**kをネイティブ実行するCPUを書いてみました。
たぶんググったら先行事例が沢山出てくると思うので、検索とかせずに、ひたすらコーディング。しばし格闘の後、できました。
module brainfuck( clk, rst_n, pc, pc_r, op, op_en, dp, d_o, w_en, w_sel, d_i, r_en, r_sel, d_en); input clk, rst_n; output [11:0] pc; input [7:0] op; output pc_r; input op_en; output [11:0] dp; output [7:0] d_o; output w_en; output w_sel; // 0: mem, 1: key input [7:0] d_i; output r_en; output r_sel; // 0: mem, 1: key input d_en; reg [11:0] pc; reg [11:0] pc_i, pc_n; reg [11:0] dp; reg [7:0] d_o; wire r_en; reg pc_r, w_en, w_sel, r_en_reg, r_sel; reg mov, mov_dir; // mov: 0, regular, 1: [ or ] reg [11:0] p_cnt; // number of parenthesis skipped reg [4:0] cur, nxt; // state machine reg [7:0] cur_op; parameter IDLE = 5'b00000, FETCH = 5'b00001, DEC = 5'b00010, MEMR = 5'b00100, MEMW = 5'b01000; reg pc_inc, pc_dec; wire mread, mwrite; // state machine always @(posedge clk or negedge rst_n) begin if (rst_n==0) cur <= IDLE; else cur <= nxt; end // state machine next state always @(cur or op_en or mread or d_en) begin case (cur) IDLE: nxt <= FETCH; FETCH: if (op_en == 1) // Next ope code came in nxt <= MEMR; MEMR: if (mread==0 | d_en == 1'b1) nxt <= MEMW; MEMW: nxt <= FETCH; default: nxt <= FETCH; // IDLE endcase end // pc change always @(posedge clk or negedge rst_n) begin if (rst_n==0) pc_i <= 0; else pc_i <= pc_n; end always @(pc_inc or pc_dec or pc_i) begin if (pc_inc==1 & pc_dec==0) pc_n <= pc_i + 1; else if (pc_inc==0 & pc_dec==1) pc_n <= pc_i - 1; else pc_n <= pc_i; end // pc_read always @(pc_inc or pc_dec or pc_n or pc_i) begin pc_r <= pc_inc | pc_dec; if (pc_inc | pc_dec) pc <= pc_n; else pc <= pc_i; end // Decoder for [ & ] always @(posedge clk or negedge rst_n) begin if (rst_n==0) begin mov <= 0; mov_dir<= 0; p_cnt <= 0; end else if (cur==MEMR) if (mov == 1) begin if ((mov_dir==0 & cur_op==8'h91) | (mov_dir==1 & cur_op==8'h93)) p_cnt <= p_cnt+1; else if (((mov_dir==0 & cur_op==8'h93) | (mov_dir==1 & cur_op==8'h91)) & (p_cnt==0)) mov <= 0; else if ((mov_dir==0 & cur_op==8'h93) | (mov_dir==1 & cur_op==8'h91)) p_cnt = p_cnt-1; end else if (d_en == 1'b1 & cur_op==8'h91 & d_i==0) begin mov <= 1; mov_dir <= 0; p_cnt <= 0; end else if (d_en == 1'b1 & cur_op==8'h93 & d_i!=0) begin mov <=1; mov_dir <= 1; p_cnt <= 0; end end // decoder for PC change always @(cur) begin case (cur) IDLE: begin pc_inc <= 1; pc_dec <= 1; end MEMW: begin pc_inc <= (mov==0 | mov_dir==0) ? 1 : 0; pc_dec <= (mov==0 | mov_dir==0) ? 0 : 1; end default: begin pc_inc <= 0; pc_dec <= 0; end endcase end always @(posedge clk or negedge rst_n) begin if (rst_n == 1'b0) cur_op <= 8'h0; else if (cur==FETCH & op_en==1) cur_op <= op; end // dp always @(posedge clk or negedge rst_n) begin if (rst_n == 1'b0) dp <= 0; else if (cur==MEMR & mov==0 & cur_op==8'h60) dp <= dp - 12'h1; else if (cur==MEMR & mov==0 & cur_op==8'h62) dp <= dp + 12'h1; end // r_en always @(posedge clk or negedge rst_n) begin if (rst_n == 1'b0) r_en_reg <= 0; else if (cur==FETCH & nxt==MEMR & mread) r_en_reg <= 1; else if (cur==MEMR & nxt==MEMR) r_en_reg <= 1; else r_en_reg <= 0; end assign r_en = (cur==FETCH & nxt==MEMR & mread) | r_en_reg; assign mread = (mov==0 & (op==8'h43 | op==8'h45 | op==8'h44 | op==8'h91 | op==8'h93)) ? 1 : 0; // data read always @(posedge clk or negedge rst_n) begin if (rst_n == 1'b0) d_o <= 0; else if (mov==1) d_o <= d_o; // no operation. Just for readability else if (cur==MEMR & d_en==1 & cur_op==8'h43) // + d_o <= d_i + 1; else if (cur==MEMR & d_en==1 & cur_op==8'h45) // - d_o <= d_i - 1; else if (cur==MEMR & d_en==1) d_o <= d_i; end // r_sel always @(posedge clk or negedge rst_n) begin if (rst_n == 1'b0) r_sel <= 0; else if (cur==FETCH & op_en==1 & op==8'h46) r_sel <= 1; else if (cur==FETCH & op_en==1) r_sel <= 0; end // write the data always @(posedge clk or negedge rst_n) begin if (rst_n == 1'b0) w_en<=0; else if (nxt == MEMW) w_en <= mwrite; else w_en <= 0; end assign mwrite = (mov==0 & (cur_op==8'h43 | cur_op==8'h45 | cur_op==8'h44)) ? 1 : 0; // w_sel always @(posedge clk or negedge rst_n) begin if (rst_n == 1'b0) w_sel <= 0; else if (cur==FETCH & op_en==1 & op==8'h44) w_sel <= 1; else if (cur==FETCH & op_en==1) w_sel <= 0; end endmodule
とりあえずシミュレーションで動作確認してみます。
先ほどのHello, World!をVerilogのテストベンチが読めるように、ASCIIコードでファイルに書きます。わかりやすいように改行が入っていますが、上と同じ物です。
@00 91 91 91 93 93 93 00 43 43 43 43 43 43 43 43 91 62 43 43 43 43 91 62 43 43 62 43 43 43 62 43 43 43 62 43 60 60 60 60 45 93 62 43 62 43 62 45 62 62 43 91 60 93 60 45 93 00 62 62 44 62 45 45 45 44 43 43 43 43 43 43 43 44 44 43 43 43 44 62 62 44 60 45 44 60 44 43 43 43 44 45 45 45 45 45 45 44 45 45 45 45 45 45 45 45 44 62 62 43 44 62 43 43 44
シミュレーション実行
わかりにくいですが、動作しました。実行結果をモニターする部分を拡大するとこんな感じです。
ちゃんと"Hello, World!"と表示されています。
一通りコーディングした後で検索したところ当然ながらBrainf**k CPUの先行事例は山ほどでてきましたが、自分の楽しみのためにやっているので問題ありません。次は少し時間がかかるかもしれませんが、論理合成して周辺回路と合わせてFPGA上で実行させるのを目標にしたいと思います。