Moiz's journal

プログラミングやFPGAなどの技術系の趣味に関するブログです

Raspberry Pi用Haribote OSの変更箇所2 - タスクスイッチ

本稿について

先日Githubに公開したラズベリーパイ用Haribote OSについてオリジナルのX86版からの変更箇所解説2回目です。齢のせいか、たったの一月で何をやったのかどんどん忘れて来ているので、なるべく急いで進めて行こうと思います。今回はタスクスイッチです。

ラズベリーパイ用Haribote OS (RPiHaribote)はこちらに公開してあります
github.com

関連記事はこちら
uzusayuu.hatenadiary.jp
uzusayuu.hatenadiary.jp
uzusayuu.hatenadiary.jp


注意事項:あくまで筆者にとってOS開発は趣味の範疇の上に勉強中の身のため、内容については不正確な点もあると思います。そのような点が見つかった場合の指摘、または、さらによい方法の提案などしていただける場合は、喜んで参考にさせていただきます。なお、本記事はあくまで上記プログラムの解説であり、ARMの解説記事ではありません。また、記事の内容の正確性や実機での動作の再現性についてはいずれも保証できませんし、本記事の内容を使って起こるいかなる損害についても責任をもちません。

オリジナルのHariboteのタスクスイッチ

元々の「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を使わないでやる方法があります。その場合は、これくらいに大変になります。

という記述がありますが、ラズベリーパイでは「これくらい」(セグメント切り替えをソフトウェアでやるくらい)大変になるわけです。(他にもっと楽な方法があるようなら教えていただければ感謝します)

RPiHariboteのタスクスイッチ

PCB (Process Control Block)

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;
};

意味は以下の通りです

  • cpsr: プロセスのCPSR(Current Processor Status Register: プロセッサーの状態の納められた特殊レジスタ)の値を保存するのに使う
  • pc: 対応するプロセスの中断したプログラムのアドレス+4*1
  • r0 - r12、sp, lr : レジスタの保存用です。(ここまでPCB。これ以降はプロセス管理に関わる情報)
  • sp_svc: スーパーバイザーモード用スタックポインタ。タスク切り替え時には使わない
  • sp_sys: システムモード用スタックポインタ。タスク切り替え時には使わない。*2
  • sp_usr: ユーザーモード用スタックポインタ。タスク切り替え時には使わない。*3
  • run_flag: アプリが動作しているかどうかのフラグ*4
  • 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の示すプロセスに復帰することができました。

タイマー割り込み以外でのタスクスイッチ

プロセスがスリープに入る場合にタイマーを待たずタスクスイッチがおきることがあります。この場合、_task_switch_asmというアセンブラのルーチンを使います。
_task_switch_asmでは、モードをIRQに切り替えて上記のタスクスイッチルーチンにジャンプしています。単純な処理なので解説は省略します。

まとめ

RPiHariboteのタスクスイッチについて解説しました。オリジナルHariboteではTSSとCPUの機能を使ってタスクスイッチしているところを、RPiHariboteではPCBとソフトウェアによって状態の待避と復帰を行っています。
短いアセンブラルーチンですが、安定して動作するようになるまでずいぶん試行錯誤を繰り返しました。現状、PCBのアドレスの保存やSPの一時保存にtextセグメント内のアドレスを使っているのがあまり好みで無いので、そのうちdataセグメントに移しておこうと思います。

*1:+4されているのは、はARMが割り込みに入るときにPC+4がLRに保存されているのでこれにあわせた

*2:アプリケーション実行中にシステムモード用スタックポインタの値を一時保存するのに使用

*3:スーパーバイザーコール中に一時的にシステムモードに移る際に、アプリケーション用のスタックポインタの値を保存しておくのに使用

*4:Hariboteではtss.ss0で判断していたが、RpiHariboteでは専用フラグをおいている

Brainf**kを直接実行するCPUを作ってみる

Brainf**kについて

先日こちらのブログを拝見しました。

itchyny.hatenablog.com

見に行ったときはLLVMについて興味があったのですが、記事中で使われているBrainf**kという言語に興味津々。恥ずかしながらこれまで存在を知りませんでした。Wikipediaによると、この言語で使われる要素は><+-.,[]だけだそうです

Brainfuck - Wikipedia

Wikipediaにのっていた例によるとHello, World!を表示するコードはこうなります。(実行部分のみ)

++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.

Wikipediaによると意味は以下の通り

  • +でポインタ値を+1
  • ーでポインタ値を-1
  • >でポインタを+1(一つ進む)
  • <でポインタを-1(一つ戻る)
  • [で、ポインタ値が0なら対応する]に進む
  • ]で、ポインタ値が非0なら対応する[に戻る
  • .でポインタ値を出力
  • ,で入力値をポインタ先に代入

ちょうど、久しぶりにハードウェア関係で何か遊びたいなとか、VerilogHDLでデザイン書いてみたいな(自分はVHDL派でした)、などと思っていたところなので、Brainf**kをネイティブ実行するCPUを書いてみました。

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

シミュレーション実行
f:id:uzusayuu:20170306152358p:plain
わかりにくいですが、動作しました。実行結果をモニターする部分を拡大するとこんな感じです。
f:id:uzusayuu:20170306152527p:plain
ちゃんと"Hello, World!"と表示されています。

終わりに

一通りコーディングした後で検索したところ当然ながらBrainf**k CPUの先行事例は山ほどでてきましたが、自分の楽しみのためにやっているので問題ありません。次は少し時間がかかるかもしれませんが、論理合成して周辺回路と合わせてFPGA上で実行させるのを目標にしたいと思います。

Raspberry Pi ZeroでHaribote OSを動かす

650円のコンピュータRaspberry Pi Zero

f:id:uzusayuu:20170226145418p:plain

先日Raspberry Pi Zeroが日本でも正式に発売されるというニュースがありました。

Raspberry Pi Zero 取扱開始のお知らせ - Raspberry Pi Shop by KSY

Raspberry Pi版Haribote (RPi Haribote)は、もともとHaribote OSを現代の実機で動かしたいという動機で始めたのですが、これまで旧機種(Raspberry Pi B Rev.2)でしか動作確認をしていないというのが気にかかっていました。良い機会ですので、現行機種のRaspberry Pi Zeroで動作確認をしてみました。

とりあえず起動

RaspbianのインストールされたMicro SDCARDを用意して、FAT領域のkernel.imgをRPiHariboteのkernel.imgに置き換えます。(もし試したいという奇特な方がいらっしゃる場合は、もとのkernel.imgを念のためリネームして保存しておくことを強くおすすめします)。RPiHaribote用kernel.imgファイルはここにあります。

github.com

USBハブを通してマウスとキーボードを接続し、ディスプレイと電源を繋げて、SDCARDを挿入して、起動。 

f:id:uzusayuu:20170224061404j:plain

 あっさり起動しました。ハブを通しての接続になるのでマウスやキーボードの動作が心配だったのですが今回使用したハブとキーボード・マウスの組み合わせでは問題無かったようです。(なお、どのような組み合わせでも動作する事を保証するわけではありません)

ただし、dirコマンドなど、ファイルアクセスがうまくいきません。

FAT32への対応とSDCARD.cの変更

SDCARDの中をよく見てみたところboot領域のフォーマットがFAT32でした。これまでのRPiHariboteはFAT16のみ対応でしたが、おそらくこういうケースは増えるだろうということで、これを機会にRPiHariboteもFAT32に対応させました。他にSDCARDの検出がうまくいっていないようなので応急処置として検知をスキップしました。この点は今後の検討課題にいれておきます。

これで無事アプリケーションの起動やテキストファイルのアクセスができるようになりました。

f:id:uzusayuu:20170225202414j:plain

音声再生の確認がまだですが、そのためにはGPIOの振替と外部回路の作成が必要なようなので、でおいおい試していこうと思います。

まとめ

Raspberry Piに移植したHaribote OSの動作確認をRaspberry Pi Zero上で行いました。

 

Raspberry Pi用Haribote OSの変更箇所1 - セグメントのページングによる置き換え

本稿について

先週Githubに公開したラズベリーパイ用Haribote OSについて書いた前回のエントリー、趣味の自作OSという非常にニッチで注目されにくい分野の記事ながら、数千のアクセスをいただきました。また、ツイッターでは、旧交を温めるきっかけになったり、新たに自作OSを趣味・仕事とされている方々とフォローし合う機会を得たりと、得がたい経験になりました。

github.com

せっかくですので記録も兼ねてラズベリーパイに移植するにあたって行った変更点を紹介したいと思います。なお、本稿ではラズベリーパイに移植したHaribote OSをRPiHariboteと呼ぶことにします。

注意事項:あくまで筆者にとってOS開発は趣味の範疇の上に勉強中の身のため、内容については不正確な点もあると思います。そのような点が見つかった場合の指摘、または、さらによい方法の提案などしていただける場合は、喜んで参考にさせていただきます。なお本記事はあくまで上記プログラムの解説であり、ARMの解説記事ではありません。また、記事の内容の正確性や実機での動作の再現性についてはいずれも保証できませんし、本記事の内容を使って起こるいかなる損害についても責任をもちません。

本題にもどります。最初はセグメントからページングへの変更についてです。

セグメントのページングによる置き換え

PCとラズベリーパイの違い

オリジナルのHaribote OSでは、セグメントを使うことによりメモリ領域ををカーネルとコンソール、また、OS側とアプリケーション側とで分割して使用しています。また、セグメントによりメモリの保護、CPUの特権の管理、割り込み先の指定、なども行っています。しかし、ラズベリーパイ(ARM)ではそもそもセグメントがありません。

これらの機能のうち特権の管理はCPSR(Current Processor Status Register)、割り込み先の指定は割り込みベクターでのジャンプ先指定で行いますが、残りはページングを使うというのがラズベリーパイ上での現実的な解決策になります。

RPiHariboteで使われているページング方法

前項でこの記事はARM自体の解説ではないと書いたばかりですが、この先の部分で必要なためARMv6のページングについて以下少しだけ説明しておきます。 

ラズベリーパイで使われているARMv6では、1MBのセクション、または、16KB(ラージページ)/4KB(スモールページ)のページ単位のページング選択できます。

アドレス変換はスモールページの場合は以下のような手準で行われます。

  1. 仮想アドレスの上位12ビットを使って、変換テーブルからページテーブルのアドレスを見つける
  2. 仮想アドレスの12ビット目から19ビット目を使い、ページテーブルから、ページのアドレスを見つける
  3. 仮想アドレスの下位12ビットを使い、物理アドレスを求める
  4. 物理アドレスのデータをアクセスする

図に書くとこのようになります

 

f:id:uzusayuu:20170220152351p:plain

この図はスモールページのページテーブルを使った場合ですが、セクション単位のマッピングを行う場合は変換テーブルが直接データのあるメモリー領域(セクション)を指定します。

仮想アドレスから物理アドレスへの変換はCPUが自動で行うので、OSがページングを使って仮想アドレスを使うには、

  1. 変換テーブルの構築
  2. ページテーブルの構築
  3. 仮想アドレスのイネーブル

という手順を経ることになります。また、異なる変換テーブル・ベース・アドレス(TTBR)に切り替えることで、仮想アドレス空間を切り替えることができます。

また、各テーブルのエントリーにはアクセス制御のビット(AP)があり、領域毎にユーザーモード・特権モード毎にアクセス権限を設定できます。

  • AP=0b00: アクセス不可
  • AP=0b01: 特権モードのみ読み書きアクセス可能
  • AP=0b10: 特権モードは読み書きアクセス可能、ユーザーモードは読み出しのみ
  • AP=0b11: 特権モード・ユーザーモード共に読み書きアクセス可能

ARMにはAP以外のアクセス制御もありますが、RPiHariboteでは使用していません。

モリーマップ

タスクごとの仮想アドレス

RPiHariboteでは大きくわけて、TASK_A用仮想アドレスマップとコンソール用仮想アドレスマップをもっています。

RPiHariboteでは仮想メモリー空間を大きく二つにわけて、0x00000000-0x7FFFFFFFをOS用、0x80000000より上ををアプリケーション用にわりあてています。

f:id:uzusayuu:20170221053817p:plain

TASK_A用仮想アドレスマップ

TASK_AはHariboteOSの画像表示やキー入力うけつけなどを行うタスクで、Hariboteのカーネルのような役目をおっています。

このタスク用の変換テーブルでは、0x00000000-0x7FFFFFFFの領域のみを物理アドレスにそのまま(変換なしで)対応させています。このうち実際に対応する物理アドレスがあるのは0x1FFFFFFFまでの領域だけです。実メモリがない0x20000000以上の領域がアクセスできるようにしてあるのは、ペリフェラルのインターフェースやMailbox *1のアドレスが0x20000000-0x7FFFFFFFの領域にマッピングされているためです。

 また、0x7FFFFFFF以下では特権モードのみがアクセスできるように(AP=0b01)、0x80000000以上はアクセス不可に(AP=0b00)設定してあります。いまのところセクション・テーブルによる変換のみを行い、ページ・テーブルは使っていません。

コンソールタスク用仮想アドレスマップ

 コンソールタスクは各コンソールウインドウ用のタスクですがアプリケーションの実行にも使われます。コンソールタスク用マップではアプリケーション用の領域をそれぞれわりあてる必要があるため、呼び出されたコンソールの数だけ仮想アドレスマップと変換テーブルが作成されます。

アプリケーションとOSの間のデータのシェアのため仮想アドレス空間の0x00000000-0x7FFFFFFFの部分はTASK_A用の変換テーブルとまったく同じです。アプリケーション用の領域は今の所先頭の64MBのみ(0x80000000-0x83FFFFFF)を使用しており、その部分のみにページをわりあてています。これは仮想アドレスにページが割り振ってあるだけで、物理メモリーアドレスが割り当てられるまでは、実メモリーは消費しません。ページサイズはスモールページ(4KB)です。

RPiHariboteではこの64MBの領域にアプリケーションのコード部分とスタックとヒープの領域を設定します。実メモリの割り当てはアプリケーションがロードされる際に行われます。

実際の処理

TASK_A用の変換テーブルの設定

オリジナルのHariboteOSでセグメントの設定をしていたinit_gdtidt関数は、RPiHariboteではページングの設定をするように完全に書き換えられています。

https://github.com/moizumi99/RPiHaribote/blob/master/haribote/bridge.c

ここでは0x00200000から始まる4096x4byteの領域に対しgenerate_section_table関数を呼び出し、4GBの全領域をカバーするセクションテーブルを作成します。またAPによるアクセス制限も同時に設定します。

コンソールタスク用の変換テーブルの設定

RPiHariboteではコンソール用の変換テーブルは、各コンソールが作られる際に同時に作成されます。コンソールが作られると、bootapck.c内のopen_constaskが呼び出され、他の処理に加えて以下の処理が行われます。

  1. memman_alloc_4kを使って16KBの領域をセクションテーブル用に確保
  2. この領域に上記のgenerate_section_tableを使ってTASK_A用と同じセクション・テーブルを設定。
  3. 64KBの領域をページテーブル用に確保(仮想アドレス領域64MB分)
  4. この領域にgenerate_page_tableを使ってページテーブルを設定(この時点ではページのエントリーがあるだけで物理メモリが設定されていないので、AP=0b00としてアクセスを禁止)

次に、実際にアプリケーションがSDCARDからロードされる際に、実メモリー上にmemman_alloc_4kでアプリケーションのコード、スタック、ヒープ領域を確保し、対応する仮想アドレスのページを設定します。この一連の処理はconsole.c内のcmd_app内で行っています。

オリジナルのHariboteではスタックとヒープのサイズはMakefile内で設定できるようになっていましたが、RPiHariboteでは今の所決め打ちのサイズになっています。仮想アドレスと実メモリ上に確保されるアドレスの対応は以下のようになっています。

内容仮想アドレス確保される実メモリサイズ
実行コード0x80000000アプリケーションのファイルサイズ
スタック0x810000002MB
ヒープ0x820000002MB

アプリケーション終了後は、ページをアクセス不可に再設定し、実メモリ上に確保した領域を開放します。

仮想アドレスのイネーブル

この項の内容はdwelch氏の以下のコードを元にしました

https://github.com/dwelch67/raspberrypi/blob/master/mmu/novectors.s

TTBRの設定やMMU(Memory Management Unit)のイネーブルはCからではできないのでアセンブラで行います。処理はasm_func.S (オリジナルには無かったRPiHaribote独自ファイル)内のstart_MMUで行います。具体的にはr0にTTBRのアドレス、r1にMMUのオン・オフの設定を入れ、start_MMUにジャンプすると

  1. キャッシュの内容を破棄
  2. TLB(変換テーブルのキャッシュのようなもの)を破棄
  3. TTBRを設定
  4. MMUをオン・オフ

という処理が行われ、これにより仮想アドレスが有効化されます。

タスクスイッチ

タスクスイッチと同時に仮想アドレスのスイッチも行われます。

具体的には各タスク毎にTTBRのアドレスをメモリ上のOS用領域内に保存しておき、タスクスイッチの際に読み出し、上記のstart_MMUを使って有効なTTBRを書き換えています。OSが使用する領域の仮想アドレスは常に物理メモリーアドレスに一致しているので、テーブルの変換によりOS側の動作が影響を受けることはありません。

そのほかの変更

ARMでは変換テーブルとページテーブルは16KBアラインする必要があるので、RPiHaribote内ではmemman_alloc_4kは16KB単位でメモリー領域を確保するように変更されています。(名前と動作が食い違ってしまっており、また他の部分では16KBでは無駄がでるので、対策を検討中)。

OSがアプリケーション内で設定したデータをアクセスできるように、仮想アドレスを物理アドレスに変換するv2pという関数を作成しました。ウインドウのバッファのアドレスなどはこの関数を用いて物理アドレスに変換され、OS側に渡されます。

効果

このようにしてページングによる仮想アドレスを設定する事により、RPiHariboteでは以下のような機能を実装しました

  1. OS領域のメモリーのアプリケーションからの保護
  2. 複数のアプリケーションを同一の仮想アドレスで同時に実行する
  3. アプリケーション領域のメモリーの、他のアプリケーションからの保護

これらはオリジナルのHariboteではセグメントを用いて実現していた物です

まとめ

Haribote OSをラズベリーパイに移植する際に行った変更のうち、セグメントをページングで置き換えた部分に関して説明しました

 

 

*1:Video coreなどの設定を行うためのインターフェース

「30日でできる!OS自作入門」のHaribote OSをラズベリーパイに移植してみた

本稿について

タイトル通りですが、HariboteOSのラズベリーパイへの移植が一段落したので報告したいと思います。

f:id:uzusayuu:20170211113857j:plain

「30日でできる!OS自作入門」とHaribote OSとは

『30日でできる!OS自作入門』とは2006年に発行された川合秀実氏のOS入門書です。Haribote OSはこの書籍内で作るOSの名前です

30日でできる! OS自作入門 | マイナビブックス

f:id:uzusayuu:20170212040924p:plain

書籍販売ページの内容紹介

プログラミングの基礎からはじめて、30日後にはウィンドウシステムを有する32bitマルチタスクOSをフルスクラッチで作り上げるという入門書。
ビギナーでも無理なく作成できるようPCの仕組み・アセンブラ・Cの解説から始まり、試行錯誤を繰り返しながらアルゴリズムを学びつつ、たのしく自由な雰囲気でOSをゼロから構築していくという、他に類を見ない手法による、趣味と実用と学習を兼ね備えたOS作成の入門書です。

 

この内容紹介の通り非常にユニークかつわかりやすい内容で、発売後10年たった現在も読み継がれています。たとえば最近ではこちらのブログで紹介されているのを目にしました。

rkx1209.hatenablog.com

名前はHaribote(張りぼて)という名称ですが、教育用のOSとしては、割り込み処理、ファイルアクセス、アプリケーションのサポート、メモリーのプロテクション、APIの実装、プリエンプティブなマルチタスクの実装、周辺機器のサポート、GUIの実装と、十分な領域をカバーしていると思います。また、改変したソフトウェアの再配布・ライセンスの変更を認めるなど、このOSをもとに実用的なOSを作成することも理論上は可能になっていて、川合氏のこの分野にかける思いの程が感じられます。

当ブログの筆者もご多分に漏れず、このOS自作入門を用いてOSを『自作』してみたのですが、なにしろ10年前の書籍と言うことで現在のPC環境とは相容れない部分も多くなってきています。たとえばHariboteはフロッピーディスクからの起動を前提にしているのですが、今時フロッピーから起動できるPCを調達するのも大変です。CDからの起動もできますが、毎回焼くのも不便です。なにしろ自作OSは起動するかどうかさえやってみないとわかりません。私自身、エミュレータQEMUやVirtual Box上で動作を確認するのみとなり、少し不満が残る結果となりました。また、内容を移しただけでは無く、自分で考えてOSを作りたいという気持ちも強まり、では他のアーキテクチャーに移植して実機上で動作してみよう、という事にしました。

 

ラズベリーパイへの移植

 簡単に移植といっても、まったくアーキテクチャーの違うCPU、プラットフォームへの移植と言うことで、変更点は多岐にわたります。特にアセンブラ部分は当然ながらすべて書き換えになりましたし、CのコードもCPUの機能やプラットフォームの機能に関わる部分は全て又は大きく変更しました。以下に主要な変更点を挙げます

  • ツールチェーンをHaribote独自の物からGNUに置き換え
  • ツールチェーンの変更に伴い一部の処理(sprintf, rand)を自作の関数で置き換え
  • アセンブラのコードをx86からARMv6に全て変更
  • ブートローダーをRaspberry Piの標準の物に置き換え
  • 割り込み処理の書き換え
  • TSSによるマルチタスクをソフトウェアによるレジスタの待避・復帰に変更
  • マルチタスクの処理の変更に伴い、スーパーバイザーモードおよびシステムモード用のスタックをタスク毎に設定
  • セグメントをページングによって置き換え(ARMにはセグメントはない)
  • ファイルアクセスをBIOSによるフロッピーディスクの読み込みから、OS内のSDCARDの読み込みに変更
  • ファイルシステムFAT12からFAT16に変更(その後FAT32に対応
  • タイマー処理の全面的な書き直し
  • キーボードとマウスの処理を外部USBデバイスドライバCSUD)により置き換え
  • キーボードとマウスの割り込み処理をタイマー割り込み時のポーリングに変更(CSUDの制限による)

  • グラフィック処理の移植。特に初期化作業は完全に書き換え
  • 音声の処理をブザーからPWMに変更。具体的にはPWMをM/Sモードで動作させ、クロックを可聴領域まで落とすことでブザーと似た音声を出力させ、パルスの周期を変えることで音程を実現しています。
  • システムコールをスーパーバイザーコールに置き換えて、上記のマルチタスク変更に対応させる
  • デバッグ用にUARTおよびJTAGに対応
  • 実行ファイルの形式をHRB形式からELF形式に変更。これにともないスタックとヒープのサイズを固定値に置き換え(今後変更を検討)
  • CPUのアーキテクチャーの違いによる小さな差異を吸収(例:ARMの32ビットアクセスは4バイトアラインのため、画像描画の高速化がもとのままでは失敗することがあった)

逆に変わっていない点としては、

  • GUI、キーバインディングに関してはもとのHariboteを踏襲
  • アプリケーションはCのソースコードレベルで互換性あり
  • 「30日で出来る!OS自作入門」に登場するアプリケーションは、グラフィックビューワー以外は全て動作する(TVIEWのみ後述のバグによる制限あり。グラフィックビューワーはアセンブラ部分があるため)

現在わかっている問題点・制限としては

  • Raspberry Pi Model B+ Rev2.0でのみ動作検証済み(ラズベリーパイ2への拡張を検討中)-> 2/25 Raspberry Pi Zeroでの動作を確認しました。
  • 使用したSDCARDのサンプルファイルにバグがあることがわかっている(検討中)-> 2/25 わかっている範囲で対応済み
  • CSUDの制限のため、キーボード・マウスに関しては、動作する機種が限られる。(今までのところケーブル接続のものは動く確率が高い)
  • アプリケーションの一つ(TVIEW)が、オプションをつけるとクラッシュする。(おそらくメモリー関係の問題。検討中)  2/19 修正済み
  • 圧縮の解凍がまだサポートされていない(これは、2017年現在、数百バイトの圧縮にどれほどの価値があるのか疑問だったためプライオリティーを下げた。今後対応予定)
  • 非矩形ウィンドウの表示時に透明部分が正しく描写されない(ウィンドウの位置を動かせば正しく表示される。検討中)
  • FAT16のサポートが最小限なため、環境によっては動かない可能性がある(拡張を検討中)-> 2/25 FAT32に対応しました
  • ELF形式の実行ファイルのサポートが最小限なため、環境によっては動かない可能性がある(拡張を検討中)

ソースコードはすべてGithubに公開しています。再配布・再ライセンスを事実上制限無く許諾してくださっている川合氏の前例にならい、ライセンスとしては改変・再頒布にほぼ制限のないunlicenseを選択しました。

github.com

使用方法などはGithub内のWikiに投稿してあります。今後ビルド方法なども追加する予定です。

スクリーンショットなど

以下、スクリーンショットなどを載せておきます。残念ながらオリジナルのHariboteと全く同じです。異なるアーキテクチャー上にまったく同じ物を再現するのがテーマだったのでしかたがないのですが、すこし寂しいところです。

f:id:uzusayuu:20170211123719j:plain

この写真中tviewが表示しているipl10.nasはテキストのサンプルとして使っているだけで、OS内では使用していません。

f:id:uzusayuu:20170211123847j:plain

動画はこちら

www.youtube.com

音楽の再生が調子っぱずれなのはマルチタスクの影響だと思いますが、まだバグが残っているのかもしれません。

参考文献・サイトなど

以下、Haribote OSのラズベリーパイへの移植にあたって使用した参考文献・サイトのうちいくつかを紹介します。これらの書籍およびウェブサイトの著者の方にはあらためて感謝の気持ちをあらわしたいと思います。

「30日でできる!OS自作入門」

30日でできる! OS自作入門 | マイナビブックス

f:id:uzusayuu:20170212040924p:plain

 すでに説明しましたので、詳細は省きますが、改めてOSを学ぶ機会を与えていただいたことを感謝したいと思います。ゼロからOSを作り上げるというスタイルのため、他のアーキテクチャーへの移植も一歩ずつ進めることが出来ました

 

Cambridge University - Baking Pi - Operating Systems Development

Computer Laboratory – Raspberry Pi: Baking Pi – Operating Systems Development

 最初にラズベリーパイでベアメタル・プログラミング(OSを使わない、ハードウェア上の低レイヤーのプログラミング)をするにあたって使ったチュートリアルです。CSUDはこちら経由で入手しました。

 

Raspberry Pi respository by dwelch67

github.com

いろいろなラズベリーパイのベアメタル・プログラミングの例が載っていて、あたらしいステップに進む度に参考にしました。

 

Valver Bare Metal Programming

www.valvers.com

わかりやすいチュートリアルです。特にMailbox(ラズベリーパイでVideo coreなどと情報をやりとりする仕組み)について非常に参考になりました

 

 OSDev Wiki - Raspberry Pi Bare Bones

Raspberry Pi Bare Bones - OSDev Wiki

 OSDevにもラズベリーパイのベアメタル(ここではBare boneと呼ばれています)についての情報があります。

 

Raspberry Pi - Bare Metal Forum

Raspberry Pi • View forum - Bare metal

 本家ラズベリーパイのサイトにも当然ながらベアメタルのForumがあります。

 

BareMetalで遊ぶ Raspberry Pi

tatsu-zine.com

 貴重な日本語の情報。表紙はかわいらしいですが、中身はすべてラズベリーパイのベア・メタルプログラミングに関するものです。JTAGを使ったデバッグの方法が非常に参考になりました

 

32ビットコンピュータをやさしく語る はじめて読む486

www.amazon.co.jp

オリジナルのHariboteの動作を理解するにあたって非常にためになりました。Githubリポジトリーがあり、現代のPC環境でサンプルコードを実行する手引きが解説されています。

github.com

改訂ARMプロセッサ-32ビットRISCのシステム・アーキテクチャ

改訂 ARMプロセッサ

書籍ページの紹介

本書は,組み込み用RISC型マイクロプロセッサとして広く普及しているARMプロセッサの解説書です.ARMプロセッサの開発当初から関わってきた著者(マンチェスター大学)が,RISCプロセッサの歴史を振り返りながら,ARMアーキテクチャを詳細にわかりやすく解説していきます.改訂版では,旧版で扱っていたARM7TDMI,ARM8の各コアに加えて,ARM9TDMI,ARM9E,ARM10TDMI,ARM10Eなどの新しいコアについても触れられています.また,RISCプロセッサの原理を学ぶ教科書としても最適です.
 原書名:ARM System-on-chip Architecture (second edition)

 絶版のようなので、当初参考文献にいれていたなかったのですが、最も良く参照した本の一つなのと、アマゾンなどで手に入るようなので加えておきます。

実をいうと10年程前に買ったっきり開いていなかったのですが、今回はとても重宝しました。特にヴァーチャルアドレッシングの説明はわかりやすく助かりました。2001年の本と言うことで今となっては内容に古い部分もありますが、そういった部分の多くは拡張部分なので現代のアップデートと共に読めば今でも参考になります。

ARM Information center

ARM Information Center

Welcome to the ARM Infocenter. The Infocenter contains all ARM non-confidential Technical Publications, including:

ARMの情報ページです。ARM自体に関わる事はここで多くの情報が得られます。

まとめ

「30日でできる!OS自作入門」のHaribote OSをラズベリーパイに移植したので、その内容を報告しました。移植した結果はGithubで公開済みです。

 

関連記事

ラズベリーパイ版Haribote OSのビルド環境とインストール方法 - Moiz's journal

Raspberry Pi ZeroでHaribote OSを動かす - Moiz's journal

Raspberry Pi用Haribote OSの変更箇所1 - セグメントのページングによる置き換え - Moiz's journal

Raspberry Pi用Haribote OSの変更箇所2 - タスクスイッチ - Moiz's journal

ラズベリーパイ上のベアメタルプログラミングにおけるメールボックス 機能 - Qiita

 

修正箇所

当初、川合氏の名前を誤表記していたことをブックマークにして指摘頂きましたので修正しました。川合氏には大変失礼いたしました。またご指摘ありがとうございます。

(2/19)参考文献を追加しました。tview関連のバグが修正されたのでその旨表記しました