Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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で公開しています。

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

レジスタアクセス

上記の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

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を使用します。

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

アドレスの相対参照

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

相対参照はpointer.offsetを使用します。
pointer.offsetは引数×型サイズだけ加算したアドレスを示すポインタを返します。u32のポインタの場合は引数1に対して4バイト加算したアドレスを示すポインタを返します。

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

volatile参照

レジスタアクセスの際に最適化を抑止するためにvolatile参照を行います。
volatile参照のためにレジスタの値を取得する際にはread_volatile、設定する際にはwrite_volatileを使用します。

まとめ

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

参考

[Lチカ] Raspberry PiでC言語からSoCのレジスタを操作してGPIOを制御する
Raspberry Pi でLチカする方法がたくさんありすぎる件について
RustでベアメタルRaspberry PiのLチカ
Rustで組み込みプログラム(Cortex-M)
Man page of MMAP
ユーザー空間とカーネル空間
x86_64 Linuxでの仮想アドレス/物理アドレス
The GNU C Library: Memory-mapped I/O - GNU.org
https://github.com/golemparts/rppal

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away