승코딩당당당

[리눅스] 라즈베리파이 커널 모듈로 LED 제어하기 본문

개발/임베디드

[리눅스] 라즈베리파이 커널 모듈로 LED 제어하기

승코딩당당당 2026. 2. 13. 12:14

 

이번 실습에서는 라즈베리파이의 GPIO 핀에 연결된 LED리눅스 커널 모듈을 통해 직접 제어해보았다. 단순히 사용자 프로그램에서 GPIO 라이브러리를 사용하는 것이 아니라, 커널 공간에서 동작하는 문자 디바이스 드라이버를 직접 작성하고 /dev/gpioled라는 디바이스 파일을 생성하여 사용자 공간과 커널 공간을 연결하는 과정을 경험한다.

 

./gpio 1 와 같은 명령이 실행되면 사용자 공간에서 write 시스템 콜이 발생하고, 이는 커널 내부의 gpio_write() 함수로 전달된다. 해당 함수에서는 메모리 매핑을 통해 GPIO 레지스터에 직접 접근하여 LED를 켜거나 끄는 동작을 수행한다. 이 과정에서 리눅스의 문자 디바이스 등록 과정(register_chrdev_region, cdev_add)커널 모듈의 로딩 및 제거(insmod, rmmod) 흐름을 함께 이해할 수 있다.

 

또한 write 함수의 반환값 처리, 사용자 버퍼 복사(copy_from_user), 그리고 Makefile에서의 탭 규칙과 같은 세부적인 요소들이 드라이버 동작에 얼마나 중요한지 확인할 수 있다.

 

실습은 VSCode로 진행한다.

 


 

1. Makefile 생성

커널 모듈은 일반 프로그램처럼 gcc로 바로 컴파일할 수 없기 때문에, 현재 커널 빌드 시스템과 연동하기 위한 Makefile이 필요하다. Makefile에 모듈로 빌드할 파일을 지정하고 make를 실행하면 .ko 형태의 커널 모듈이 생성된다.

 

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

 


 

2. gpio_module.c 파일 생성

다음으로 커널에서 동작할 드라이버 코드를 작성하기 위해 gpio_module.c 파일을 생성한다. 이 파일은 커널 공간에서 실행되는 모듈 코드이다.

 

gpio_module.c에서는 문자 디바이스를 등록하고, 사용자 공간에서 /dev/gpioled로 접근했을 때 호출될 open, read, write, release 함수들을 구현한다. 특히 write() 함수에서는 사용자로부터 전달된 값을 받아 GPIO 레지스터에 직접 접근하여 LED를 켜거나 끄는 동작을 수행한다.

 

또한 ioremap()을 통해 GPIO의 물리 주소를 가상 주소로 매핑하고, 메모리 매핑 방식(Memory Mapped I/O)으로 레지스터를 제어하도록 구성하였다. 이를 통해 리눅스 커널이 하드웨어를 직접 제어하는 구조를 이해할 수 있다.

 

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/module.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/string.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))

volatile unsigned int *gpio;

#define GPIO_MAJOR 200
#define GPIO_MINOR 0
#define GPIO_DEVICE "gpioled"
#define GPIO_LED 17

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,
};

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 *inode, struct file *fil)
{
    printk("GPIO Device Opened(%d/%d)\n", imajor(inode), iminor(inode));
    return 0;
}

static int gpio_close(struct inode *inode, struct file *fil)
{
    printk("GPIO Device Closed(%d/%d)\n",
       MAJOR(fil->f_path.dentry->d_inode->i_rdev),
       MINOR(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 kernel");
    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);
    (!strcmp(msg, "0")) ? GPIO_CLR(GPIO_LED) : GPIO_SET(GPIO_LED);    printk("GPIO Device(%d) write : %s(%d)\n",
              MAJOR(inode->f_path.dentry->d_inode->i_rdev), msg, len);
    return count;
}

 


 

3. gpio.c 파일 생성

다음으로 사용자 공간에서 드라이버를 테스트하기 위한 gpio.c 파일을 작성한다. 이 프로그램은 /dev/gpioled 디바이스 파일을 열어 사용자가 입력한 값을 커널로 전달하고, 그 결과를 다시 읽어오는 역할을 한다.

 

프로그램이 실행되면 open()을 통해 /dev/gpioled를 열고, write() 시스템 콜을 이용해 사용자가 입력한 값을 드라이버의 gpio_write() 함수로 전달한다. 이후 read()를 호출하면 커널 내부의 gpio_read()가 실행되어 현재 상태 값을 사용자 공간으로 반환하게 된다.

 

즉, 이 프로그램은 커널 드라이버가 정상적으로 동작하는지 확인하기 위한 간단한 인터페이스 역할을 하며, 사용자 공간과 커널 공간이 어떻게 연결되는지를 직접 체험할 수 있는 예제이다.

 

#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;
}

 


 

4. 빌드하기

모듈을 insmod로 삽입한 뒤 lsmod를 통해 정상적으로 로드되었음을 확인하였다.

또한 /dev/gpioled가 character device로 생성되었으며, major 번호 200을 통해 커널 내부의 gpio_module과 연결되어 있음을 확인할 수 있었다. 이는 사용자 공간과 커널 드라이버가 성공적으로 연결된 상태를 의미한다.

 

make 실행 후 디렉토리에 여러 개의 파일이 생성되었는데, 이 중 .ko 파일이 실제 커널에 삽입되는 모듈 파일이다. .o, .mod.c, .mod.o 파일은 커널 빌드 시스템이 자동으로 생성한 중간 산출물이며, 개발자는 최종 결과물인 .ko 파일만 확인하면 된다.

(hello_module 파일들은 이전 실습 내용이므로 무시하셔도 됩니다.)

 

/dev/gpioled 준비 확인 후 gpio.c 파일을 컴파일 해준다.

 


 

5. 결과

아래와 같이 잘 동작하는 것을 확인할 수 있다.

./gpio 1을 입력하면 LED가 켜지고, ./gpio 0을 입력하면 LED가 꺼진다.

또한 LED는 gpio 17에 연결해줘야 한다.