Nullable

旧レガシーガジェット研究所

Raspberry Pi 3でGPIOライブラリを書く

概要

Raspberry PiのGPIOを対象としたライブラリを作成する。既存のライブラリなどは数多く存在するが実際に自分で作成することでその内部を理解できると思う。

環境

Paspberry Pi 3 Model B

$ sudo cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 9 (stretch)"
NAME="Raspbian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"
$ sudo uname -a
Linux raspberrypi 4.9.41-v7+ #1023 SMP Tue Aug 8 16:00:15 BST 2017 armv7l GNU/Linux

chipset

まずデータシートからチップセットを調べる。

Processer - Broadcom BCM2387 chipset. - 1.2GHz Quad-Core ARM Cortex-A53 (64Bit)

引用:https://www.terraelectronica.ru/pdf/show?pdf_file=%252Fds%252Fpdf%252FT%252FTechicRP3.pdf

上記からBCM2387であることがわかった。

BCM2387

https://web.stanford.edu/class/cs140e/docs/BCM2837-ARM-Peripherals.pdf

上記のデータシートから抜粋したメモリマップが以下。

ペリフェラルレジスタ

ペリフェラルバイスレジスタは、ペリフェラルプログラマブル・カウンタや割り込み制御、シリアル通信ポートなどのハードウェア)の動作を設定したり、動作状況を読み出したりするためのレジスタである。 引用: https://ja.wikipedia.org/wiki/%E3%83%AC%E3%82%B8%E3%82%B9%E3%82%BF_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF)#%E3%83%9A%E3%83%AA%E3%83%95%E3%82%A7%E3%83%A9%E3%83%AB%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%81%AE%E3%83%AC%E3%82%B8%E3%82%B9%E3%82%BF

データシートを見ると以下のような記述があった。

Peripherals (at physical address 0x3F000000 on) are mapped into the kernel virtual address space starting at address 0xF2000000. 引用: https://web.stanford.edu/class/cs140e/docs/BCM2837-ARM-Peripherals.pdf p6

上記からペリフェラルレジスタ0x3F000000からマッピングされていることがわかる。

以下のドキュメントからbcm_host_get_peripheral_address()ペリフェラルレジスタのアドレスを取得できることがわかったので実際に以下のコードで確かめる。

https://www.raspberrypi.org/documentation/hardware/raspberrypi/peripheral_addresses.md

$ cat samples/get_peripheral_address.c
#include <stdio.h>
#include <bcm_host.h>

int main(int argc, char* argv[]) {
  printf("0x%08X\n", bcm_host_get_peripheral_address());
  return 0;
}
$ gcc samples/get_peripheral_address.c -I/opt/vc/include -L/opt/vc/lib -lbcm_host -o myfile -Wimplicit-function-declaration
$ ./myfile
0x3F000000

加えて以下の/proc/iomemからもメモリマップが確認できる。

$ sudo cat /proc/iomem
00000000-3b3fffff : System RAM
  00008000-00afffff : Kernel code
  00c00000-00d3da63 : Kernel data
3f006000-3f006fff : dwc_otg
3f007000-3f007eff : /soc/dma@7e007000
3f00b840-3f00b84e : /soc/vchiq
3f00b880-3f00b8bf : /soc/mailbox@7e00b880
3f101000-3f102fff : /soc/cprman@7e101000
3f200000-3f2000b3 : /soc/gpio@7e200000
3f201000-3f201fff : /soc/serial@7e201000
  3f201000-3f201fff : /soc/serial@7e201000
3f202000-3f2020ff : /soc/sdhost@7e202000
3f215000-3f215007 : /soc/aux@0x7e215000
3f300000-3f3000ff : /soc/mmc@7e300000
3f980000-3f98ffff : dwc_otg

GPIO制御用レジスタ

The GPIO has 41 registers. All accesses are assumed to be 32-bit. https://web.stanford.edu/class/cs140e/docs/BCM2837-ARM-Peripherals.pdf p90

BCM2387のデータシート p90にGPIOレジスタマッピング表が記載されている。

Address Field Name Description Size Read/Write
0x7E200000 GPFSEL0 GPIO Function Select 0 32 R/W
0x7E200000 GPFSEL0 GPIO Function Select 0 32 R/W
0x7E200004 GPFSEL1 GPIO Function Select 1 32 R/W
0x7E200008 GPFSEL2 GPIO Function Select 2 32 R/W
0x7E20000C GPFSEL3 GPIO Function Select 3 32 R/W
0x7E200010 GPFSEL4 GPIO Function Select 4 32 R/W
0x7E200014 GPFSEL5 GPIO Function Select 5 32 R/W
0x7E200018 - Reserved - -
0x7E20001C GPSET0 GPIO Pin Output Set 0 32 W
0x7E200020 GPSET1 GPIO Pin Output Set 1 32 W
0x7E200024 - Reserved - -
0x7E200028 GPCLR0 GPIO Pin Output Clear 0 32 W
0x7E20002C GPCLR1 GPIO Pin Output Clear 1 32 W
0x7E200030 - Reserved - -
0x7E200034 GPLEV0 GPIO Pin Level 0 32 R
0x7E200038 GPLEV1 GPIO Pin Level 1 32 R

GPIO "Function Select" Registers (GPFSEL0 ~ GPFSEL5)

The function select registers are used to define the operation of the general-purpose I/O pins https://web.stanford.edu/class/cs140e/docs/BCM2837-ARM-Peripherals.pdf p91

上記にもあるようにGPFSEL(n)はGPIOピンの操作を定義するためのものとなっている。

GPFSEL0の使用方法と各ビットの対応の表を以下に抜粋した。

Bit(s) Field Name Description Type Reset
31-30 --- Reserved R 0
29-27 FSEL9 FSEL9 - Function Select 9

000 = GPIO Pin 9 is an input
001 = GPIO Pin 9 is an output
R/W 0
26-24 FSEL8 FSEL8 - Function Select 8 R/W 0
23-21 FSEL7 FSEL7 - Function Select 7 R/W 0
20-18 FSEL6 FSEL6 - Function Select 6 R/W 0
17-15 FSEL5 FSEL5 - Function Select 5 R/W 0
14-12 FSEL4 FSEL4 - Function Select 4 R/W 0
11-9 FSEL3 FSEL3 - Function Select 3 R/W 0
8-6 FSEL2 FSEL2 - Function Select 2 R/W 0
5-3 FSEL1 FSEL1 - Function Select 1 R/W 0
2-0 FSEL0 FSEL0 - Function Select 0 R/W 0

上記から3bitずつが各ピンに対応していることがわかる。上記と同じ要領でGPFSEL5まで続き54ピン全てに対応する。

GPIO Pin Output Set Registers (GPSET0 ~ GPSET1)

The output set registers are used to set a GPIO pin. https://web.stanford.edu/class/cs140e/docs/BCM2837-ARM-Peripherals.pdf p95

アウトプット用のレジスタで各ビットが各ピンに対応している。

GPSET0を例に対応表をいかに示す。

Bit(s) Field Name Description Type Reset
31-0 SETn (n=0..31) 0 = No effect
1 = Set GPIO pin n
R/W 0

上記と同じ要領でGPSET1を用いて54ピンまで対応している。

GPIO Pin Output Clear Registers (GPCLR0 ~ GPCLR1)

The output clear registers) are used to clear a GPIO pin. https://web.stanford.edu/class/cs140e/docs/BCM2837-ARM-Peripherals.pdf p95

これはアウトプットのクリア用レジスタで先ほどと同様に各ビットが各ピンに対応している。

GPCLR0を例に対応表をいかに示す。

Bit(s) Field Name Description Type Reset
31-0 CLRn (n=0..31) 0 = No effect
1 = Clear GPIO pin n
R/W 0

上記と同じ要領でGPCLR1を用いて54ピンまで対応している。

GPIO Pin Level Registers (GPLEV0 ~ GPLEV1)

The pin level registers return the actual value of the pin. https://web.stanford.edu/class/cs140e/docs/BCM2837-ARM-Peripherals.pdf p96

ピンの値を取得する用のレジスタで先ほどと同様に各ビットが各ピンに対応している。

GPLEV0を例に対応表をいかに示す。

Bit(s) Field Name Description Type Reset
31-0 LEVn (n=0..31) 0 = GPIO pin n is low 1 = GPIO pin n is high R/W|0

上記と同じ要領でGPLEV1を用いて54ピンまで対応している。

実装に関して

  • /dev/mem
  • mmap()
  • PAGE_SIZE

/dev/mem

mem はコンピュータのメインメモリーイメージのキャラクターデバイスファ イル(character device file)である。 https://linuxjm.osdn.jp/html/LDP_man-pages/man4/mem.4.html

当該デバイスファイルを用いることで物理メモリにアクセスすることでき、先ほど調べたペリフェラルレジスタマッピングされているアドレスにアクセスする。

mmap()

ファイルやデバイスをメモリーにマップ/アンマップする。 https://linuxjm.osdn.jp/html/LDP_man-pages/man2/mmap.2.html

当該関数を用いて先ほどのデバイスファイルをプロセスのメモリにマッピングしアクセスを行う。

PAGE_SIZE

仮想メモリを扱う単位であるページのサイズを取得する。これはmmap()関数でマッピングするメモリのサイズに用いる(ページサイズでマッピングするのが定石らしい)

$ getconf PAGE_SIZE
4096

実際にアクセスしてみる。

$ cat write_peripheral_register.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>

#define PERIPHERAL_ADDRESS 0x3F000000
#define GPIO_ADDRESS (PERIPHERAL_ADDRESS + 0x00200000)
#define PAGE_SIZE 4096
#define GPFSEL0_OFFSET 0x0000
#define GPSET0_OFFSET 0x001C
#define GPCLR0_OFFSET 0x0028
#define GPLEV0_OFFSET 0x0034

#define DEVICE_FILE "/dev/mem"
#define MEMORY(addr) (*((volatile unsigned int*)(addr)))
#define GPFSEL0 MEMORY(address + GPFSEL0_OFFSET)
#define GPSET0 MEMORY(address + GPSET0_OFFSET)
#define GPCLR0 MEMORY(address + GPCLR0_OFFSET)

#define SEL_IN(n) (0b000 << (n * 3))
#define SEL_OUT(n) (0b001 << (n * 3))
#define SET(n) (1 << n)
#define CLR(n) (1 << n)

int main(int argc, char* argv[]) {
        unsigned int address;
        int fd;

  if ((fd = open(DEVICE_FILE, O_RDWR | O_SYNC)) < 0) {
    perror("failed to open()\n");
    return -1;
  }

  address = (unsigned int)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, GPIO_ADDRESS);
  if (address == MAP_FAILED) {
    perror("failed with mmap()\n");
    close(fd);
    return -1;
  }

  GPFSEL0 = SEL_OUT(4);
  GPSET0 = SET(4);
  sleep(1);
  GPCLR0 = CLR(4);

  munmap((void*)address, PAGE_SIZE);
  close(fd);

  return 0;
}

MEMOEYマクロは少々複雑だがアドレスとして保存した値をunsigned intのポインタに変換しそのアドレスを参照しにいっているものである。

コンパイルすると以下のエラーが出力されるが問題はない。

$ gcc write_peripheral_register.c
write_peripheral_register.c: In function 'main':
write_peripheral_register.c:37:15: warning: comparison between pointer and integer
   if (address == MAP_FAILED) {
               ^~

問題なく実行することが確認できた。

$ sudo ./a.out
$

ライブラリ化

上記をより汎用的にし、GPIOをデジタルピンとしてON/OFF切り替え及びピンの状態を取得するライブラリを作成した。

https://github.com/k-onishi/gpio-lib

参考文献

(上記の記事には本当に感謝です)