가장 기본적인 디지털 입출력 포트
논리적 개념: 0 or 1을 입력받거나 출력하는 포트
전기적 개념: 0V or 3.3V(5V)를 입력받거나 출력하는 포트

라즈베리파이 4B/5B 기준으로 40개의 핀 헤더에 26개의 GPIO 핀이 있으며 그 외에 5V 핀, 3.3V 핀 UART 핀 등이 있다.
터미널에서 pinout 커멘드를 통해 확인이 가능하다.

다이오드는 극성이 존재한다. 다리 길이가 긴 쪽이 (+), 짧은 쪽이 (-)이다. 아니면 위에서 봤을 때 절단면이 있는 부분이 (-), 없는 부분이 (+)이다.

또한 LED 회로 구성에 필수적인 저항에 대해 알아보자.
저항의 종류는 4밴드, 5밴드, 6밴드가 있다.

4밴드

5밴드, 6밴드

PC와 유사하게 시스템 버스로 구성되어 있으며, 라즈베리파이와 브레드보드 사이에 점퍼케이블을 이용하여 연결한다.
라즈베리파이의 GPIO 제어
① 출력할 GPIO 번호 지정
② Input or Output 설정
③ 해당 GPIO 값 (0 or 1) 전송
그리고 라즈베리파이의 주변장치 제어 방식에는 아래와 같이 3가지 방법이 있다.
sysfs 인터페이스
libgpiod
mmap I/O

먼저 libgpiod 환경을 설정해보자
~$ sudo apt install gpiod
~$ sudo apt install libgpiod-dev
설치 확인을 위해 gpiodetect 명령을 입력해보자.
GPIO 칩 목록과 각 칩별 라인(핀) 개수를 출력해준다.
~$ gpiodetect
gpiochip0 [pinctrl-rpl] (54 lines)
gpiochip10 [gpio-brcmstb@107d508500] (32 lines)
gpiochip11 [gpio-brcmstb@107d508520] (4 lines)
gpiochip12 [gpio-brcmstb@107d517c00] (17 lines)
gpiochip13 [gpio-brcmstb@107d517c20] (6 lines)
gpioinfo: 특정 GPIO 칩에 대한 정보를 출력하여 각 GPIO 라인의 상태를 확인한다.
gpioget: GPIO 라인의 입력 값을 읽어서 특정 GPIO 핀의 현재 상태 (0 or 1)를 확인한다.
~$ gpioget gpiochip0 18
0
~$
gpioset: GPIO 라인의 출력 값을 설정하여 특정 GPIO 핀의 값을 1(=HIGH) or 0(=LOW)로 설정한다.
~$ gpioset gpiochip0 18=1
~$ gpioset gpiochip0 18=0
gpiomon: 특정 GPIO 라인에서 이벤트(상승 엣지 or 하강 엣지)를 모니터링해준다.
~$ gpiomon gpiochip0 17
event: FALLING EDGE offset: 17 timestamp:...
event: RISING EDGE offset: 17 timestamp:...
event: FALLING EDGE offset: 17 timestamp:...
gpiofind: 특정 라인 이름을 기준으로 GPIO 라인의 번호 탐색
~$ gpiofind "GPIO18"
gpiochip0 18
#include <gpiod.h> //libgpiod 라이브러리
#include <stdio.h>
#include <unistd.h>
#define CONSUMER "Consumer" //소비자 이름
#define CHIP "gpiochip0" //GPIO 칩 이름
int ledControl(int gpio)
{
//1. GPIO 칩 열기
struct gpiod_chip* chip = gpio_chip_open_by_name(CHIP);
//2. GPIO 라인 가져오기 (GPIO 18)
struct gpiod_line* line = gpiod_chip_get_line(chip, gpio);
//3. GPIO 라인을 출력 모드로 설정 (기본값 LOW)
if (gpiod_line_request_output(line, CONSUMER, 0) < 0)
{
perror("Failed to request GPIO line as output");
gpiod_chip_close(chip);
return 1;
}
//4. GPIO 핀을 HIGH로 설정
gpiod_line_set_value(line, 1);
getchar(); //LED 확인을 위한 대기
//5. 리소스 해제 및 종료
gpiod_line_release(line); //GPIO pin 해제
gpiod_chip_close(chip); //GPIO 칩 사용 종료
return 0;
}
int main(int argc, char** argv)
{
int gno;
if (argc < 2)
{
printf("Usage: %s GPIO_NO\n", argv[0]);
return -1;
}
gno = atoi(argv[1]);
ledControl(gno);
return 0;
}
libgpiod를 사용할 경우 gcc 컴파일 시 -lgpiod 옵션을 함께 붙여야 한다.
~$ gcc -o gpioled02 gpioled02.c -lgpiod
~$ ./gpioled02 18
라즈베리 파이에 탑재된 브로드컴 CPU를 위한 C언어 기반 GPIO 액세스 라이브러리
라즈베리 파이에서 간단하게 GPIO 접근이 가능하며, 내부 코드는 기본적으로 mmapIO로 구현되어 있다.
git에서 무료로 다운이 가능하므로, 먼저 git 패키지부터 설치하고 wiringPi를 빌드해보자.
git clone https://github.com/WiringPi/WiringPi.git
cd WiringPi/
git pull origin
./build
wiringPi의 핀 번호 매핑 정보 확인하기 위해 gpio readall 명령을 입력해보자.

아마도 위의 구성도 그림과 비슷한 모습이 나올 것이다.

아래 그림은 라즈베리 파이의 입력 핀에 대한 정보를 사진과 함께 설명해 놓은 것이다.
직관적으로 알아보기 젤 편하다.

#include <wiringPi.h>
#include <stdio.h>
#include <stdlib.h>
int ledControl(int gpio)
{
int i;
pinMode(gpio, OUTPUT);
for (i = 0; i < 5; i++)
{
digitalWrite(gpio, HIGH);
delay(1000);
digitalWrite(gpio, LOW);
}
return 0;
}
int main(int argc, char** argv)
{
int gno;
if (argc < 2)
{
printf("Usage: %s GPIO_NO\n", argv[0]);
return -1;
}
gno = atoi(argv[1]);
wiringPiSetupGpio();
ledControl(gno);
return 0;
}
libgpiod를 사용할 경우 -lgpiod 옵션을 붙이듯이 wiringPi를 이용하여 컴파일할 경우 -lwiringPi 옵션을 붙여야 한다.
또한 디바이스 파일에 접근하기 위해서는 root 권한이 필요하므로 sudo 명령이 필요하다.
~$ gcc -o wiringled wiringled.c -lwiringPi
~$ sudo ./wiringled 18
위의 예시 코드에 쓰인 wiringPi의 주요 함수에 대해 알아보자.
Setup 함수: wiringPi 초기화 함수
int wiringPiSetup(void); //wiringPi 핀 번호 사용
int wiringPiSetupGpio(void); //GPIO 핀 번호 사용 (BCM; Broadcom 모드)
int wiringPiSetupPhys(void); //물리적 핀 번호 사용
int wiringPiSetupSys(void); //wiringPi 핀 번호 사용, sys파일* 인터페이스를 통해 하드웨어 제어
우리는 대부분 GPIO 핀 번호를 기준으로 사용하므로 wiringSetupGPIO(void) 함수를 사용하는 것이다.
void pinMode(int pin, int mode);
pin: 핀 번호를 설정
mode: 해당 핀의 모드를 설정
void digitalWrite(int pin, int value);
pin: 핀 번호를 설정
value: 해당 핀에 출력할 값을 설정
void delay(unsigned int howLong);
howLong: 얼마나 지연시킬 것인지 밀리초(ms)단위로 값을 설정
#include <wiringPi.h>
#include <stdio.h>
#include <stdlib.h>
int ledControl(int gpio1, int gpio2, int sec)
{
pinMode(gpio1, OUTPUT);
pinMode(gpio2, OUTPUT);
digitalWrite(gpio1, HIGH);
digitalWrite(gpio2, HIGH);
while (TRUE)
{
digitalWrite(gpio1, HIGH);
delay(sec / 2);
digitalWrite(gpio1, LOW);
digitalWrite(gpio2, HIGH);
delay(sec / 2);
digitalWrite(gpio2, LOW);
}
return 0;
}
int main(int argc, char** argv)
{
int gno1;
int gno2;
int sec;
if (argc < 2)
{
printf("Usage: %s GPIO_NO\n", argv[0]);
return -1;
}
gno1 = atoi(argv[1]);
gno2 = atoi(argv[2]);
sec = atoi(argv[3]);
wiringPiSetupGpio();
ledControl(gno1, gno2, sec);
return 0;
}
GPIO는 입출력이 모두 가능한 인터페이스인데, 지금까지 LED를 켜고 끄는 것은 출력에 해당하는 내용이다.
사용자 환경과 상호작용하기 위해서는 입력 수단이 필요한데, 가장 대표적인 입력 수단은 스위치이다. 지금부터 스위치를 이용하여 회로를 구성하고 코드를 작성해보자.
먼저 스위치란 끊어진 두 개의 금속을 연결하는 방식으로 전기의 흐름을 제어한다. 스위치를 누르면 전기가 통하고 스위치를 떼면 전기가 통하지 않는 방식이다. (당연한 얘기)
스위치 종류에는 DIP, 토글, 택트, 슬라이드 스위치가 있으며 이중 택트(Tact) 스위치는 버튼을 누르고 있는 동안 ON, 손을 떼면 OFF되는 방식이다.

저항과 스위치의 위치가 어디에 달려있냐에 따라 Pull-up, Pull-down으로 나뉜다. 만약 아래와 같이 단순히 전압과 스위치만 연결하게 되면 플로팅 상태가 되어 전류의 흐름이 불분명해진다.

☃️ 플로팅 상태란(Floating State)란?
입력 단자 주위의 정전기나 잡음에 의해 오류가 발생하여 입력 핀에 0V인지 5V인지 확인이 어려운 상태를 말함.
따라서 스위치와 함께 반드시 저항을 달아주게 되는데 위치에 따라 구성방식이 조금 다르다.
먼저 Pull-up은 스위치보다 저항이 위에 달려있는 것이다.

이 경우 스위치가 열려있는 경우(버튼을 안눌렀을 때) 3.3V에 오는 전압이 INPUT과 연결되어 HIGH로 인식되고, 스위치가 닫혀있는 경우(버튼을 눌렀을 때) GND와 연결되어 LOW로 인식한다.
반대로 Pull-down 저항은 스위치보다 저항이 아래에 달려있는 것이다.

이 경우 스위치가 열려있는 경우(버튼을 안눌렀을 때) INPUT이 바로 GND로 연결되어 LOW로 인식, 스위치가 닫혀있는 경우(버튼을 눌렀을 때) 3.3V 전압이 INPUT과 연결되어 HIGH로 인식한다.
이를 이용하여 GPIO 입력 모드 테스트를 위해 아래와 같이 회로를 구성하고, 코딩을 통해 버튼과 LED를 상호작용해보자.

#include <wiringPi.h>
#include <stdio.h>
#include <stdlib.h>
int ledControl(int gpio1, int gpio2)
{
pinMode(gpio1, INPUT);
pinMode(gpio2, OUTPUT);
digitalWrite(gpio2, LOW);
while (TRUE)
{
if (digitalRead(gpio1) == 0)
{
digitalWrite(gpio2, HIGH);
}
else if (digitalRead(gpio1) == 1)
{
digitalWrite(gpio2, LOW);
}
}
return 0;
}
int main(int argc, char** argv)
{
int gno1;
int gno2;
if (argc < 3)
{
printf("Usage: %s GPIO_NO\n", argv[0]);
return -1;
}
gno1 = atoi(argv[1]);
gno2 = atoi(argv[2]);
wiringPiSetupGpio();
ledControl(gno1, gno2);
return 0;
}
이 코드에서 중요한 부분은 현재 회로 구성이 Pull-up으로 구성되어 있으므로
스위치가 LOW인 경우 (digitalRead(gpio1) == 0) 버튼을 누르는 동작이므로 LED 출력이 HIGH가 된다.
반대로 스위치가 HIGH인 경우 (digitalRead(gpio1) == 1) 버튼을 떼는 동작이므로 LED 출력이 LOW가 된다.
지금까지 LED를 연결하는 방식은 Source current driving 방식이다.

Source 방식은 우리가 상식적?으로 아는 바와 같이 LED 출력을 HIGH로 설정하면 불이 켜지고, LOW로 설정하면 불이 꺼진다.
반대로, Sink current driving 방식으로 변경할 경우 회로 구성이 다르다.

출력으로 향하는 곳이 GND가 아니라 GPIO pin이고, 입력이 Vcc로 연결되어 있다. 따라서 LED 출력을 LOW로 설정하면 불이 켜지고, HIGH로 설정하면 불이 꺼진다.
아래는 회로를 Sink currnet driving 방식으로 바꾼 경우 수정한 코드이다.
#include <wiringPi.h>
#include <stdio.h>
#include <stdlib.h>
int ledControl(int gpio1, int gpio2)
{
pinMode(gpio1, INPUT);
pinMode(gpio2, OUTPUT);
digitalWrite(gpio2, LOW);
while (TRUE)
{
if (digitalRead(gpio1) == LOW)
{
digitalWrite(gpio2, LOW);
}
else if (digitalRead(gpio1) == HIGH)
{
digitalWrite(gpio2, HIGH);
}
}
return 0;
}
int main(int argc, char** argv)
{
int gno1;
int gno2;
if (argc < 3)
{
printf("Usage: %s GPIO_NO\n", argv[0]);
return -1;
}
gno1 = atoi(argv[1]);
gno2 = atoi(argv[2]);
wiringPiSetupGpio();
ledControl(gno1, gno2);
return 0;
}
변경된 부분은 (digitalRead(gpio1) == LOW)인 경우 LED 출력을 LOW, (digitalRead(gpio2) == HIGH)인 경우 LED 출력을 HIGH로 반전시킨 것 밖에 없다.
#include <wiringPi.h>
#include <stdio.h>
#include <stdlib.h>
int ledControl(int gpio1)
{
pinMode(gpio1, OUTPUT);
while (TRUE)
{
digitalWrite(gpio1, HIGH);
delay(500);
digitalWrite(gpio1, LOW);
delay(500);
}
return 0;
}
int main(int argc, char** argv)
{
int gno1;
if (argc < 2)
{
printf("Usage: %s GPIO_NO1 GPIO_NO2\n", argv[0]);
return -1;
}
gno1 = atoi(argv[1]);
wiringPiSetupGpio();
ledControl(gno1);
return 0;
}
1 Hz -> 5 Hz -> 10 Hz로 주기를 바꾸는 코드이다.
#include <wiringPi.h>
#include <stdio.h>
#include <stdlib.h>
int ledControl(int gpio1, int gpio2)
{
pinMode(gpio1, INPUT);
pinMode(gpio2, OUTPUT);
int frequencies[] = {1, 5, 10}; // Hz
int numFrequencies = sizeof(frequencies) / sizeof(frequencies[0]);
int currentFreqIndex = 0;
int buttonPressed = 0;
while (TRUE)
{
// 스위치가 눌렸을 때 주기 변경
if (digitalRead(gpio1) == 0 && !buttonPressed)
{
buttonPressed = 1; // 버튼이 눌렸음을 기록
currentFreqIndex = (currentFreqIndex + 1) % numFrequencies; // 1Hz -> 5Hz -> 10Hz 순환
// 현재 설정된 점멸 주기 출력
printf("Current frequency: %d Hz\n", frequencies[currentFreqIndex]);
}
else if (digitalRead(gpio1) == 1)
{
buttonPressed = 0; // 버튼이 떼어졌을 때 상태 초기화
}
// 주기에 맞춰 LED 점멸
int delayTime = 1000 / frequencies[currentFreqIndex]; // 주기 계산 (ms)
digitalWrite(gpio2, HIGH);
delay(delayTime / 2); // LED 켜기
digitalWrite(gpio2, LOW);
delay(delayTime / 2); // LED 끄기
}
return 0;
}
int main(int argc, char** argv)
{
int gno1;
int gno2;
if (argc < 3)
{
printf("Usage: %s GPIO_NO1 GPIO_NO2\n", argv[0]);
return -1;
}
gno1 = atoi(argv[1]);
gno2 = atoi(argv[2]);
wiringPiSetupGpio();
ledControl(gno1, gno2);
return 0;
}