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
  } 
  ...
 

2015년 9월 6일 일요일

라즈베리파이2에서 NEON을 사용하기 위해 gcc-5.1.0 컴파일하기

라즈베리 파이에 사용된 프로세서에는 NEON이라는 SIMD (Single Instruction Multiple Data)처리 유닛이 들어있다. 이 NEON을 사용하면 최대 128비트까지 동시에 처리가 가능해 지기 때문에 프로그램 실행에 상당한 성능 향상을 가져올 수 있게 된다. x86계열로 치면 MMX, SSE, AVX 명령등에 이에 해당한다.


char 타입의 연산인 경우 약 5배, short 타입 연산은 약 3배, int 타입 연산은 약 1.7배, float 타입 연산은 약 2.3배 정도로 실행 속도가 향상된다. 즉 반복적인 연산이 많은 scientific computation이나 signal processing, 암호화, 압축, 인코딩 등에 사용하면 매우 유용하다.



부동 소수점만 보면 단정도(single precision)의 경우 라즈베리 파이1이 약 70 MFlops, 라즈베리 파이2에서 NEON을 사용하지 않으며 약 150 MFlops, NEON을 사용하면 약 310 MFlops의 속도가 나온다. 부동 소수점 배정도(double precision)의 경우 라즈베리 파이1이 약 35 MFlops, 라즈베리 파이2에서는 약 150 MFlops정도의 속도가 나오게 된다. (부동소수점 연산에서 배정도의 경우 NEON이 지원하지 않음)

단 컴파일 시 NEON 명령을 지원하도록 하려면 현재 distribution에 포함되어 있는 gcc로는 안되고 NEON을 지원하도록 gcc-5.1.0을 설치해 줘야 한다.

먼저 gcc-5.1.0 소스코드를 다운로드 한다.

$ curl -O http://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-5.1.0/gcc-5.1.0.tar.bz2
$ tar xvf gcc-5.1.0.tar.bz2

gcc를 컴파일 하기 위해서는 디폴트 swap 파티션 용량이 부족하기 때문에 swap 파티션 용량을 늘려줘야 한다.

$ sudo dd if=/dev/zero of=/swapfile bs=1M count=2048
$ sudo chmod 600 /swapfile
$ sudo mkswap /swapfile
$ sudo swapon /swapfile

현재 apt-get으로 설치되는 autoconf 버젼은 2.69인데 gcc-5.1.0은 2.64를 사용하도록 설정되어 있다. 그래서 이걸 2.69를 사용하도록 수정해 줘야 한다.

(1) gcc-5.1.0 디렉토리에 있는 configure.ac 파일 안의 AC_PREREQ 값을 '2.64'에서 '2.69'로 수정
(2) gcc-5.1.0/config 디렉토리에 있는 override.m4 파일 안의 _GCC_AUTOCONF_VERSION 값을  '2.64'에서 '2.69'로 수정

GCC-5.1.0에서는 병렬화를 위해 언어확장으로 cilk를 표준으로 사용하므로 cilk도 활성화 시켜 줌

(1) gcc-5.1.0/libcilkrts 디렉토리에 있는 configure.tgt 안의 UNSUPPORTED=1 을 앞에 '#'를 붙여 comment out 시킴 ('1'을 '0'으로 바꾸는건 안됨)
(2) gcc-5.1.0/libcilkrts/runtime/config/generic 디렉토리에 있는 cilk-abi-vla.c 안의 vla_internal_heap_free 함수를 호출하는 부분에서 첫번째 인자를 't'에서 'p'로 수정
(3) gcc-5.1.0/libcilkrts/runtime/config/generic 디렉토리에 있는 os_fence.h 안의 __cilkrts_fence의 정의 부분의 맨 앞에 '//'를 추가해 comment out 시킴
COMMON_SYSDEP void __cilkrts_fence(void); /// < MFENCE instruction
(4) gcc-5.1.0/libcilkrts/runtime/config/generic 디렉토리에 있는 os_fence.h 파일에 아래줄을 추가
#define __cilkrts_fence() __asm__ volatile ("DSB")

빌드를 위해 GMP, MPFR, LIBMPC가 필요하므로 설치해 줌

$ sudo apt-get install libgmp-dev libmpfr-dev libmpc-dev

빌드를 시작

$ mkdir b; cd b
$ ../configure --enable-languages=c,c++ \
--prefix=/usr/local/gcc-5.1.0 \
--target=arm-linux-gnueabihf \
--with-arch=armv7-a \
--with-fpu=vfp \
--with-float=hard \
--build=arm-linux-gnueabihf \
--host=arm-linux-gnueabihf
$ make
$ sudo make install

설치가 완료된 후 gcc-5.1.0을 사용하도록 환경변수를 설정해 준다. '.profile' 또는 '.bashrc' 파일의 맨 마지막에 다음 두 줄을 추가한다.

export PATH=/usr/local/gcc-5.1.0/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/gcc-5.1.0/lib:$LD_LIBRARY_PATH

라즈베리파이1에서 컴파일 하는 경우 configure 옵션을 다르게 줘야 한다.

$ ../configure --enable-languages=c,c++ \
--prefix=/usr/local/gcc-5.1.0 \
--target=arm-linux-gnueabihf \ 
--with-fpu=vfp \
--with-float=hard \
--build=arm-linux-gnueabihf \
--host=arm-linux-gnueabihf

빌드에는 상당한 인내심이 필요하다. 'make' 명령으로 빌드하는데 라즈베리파이2에서 대략 30시간 정도 걸렸다.

바로 사용하고 싶은 사람들은 아래 링크에서 컴파일 된 gcc-5.1.0을 받아 /usr/local 디렉토리에 풀어주고 환경변수 설정만 하면 된다.

gcc-5.1.0.compiled_for_rpi2.tgz