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