라즈베리파이에서 디바이스 드라이버 만들어보기!
디바이스 드라이버 만들어보기
디바이스 트리에 대해서 공부를 하다보니 갑자기 디바이스 드라이버 이야기가 나왔고, 실습을 진행하다가 정신을 차려보니 어느새 내가 GPIO 드라이버를 하나 만들어 커널에 등록하고 GPIO 제어를 하고 있었다. 여기까지 오려고 한 건 아니였는데,,, 그러다보니 글이 길어져서 디바이스 트리 글에서 따로 분리했다! 이것도 파보면 엄청나게 방대한 내용이 있을 것 같아서 두렵긴 하다….
ㅤㅤ
디바이스 드라이버 소스코드 컴파일
~/led_driver 라는 폴더를 하나 만들어주고, 아래 2개의 파일을 추가해보자. 위에 있는 led-gpio.c 파일은 디바이스 드라이버의 소스코드이고, 아래 Makefile 은 이를 빌드하기위한 파일이다.
// led-gpio.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
struct my_led_data {
int gpio_num;
const char *label;
};
static int my_led_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct my_led_data *led;
int ret;
printk("my_led: probe() called!\n");
led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
if (!led)
return -ENOMEM;
// 디바이스 트리에서 'gpios' 프로퍼티 읽기
led->gpio_num = of_get_named_gpio(np, "gpios", 0);
if (led->gpio_num < 0) {
dev_err(dev, "Failed to get GPIO from DT\n");
return led->gpio_num;
}
// 디바이스 트리에서 'label' 프로퍼티 읽기
of_property_read_string(np, "label", &led->label);
printk("my_led: GPIO number from DT = %d\n", led->gpio_num);
printk("my_led: Label from DT = %s\n", led->label);
// GPIO 요청 및 초기화
ret = devm_gpio_request(dev, led->gpio_num, led->label);
if (ret) {
dev_err(dev, "Failed to request GPIO %d\n", led->gpio_num);
return ret;
}
gpio_direction_output(led->gpio_num, 0); // 출력 모드, LOW
platform_set_drvdata(pdev, led);
return 0;
}
// 타입을 int -> void로 변경해줘야 에러가 안난다
static void my_led_remove(struct platform_device *pdev)
{
struct my_led_data *led = platform_get_drvdata(pdev);
gpio_set_value(led->gpio_num, 0); // LED 끄기
printk("my_led: remove() called\n");
// return 0;
}
// ★ 핵심: compatible 매칭 테이블
static const struct of_device_id my_led_of_match[] = {
{ .compatible = "mycompany,gpio-led" }, // DT의 compatible과 일치!
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);
static struct platform_driver my_led_driver = {
.probe = my_led_probe,
.remove = my_led_remove,
.driver = {
.name = "my-gpio-led",
.of_match_table = my_led_of_match, // ← 매칭 테이블 등록
},
};
module_platform_driver(my_led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Sungmin");
MODULE_DESCRIPTION("GPIO LED driver with Device Tree");
// Makefile
obj-m += led-gpio.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
ㅤㅤ
그다음, make 명령어로 만들어준 드라이버 코드를 컴파일하여 led-gpio.ko 파일을 만들어준다.


ㅤㅤ
디바이스 트리 오버레이 작성
그 다음, 디바이스 트리 오버레이를 작성해준다. 라즈베리파이에서는 기존 디바이스 트리를 직접 수정하는 대신에 이 ‘오버레이’라는 방식을 사용한다. 만약에 내가 직접 dtb 파일을 수정했다면 → OS 업데이트와 함께 내가 수정한 내용이 없어질 수 있음 + 다른 HW 설정 추가 시 충돌 가능. 그래서 이 대신에, 기존의 디바이스 트리에 내가 추가한 디바이스를 덧붙이는 방식인 오버레이를 사용한다.
ㅤㅤ
my-led-overlay.dts 파일을 만들어주고, 아래처럼 내용을 넣어주자. 이게 아까 위에서 말해왔던 디바이스 트리의 소스코드이다. 이걸 컴파일하면 dtb 파일이 된다.
- 드라이버 이름은 led_device
- 안에 compatible, gpios, label, status 같은 프로퍼티가 전달된다.
- 여기에서 GPIO 17번 핀과 매핑 (물리 번호 11번)해주었다.
// my-led-overlay.dts
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2711";
fragment@0 {
target-path = "/";
__overlay__ {
my_led: led_device {
compatible = "mycompany,gpio-led";
gpios = <&gpio 17 0>;
label = "status-led";
status = "okay";
};
};
};
};
ㅤㅤ
그 다음, 오버레이를 컴파일해 dtbo 파일(dtb overlay)을 만들고, 오버레이 폴더 내에 이걸 복사해서 설치해준다. 또, 부팅 시에 이 오버레이를 인지할 수 있도록 config 에 추가해준다.
// 오버레이 컴파일
dtc -@ -I dts -O dtb -o my-led.dtbo my-led-overlay.dts
// 오버레이 설치
sudo cp my-led.dtbo /boot/firmware/overlays/
// 부팅 시 오버레이 로드 설정
sudo nano /boot/firmware/config.txt
// 여기에 들어가서 파일 맨 끝에 아래 줄을 추가하기
dtoverlay=my-led
ㅤㅤ
주소가 /boot/firmware/overlays, /boot/firmware/config.txt 로 변경되었으니, 챗GPT에게 속지말자.
ㅤㅤ


ㅤㅤ
그 다음, 재부팅해서 정상적으로 디바이스 트리에 내가 추가한 내용들이 등록되었는지 확인해보자.
sudo reboot
ㅤㅤ
디바이스 트리에서 내 장치 확인하기
/proc/device-tree 에서 내가 추가해준 “led_device”를 찾을 수 있다. 내부에 들어가서 파일들이 있는데, 여기에 cat 명령어로 내용을 확인해보면 내가 전달했던 속성들 그대로 튀어나오는 것을 볼 수 있다.


ㅤㅤ
이걸 보고 “트리 구조 조차 리눅스에서는 파일로 관리되는구나”라고 느꼈다. 아닐수도 있음 ㅋㅋ
ㅤㅤ
디바이스와 드라이버의 바인딩
이 모듈을 동적으로 설치하는 명령어인 install module insmod 를 실행해보면, 메시지로 “probe() called!” 라는게 튀어나오는걸 볼 수 있다. 이게 드라이버 소스코드에 내가 적어줬던 probe() 의 내용임!!
pi07@pi07:~/led_driver $ sudo insmod led-gpio.ko
pi07@pi07:~/led_driver $ dmesg | tail -5
[ 9.938885] brcmfmac: brcmf_cfg80211_set_power_mgmt: power save enabled
[ 560.437626] led_gpio: loading out-of-tree module taints kernel.
[ 560.438447] my_led: probe() called!
[ 560.438464] my_led: GPIO number from DT = 529
[ 560.438467] my_led: Label from DT = status-led
ㅤㅤ
여기에서 커널이 인식하는 모든 하드웨어 장치, 드라이버, 커널 객체들의 정보를 다루는 /sys 내부에서 드라이버들이 등록되어있는 곳을 찾아가서 내가 등록해준 디바이스 드라이버인 my-gpio-led 를 찾아주었다. 파일을 찍어보면 이게 링크로 해서 내가 아까 디바이스 트리에 추가해줬던 그 녀석들이 보인다. 여기까지 잘 들어왔다면 디바이스 트리에 정의된 디바이스(led_device)와 드라이버 (my-gpio-led)가 바인딩 되었다는 의미이다! 바인딩이 probe() 이다!!

ㅤㅤ
커널 님께서 나를 기억해주셨어!!!
ㅤㅤ
✓ 디바이스 트리 오버레이 로드됨 → /proc/device-tree/led_device/
✓ 드라이버 모듈 로드됨 → lsmod | grep led_gpio
✓ compatible 매칭 성공 → "mycompany,gpio-led"
✓ 바인딩 완료 → /sys/bus/platform/drivers/my-gpio-led/led_device
✓ probe() 호출됨 → dmesg에 메시지 출력
ㅤㅤ
동작하는 디바이스 드라이버 만들기
위의 코드를 사용하면 사용자가 GPIO를 제어할 수 있는 방법이 없다. 그냥 정해진 상태를 유지할 뿐. 나는 이 디바이스 드라이버를 C 코드를 통해 사용하면서 실제 HW를 제어하도록 해보고 싶었다.
ㅤㅤ
sysfs 방식으로 구현하기
디바이스 드라이버가 sysfs 지원하도록 수정해보자. 아까 작성했던 led-gpio.c 파일을 수정해주고 make 로 컴파일하자.
ㅤㅤ
아래 코드에서 변경된 주요 부분은 sysfs 를 이용하도록 되었다. 파일에 echo를 통해 쓰기를 한다면 led_state_store() 를 통해 GPIO를 제어하고, read를 하면 이 파일의 값을 읽어서 반환하도록 설정되었다.
// sysfs read: cat /sys/.../led_state
static ssize_t led_state_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct my_led_data *led = dev_get_drvdata(dev);
return sprintf(buf, "%d\n", led->state);
}
// sysfs write: echo 1 > /sys/.../led_state
static ssize_t led_state_store(struct device *dev,
struct device_attribute *attr,
const char *buf,
size_t count)
{
struct my_led_data *led = dev_get_drvdata(dev);
int value;
int ret;
ret = kstrtoint(buf, 10, &value);
if (ret < 0)
return ret;
// 0 또는 1만 허용
if (value != 0 && value != 1)
return -EINVAL;
// GPIO 제어
gpio_set_value(led->gpio_num, value);
led->state = value;
pr_info("my_led: LED state changed to %d\n", value);
return count;
}
// sysfs 파일 속성 정의
static DEVICE_ATTR(led_state, 0664, led_state_show, led_state_store);
ㅤㅤ
led-gpio.c의 수정된 전문은 아래와 같다.
// led-gpio.c (sysfs 지원 버전)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
struct my_led_data {
int gpio_num;
const char *label;
int state; // LED 상태 저장
};
// sysfs read: cat /sys/.../led_state
static ssize_t led_state_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct my_led_data *led = dev_get_drvdata(dev);
return sprintf(buf, "%d\n", led->state);
}
// sysfs write: echo 1 > /sys/.../led_state
static ssize_t led_state_store(struct device *dev,
struct device_attribute *attr,
const char *buf,
size_t count)
{
struct my_led_data *led = dev_get_drvdata(dev);
int value;
int ret;
ret = kstrtoint(buf, 10, &value);
if (ret < 0)
return ret;
// 0 또는 1만 허용
if (value != 0 && value != 1)
return -EINVAL;
// GPIO 제어
gpio_set_value(led->gpio_num, value);
led->state = value;
pr_info("my_led: LED state changed to %d\n", value);
return count;
}
// sysfs 파일 속성 정의
static DEVICE_ATTR(led_state, 0664, led_state_show, led_state_store);
static int my_led_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct my_led_data *led;
int ret;
pr_info("my_led: probe() called!\n");
led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
if (!led)
return -ENOMEM;
// 디바이스 트리에서 GPIO 번호 읽기
led->gpio_num = of_get_named_gpio(np, "gpios", 0);
if (led->gpio_num < 0) {
dev_err(dev, "Failed to get GPIO from device tree\n");
return led->gpio_num;
}
of_property_read_string(np, "label", &led->label);
pr_info("my_led: GPIO number = %d\n", led->gpio_num);
pr_info("my_led: Label = %s\n", led->label);
// GPIO 요청
ret = devm_gpio_request(dev, led->gpio_num, led->label);
if (ret) {
dev_err(dev, "Failed to request GPIO %d\n", led->gpio_num);
return ret;
}
// GPIO를 출력으로 설정, 초기값 0 (OFF)
gpio_direction_output(led->gpio_num, 0);
led->state = 0;
// platform device에 데이터 연결
dev_set_drvdata(dev, led);
// sysfs 파일 생성
ret = device_create_file(dev, &dev_attr_led_state);
if (ret) {
dev_err(dev, "Failed to create sysfs file\n");
return ret;
}
pr_info("my_led: LED driver initialized, sysfs created\n");
return 0;
}
static void my_led_remove(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct my_led_data *led = dev_get_drvdata(dev);
// sysfs 파일 제거
device_remove_file(dev, &dev_attr_led_state);
// LED 끄기
gpio_set_value(led->gpio_num, 0);
pr_info("my_led: LED driver removed\n");
}
static const struct of_device_id my_led_of_match[] = {
{ .compatible = "mycompany,gpio-led" },
{ }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);
static struct platform_driver my_led_driver = {
.probe = my_led_probe,
.remove = my_led_remove,
.driver = {
.name = "my-gpio-led",
.of_match_table = my_led_of_match,
},
};
module_platform_driver(my_led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Sungmin");
MODULE_DESCRIPTION("GPIO LED driver with sysfs interface");
ㅤㅤ
make로 컴파일까지 잘 해줬다면 기존의 모듈은 내리고 새로운 모듈을 다시 올려준다.
sudo rmmod led_gpio
sudo insmod led-gpio.ko
// 잘 설정이 되었는지는 찍어보면 알겠지
dmesg | tail -10

ㅤㅤ
그러면 방금 우리가 만들어준 설정대로 sysfs 파일인 led_state가 led_device 아래에 쇽 들어온 것을 볼 수 있다. 이 파일의 권한도 664로 설정한 대로 그대로 들어와있다. (저번에 살펴본 것처럼, 파일 크기가 4096 ? 이거 sysfs 파일일 가능성이 매우 높다)
ls -l /sys/devices/platform/led_device

ㅤㅤ
그러면 이제 sysfs 를 사용하는 것처럼 이 파일에 1과 0을 넣어주면서 GPIO 17번 핀을 제어할 수 있다. 로그에도 이렇게 0, 1을 써주면 변경되었다고 출력이 되고, 실제 GPIO Pin으로도 출력이 전달되는 것을 확인할 수 있다.

ㅤㅤ
C 파일에서 쓰려면?
만약 이걸 C 파일에서 코드로 제어해보고 싶다면? sysfs를 사용하는 방식 그대로 이 led_state 파일에다가 값을 써주면서 제어할 수 있다.
#define LED_PATH "/sys/devices/platform/led_device/led_state"
int led_write(int value)
{
int fd;
char buf[2];
fd = open(LED_PATH, O_WRONLY);
if (fd < 0) {
perror("Failed to open LED device");
return -1;
}
snprintf(buf, sizeof(buf), "%d", value);
if (write(fd, buf, strlen(buf)) < 0) {
perror("Failed to write to LED");
close(fd);
return -1;
}
close(fd);
return 0;
}
ㅤㅤ
물론 이제 함수처럼 쉽게 불러다가 쓰도록 만들기 위해서는 또 더 복잡하게 작업을 하면 되겠지만, 이렇게 하면 작동한다는 과정을 눈으로 직접 확인한게 큰 의의가 있을 것 같다. 이후의 작업에 대해서는 또 공부할 게 많기 때문에 여기까지만 건드리고 넘어가자.
ㅤㅤ
혹시 GPIO 건드리는 함수는 조상님이 짜주시나?
위에 led-gpio.c 파일에서 led_state_store() 함수를 보면 gpio_set_value() 함수를 통해 이 GPIO 로 나가는 신호를 변경하게된다. 그런데 이건 내가 짜준 코드가 아닌데. 사실상 내가 디바이스 드라이버를 통해 하고싶었던 일을 저 함수 한 줄이 대신 해주는 것 같은 불길한 느낌이 들었다.
ㅤㅤ
이 함수는 리눅스 커널의 하위 API로 미리 만들어져있는 함수이다. 훨씬 아래쪽 레벨에서 기본 GPIO Pin 같은 HW를 제어하라고 일부러 만들어둔 API 이기 때문에, 여기에서도 크게 마법은 없다. 아마 직접 HW 레벨까지 내려가서 제어를 해주는 코드이겠지.
진짜 개발자 조상님들이 짜주신 코드이다. 아직 얼마 안됐으니 다들 살아계시긴 할 듯.
ㅤㅤ
통신을 위해서는 혹시 내가 직접 코드를 작성해야하는건가 두려움에 10초 정도 떨었는데, 제미나이 피셜로는 아래 API 들을 이용하면 되기 때문에 걱정 안해도 된다고 한다.
| 통신/기능 분류 | 표준 커널 서브시스템 | 드라이버가 호출하는 API 예시 |
|---|---|---|
| 직렬 통신 | UART/TTY 서브시스템 | tty_register_driver(), tty_insert_flip_char() |
| I2C 통신 | I2C 서브시스템 | i2c_transfer(), i2c_master_recv() |
| SPI 통신 | SPI 서브시스템 | spi_write(), spi_read(), spi_sync() |
| 메모리 할당 | 메모리 관리 | kmalloc(), kzalloc() |
| 타이머/시간 | 타이머 서브시스템 | mod_timer(), jiffies |
| 인터럽트 처리 | IRQ 서브시스템 | request_irq(), free_irq() |
ㅤㅤ
그치만 임베디드 세상에 내가 발을 들인 이상, 이런 드라이버를 직접 작성해야 하는 경우가 꽤나 많다고 한다.

ㅤㅤ
디바이스 드라이버 팀에서 아마 이런 부분들에 대한 작업을 하지 않을까 추측해본다!
'Embedded System > Embedded Linux' 카테고리의 다른 글
| [Embedded Linux] Linux의 디바이스 트리 (0) | 2025.11.29 |
|---|---|
| [Embedded Linux] 프로세스간 통신 IPC - POSIX 기반의 IPC (1) | 2025.11.29 |
| [Embedded Linux] 프로세스간 통신 IPC - System V 기반의 IPC (0) | 2025.11.29 |
| [Embedded Linux] 프로세스간 통신 IPC - 파일 기반의 IPC (0) | 2025.11.29 |
| [Embedded Linux] Linux 시그널 (0) | 2025.11.29 |