2015년 9월 9일 수요일

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



라즈베리 파이에서 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
  } 
  ...
 

댓글 11개:

  1. 좋은 글 감사합니다.
    궁금한 것이 있는데, WiringPi 로 C 나 Python 으로 제어가 가능하던데,
    이걸로 고속 제어가 안되나요 ?
    고속제어를 하려면 단순히 C 를 사용해야하나요 ?
    Python 을 사용하면 어려운건지요 ?

    답글삭제
    답글
    1. 결국 라이브러리들도 내부적으로는 레지스터를 제어하지만 추가 오버헤드가 많아 아무래도 느릴 수 밖에 없습니다.

      참고로 라즈베리 파이 2에서 python으로 GPIO를 제어하는 경우 최대 197 KHz 정도의 속도로 제어가 가능합니다만 역시 레지스터 직접 제어를 했을 때의 50MHz에 비하면 한참 느리죠.

      원하시는 고속이 어느 정도냐에 따라 고르셔야겠죠.

      삭제
  2. 너무 좋은글 감사합니다
    하지만, 제가 지금 GPIO pin에 write를 하는것이 아니라 고속으로 gpio의 값을 read 해오고 싶은데 혹시 관련자료나 read해야할 때는 어떤식으로 함수를 진행해야 하는지 알려주실 수 있나요?

    답글삭제
  3. 좋을글 감사합니다!^^
    하지만, 제가 지금 딱 원하고 있는게 GPIO를 고속으로 write 하는것이 아닌 고속으로 read 해오고 싶은데 혹시 관련자료나, 그에 맞는 함수를 알려주실 순 없으신가요? 또 라즈베리파이3를 기준으로 진행해보고 싶은데 라즈베리 파이3는 BCM2837을 쓴다고 하더라고요. 혹시 pi3에서는 memory map이 바뀌진 않나요...? 혹, pi3의 memory map을 기준으로 설명 구지 안해주셔도 상관없고 read를 할때는 어떻게 될지 정도의 설명 해주실 수 없으신가요..?

    답글삭제
    답글
    1. 먼저 BCM2837(RPi3)의 레지스터 맵은 RPi2와 동일합니다. 그러므로 위의 코드를 그대로 사용하셔도 상관 없습니다.
      그리고 GPIO 값을 읽으려면 다음의 함수를 사용하시면 됩니다.

      uint8_t gpio_read(unsigned int *addr, int port)
      {
      ... // port 값이 범위(0~31)를 벗어나는가 체크하는 부분
      return (((*(addr+13))>>port) & 1);
      }

      삭제
    2. 빠른답변 감사합니다.! 프로젝트를 진행하고 있는데 많이 도움이 되었어요... low level 단계로 들어가서 하드웨어를 control 해서 gpio를 굉장히 빠른 속도로 read해야 하는지라...

      삭제
    3. 한가지 더 여쭤봐도 될까요? 베이스 어드레스값을 0x3f20000 값으로 아예 고정해놓고
      *(0x3f200000+7) = 1<<0;
      이런식으로 쓰면 되는데,왜 base address를 get 하는 함수를 따로 두어서 base address값을 받아오는 거죠...? 초보가 질문합니다...

      삭제
    4. 실제 물리 주소는 0x3f200000 이지만 그게 mmap을 통해 매핑되면 매핑된 영역의 주소(프로세스에서 사용하는 가상 주소)는 매번 변경될 수 있기 때문입니다.

      삭제
  4. 지금 해보니
    *(0x3f200000+7)=1<<0 이런식으로 c를 작성하면 에러가 나네요...? *뒤에는 포인터 변수가 와야한다고 에러가 뜨는데...
    그럼 직접적으로 포인터 주소값을 숫자로 입력하면 안되는건가요..? 아니면 제가 뭘 잘못하고 있나요?

    답글삭제
  5. 계속 open 부분에서 /dev/mem 을 열지못하고있습니다 혹시 도움좀 구할수있을까요?
    실행파일에 sudo 를 붙이고 실행하는데 열지못하네요.

    답글삭제
  6. 지나가다가 도움을 받아서 감사 답변 드립니다. 위의 분은 /dev/mem 열리지 않는건
    펌웨어 커널 버전이 문제입니다. 4.4.49 정도 쓰면 문제 없더군요..

    답글삭제