bobuhiro11's diary

piでベアメタルプログラミング

13 Jan 2014

0. はじめに

実機でhello worldを目標に簡単なブートプログラムを書きます. せっかく友人からもらったraspberry piを遊ばせているので いいおもちゃになりそうです. ただ,ほとんどhttp://wiki.osdev.org/ARM_RaspberryPi_Tutorial_Cを参考にしているので 詳細を知りたい方は原文を読んでいただけたらと思います. 前半はエミュレータとクロスコンパイラの準備です. 全部コピペしてくれれば動くようにはしているつもりです.

対象はraspberry pi Model Bと呼ばれるものです.(これしか持ってないです) wikipediaより

1. エミュレータ

まずqemuを使って,raspbianが動作するか確認します.

もともとインストールされていたqemuは qemu-system-arm -cpu ?にarm1176は含まれていますが qemu-system-arm -M ?にBCM2835ボードが含まれていないようなので, 代わりにversatilepbを使います. そのため実機とエミュレータでハードウェアのアドレス(GIOやUART)が 変わるので注意が必要です.

-cpuでcpuの種類,-mでメモリサイズ(MB),-hdaでイメージファイル, -appendでを指定します. -kernelでホスト環境にカーネルのファイルを指定します. raspbianのイメージファイルはここから. http://www.raspberrypi.org/downloads

$ wget http://xecdesign.com/downloads/linux-qemu/kernel-qemu
$ wget http://files.velocix.com/c1410/images/raspbian/2012-10-28-wheezy-raspbian/2012-10-28-wheezy-raspbian.zip
$ unzip 2012-10-28-wheezy-raspbian.zip
$ qemu-system-arm \
  -kernel kernel-qemu \
  -M versatilepb \
  -cpu arm1176 -m 256 \
  -append "root=/dev/sda2 panic=1" -hda 2012-10-28-wheezy-raspbian.img

2. クロスコンパイラ

まず,binutilsをインストールします. アセンブラ,リンカ,などをまとめたものです. 今後何か移植するかも知れないのでnewlibも含めておきます. 恐らくこのステップが一番大変です. 何度もやり直しましたorz

TARGETをarm-none-eabi, PREFIXを/usr/local/cross-piにします.

$ export LDFLAGS="-L/opt/local/lib"
$ export CFLAGS="-I/usr/local/include -O2"
$ cd /usr/local
$ mkdir cross-pi

$ cd /usr/local/src
$ wget http://ftp.gnu.org/gnu/binutils/binutils-2.23.tar.gz
$ tar xvf binutils-2.23.tar.gz
$ cd binutils-2.23
$ ./configure --target=arm-none-eabi --prefix=/usr/local/cross-pi
$ make
$ make install

$ cd /usr/local/src
$ wget http://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-4.8.2/gcc-4.8.2.tar.gz
$ wget ftp://sourceware.org/pub/newlib/newlib-1.20.0.tar.gz
$ tar xvf gcc-4.8.2.tar.gz
$ tar xvf newlib-1.20.0.tar.gz
$ cd gcc-4.8.2
$ ln -s ../newlib-1.20.0/newlib .
$ mkdir work
$ cd work
$ ../configure --prefix=/usr/local/cross-pi --target=arm-none-eabi --enable-multilib --with-newlib --enable-languages="c,c++" --enable-interwork
$ make
$ make install

3. カーネル

以下のファイルで構成されます.

3.1 include/mmio.h

外部端子は,メモリに配置されているので(Memory Mapped IO) それを操作する関数群です. inlineをつけてインライン展開するように,volatileをつけて 最適化を行わないようにしています.

#ifndef MMIO_H
#define MMIO_H

#include <stdint.h>

/* MMIOに書き込む */
static inline void mmio_write(uint32_t reg, uint32_t data) {
	uint32_t *ptr = (uint32_t*)reg;
	asm volatile("__mmio_write_%=: str %[data], [%[reg]]"
			: : [reg]"r"(ptr), [data]"r"(data));
}

/* MMIOから読み込む */
static inline uint32_t mmio_read(uint32_t reg) {
	uint32_t *ptr = (uint32_t*)reg;
	uint32_t data;
	asm volatile("ldr %[data], [%[reg]]"
			: [data]"=r"(data) : [reg]"r"(ptr));
	return data;
}

#endif

3.2 include/uart.h

MMIOの具体的なアドレスを定義しておきます. IS_EMULATEが見苦しいです.ごめんなさい. (Makefileの方に書きます.) 実機のほうは,http://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf を参照すればよさそうです. 0x7Ennnnnn(バスアドレス)は,0x20nnnnnn(物理アドレス)に対応します. p5,6に書いてます. p89にGPIOのベースアドレスがあります. p175からは,UARTのベース物理アドレスや, DR,RSRECRなどのオフセットがあります.(これは実機,versatile共通化かな) エミュレータのほうは,http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0224i/Chdbeibh.htmlの4.1 Memory Mapなどに書いてあります.

#ifndef UART_H
#define UART_H

/* 
 * 1: エミュレータ
 * 0: 実機
 */
#define IS_EMULATE 1
 
#include <stdint.h>

enum {
	#if IS_EMULATE == 1	
		/* エミュレータ */
		GPIO_BASE  = 0x101E4000,
		UART0_BASE = 0x101F1000,
	#else
		/* 実機 */
		GPIO_BASE  = 0x20200000,
		UART0_BASE = 0x20201000,
	#endif

	/* Controls actuation of pull up/down to ALL GPIO pins. */
	GPPUD = (GPIO_BASE + 0x94),

	/* Controls actuation of pull up/down for specific GPIO pin. */
	GPPUDCLK0 = (GPIO_BASE + 0x98),

	/* UART0 */
	UART0_DR     = (UART0_BASE + 0x00),
	UART0_RSRECR = (UART0_BASE + 0x04),
	UART0_FR     = (UART0_BASE + 0x18),
	UART0_ILPR   = (UART0_BASE + 0x20),
	UART0_IBRD   = (UART0_BASE + 0x24),
	UART0_FBRD   = (UART0_BASE + 0x28),
	UART0_LCRH   = (UART0_BASE + 0x2C),
	UART0_CR     = (UART0_BASE + 0x30),
	UART0_IFLS   = (UART0_BASE + 0x34),
	UART0_IMSC   = (UART0_BASE + 0x38),
	UART0_RIS    = (UART0_BASE + 0x3C),
	UART0_MIS    = (UART0_BASE + 0x40),
	UART0_ICR    = (UART0_BASE + 0x44),
	UART0_DMACR  = (UART0_BASE + 0x48),
	UART0_ITCR   = (UART0_BASE + 0x80),
	UART0_ITIP   = (UART0_BASE + 0x84),
	UART0_ITOP   = (UART0_BASE + 0x88),
	UART0_TDR    = (UART0_BASE + 0x8C),
};

void uart_init();
void uart_putc(uint8_t byte);
void uart_puts(const char *str);
uint8_t uart_getc();

#endif 

リンカスクリプトです. メモリの配置を決めます. ENTRY(Start)はカーネルイメージのエントリポイントを表します. (シンボルStartはboot.Sで定義されています.) 一旦elfに変換してから生バイナリにするのでリンカにとってこうしたほうが良さそうです. (よくわかんない) “.”が現在のメモリアドレスです. textセグメントの先頭に.text.bootセクションを配置します. その後ろに残りのtextセグメントを配置します. _text_startや_text_endなどをわざわざ宣言しているのはソースコードから利用するためです. 実際_bss_startはboot.Sから利用されています.(0初期化の変数群) . = ALIGN(4096)で,4096バイトごとにalignmentしています. 別々のセグメントが同じページに混じるとパーミッションとかがまずいので.

ENTRY(Start)
 
SECTIONS
{
    /* 
     * Starts at LOADER_ADDR. 
     * 0x8000 ... 実機
     * 0x10000 ... qemu
     * */
    //. = 0x8000;
    . = 0x10000;
    _start = .;
    _text_start = .;
    .text :
    {
        KEEP(*(.text.boot)) /* テキストセグメントの先頭は.text.bootセクション */
        *(.text)            /* その後ろに他の.textセクションを配置 */
    }
    . = ALIGN(4096);
    _text_end = .;
 
    _rodata_start = .;
    .rodata :
    {
        *(.rodata)
    }
    . = ALIGN(4096);
    _rodata_end = .;
 
    _data_start = .;
    .data :
    {
        *(.data)
    }
    . = ALIGN(4096);
    _data_end = .;
 
    _bss_start = .;
    .bss :
    {
        bss = .;
        *(.bss)
    }
    . = ALIGN(4096);
    _bss_end = .;
    end = .;
    _end = .;
}

3.4 boot.S

.text.bootセクションを設定し,テキストセグメントの先頭に配置するようにします. その後,spを適当に設定し,bssエリアを0クリアし,kernel_main(C言語)にジャンプします.

.section ".text.boot"
 
.globl Start
 
/* 
 * Entry point for the kernel.
 * r15 -> プログラムカウンタ( 実機なら0x8000,エミュレータなら0x10000)
 * r0 -> 0x00000000
 * r1 -> 0x00000C42
 * r2 -> 0x00000100 ATAGS
 * kernel_mainのため,r0-r2を使わない
 */
Start:
  /* スタックポイント初期化 */
  mov sp, #0x8000
 
  /* bss0クリア */
  ldr r4, =_bss_start
  ldr r9, =_bss_end
  mov r5, #0
  mov r6, #0
  mov r7, #0
  mov r8, #0
  b   2f
 
1:
  /* アドレスr4にr5-r8をストア,r4をインクリメント
   * http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0170b/BABEFCIB.html
   */
  stmia r4!, {r5-r8}
 
  /* bss_endより小さければループ */
2:
  cmp r4, r9
  blo 1b
 
  /* kernel_mainへジャンプ */
  ldr r3, =kernel_main
  blx r3
 
halt:
  wfe
  b halt

3.5 main.c

苦労してnewlibを入れたのでwrite()やstrlen()が問題なく使えます. write()については中身を書いてやらないといけません. (またあとで説明します.) GPUのブートローダ(?)から,レジスタr0-r2を通して カーネルにパラメータが渡され,boot.Sからkernel_mainに渡されます.

#include <stdint.h>
#include <uart.h>
#include <stdlib.h>

const char *hello="Hello, cinderella of the tea plantation.\n";
const char *halt="** halt **\n";

void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags) {
	int *a;
	int i;
	char s[30];
	(void)r0;
	(void)r1;
	(void)atags;

	uart_init();

	uart_puts(hello);
	write(0,hello,strlen(hello));

	// Wait a bit
	for(volatile int i = 0; i < 10000000; ++i) { }

	uart_puts(halt);
	while(1){
		uart_putc(uart_getc());
	}
}

3.6 uart.c

UARTの初期化と通信の関数群です. BCM2835 ARM Peripherals (pdfの資料) のp101あたりに初期化手順があります. 150サイクル待ちましょうとか書いてます.

#include<stdint.h>
#include<mmio.h>
#include<uart.h>


/*
 * countサイクル待つ
 *
 * ループを最適化しない
 *
 * 00000030 <__delay_16>:
 * 30:   e2533001        subs    r3, r3, #1
 * 34:   1afffffd        bne     30 <__delay_16>
 */
static void delay(int32_t count) {
	asm volatile("__delay_%=: subs %[count], %[count], #1; bne __delay_%=\n"
			: : [count]"r"(count) : "cc");
}

/*
 * UART0の初期化
 *
 *	   1. GPのIXDとRXDを有効にする
 *	   2. ボーレートを計算
 *	   3. 割り込みを有効にする
 */
void uart_init() {
	/* UART0を無効に Control Registerを0に */
	mmio_write(UART0_CR, 0x00000000);

	/* GPIO pin 14,15を初期化 
	 * シリアル通信に使うIXDとRXDに対応 p102より
	 * GPIOには割り当て方が6種類ある.ここではALT0を使っているよう
	 * GRIO pin 14: IXD0
	 * GRIO pin 15: RXD0
	 */

	/* GP pull-up,downを無効にして150サイクル待つ */
	mmio_write(GPPUD, 0x00000000);
	delay(150);

	/* GP pull-up,down の14,15ビット目をセットし,150サイクル待つ */
	mmio_write(GPPUDCLK0, (1 << 14) | (1 << 15));
	delay(150);

	/* GPPUDCLK0 を0に */
	mmio_write(GPPUDCLK0, 0x00000000);

	/* Interrupt Clear Register 割り込みを無効にする */
	mmio_write(UART0_ICR, 0x7FF);

	/* ボーレート(シリアル通信に使う)の整数部・小数部の計算
	 *
	 * Divider = UART_CLOCK/(16 * Baud)
	 * Fraction part register = (Fractional part * 64) + 0.5
	 * UART_CLOCK = 3000000; Baud = 115200.
	 *
	 * Divider = 3000000/(16 * 115200) = 1.627 = ~1.
	 * Fractional part register = (.627 * 64) + 0.5 = 40.6 = ~40.
	 *
	 */
	mmio_write(UART0_IBRD, 1);
	mmio_write(UART0_FBRD, 40);

	/* FIFO,8bit通信を有効にする.
	 * 4bit目: FIFOを有効に
	 * 5,6bit目: 1フレームの通信量 11なら8 bit/フレーム
	 */
	mmio_write(UART0_LCRH, (1 << 4) | (1 << 5) | (1 << 6));

	/* 全ての割り込みを有効に 
	 * 1:有効 0:無効
	 * 1bit目: uUARTCTS modern 割り込み
	 * 4bit目: 受信割り込み
	 * 5bit目: 送信割り込み
	 * 6bit目: 受信タイムアウト割り込み
	 * 7bit目: フレームエラー割り込み
	 * 8bit目: パリティエラー割り込み
	 * 9bit目: Breakエラー割り込み
	 * 10bit目: オーバーランエラー割り込み
	 */
	mmio_write(UART0_IMSC, (1 << 1) | (1 << 4) | (1 << 5) |
			(1 << 6) | (1 << 7) | (1 << 8) |
			(1 << 9) | (1 << 10));

	/* 0bit目: UARTを有効に
	 * 8bit目: 送信を有効に
	 * 9bit目: 受信を有効に
	 */
	mmio_write(UART0_CR, (1 << 0) | (1 << 8) | (1 << 9));
}

/*
 * 1バイトシリアル送信
 */
void uart_putc(uint8_t byte) {
	/* FR 5bit目: 送信FIFOがいっぱいなら1 */
	while (1) {
		if (!(mmio_read(UART0_FR) & (1 << 5))) {
			break;
		}
	}
	mmio_write(UART0_DR, byte);
}

/*
 * 0終端文字列をシリアル送信
 */
void uart_puts(const char *str) {
	while (*str) {
		uart_putc(*str++);
	}
}

/*
 * 1バイトシリアル受信
 */
uint8_t uart_getc() {
	/* FR 4bit目: 受信FIFOが空なら1 */
	while(1) {
		if (!(mmio_read(UART0_FR) & (1 << 4))) {
			break;
		}
	}
	return mmio_read(UART0_DR);
}

3.7 syscalls.c

newlibが使えるようにシステムコールを書いていきます. リンクの際に足りないものを増やしていきました. とりあえずwrite()でシリアル送信するようにしておきます.

#include <errno.h>
#include <uart.h>

#undef errno
extern int errno;

/* 
 * send message to uart0 (serial port)
 */
int _write(int fd, char *ptr, int len){
	int i=0;
	while(i<len && ptr[i]!='\0')
		uart_putc(ptr[i++]);
	return i;
}

int _close(int file) {
	return -1;
}
int _fstat(){
	return -1;
}
int _sbrk(){
	return -1;
}
int _kill(){
	return -1;
}
int _exit(){
	return -1;
}
int _getpid(){
	return -1;
}

int _gettimeofday(){
	return -1;
}
int _isatty(){
	return -1;
}
int _lseek(){
	return -1;
}
int _read(){
	return -1;
}

3.8 Makefile

ソースファイル(*.c,*.S) -> オブジェクトファイル(*.o) -> 実行可能ファイル(*.elfファイル) -> バイナリファイル(*.img)

と変換しています. CFLAGSには組み込み,カーネル向けの fpic,nostdlib,nostartfiles,ffreestanding,nodefaultlibs を指定しています. elf - 生バイナリ - アセンブラの変換はこのページが便利です. http://d.hatena.ne.jp/ken_2501jp/20121107/1352311439%5D

PREFIX = /usr/local/cross-pi
ARMGNU = $(PREFIX)/bin/arm-none-eabi
QEMU = /usr/local/bin/qemu-system-arm

# ソースファイル
SOURCES_ASM := $(wildcard *.S)
SOURCES_C   := $(wildcard *.c)
 
# オブジェクトファイル
# 	$(patsubst 検索文字列,置換後文字列,対象文字列)
OBJS        := $(patsubst %.S,%.o,$(SOURCES_ASM))
OBJS        += $(patsubst %.c,%.o,$(SOURCES_C))

# elfフォーマット
ELF					:= kernel.elf

# 生バイナリ
BINARY 			:= kernel.img

# リスト
LIST 				:= kernel.list

# マップ
MAP					:= kernel.map
 
# 依存関係を出力
DEPENDFLAGS := -MD -MP
# インクルードパスを指定 (gccで使う)
INCLUDES    := -I include -I /usr/local/src/mruby/include
# ライブラリパスを指定 (ldで使う) 順番も関係するので注意
# /usr/local/src/mruby/build/arm/lib/libmruby_core.a 
LIBS 				:= /usr/local/src/mruby/build/arm/lib/libmruby.a \
/usr/local/cross-pi/arm-none-eabi/lib/libc.a \
/usr/local/cross-pi/arm-none-eabi/lib/libm.a \
/usr/local/cross-pi/arm-none-eabi/lib/libg.a	\
/usr/local/cross-pi/lib/gcc/arm-none-eabi/4.8.2/libgcc.a

# CLAGSベース
#
#		標準ライブラリや,main関数に依存しないように
#		fpic: 位置独立コード
#		nostartfiles, ffreestanding, nodefaultlibs: 組み込みやカーネル向け
#		fno-builtin: ビルトイン関数は置換しない
#		fomit-frame-pointer: 関数呼び出し時にフレームポインタは使わない
#
#		pendatic: mrubyのヘッダ内でgcc拡張機能を使っているようなので外す
BASEFLAGS   := -O2 -fpic -nostdlib
BASEFLAGS   += -nostartfiles -ffreestanding -nodefaultlibs
BASEFLAGS   += -fno-builtin -fomit-frame-pointer -mcpu=arm1176jzf-s
ASFLAGS     := $(INCLUDES) $(DEPENDFLAGS) -D__ASSEMBLY__
# CFLAGS
# c99を使う
CFLAGS      := $(INCLUDES) $(DEPENDFLAGS) $(BASEFLAGS) $(WARNFLAGS)
CFLAGS      += -std=gnu99

# qemuオプション
CPU 	   := arm1176
MEM        := 256
#MACHINE    := realview-eb
MACHINE    := versatilepb
SERIAL	   := stdio
QEMU_OPT   := -nographic -m $(MEM) -M $(MACHINE) -cpu $(CPU) -serial $(SERIAL)
 
# イメージファイル
all: $(BINARY) $(LIST)

# qemuで実行
run: $(ELF)
	$(QEMU)  $(QEMU_OPT) -kernel $(ELF)
 
include $(wildcard *.d)
 
# linkしてkernel.elf
$(ELF): $(OBJS) link-arm-eabi.ld
	$(ARMGNU)-ld -o $@ $(OBJS) $(LIBS) -Tlink-arm-eabi.ld -Map $(MAP)

# kernel.elfからkernel.list
$(LIST) : $(ELF)
	$(ARMGNU)-objdump -d $(ELF) > $(LIST)
 
# kernel.elfから生バイナリkernel.imgへ
$(BINARY): kernel.elf
	$(ARMGNU)-objcopy $(ELF) -O binary $(BINARY)
 
clean:
	$(RM) -f $(OBJS) $(BINARY) $(ELF) $(MAP) $(LIST)

# *.dも含めて削除
dist-clean: clean
	$(RM) -f *.d
 
# *.cから*.oファイルに
%.o: %.c Makefile
	$(ARMGNU)-gcc $(CFLAGS)  -c $< -o $@
 
# .Sから*.oファイルに
%.o: %.S Makefile
	$(ARMGNU)-gcc $(ASFLAGS) -c $< -o $@

4. おわりに

$ makeとするとイメージファイルができ,$ make runとすると qemu上で動作します. kernel.elfとkernel.imgがちゃんと出来ているのも確認できます. alignmentも上手くできていました.

$ arm-none-eabi-objdump -D kernel.elf | head -n 50

kernel.elf:     ファイル形式 elf32-littlearm

セクション .text の逆アセンブル:

00010000 <Start>:
   10000:	e3a0d902 	mov	sp, #32768	; 0x8000
   10004:	e59f4030 	ldr	r4, [pc, #48]	; 1003c <halt+0x8>
   10008:	e59f9030 	ldr	r9, [pc, #48]	; 10040 <halt+0xc>

$ hexdump kernel.img  | head -n 10
0000000 02 d9 a0 e3 30 40 9f e5 30 90 9f e5 00 50 a0 e3
0000010 00 60 a0 e3 00 70 a0 e3 00 80 a0 e3 00 00 00 ea

Raspberry piのブートプロセスも気になったので,このページを読んでみました. http://elinux.org/RPi_Software#GPU_bootloaders

Raspberry piに電源を入れたとき,CPUはhaltされていてGPUの中の小さなRISC CPUが SoC(System on Chip)内のプログラムを実行します. そのためブートプロセスはGPUで実行されることになります.

  1. 1st ブートローダ: FAT32フォーマットされたSDカードのブートパーティションをマウントします. SoC内にあるので修正できません.
  2. 2nd ブートローダ(bootcode.bin): GPUファームウェアをSDカードから取り出すのに使います.
  3. GPUファームウェア(start.elf): fixup.datによりGPUとCPUで使うSDRAMのパーティションを設定します. (ここで,CPUがGPUから起こされる)
  4. ユーザコード(kernel.img): CPUにより,Linuxカーネル(kernel.img)や,他のブートローダ(U-Bootなど), OSを持たないアプリケーションなどが実行される. (config.txtにkernel=u-boot.binのようにすれば好きなイメージファイルが 実行される. それと,cmdline.txtにカーネルオプションがかける.)

2012/10/19までは3rdブートローダもあったみたいだが,今は必須ではないようです.

1st,2ndブートローダを作るのは大変ですが,オレオレカーネルを作る方のため?に, githubにbootconde.bin,start.elfが公開されているので大丈夫です. https://github.com/raspberrypi/firmware/tree/master/boot バイナリだけでソースコードは公開されていないみたいです. 公開されたとしてもarm cpuじゃなくてGPUのほうだから読めるのかどうか. 設定ファイルはconfig.txtで例えば, GPUのメモリを16MBにするときは,config.txtでgpu_mem=16としてfixup_cd.dat, start_cd.elf を追加すれば良さそうです.

http://wiki.gentoo.org/wiki/Raspberry_Pi を見てみると,SDカードの構成のサンプルがあります. 今回はファイルシステムは使わないのでブートパーティションだけあれば十分ですが.

さらにもう少し調べてみるとCPUがカーネルを走らせ始めた後もGPUのコードはアンロード されないということが分かりました. GPUは,VCOS (Video Core Operating System)と呼ばれる小さなOSを走らせ, カーネル側とmailboxというプロトコルや割り込みを通して,グラフィックの 操作を行います. 驚いたことにGPUはグラフィックだけでなくクロック制御やオーディオの制御 も行うようです.

せっかくnewlibを使えるようになったので何か移植してみようかと思います. 移植性のよいmrubyか,オレオレschemeか,その辺りになりそうです.

参考

サンプルベアメタルカーネル


comments powered by Disqus < 2013年総括 3imp ヒープ型VM >