【Rust】Raspberry Pi 3でGPIOのレジスタを叩いてLチカ


RustでGPIOのレジスタを直接叩いてLEDをチカチカさせることに成功したので記載します。
RustもRaspberry Piも初心者なので不適切な箇所があればご指摘頂けると幸いです。

## 動作環境
* Raspberry Pi 3 Model B+
* raspbian 8.0
* Rust 1.21.0
* libc v0.2.31

### 回路構成
以下の図のようにGPIO27にLEDと抵抗を接続します。

```
+-----------------------------+
|                             |
|                             |   +--------+
|                       GPIO27+---+ 10ohms +--------------+|>|+------------+
|                             |   +--------+               LED             |
|       Raspberry pi 3        |       R1                                   |
|                             |                                            |
|                             |                                            |
|                          GND+--------------------------------------------+
|                             |
+-----------------------------+
```

## Lチカを実際にやってみる
以下はLEDの点滅を5回行うソースコードです。
プロジェクト一式は[Github](https://github.com/hukatama024e/rust_rpi_blink_led)で公開しています。

```Rust
extern crate libc;

use std::io;
use std::thread;
use std::time::Duration;
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::ptr::{ self, read_volatile, write_volatile };

//GPIO 仮想アドレス
const GPIO_ADDR : libc::off_t      = 0x3F200000; // BCM2836 and BCM2837 GPIO address

const MEM_BLK_SIZE : libc::size_t  = 4096;       // page size(4KB)

const GPFSEL2_OFFSET_ADDR : isize  = 0x02;       // 0x08 / 4
const GPSET0_OFFSET_ADDR : isize   = 0x07;       // 0x1C / 4
const GPCLR0_OFFSET_ADDR : isize   = 0x0A;       // 0x28 / 4 
const GPLEV0_OFFSET_ADDR : isize   = 0x0D;       // 0x34 / 4

const FSEL27_BIT : isize    = 21;
const SET27_BIT : isize     = 27;
const CLR27_BIT : isize     = 27;
const LEV27_BIT : isize     = 27;

const GPFSEL_INPUT : u32    = 0;
const GPFSEL_OUTPUT : u32   = 1;
const FSEL_MASK_BASE : u32  = 0x00000007;

const GPLEV_HIGH : u32      = 1;
const ON : u32              = 1;

const SLEEP_DELAY : u64     = 1;

fn main() {
    let mut blink_cnt : i32 = 0;
    
    let gpio_ptr : *mut u32 = map_gpio().expect( "failed to gpio mapping" );
    let fsel2_reg : *mut u32 = unsafe { gpio_ptr.offset( GPFSEL2_OFFSET_ADDR ) };
    let clr0_reg : *mut u32 = unsafe { gpio_ptr.offset( GPCLR0_OFFSET_ADDR ) };

    unsafe {
        //GPIO27を出力設定
        write_volatile( fsel2_reg, ( *fsel2_reg & ( FSEL_MASK_BASE << FSEL27_BIT ) ) |
                                     ( GPFSEL_OUTPUT << FSEL27_BIT ) );
    }
    
    //10秒間LEDをチカチカさせる
    while blink_cnt < 10 {
        blink_led( gpio_ptr );
        thread::sleep( Duration::from_secs( SLEEP_DELAY ) );
        blink_cnt = blink_cnt + 1;
    }
    
    //後始末
    unsafe {
        write_volatile( clr0_reg, *clr0_reg | ( ( ON << CLR27_BIT ) ) );
        write_volatile( fsel2_reg, ( *fsel2_reg & ( FSEL_MASK_BASE << FSEL27_BIT ) ) | 
                        ( GPFSEL_INPUT << FSEL27_BIT ) );
    }
}

//メモリマッピング
fn map_gpio() -> io::Result<*mut u32>  {
    let mem_file = OpenOptions::new()
                .read( true )
                .write( true )
                .custom_flags( libc::O_SYNC )
                .open( "/dev/gpiomem" )
                .expect( "can't open /dev/gpiomem" );

    unsafe {
        let gpio_ptr = libc::mmap( ptr::null_mut(),
                                   MEM_BLK_SIZE,
                                   libc::PROT_READ | libc::PROT_WRITE,
                                   libc::MAP_SHARED,
                                   mem_file.as_raw_fd(),
                                   GPIO_ADDR );

        if gpio_ptr == libc::MAP_FAILED {
            Err( io::Error::last_os_error() )
        }
        else
        {
            Ok( gpio_ptr as *mut u32 )
        }
    }
}

//GPIO出力を反転させてLEDをチカチカさせる
fn blink_led( gpio_ptr : *mut u32 ) {
    let set0_reg : *mut u32 = unsafe { gpio_ptr.offset( GPSET0_OFFSET_ADDR ) };
    let clr0_reg : *mut u32 = unsafe { gpio_ptr.offset( GPCLR0_OFFSET_ADDR ) };
    let lev0_reg : *mut u32 = unsafe { gpio_ptr.offset( GPLEV0_OFFSET_ADDR ) };
    let level : u32;
 
    unsafe {
       //現在のGPIO27の出力状態を取得
       level = ( read_volatile( lev0_reg ) >> LEV27_BIT ) & 0x00000001;
    }
    
    if level == GPLEV_HIGH {
        unsafe {
            //現在のGPIO27の出力状態がHIGHならば出力解除(LOWにする)
            write_volatile( clr0_reg, *clr0_reg | ( ON << CLR27_BIT ) );
        }
    }
    else
    {
        unsafe {
            //現在のGPIO27の出力状態がLOWならば出力設定(HIGHにする)
            write_volatile( set0_reg, *set0_reg | ( ON << SET27_BIT ) );
        }
    }
}
```

## GPIOレジスタについて
GPIOのPeripheralsレジスタを直接設定・取得することでGPIOを操作します。
以下、今回Lチカさせる際に使用するレジスタについて記載します。

| アドレス|レジスタ名 | 用途 | 備考 |
|:------------- |:---------- |:---------------------|:---------------------------- |
| 0x7E200008 | GPFSEL2 | GPIO機能設定        | GPIO27は21-23bit目に対応 |
| 0x7E20001C | GPSET0   | GPIO出力設定        | GPIO27は27bit目に対応      |
| 0x7E200028 | GPCLR0   | GPIO出力解除        | GPIO27は27bit目に対応      |
| 0x7E200034 | GPLEV9   | GPIO出力状態取得 | GPIO27は27bit目に対応      |

各レジスタの詳細やその他のGPIOのレジスタはBCM2835 Peripheralのデータシート(page 90～)に記載されていますので参照してください。
[BCM2835-ARM-Peripherals.pdf](https://www.raspberrypi.org/documentation//hardware/raspberrypi/bcm2835/BCM2835-ARM-Peripherals.pdf)

## レジスタアクセス
上記のGPIOレジスタのアドレスはバスアドレスですので直接参照してもGPIOレジスタにアクセス出来ません。
( BCM2835 Peripheralのデータシート(page 6)を参照)

> 1.2.4 Bus addresses
> The peripheral addresses specified in this document are bus addresses. Software directly
> accessing peripherals must translate these addresses into physical or virtual addresses, as
> described above. 

アクセスするためにはメモリマッピングを行い、GPIOレジスタの物理アドレスとユーザ空間の仮想アドレスに割り当てる必要があります。

## Raspberry Piのアドレス空間
以下、Raspberry Piのアドレスについて記載します。
各アドレスについて、GPIOレジスタのアドレスはPeripheralのアドレス+0x200000になります。

| 名称 | 概要 | Peripheralの開始アドレス | 備考 |
|:------------------------------ |:---------------------------------------- |:---------------------------|:---------------------------- |
| バスアドレス                         | CPU(BCM2837)で使用するアドレス    | 0xFE000000                     | データシート記載のアドレス |
| 物理アドレス                         | 実際のアドレス                                     | 0x3F000000                      |   |
| 仮想アドレス(カーネル空間) | カーネルが使用する仮想的なアドレス  | 0xF2000000                      |   |
| 仮想アドレス(ユーザ空間      | ユーザが使用できる仮想的なアドレス  | 0x00000000-0xC0000000 | メモリマッピング時に割り当てられる |

以上はBCM2835 Peripheralのデータシート(page 5)の図が参考になると思います。

またデータシート上では物理アドレスは0x20000000と記載されていますが、Raspberry Pi 3(BCM2837)の場合は0x3F000000になります(詳細は以下を参照)。
[Peripheral Addresses](https://www.raspberrypi.org/documentation/hardware/raspberrypi/peripheral_addresses.md)

> This is 0x20000000 on the Pi Zero, Pi Zero W, and the first generation of the Raspberry Pi and Compute Module, and 0x3f000000 on the Pi 2, Pi 3 and Compute Module 3.

## メモリマッピング
メモリマッピングを行い、物理アドレスとユーザ空間の仮想アドレスとを対応づけてGPIOレジスタにアクセス出来るようにします。メモリマッピングは[libcのmmap](https://docs.rs/libc/0.2.31/libc/fn.mmap.html)を使用します。

mmapはページング形式でメモリマッピングを行うのでlengthはページサイズの最小(4096)を指定し、offsetにgpioのレジスタの開始(仮想)アドレスを指定します。
また、mmapで指定するデバイスはdev/gpiomemを使用します(dev/memでも動作しますが、その場合はアクセスのためにroot権限が必要になります。)

## アドレスの相対参照
メモリマッピング成功でmmapはマッピングしたメモリのアドレスを示すポインタを返します。返ってくるポインタはカーネルが選択するため不定となります。そのため、GPIOの各レジスタを参照するためにアドレスの相対参照を行います。

相対参照は[pointer.offset](https://doc.rust-lang.org/std/primitive.pointer.html#method.offset)を使用します。
pointer.offsetは引数×型サイズだけ加算したアドレスを示すポインタを返します。u32のポインタの場合は引数1に対して4バイト加算したアドレスを示すポインタを返します。

例えばGPFSEL2のアドレスはGPIOの開始アドレス+0x00000008ですが、u32のポインタであることを考慮してpointer.offsetの引数に0x00000002を指定する必要があります。

## volatile参照
レジスタアクセスの際に最適化を抑止するためにvolatile参照を行います。
volatile参照のためにレジスタの値を取得する際には[read_volatile](https://doc.rust-lang.org/nightly/std/ptr/fn.read_volatile.html)、設定する際には[write_volatile](https://doc.rust-lang.org/nightly/std/ptr/fn.write_volatile.html)を使用します。

## まとめ
 * GPIOのレジスタを使用してGPIOを操作することでLチカを行う。
*  データシートのアドレスはバスアドレスなので、物理アドレスをユーザ空間の仮想アドレスにメモリマッピングする必要がある。
*  メモリマッピングして取得したユーザ空間の仮想アドレスを示すポインタを相対参照を行うためにpointer.offsetを使用する。
*  レジスタアクセスの際に最適化を抑止するためにread_volatile/write_volatileを使用する。

## 参考
[[Lチカ] Raspberry PiでC言語からSoCのレジスタを操作してGPIOを制御する](https://qiita.com/moutend/items/534d597cf5c867273319)
[Raspberry Pi でLチカする方法がたくさんありすぎる件について](https://qiita.com/rukihena/items/fb94a2cdcbb37ec8ced7)
[RustでベアメタルRaspberry PiのLチカ](http://keens.github.io/blog/2016/05/04/rustdebeametaruraspberry_pinolchika/)
[Rustで組み込みプログラム(Cortex-M)](http://park11.wakwak.com/~nkon/homepc/rust_embedded/)
[Man page of MMAP](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/mmap.2.html)
[ユーザー空間とカーネル空間](http://www.linux-dvr.biz/archives/39)
[x86_64 Linuxでの仮想アドレス/物理アドレス](http://mmi.hatenablog.com/entry/2017/03/21/151320)
[The GNU C Library: Memory-mapped I/O - GNU.org](http://www.gnu.org/software/libc/manual/html_node/Memory_002dmapped-I_002fO.html)
https://github.com/golemparts/rppal