ラズパイ3でベアメタル - その2:GPIO制御


「その1:何もしない無限ループプログラム」からの続きです。
今回はGPIOのあるピンのHigh/Lowを制御するところまでをまとめてみます。


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

1. 目標

GPIOはGeneral Pin Input/Outputの略で、汎用的にピンの入出力を決める機能です(日本語にしただけですが。。)。
ここではGPIO16番のピンのHigh/Lowを制御してみます。


GPIO16番のピンの場所は以下の記事が参考になります。
(Raspberry Pi 2用の記事ですが、Raspberry Pi 3も同じです。)

また、Raspberry Pi公式から回路図の一部は公開されています。

2. GPIO16番を制御するためにやらなければならないこと

VideoCoreがARMのCoreの実行を開始してから、
GPIO16番を制御するために最低限やらなければならないことは以下の2つです。
1. ピンに割り当てられている機能をGPIOに設定する(GPFSELレジスタ)
2. GPIO16番ピンをHigh、あるいはLowに設定する(GPSET/GPCLRレジスタ)


これら2つの設定はレジスタを通して行います。
ARMの場合、レジスタはメモリに割り当てられている(マップされている)ので、
特定のアドレスへ読み書きすれば、その特定のアドレスにマップされているレジスタを読み書きできます。


GPIOのレジスタのアドレスについて、Raspberry Pi 3としての資料は見つけることができなかったのですが、
初代Raspberry Piの以下のページの「Peripheral specification(*1)」が参考になります。


ただし、GPIO含む周辺機能のアドレス変換の仕様が変わっている都合上、Raspberry Pi 2/3の場合、
(*1)の資料のアドレス表記の上位8ビットを0x3fへ読み替える必要があります。
そのため、(*1)のP.90に記載されているGPIOに関するレジスタのアドレスについて、
例えばGPFSEL1は「0x7E20 0004」から「0x3F20 0004」へ読み替えを行います。

3. GPFSEL・GPSET・GPCLRの3つのレジスタについて

3.1. GPFSEL(GPIO Function Select)

GPFSELはGPIOの機能を設定するレジスタで、(*1)のP.91から説明があります。


GPFSEL0〜GPFSEL5の6つのレジスタ(各32ビット)があり、
GPIO16番の機能設定はGPFSEL1レジスタのFSEL16ビットで行います((*1)P.92のTable 6-3)。
FSELビットの設定値は、Table 6-3のFSEL19の説明の通りですが、GPIO出力ピンを設定するには0b001を設定します。

3.2. GPSET/GPCLR

GPSET/GPCLRはGPIO出力を設定するレジスタで、GPSETでHighを、GPCLRでLowを設定します。
レジスタの各ビットが各GPIOピンに対応しており、GPIO16番はGPSET0/GPCLR0の下から16ビット目です。

4. C言語を使えるようにする

メモリ上のあるアドレスへのデータ書き込みともなると、
アセンブラよりC言語の方が少ない行数で書けるので、
ここでC言語を使用できるようにします。


C言語で書いたコードを使用するためにアセンブラでやらなければならないことは
最低限以下の2つです。

1. スタックポインタ(sp)を設定
2. C言語の関数へジャンプ


これら2つをアセンブラで書くと以下のとおりです。
ARM Coreの実行開始アドレスが0x0008 0000で、spはそこより若いアドレスの領域を使うようにしています。
■ start.S

	mov	sp, #0x80000	/* スタックポインタを設定 */
	bl	main		/* C言語のmain関数へジャンプ */


なお、main関数からリターンすることを想定していないので、
main関数には無限ループを用意するなどして関数から抜けないようにすることとします。


前回の何もしない無限ループプログラムをCで書くと以下のとおりです。
■ loop.c

int main(void)
{
	while (1);
}


以上の2つのファイルからkernel8.imgを生成する手順は以下のとおりです。

aarch64-linux-gnu-as -o start.o start.S
aarch64-linux-gnu-gcc -c -o loop.o loop.c
aarch64-linux-gnu-ld -Ttext 0x80000 -o kernel8.elf start.o loop.o
aarch64-linux-gnu-objcopy -O binary kernel8.elf kernel8.img

ここでのプログラムはコードのみでデータは無いので、
リンカスクリプトは用意せず、ldコマンドの引数でテキストセクションの開始アドレスのみ指定しています。
(0x80000はVideoCoreがARM Coreの実行を開始させる際のARM Coreの実行開始アドレスです。)

5. C言語からGPIO16番のHigh/Lowを制御する

以上の説明を踏まえ、GPIO16番ピンをHighにするプログラムは以下のとおりです。

#define GPFSEL1	0x3F200004
#define GPSET0	0x3F20001C

int main(void)
{
	*(volatile unsigned int *)GPFSEL1 = 0x01 << (3 * 6);

	*(volatile unsigned int *)GPSET0 = 0x01 << 16;

	while (1);

	return 0;
}


逆にGPIO16番をLowにする際は、
以下のように新たにGPCLR0レジスタ用の定数を定義し、

#define GPCLR0  0x3F200028

main()内の「GPSET0」の箇所を「GPCLR0」へ書き換えるだけです。

6. 動作確認

GPIOピンのHigh/Lowの確認にはテスターやオシロスコープを使用していましたが、
5VのGPIOなので、LEDを光らせるなら150Ω程度の抵抗を接続すると良いと思います。
(順方向電圧(VF)=2.0V、順方向電流(IF)=20mAほどを想定)