CS 지식 정리/라즈베리파이-C언어

라즈베리파이를 C언어로 컨트롤하자 - 레지스터 직접 접근 방법 이해

termuni 2025. 2. 25. 19:40

https://elinux.org/RPi_GPIO_Code_Samples#Direct_register_access

 

RPi GPIO Code Samples - eLinux.org

The Raspberry Pi GPIOs can be controlled using many programming languages. C Examples in different C-Languages. Direct register access Gert van Loo & Dom, have provided some tested code which accesses the GPIO pins through direct GPIO register manipulation

elinux.org

이 페이지에서 참고하여 Direct Access를 하는 법을 보겠다.

 


GPIO 시작 레지스터 주소(PERI_BASE) 설정

#define BCM2708_PERI_BASE        0x20000000
#define GPIO_BASE                (BCM2708_PERI_BASE + 0x200000) /* GPIO controller */

(예제코드)

 

여기서 BCM2708_PERI_BASE가 무엇인지 찾아보니, 라즈베리 파이 보드의 종류에 따라 다르다고 한다.

 

그래서 내가 가진 보드는 어떤건지 보려고 했는데.. 라즈베리파이 5인데 BCM 몇인지 찾을 수가 없다.

쿨링 팬을 CPU 위에 붙여버린 것..!

그래서 위쪽 기판에서 정보를 찾기도 어렵고, 그렇다고 쿨링 팬을 뜯자니 단단히 고정되가지고 뜯기도 어렵다... ㅠ

 

아무래도, 라즈베리파이를 연결해서 직접 확인해보아야 할 것 같다.

이는 추후 확인 후 올리겠다.

 

라즈베리파이 5에 대해 찾아보니 데이터시트가 발견되었고, 이를 바탕으로 정리하겠다!

raspberry-pi-5-product-brief.pdf
1.06MB

 

rp1-peripherals.pdf
3.35MB

 

우선 BCM2712을 기반으로 작성하고 정리하겠다.

.

이 값이 시작으로, Address Map에서 찾아왔다. (p.7)

아무튼 Register는 이렇게 0x400d0000으로 시작하게 되니, 위의 예제 코드에서 값을 변경해줄 필요가 있다.

#define BCM2712_PERI_BASE  0x40000000
#define GPIO_BASE          (BCM2712_PERI_BASE + 0x000D0000)  // 0x400d0000

(라즈베리파이 5용)


헤더 추가

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

 

유명한 헤더인 stdio.h, stdlib.h 를 빼고 나머지에 대해 알아보겠다.

 

  • fcntl.h: 파일 조작 (open, close 등)
  • sys/mman.h: 메모리 매핑 관련 함수 (mmap)
  • unistd.h: Unix 시스템 호출 관련

fcntl.h 는 .txt 와 같은 파일을 읽을 때도 많이 쓰인다고 한다. 잘 알아둬야 할 것 같다!

나머지는 음.. 이후 코드를 보다보면 관련 코드가 나올테니, 그 코드의 헤더를 잘 적어두겠다.


GPIO 메모리 매핑 관련 전역 변수

int  mem_fd;
void *gpio_map;

// I/O access
volatile unsigned *gpio;

 

/* open /dev/mem */
   if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) {
      printf("can't open /dev/mem \n");
      exit(-1);
   }

 

이 mem_fd라는 변수가 무엇인지 설명하기 위해서는 이 문장이 필수적이라 가져왔다.

 

어쨋든, 이 파일 경로를 얻기 위함이고, 뒤의 O_RDWR와 같은 것은 fcntl 헤더에서 제공하는 변수이다.

이는 나중에 다시 설명 예정!

 

위의 변수들을 정리하면 다음과 같다.

 

  • mem_fd: /dev/mem 파일을 열기 위한 파일 디스크립터
  • gpio_map: mmap()을 통해 매핑된 메모리 주소
  • gpio: gpio_map을 통해 실제로 접근할 GPIO 제어 레지스터 포인터

 

이때 gpio에 Volatile을 붙이는 이유는 다음 블로그를 참고하면 좋을 것 같다.

(2012년도의 임베디드 개발자분.. 최고십니다..! 블로그가 네이버지만 믿음이 너무 가는 그런 마법..)

https://blog.naver.com/eslectures/80143556699

 

C 언어의 volatile 키워드

volatile은 C 언어의 많은 키워드 (keyword) 중에서 사용 빈도도 낮고 아마도 프로그래머들이 가장 잘 이...

blog.naver.com

 

(대충 while문을 돌릴 때 define 한 상수에 대해서 최적화 시 한 번만 확인하는데, 이를 volatile로 하면 define 한 상수나 주소를 계속 확인한다)


GPIO 관련 매크로 함수 정의

#define INP_GPIO(g) *(gpio+((g)/10)) &= ~(7<<(((g)%10)*3))
#define OUT_GPIO(g) *(gpio+((g)/10)) |=  (1<<(((g)%10)*3))

#define GPIO_SET *(gpio+7)  // sets   bits which are 1 ignores bits which are 0
#define GPIO_CLR *(gpio+10) // clears bits which are 1 ignores bits which are 0

#define GET_GPIO(g) (*(gpio+13)&(1<<g)) // 0 if LOW, (1<<g) if HIGH

 

  • INP_GPIO(g) / OUT_GPIO(g) → 해당 GPIO 핀을 입력(000) / 출력(001)로 설정
  • GPIO_SET / GPIO_CLR → 해당 GPIO 핀을 HIGH / LOW로 설정
  • GET_GPIO(g) → 핀의 현재 상태를 읽어 내며, 0이면 LOW, 1이면 HIGH

setup_io() 함수 : 메모리 매핑 설정

void setup_io()
{
   /* open /dev/mem */
   if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) {
      printf("can't open /dev/mem \n");
      exit(-1);
   }

   /* mmap GPIO */
   gpio_map = mmap(
      NULL,             //Any adddress in our space will do
      BLOCK_SIZE,       //Map length
      PROT_READ|PROT_WRITE,// Enable reading & writting to mapped memory
      MAP_SHARED,       //Shared with other processes
      mem_fd,           //File to map
      GPIO_BASE         //Offset to GPIO peripheral
   );

   close(mem_fd); //No need to keep mem_fd open after mmap

   if (gpio_map == MAP_FAILED) {
      printf("mmap error %d\n", (int)gpio_map);//errno also set!
      exit(-1);
   }

   // Always use volatile pointer!
   gpio = (volatile unsigned *)gpio_map;


} // setup_io

 

우선 open 함수를 통해, /dev/mem을 열어 메모리에 직접 접근하며, O_RDWR | O_SYNC 옵션을 사용하여 읽기/쓰기가 가능하며 동기화 된 상태로 열게된다.

 

이후 mmap 함수를 통해 GPIO의 레지스터 영역을 가상 메모리에 매핑하여, gpio_map은 GPIO 물리 주소에 대한 포인터가 된다.

 

gpio는 volatile 선언을 통해 gpio_map의 시작 주소, 즉 GPIO 레지스터의 시작 주소를 가리키게 되며, volatile을 통해 최적화를 방지한다. (한 번만 사용되는 것이 아닌, 여러번 사용 될 수 있도록!)


이후 Main에 있는 코드는 GPIO 중 필요한 것들을 껐다 키는 함수로, LED 등을 테스트하는데 사용된다.

 

분석하는 도중, 궁금증이 생겼다. 어떻게 GPIO_SET과 GPIO_CLR를 통해 특정 핀을 HIGH/LOW로 설정하는 것일까?

 

그래서 이 궁금증을 해결하기 위해 GPT에게 물어봤더니, 다음과 같이 정리되었다.

 

🔹 GPIO_SET / GPIO_CLR 분석 (왜 특정 핀을 지정하지 않는가?)

#define GPIO_SET *(gpio+7)  // 1을 설정하면 해당 비트의 GPIO가 HIGH로 변경됨
#define GPIO_CLR *(gpio+10) // 1을 설정하면 해당 비트의 GPIO가 LOW로 변경됨

이 매크로는 gpio 포인터를 이용해 특정 오프셋(7, 10)을 가진 레지스터에 접근하는 방식입니다.
✅ 하지만, 여기서는 GPIO_SET과 GPIO_CLR만 사용했을 뿐, 어떤 핀을 설정할 것인지 직접적으로 명시하지 않았습니다.
그 이유는 비트 연산을 통해 원하는 핀을 선택하기 때문입니다.


🔹 GPIO 레지스터 구조 (메모리 맵)

📌 Raspberry Pi의 GPIO 관련 주요 레지스터

Raspberry Pi의 GPIO 레지스터는 특정 오프셋을 가지고 있으며, 각 비트가 특정 핀을 담당합니다.

레지스터 오프셋 설명

GPFSELn (Function Select) 0x00 ~ 0x0C GPIO 핀의 기능 선택 (입출력 모드 등)
GPSETn (Output Set) 0x1C 해당 비트가 1이면 해당 GPIO를 HIGH
GPCLRn (Output Clear) 0x28 해당 비트가 1이면 해당 GPIO를 LOW
  • 각 비트가 개별 GPIO 핀을 담당합니다.
    • 예: GPIO_SET = (1 << 5); → GPIO 5번 핀을 HIGH로 설정
    • 예: GPIO_CLR = (1 << 5); → GPIO 5번 핀을 LOW로 설정

🔹 GPIO_SET와 GPIO_CLR가 동작하는 원리

#define GPIO_SET *(gpio+7)
#define GPIO_CLR *(gpio+10)

 gpio 포인터는 GPIO의 베이스 주소를 가리키고 있으며,
 gpio+7은 GPIO_SET 레지스터 (GPSET0, 0x1C)를 의미합니다.
 gpio+10은 GPIO_CLR 레지스터 (GPCLR0, 0x28)를 의미합니다.

📌 특정 GPIO 핀을 HIGH로 설정하는 방법

GPIO_SET = (1 << 5);  // GPIO 5번 핀을 HIGH로 설정
  • GPIO_SET = (1 << 5); 는 GPIO_SET 레지스터의 5번째 비트를 1로 설정하는 의미입니다.
  • GPIO_SET은 쓰기 전용 (Write-only) 레지스터이므로, 특정 핀을 HIGH로 만들려면 해당 비트에 1을 쓰면 됩니다.
  • 다른 비트는 영향을 받지 않으며, 해당 비트만 변경됨
  • 즉, GPIO_SET 자체를 직접 쓰는 것이 아니라, 비트 연산을 통해 특정 핀을 설정하는 것!

🔽 메모리 예시 (GPSET의 초기 상태)

비트 번호 31 30 ... 6 5 4 3 2 1 0

  31 30   6 5 4 3 2 1 0
0 0 ... 0 0 0 0 0 0 0

 

🔽 GPIO_SET = (1 << 5); 실행 후

비트 번호 31 30 ... 6 5 4 3 2 1 0

  31 30   6 5 4 3 2 1 0
0 0 ... 0 1 0 0 0 0 0

 

✅ 이후에 다시 GPIO_SET에 값을 써도 기존에 HIGH였던 핀은 영향을 받지 않음 (Write-only)


📌 특정 GPIO 핀을 LOW로 설정하는 방법

GPIO_CLR = (1 << 5);  // GPIO 5번 핀을 LOW로 설정
  • GPIO_CLR도 마찬가지로 쓰기 전용 레지스터이며, 해당 핀을 0으로 만듭니다.

🔽 GPIO_CLR = (1 << 5); 실행 후

비트 번호 31 30 ... 6 5 4 3 2 1 0

  31 30   6 5 4 3 2 1 0
0 0 ... 0 0 0 0 0 0 0

 


대강 요약하자면

(1<<5) 를 통해 bit를 5만큼 shift 해준 뒤, 이를 적용한다고 한다.

그렇다면 이것은 어떻게 적용이 되는가?

 

위 사진을 보면, 출력 신호와 입력 신호를 분리해서 넣는다는 것을 알 수 있다.

즉, GPSET를 통해 특정 핀을 HIGH 설정 시 해당 값이 Output Data를 통해 바뀌고, 이를 통해 PAD로 전해진 다음 이를 실제 GPIO인 GPLEV에 적용 시켜준다. 

이때 GPLEV : GPIO Pin Level Register 이며, 읽기 전용 레지스터 이다.

 

또한 PADS에 대해서 주요 기능은 다음과 같다.

 

아무튼, GPSET과 GPCLR, 그리고 PADS를 통해 GPLEV와 동기화 시켜줌으로서, GPIO의 전기 특성을 UP/DOWN 시켜주는 것.


아무래도 독학이고, 거기에다가 GPT를 사용해서 정리한 내용이 많기 때문에 틀린 내용이 많을 수밖에 없다.

보다보면 앞뒤가 안 맞는 말이 있을 수 있다(예를들어 GPIO 핀 40개인데 32개라서 물어보니, GPLEV0도 있고 GPLEV1도 있다고 한다).

그러니 항상!! 팩트 체크 알아서 해서 보시길 바랍니다!!