Raspberry PiでLチカ

ハードウェア版Hello, WorldであるところのLチカ(LED点灯)をRaspberry Pi(以下RPI)でやってみたいと思います。使用するのはRaspberry Pi B+モデルです。

やることは2ステップです。

  1. LEDに対応するピンを出力モードに設定する。
  2. 対応するピンの出力ビットを立てる。

がその前にRPIにおけるハードウェア制御について触れます。

なおビルドや実機での動かし方などは割愛するので以下のサイトを参考にしてください。

RPIでのハードウェア制御

ハードウェアはそれぞれ自身を制御するための記憶領域を持っており、これをレジスタと呼びます。このレジスタにCPUから値を読み書きすることで制御を行います。例えばLEDの場合「このレジスタの値がxxxxの時にLEDがつく」とハードウェアの仕様で決められているので、この値をCPUから書き込むことで点灯させます。

CPUからレジスタの値を読み書きするにはいくつか方法があるらしいのですが、RPIはメモリマップトIOと呼ばれる方式を採用しています。 これはハードウェアのレジスタをメモリに対応づけることで、メモリの読み書きを通してハードウェアを制御する方式です。レジスタに値を書き込みたいときはそのレジスタに対応するメモリ上のアドレスに値を書き込むだけで同時にハードウェアのレジスタを書き換えることができます。読み出しに関しても同様にメモリ上の値を読むだけです。メモリに対する通常のロード・ストア命令でハードウェアを制御できるシンプルさが利点です。 LEDのレジスタもこの方式に従いメモリ上にマップされているので、まずはじめにこれがどこにあるのかを調べることからはじめます。

ところでRPIにはいろんなハードウェアを接続するための汎用的な入出力ピンが存在します。この汎用IOピンの集合をGPIOと呼び全部で53のピンがあります。RPIにはじめから組み込まれているLEDもこの汎用ピンの一つを利用しており、53あるピンのうち47番目(RPI B+の場合)に接続されています。 つまり「LEDを制御するレジスタがメモリ上のどこにマップされているか」は言い換えれば「GPIOの47番ピンがメモリ上のどこにマップされているか」という問題になります。

どのレジスタがメモリ上のどこにマップされているのか、というのはARMのペリフェラル(周辺装置)マニュアルに書いてあります。これのp.89以降にGPIOに関する記述があり、GPIOがアドレス0x20200000の位置(RPI B+の場合)からはじまることがわかります1。今回必要なレジスタはモードの設定用のGPSELレジスタおよび出力用のGPSETレジスタです。書き込む値については実際にコードを見ながら触れたいと思います。


実際にLEDを点灯してみます。先にkernel_mainのコード全体は以下のとおりです。

void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags)
{
    (void) r0;
    (void) r1;
    (void) atags;

    *(volatile uint32_t *) 0x20200010 = (1 << 21);
    *(volatile uint32_t *) 0x20200020 = (1 << 15);

    while (1) {
    }
}

出力モードの設定

GPIOピンの制御モードには1ピンあたり3bit与えられているため、一つのレジスタ(32bit)で管理できるのは32 / 3 = 10ピンです。よって53ピンあるGPIOの制御用レジスタGPSELは0~5の6つに分かれており、それぞれ10ピンずつ割り当てられています。

今回の目的である47番ピンはSEL4ということになり、対応する3bitは21~23番目ということになります。SEL4はメモリ上0x20200010にあり、出力のモードは0b001ということになっているため21番目のビットを立てれば完了です。

*(volatile uint32_t *) 0x20200010 = (1 << 21);

(volatileというのが気になるかもしれません。この行はメモリに値を書き込むだけの処理のため、コンパイラが勝手に「この処理いらん」と判断するなどの最適化がはたらき、意図しているのと違うコードが生成されてしまう可能性があります。volatileはそれを回避するために付け足しています。)

出力レジスタのビットをたてる

ピンをHIGHにするGPSETレジスタは2つあり、1つめが0~31番ピン、2つめが32~53番ピンに対応します。今回47番ピンなので、2つ目のレジスタ47 - 32 = 15番目のビットを立てれば良いです。SET1レジスタはメモリ上0x20200020にあります。

*(volatile uint32_t *) 0x20200020 = (1 << 15);

以上をビルドして実機で動かすとLEDが点灯するのが確認できると思います。

Refs


もし点灯だけではなく点滅もしたい場合も手順は同じで、今度はピンをLOWにするGPCLRレジスタを使います。CLR1レジスタは0x2020002Cにあるため、

*(volatile uint32_t *) 0x2020002C = (1 << 15);

をするだけです。

点滅しているのががわかるように、CPUを空回しするdelay関数を以下のように用意します。

// https://jsandler18.github.io/tutorial/boot.html より
void delay(int32_t count)
{
    asm volatile("__delay_%=: subs %[count], %[count], #1; bne __delay_%=\n"
        : "=r"(count): [count]"0"(count) : "cc");
}

これをつかったkernel_main全体は、

void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags)
{
    (void) r0;
    (void) r1;
    (void) atags;

    *(volatile uint32_t *) 0x20200010 = (1 << 21);

    while (1) {
        *(volatile uint32_t *) 0x20200020 = (1 << 15);
        delay(500000);
        *(volatile uint32_t *) 0x2020002C = (1 << 15);
        delay(500000);
    }
}

になり、これを実行するとLEDが点滅するのが確認できるはずです。


  1. 0x7E200000と書いてありますが、違うメモリ空間上(VC CPU bus address)での値なのでARM physical address上の値である0x20200000を利用する。ペリフェラルマニュアルp.5の図を参照。