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