본문 바로가기

언어/라즈베리파이

라즈베리 파이 GPIO를 고속으로 제어하기

출처: https://arsviator.blogspot.kr/2015/09/gpio.html





라즈베리 파이에서 GPIO 핀을 제어하는 가장 쉬운 방법은 sysfs를 사용하는 것이다.

쉘에서라면 다음과 같은 식으로 GPIO 핀을 제어할 수 있다.

$ echo "4" > /sys/class/gpio/export
$ echo "out" > /sys/class/gpio/gpio4/direction
 
# Set up GPIO 7 and set to input
$ echo "7" > /sys/class/gpio/export
$ echo "in" > /sys/class/gpio/gpio7/direction
 
# Write output
$ echo "1" > /sys/class/gpio/gpio4/value
 
# Read from input
$ cat /sys/class/gpio/gpio7/value 
 
# Clean up
$ echo "4" > /sys/class/gpio/unexport
$ echo "7" > /sys/class/gpio/unexport
 

프로그램 상에서라면 '/sys/class/gpio/' 디렉토리 내의 파일들을 open한 후 read/write로 GPIO를 제어할 수 있다.

이렇게 sysfs를 사용하면 매 비트를 조작할 때 마다 오버헤드가 커서 라즈베리 파이2에서 단순히 GPIO 핀을 H/L로 토글하는데도 6.8KHz로밖에 동작할 수 없다.

하지만 라즈베리 파이2에 들어있는 SoC인 BCM2836의 GPIO레지스터를 직접 조작하면 무려 50MHz의 속도로 GPIO핀을 토글할 수 있게 된다. (약 7350배의 속도 향상) 게다가 sysfs를 사용하는 경우는 한번에 한 비트씩만 조작이 가능하지만 GPIO 레지스터를 조작하면 한번에 여러 비트의 GPIO 조작이 가능하기 때문에 더욱 더 고속 제어가 가능해진다.

BCM2836의 경우 I/O제어 레지스터는 0x3f20 0000 ~ 0x3f20 00b0의 메모리 공간에 매핑되어 있다.

리눅스에서 프로세스는 하드웨어나 물리 주소에 직접 접근이 불가능하다. 그러므로 이 문제를 해결하기 위해 사용되는 것이 '/dev/mem' 디바이스이다. /dev/mem은 메모리 공간에 해당하는 가상 파일이다. 그러므로 이 파일을 오픈해서 파일에 read/write하면 파일이 매핑되어 있는 메모리 공간에 값을 읽고 쓸 수 있게 되는 것이다.

그런데 파일에 값을 read/write 하는건 귀찮기 때문에 등장하는 것이 mmap 함수이다. mmap은 파일의 일부를 메모리처럼 억세스 할 수 있게 해 준다. C에서 말하자면 파일의 특정 장소를 포인터로 지시해 그 포인터를 통해 직접 읽고 쓰기가 가능해진다.

아래 코드가 라즈베리 파이2에서 GPIO 레지스터에 대한 포인터를 얻어오는 함수이다. 

unsigned int *get_base_addr()
{
  int fd=open("/dev/mem/", O_RDWR | O_SYNC);
  if (fd<0) {
    printf("can not open /dev/mem\n"); exit(-1);
  }
  #define PAGE_SIZE (4096)
  void *mmaped = mmap(NULL,
                      PAGE_SIZE,
                      PROT_READ | PROT_WRITE,
                      MAP_SHARED,
                      fd,
                      0x3f200000);
  if (mmaped<0) {
    printf("mmap failed\n"); exit(-1);
  }
  close(fd);
  return (unsigned int *)mmaped;
}
 

 
주의) 라즈베리 파이2는 BCM2836 SoC를 사용하는데 여기서는 GPIO 레지스터가 0x3f20 0000번지에 
매핑되어 있지만 라즈베리 파이1의 BCM2835는 0x2020 0000번지에 매핑되어 있다.
즉 라즈베리 파이1에서 사용하려면 위의 함수에서 0x3f200000을 0x20200000으로 변경 해 줘야만 한다.
 
포인터를 얻었으면 사용하고자 하는 GPIO 핀을 입력 또는 출력으로 사용할 지 설정해 줘야 한다. 
각 I/O 포트의 설정은 3비트 값을 사용한다. 32비트 레지스터에 각각 10개 I/O핀을 설정한다.
 
예를 들어 GPIO0의 설정 레지스터는 0x3f20 0000 번지의 32비트 값 중 비트 2~0, GPIO11의 
설정 레지스터는 0x3f20 0004 번지의 32비트 값 중 비트 5~3이 된다.
공식화 하면 GPIO p의 설정 레지스터는 0x3f200000+(p/10) 번지의 비트 (p%10)*3+2~(p%10)*3가 된다.
 
아래 코드는 포트 설정을 위한 함수이다. 
 
void gpio_mode(unsigned int *addr, int port, int mode)
{
  if (0<port || port >31) {
    printf("port out of range: %d\n", port);
    exit(-1);
  }
  unsigned int *a = addr + (port/10);
  unsigned int mask = ~(0x7 << ((port%10) * 3));
  *a &= mask;
  *a |= (mode & 0x7) << ((port%10) * 3);
}  
 
포트 설정이 되었으면 이제 포트 값을 H/L로 변경할 수 있다. 
여기서는 GPIO핀을 H로 만들때 사용하는 레지스터와 L로 만들때 사용하는 레지스터가 따로 있다. 
즉 H로 만들 때 사용하는 레지스터에 '1'을 써 넣은 포트만 값이 H로 바뀌고 나머지 포트는 값이 
그대로 유지된다.
마찬가지로 L로 만들 때 사용하는 레지스터에 '1'을 써 넣은 포트만 값이 L가 되고 나머지 비트는 
값에 변화가 없게 된다.
예를 들어 GPIO0의 값을 H로 하고 싶으면 0x3f20 001c 번지에 0x0000 0001을 써 주면 GPIO0만 
H가 되고 나머지 GPIO는 값의 변화가 없다.
동일하게 GPIO1의 값을 L로 하고 싶으면 0x3f20 0028 번지에 0x0000 0002를 써 주면 된다. 
이렇게 레지스터를 이용하면 한번에 여러 비트를 H로 만들거나, 여러 비트를 L로 만들어 줄 수 있다.
 
void gpio_set(unsigned int *addr, int port)
{
  if (0<port || port>31) {
    printf("set: port out of range: %d\n", port);
    exit(-1);
  }
  *(addr+7) = 0x1 << port;
}

void gpio_clear(unsigned int *addr, int port)
{
  if (0<port || port>31) {
    printf("clear: port out of range: %d\n", port);
    exit(-1);
  }
  *(addr+10) = 0x1 << port;
} 
 
위의 함수들을 사용해 GPIO0를 제어하는 코드의 틀은 다음과 같다.
 
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mmap.h>
#include <unistd.h>

#define GPIO_IN     0
#define GPIO_OUT    1

unsigned int *get_base_addr();
void gpio_mode(unsigned int *, int, int);
void gpio_set(unsigned int *, int);
void gpio_clear(unsigned int *, int);

int main(int argc, char **argv)
{ 
  volatile unsigned int *addr = get_base_addr();
 
  ... 
  gpio_mode(addr, 0, GPIO_OUT);
  ...
  ...
  gpio_set(addr, 0);
  ... 
  gpio_clear(addr, 0);
  ...
}
 
참고로 라즈베리 파이2에서 모든 함수 호출이나 딜레이 없이 가장 빨리 GPIO 포트를 토글하는 경우 
최대 50MHz로 스위칭이 가능하다.
 
  ...
  for (;;) {
    *(0x3f200000+7) = 1<<0;   // GPIO 0 set
    *(0x3f200000+10) = 1<<0;  // GPIO 0 clear
  } 
  ...