ラズパイ3でベアメタル - QEMUでUART(PL011)

「キー入力と文字の画面表示」くらいはRaspberry Pi 3実機を使わずに、
QEMUで動作確認できると便利です。


現状のQEMU(Debian JessieでAPTでインストールできるもの)でも、
UARTでPL011(*1)が利用可能です。
(*1): PrimeCell UART(PL011) Technical Reference Manual


今回は、PL011 UART向けにこれまでのUARTのプログラムを書き換え、
QEMU上で動作確認してみます。


なお、Raspberry Pi 3で64bitベアメタル(bare metal)プログラミングを試してみる
本シリーズの目次はコチラです。


1. Mini UARTからの変更点

Mini UARTからの変更点は主に以下の点です。
1. レジスタ名とアドレス
2. FIFOステータスを示すレジスタのビットの意味の読み替え


「送受信のFIFOがあり、
FIFOステータスのレジスタを確認してから、
FIFOへアクセスする」という
UARTの基本的な点は(当たり前かも知れませんが、)変わりません。

2. PL011のレジスタについて

それでは、PL011で使用するレジスタを見ていきます。

2.1. UARTFR: フラグレジスタ

FIFOのステータスを確認するレジスタは、PL011ではUARTFR(UART Flag Register)です。

  • ビット5: TXFF(送信FIFOフル)
    • このビットが1でない間は送信FIFOへデータを追加できる
  • ビット4: RXFE(受信FIFOエンプティ)
    • このビットが1でない間は受信FIFOからデータを取り出せる
2.2. UARTDR: データレジスタ

FIFOへのデータ追加と取り出しを行うレジスタは、PL011ではUARTDR(UART Data Register)です。


使い方はMini UARTのIOレジスタと同じです。

  • UARTDRの読み出し: 受信FIFOからデータ取り出し
  • UARTDRへ書き込み: 送信FIFOへデータ追加

3. ソースコード

これまで作成した以下の2つのUARTプログラムをQEMU向けに変更します。


なお、作成したプログラムはMakefile付きで以下のGitHubリポジトリの、

それぞれ以下のディレクトリへ追加しています。

  • その3「'A'を送信し続けるプログラム」: uart_tx_char_simple_qemu
  • その4「エコーバックプログラム」: uart_echo_back_simple_qemu
3.0. start.Sの変更

その3と4のプログラム両方に共通する変更点として、プログラムの実行開始アドレスの違いがあります。


QEMUでは実行開始アドレスは0x40080000なので、
スタック領域も0x40080000より上(アドレスが小さくなる方向)の領域を使用するよう、
スタックポインタ(sp)に0x40080000を設定します。

	mov	w0, #0x40080000	/* 汎用32ビットレジスタw0へ0x40080000を格納 */
	mov	wsp, w0		/* w0をspの下位32ビット(wsp)へ格納 */
	bl	main		/* mainへ分岐 */


また、ビルド時のldコマンドで指定する-Ttextオプション(textセクション開始アドレス)も0x40080000へ変更します。
改めてまとめると以下のとおりです。
(今回も依然としてtextセクションしか使用しないプログラムなので、リンカスクリプトはサボり、
ldコマンドでtextセクションのみ指定しています。)

[PC]$ aarch64-linux-gnu-as -o start.o start.S
[PC]$ aarch64-linux-gnu-gcc -c -o loop.o loop.c
[PC]$ aarch64-linux-gnu-ld -Ttext 0x40080000 -o kernel8.elf start.o loop.o
[PC]$ aarch64-linux-gnu-objcopy -O binary kernel8.elf kernel8.img

なお、上述のGitHubリポジトリには、これらのコマンドを記述したMakefileを追加しているので、
それぞれのプログラムのディレクトリへ移動し、makeを実行するだけでビルドできます。

3.1. 'A'を送信し続けるプログラム

変更点はレジスタとビットフィールドの定数のみです。

#define PL011_UARTDR		(*(volatile unsigned int *)0x09000000)
#define PL011_UARTFR		(*(volatile unsigned int *)0x09000018)
#define PL011_UARTFR_TXFF	(1U << 5)

int main(void)
{
	while (1) {
		while (PL011_UARTFR & PL011_UARTFR_TXFF);
		PL011_UARTDR = (unsigned int)'A';
	}

	return 0;
}
3.2. エコーバックプログラム

こちらも同じく、変更点はレジスタとビットフィールドの定数のみです。

#define PL011_UARTDR		(*(volatile unsigned int *)0x09000000)
#define PL011_UARTFR		(*(volatile unsigned int *)0x09000018)
#define PL011_UARTFR_TXFF	(1U << 5)
#define PL011_UARTFR_RXFE	(1U << 4)

int main(void)
{
	volatile char ch;

	while (1) {
		while (PL011_UARTFR & PL011_UARTFR_RXFE);
		ch = (char)PL011_UARTDR;

		while (PL011_UARTFR & PL011_UARTFR_TXFF);
		PL011_UARTDR = (unsigned int)ch;
	}

	return 0;
}

4. 動作確認

上述のビルド方法でビルドし、できあがったkernel8.imgは、
以下のコマンドで、QEMU上で実行できます。

[PC]$ qemu-system-aarch64 -cpu cortex-a57 -M virt -kernel kernel8.img


QEMU起動後、「Ctrl+Alt+2」でシリアルポートの画面へ切り替えられます。


なお、起動直後の"(qemu)"のプロンプトが表示された状態は、
QEMUモニター」というQEMUのモードで、シリアルポート画面からは「Ctrl+Alt+1」で切り替えられます。
このモードでは、汎用レジスタの値や、任意のアドレス上の値を表示できたり、デバッグするのにとても便利です。
詳細は以下のマニュアルなどが参考になります。