ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 디바이스 드라이버 / LED 제어
    Linux/Linux Device Driver 2020. 9. 1. 12:44

    디바이스 드라이버로 하드웨어를 제어하기 전에 응용프로그램에서 하드웨어를 제어하여 LED를 출력해보자.

     

    응용프로그램에서 GPIO를 제어하여 LED를 제어하는 방법

    첫 번째 방법은 wiringPi 라이브러리를 이용하는 방법이다.

    몇 번핀을 쓸 것인지만 정하고 적절한 함수에 해당 핀 번호를 매개변수로 넘겨주면 알아서 제어가 된다. 실제로 핀 설정하기전에 wiringPiSetup()이라는 함수가 수행되는데 이 함수를 보면 세 번째 방법에서 수행하는 GPIO를 제어할 수 있는 메모리 영역을 mmap함수(매핑)를 수행한다. wiringPi 라이브러리를 사용하여 생각할게 없고 쉽다. 하지만 공부에는 전혀 도움이 안돼서 안 썼다.매핑도 직접해봐야 공부가 된다.

     

    두 번째 방법은 /sys/class/gpio를 이용하는 방법이다.

    응용프로그램에서는 디바이스 파일(/sys/class/gpio)로 하드웨어를 제어하기 위해 보통 read(), write(), ioctl() 함수를 사용한다. 이러한 함수들은 프로세스 메모리 공간과 커널 메모리 공간 사이의 메모리 전달 과정이 수반되기 때문에 매우 비효율적이다. 다시 말해 하드웨어에서 데이터를 읽으면 커널 영역으로 데이터를 전달해주고 커널 영역에 있는 데이터를 유저 영역으로 전달해줘야기 때문이다. 그래서 많은 용량의 데이터가 빠르게 전달되어야 하는 사운드나 비디오 장치에 메모리 복사가 수반되는 read(), write(), ioctl() 함수를 사용한다면 시스템 성능이 저하된다.

     

    세 번째 방법은 매핑을 통해 제어하는 방법이다.

    이 방법은 mmap() 함수를 이용하여 응용프로그램에서 직접 하드웨어의 I/O 주소 공간을 메모리 복사 없이 직접적으로 사용할 수 있다. 그래서 2번째 방법보다 빠르다. mmap() 함수는 원래 물리 주소를 이용해 파일에 접근하도록 하는 함수다. 그러나 디바이스 파일에 적용할 경우에는 디바이스에서 제공하는 물리 주소(I/O 메모리 주소 또는 할당된 메모리 공간 주소)를 응용 프로그램에서 사용할 수 있게 한다. 이게 가능한 이유는 ARM 같은 RISC계열 CPU의 경우, 하드웨어와 기계어 명령을 단순화하기 위해 메모리 맵 입출력 방식을 사용하기 때문이다. 메모리 맵 입출력 방식은 메모리 상의 특정 주소에 접근하면 변수처럼 하드웨어에 값을 읽거나 쓸 수 있다. 즉, GPIO는 메모리상에 정해진 영역이 있기 때문에 해당 부분에 접근만 하면 GPIO를 제어할 수 있다는 뜻이다. 그렇다면 물리 주소의 디바이스 파일이 뭔지, 몇 번지에 I/O peripheral이 있는지 알아야 하고 그 I/O peripheral 내의 얼마만큼 떨어진 곳에 GPIO의 시작이 되는지를 알아야 한다. 알아낸 주소는 매핑을 통하여 간접적으로 접근할 수 있다.

     

     

    실습

    나는 세 번째 방법으로 실습한다.

     

     

    1. 하드웨어 연결

    하드웨어 연결은 18번과 GND(아무거나 1개)를 LED에 연결했다. LED의 다리가 긴쪽에 18번이 연결되어야 하고 짧은 쪽이 GND에 연결되야한다.

     

    2. 메모리상의 GPIO 영역 알아내기

    BCM2837-ARM-Peripherals.pdf
    3.61MB

    위 데이터 시트를 보면

    주변 장치 물리 주소 영역이 0x3F000000 ~ 0x3FFFFFFF이라고 나와있다.

    이 영역은 0x7E000000을 시작으로 매핑되어 있다고 하니

    주변 장치 가상 주소의 시작점은 0x7E000000 ~ 0x7EFFFFFF일 것이다.

     

     

    GPIO의 가상 주소의 시작점은 0x7E200000이라고 나와있다. (데이터 시트 90page 참조)

     

    GPIO의 가상 주소 시작점(0x7E200000)은 주변 장치 가상 주소 시작점(0x7E000000)으로부터 0x00200000만큼 떨어져 있다.

     

    그럼 GPIO의 물리 주소의 시작점도 주변 장치 물리 주소 시작점으로부터 0x00200000만큼 떨어져 있을 것이다.

     

    그럼 GPIO의 물리 주소의 시작점은 0x3F200000일 것이다.

     

    3. 레지스터 설정 방법

    LED를 켜기 위해서는 GPIO 레지스터를 설정해줘야 한다.

     

    1. GPIO Function Select Registers (GPFSELn) 설정

    GPIO 18번 핀으로 데이터를 받는것이 아니라 보내는 것이기 때문에 GPIO 18번 핀을 output으로 설정하기 위해서는 GPIO Function Select Registers (GPFSEL1) 레지스터를 사용한다. 

     

    위 그림을 보면 GPIO 18번 핀은 GPFSEL1 레지스터 26-24비트로 output mode로 설정할 수 있다.

     

    첫번째 그림(데이터시트 90page)에서 GPFSEL1 레지스터의 주소는 0x7E200004로 나와있다.

     

    0x7E200004에다가 0x01000000을 집어 넣으면 된다. 

     

    밑의 코드에서 GPIO_OUT() 매크로 함수는 output을 설정하는 함수인데

     

    GPIO_OUT(18)은 (*(0x7E200000 + (18/10)) |= (1<<((18%10)*3)))으로 치환된다.

    = (*(0x7E200000 + 1) |= (1<<(8*3)))

    = (*(0x7E200004) |= (1<<24))

    = (*(0x7E200004) = *(0x7E200004) | (1<<24))

    = (*(0x7E200004) = *(0x7E200004) | 1<<24)

    = (*(0x7E200004) = *(0x7E200004) | 0x01000000)

    = *(0x7E200004) = *(0x7E200004) | 0x01000000

     

    이런식으로

     

    GPIO Pin Output Set Registers (GPSET0)의 18번 비트에 1 해주면 LED가 켜지고,

    GPIO Pin Output Clear Registers (GPCLR0)의 18번 비트에 1로 설정해주면 LED가 꺼진다.

     

    3. 코드 작성

    pi@raspberrypi:~/Documents/ledtest $ nano led_app.c

    #include <stdio.h>
    #include <fcntl.h>
    #include <sys/mman.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    #define GPIO_BASE 0x3F200000
    #define GPIO_SIZE 180
    
    #define GPIO_IN(g)	(*(gpio+((g)/10)) &= ~(7<<(((g)%10)*3)))
    #define GPIO_OUT(g)	(*(gpio+((g)/10)) |= (1<<(((g)%10)*3)))
    #define GPIO_SET(g)	(*(gpio+7) = 1<<g)
    #define GPIO_CLR(g)	(*(gpio+10) = 1<<g)
    #define GPIO_GET(g)	(*(gpio+13)&(1<<g))
    
    volatile unsigned *gpio;
    
    int main(int argc, char **argv)
    {
       int gno, i, mem_fd;
       void* gpio_map;
    
       if(argc < 2)
       {
          printf("Usage : %s GPIO_NO\n", argv[0]);
    
          return -1;
       }
    
       gno = atoi(argv[1]);
    
       if( ( mem_fd = open("/dev/mem", O_RDWR | O_SYNC) ) < 0)
       {
          perror("open() /dev/mem\n");
    
          return -1;
       }
    
       gpio_map = mmap(NULL, GPIO_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, mem_fd, GPIO_BASE);
    
       if(gpio_map == MAP_FAILED)
       {
          printf("[Error] mmap() : %d\n", (int)gpio_map);
    
          perror -1;
       }
    
       gpio = (volatile unsigned*)gpio_map;
    
       GPIO_OUT(gno);
    
       for(i = 0; i < 5; i++)
       {
          GPIO_SET(gno);
          sleep(1);
    
          GPIO_CLR(gno);
          sleep(1);
       }
    
       munmap(gpio_map, GPIO_SIZE);
       close(mem_fd);
    
       return 0;
    }

    pi@raspberrypi:~/Documents/ledtest $ gcc -o led_app led_app.c

     

    pi@raspberrypi:~/Documents/ledtest $ sudo ./led_app 18

     

    10초간 깜빡깜빡거린다..

     

    성공..

     

     

     

     

    /dev/mem 디바이스 파일이 아닌 /dev/gpiomem 디바이스 파일을 사용하고 싶다면 /dev/gpiomem 디바이스 파일의 시작점이 0x3F200000 부터 시작이기 때문에

    #define GPIO_BASE 0x3F200000 -> #define GPIO_BASE 0x0으로 변경해주면 된다.

     

    mmap() 함수를 사용하면 무조건 물리 주소에 접근이 가능하냐? 아니다. 매핑을 할 때도 가능한 구역이 있고 절대 불가능한 영역이 있다. I/O peripheral이 존재하는 공간의 경우에는 접근이 가능한 영역이어서 에러가 나지 않은 것뿐이다. 

     

    저수준 입출력 함수로 접근하면 느리고 매핑으로 접근하면 빠르다.

     

     

    물리 주소를 아는데 왜 매핑을 할까?

    CPU는 MMU가 enable된 이후 부터는 모든 주소를 가상주소로 인식하기 때문에 커널에서 물리 주소에 직접 접근이 가능하더라도 이상한 곳에 접근하게 된다. 그래서 mmap()이라는 매핑함수로 물리주소를 매핑하여 나온 가상 주소로 다시 접근하면 물리 주소로 변환되어 접근하게 되는것이다.

     

     

    유저영역에서 하드웨어 제어를 해봤다. 

     

    그럼 디바이스 드라이버에서는 다르냐? 똑같다. 하드웨어를 제어하는 기본 개념은 같다

    댓글

Designed by Tistory.