코드를 느껴바라

[임베디드] 리눅스 디바이스 드라이버로 GPIO LED 제어하기 (Linux Kernel) 본문

개발/임베디드(Embedded)

[임베디드] 리눅스 디바이스 드라이버로 GPIO LED 제어하기 (Linux Kernel)

feelTheCode 2026. 2. 13. 14:43

리눅스 디바이스 드라이버로 GPIO LED 제어하기

오늘은 리눅스 커널 레벨에서 하드웨어를 직접 제어하는 Character Device Driver를 작성하고, 유저 응용 프로그램에서 이를 호출해 LED를 켜고 끄는 실습을 진행했습니다.

전체 코드는 포스팅 제일 하단에 위치하고 있습니다.!!

1. 실습 개요 (What I did)

리눅스 시스템(User Space)에서는 하드웨어에 직접 접근할 수 없습니다. 따라서 Kernel Space에서 동작하는 모듈을 만들어 물리 메모리에 접근하고, 유저 공간에서 open, write, read 시스템 콜을 통해 LED를 제어하는 구조를 구현했습니다.

  • 타겟 보드: 라즈베리파이 4 (추정: Base Address가 0xFE200000임)
  • 제어 핀: GPIO 17번
  • 주요 기능:
  • 1 입력: LED ON
  • 0 입력: LED OFF
  • 숫자(N) 입력: N회 깜빡임 (Blink)

2. 드라이버 코드 작성 (Kernel Space)

먼저 커널 모듈 소스 (gpio_module.c)입니다. 이 코드는 물리 메모 주소를 가상 주소로 매핑하여 GPIO 레지스터를 직접 조작합니다.

주요 포인트

  1. ioremap: 0xFE200000 (물리 주소)를 커널 가상 주소로 매핑하여 하드웨어 접근 권한 획득.
  2. register_chrdev_region & `cdev_add:/dev/gpioled`라는 장치 드라이버를 커널에 등록.
  3. file_operations: 유저의 open, read, write 요청을 처리할 함수 연결.
// gpio_module.c 

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/module.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/delay.h>

MODULE_LICENSE("GPL");

#define GPIO_BASE (0xFE200000) 
#define GPIO_SIZE (256) 

#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))

static volatile unsigned int *gpio;

/* 디바이스 파일의 주 번호와 부 번호 */
#define GPIO_MAJOR 200
#define GPIO_MINOR 0
#define GPIO_DEVICE "gpioled"
#define GPIO_LED 17
#define BLOCK_SIZE 100

static char msg[BLOCK_SIZE] = {0};

static int gpio_open(struct inode* inod, struct file* fil){
    printk("GPIO Device opened\n");
    return 0;
}

static ssize_t gpio_write(struct file* inode, const char *buff, size_t len, loff_t* off){
    // 유저 영역에서 데이터 가져오기 (copy_from_user)
    copy_from_user(msg, buff, len);

    if (!strcmp(msg, "0")) {
        GPIO_CLR(GPIO_LED); // LED OFF
    } else if (!strcmp(msg, "1")) {
        GPIO_SET(GPIO_LED); // LED ON
    } else {
        // 숫자만큼 깜빡이기
        int val;
        kstrtoint(msg, 10, &val);
        for (int i = 0; i < val; i++) {
            GPIO_SET(GPIO_LED);
            ssleep(1);
            GPIO_CLR(GPIO_LED);
            ssleep(1);
        }
    }
    return count;
}

// ... (나머지 함수 생략)

3. 유저 어플리케이션 작성 (User Space)

드라이버가 잘 작동하는지 테스트할 응용 프로그램(gpio.c)입니다. 디바이스 파일을 열어서 명령어를 쓰고, 결과를 읽어옵니다.

// gpio.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define BUFFSIZ 100

int main(int argc, char **argv){
    char buf[BUFFSIZ];
    int fd = -1;

    memset(buf, 0, BUFFSIZ);
    printf("GPIO SET : %s\n", argv[1]);

    // 1. 디바이스 파일 열기
    fd = open("/dev/gpioled", O_RDWR);

    // 2. 커널로 명령 전송 (write)
    write(fd, argv[1], strlen(argv[1]));

    // 3. 커널로부터 응답 수신 (read)
    read(fd, buf, BUFFSIZ);

    printf("Read Data : %s\n", buf);

    close(fd);
    return 0;
}

4. 빌드 및 실행 과정 (How to run)

터미널에서 실제로 수행한 명령어 순서입니다.

1) 커널 모듈 빌드 (Makefile 이용)

먼저 드라이버를 컴파일하여 .ko 파일을 생성합니다.

$ make

Makefile코드는 이렇습니다.

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

obj-m := gpio_module.o

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

결과: gpio_module.ko 파일이 생성됩니다.

2) 커널 모듈 적재 (insmod)

생성된 모듈을 커널에 적재합니다. 관리자 권한(sudo)이 필요합니다.
적재를 하지 않는다면 아무런 동작의 변화가 없음.

$ sudo insmod gpio_module.ko

3) 디바이스 파일 생성 (mknod)

드라이버는 로드되었지만, 유저가 접근할 "파일"이 필요합니다. 소스 코드에 정의된 Major 번호(200)를 사용하여 디바이스 노드를 만듭니다.

# 디바이스 노드 생성 (c: 문자 디바이스, 200: 주번호, 0: 부번호)
$ sudo mknod /dev/gpioled c 200 0

# 누구나 접근 가능하도록 권한 변경
$ sudo chmod 666 /dev/gpioled

4) 어플리케이션 컴파일 (코드는 포스팅 제일 하단에 위치)

테스트용 C 프로그램을 컴파일합니다.

$ gcc -o gpio gpio.c

5. 테스트 결과

이제 실제로 LED를 제어해 봅니다.

1. LED 켜기

$ ./gpio 1
GPIO SET : 1
Read Data : LED ON from Kerne

결과: GPIO 17번 핀에 연결된 LED가 켜집니다.

2. LED 끄기

$ ./gpio 0
GPIO SET : 0
Read Data : LED OFF from Kerne

결과: LED가 꺼집니다.

3. LED 깜빡이기 (예: 3회)

$ ./gpio 3
GPIO SET : 3
# (3초간 깜빡거림)
Read Data : LED BLINK from Kerne

4. 커널 로그 확인
printk로 출력한 로그는 dmesg 명령어로 확인할 수 있습니다.

$ dmesg | tail
[ 1234.5678] Hello LED module!
[ 1234.5678] 'mknod /dev/gpioled c 200 0'
[ 1234.5678] GPIO Device opened(200/0)
[ 1234.5678] GPIO Device (200) write : 1(1)

6. 마무리 및 정리 (Clean up)

실습이 끝나면 리소스를 해제해야 합니다.

# 커널 모듈 제거
$ sudo rmmod gpio_module.ko

이번 실습을 통해 유저 영역의 요청이 System Call을 통해 커널 영역으로 전달되고, 커널 모듈이 Mapping된 주소를 통해 하드웨어를 제어하는 전체 흐름을 이해할 수 있었습니다.

gpio_module.c 및 gpio.c 전체코드 공유

gpio_module.c

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/module.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/delay.h>

MODULE_LICENSE("GPL");

#define GPIO_BASE (0xFE200000) 
#define GPIO_SIZE (256) 

#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))

static volatile unsigned int *gpio;

/* 디바이스 파일의 주 번호와 부 번호 */
#define GPIO_MAJOR 200
#define GPIO_MINOR 0
#define GPIO_DEVICE "gpioled"
#define GPIO_LED 17
#define BLOCK_SIZE 100

static char msg[BLOCK_SIZE] = {0};

/* 입출력 함수를 위한 선언 */
static int gpio_open(struct inode*, struct file*);
static ssize_t gpio_read(struct file*, char *, size_t, loff_t*);
static ssize_t gpio_write(struct file*, const char *, size_t, loff_t*);
static int gpio_close(struct inode*, struct file*);

static struct file_operations gpio_fops = {
    .owner = THIS_MODULE,
    .read = gpio_read,
    .write = gpio_write,
    .open = gpio_open,
    .release = gpio_close,
};

static struct cdev gpio_cdev;

int init_module(void){
    dev_t devno;
    unsigned int count;
    static void *map;
    int err;

    printk(KERN_INFO "Hello LED module!\n");

    devno = MKDEV(GPIO_MAJOR, GPIO_MINOR);
    register_chrdev_region(devno, 1, GPIO_DEVICE);

    cdev_init(&gpio_cdev, &gpio_fops);
    gpio_cdev.owner = THIS_MODULE;
    count = 1;

    err = cdev_add(&gpio_cdev, devno, count);
    if(err < 0){
        printk("Error : Device Add\n");
        return -1;
    }

    printk("'mknod /dev/%s c %d 0'\n", GPIO_DEVICE, GPIO_MAJOR);
    printk("'chmod 666 /dev/%s'\n", GPIO_DEVICE);

    map = ioremap(GPIO_BASE, GPIO_SIZE);
    if(!map){
        printk("Error : mapping GPIO memory\n");
        iounmap(map);
        return -EBUSY;
    }
    gpio = (volatile unsigned int*)map;

    GPIO_IN(GPIO_LED);
    GPIO_OUT(GPIO_LED);

    return 0;
}

void cleanup_module(void){
    dev_t devno = MKDEV(GPIO_MAJOR, GPIO_MINOR);
    unregister_chrdev_region(devno, 1);

    cdev_del(&gpio_cdev);
    if(gpio){
        iounmap(gpio);
    }
    module_put(THIS_MODULE);
}

static int gpio_open(struct inode* inod, struct file* fil){
    printk("GPIO Device opened(%d/%d)\n", imajor(inod), iminor(inod));
    return 0;
}

static int gpio_close(struct inode* inod, struct file* fil){
    printk("GPIO Device closed(%d)\n", MAJOR(fil->f_path.dentry->d_inode->i_rdev));
    return 0;
}

static ssize_t gpio_read(struct file* inode, char *buff, size_t len, loff_t* off){
    int count;
    strcat(msg, " from Kerne");
    count = copy_to_user(buff, msg, strlen(msg) + 1);
    printk("GPIO Device(%d) read : %s(%d)\n", MAJOR(inode->f_path.dentry->d_inode->i_rdev), msg, count);
    return count;
}

static ssize_t gpio_write(struct file* inode, const char *buff, size_t len, loff_t* off){
    short count;
    memset(msg, 0, BLOCK_SIZE);
    count = copy_from_user(msg, buff, len);
    if (!strcmp(msg, "0")) {
        GPIO_CLR(GPIO_LED);
        snprintf(msg, BLOCK_SIZE, "LED OFF");
    }
    else if (!strcmp(msg, "1")) {
        GPIO_SET(GPIO_LED);
        snprintf(msg, BLOCK_SIZE, "LED ON");
    }
    else {
        int val;
        if(kstrtoint(msg, 10, &val) < 0) {
            snprintf(msg, BLOCK_SIZE, "Invalid Command");
            printk("GPIO Device (%d) write : %s(%d)\n", MAJOR(inode->f_path.dentry->d_inode->i_rdev), msg, len);
            return count;
        }
        for (int i = 0; i < val; i++) {
            GPIO_SET(GPIO_LED);
            ssleep(1);
            GPIO_CLR(GPIO_LED);
            ssleep(1);
        }
        snprintf(msg, BLOCK_SIZE, "LED BLINK");
    }

    printk("GPIO Device (%d) write : %s(%d)\n", MAJOR(inode->f_path.dentry->d_inode->i_rdev), msg, len);
    return count;
}

gpio.c

    #include<stdio.h>
    #include<fcntl.h>
    #include<unistd.h>
    #include<string.h>

    #define BUFFSIZ 100

    int main(int argc, char **argv){

        char buf[BUFFSIZ];
        char i = 0;
        int fd = -1;

        memset(buf, 0, BUFFSIZ);
        printf("GPIO SET : %s\n", argv[1]);

        fd = open("/dev/gpioled", O_RDWR);
        write(fd, argv[1], strlen(argv[1]));
        read(fd, buf, BUFFSIZ);
        printf("Read Data : %s\n", buf);

        close(fd);
        return 0;
    }
반응형