Linux 디바이스 트리 기반 LED 문자 드라이버 개발

이 문서는 Linux 커널 5.2.8 버전과 BusyBox 1.25.0 환경에서 디바이스 트리를 사용하여 LED 문자 장치 드라이버를 개발하는 방법을 다룹니다. 이전의 직접 메모리 매핑 방식과 달리, 디바이스 트리를 통해 GPIO 관련 레지스터의 물리 주소를 전달받고, 이를 이용하여 GPIO를 초기화하고 제어합니다.

1. 디바이스 트리 수정

LED 장치 노드를 디바이스 트리 소스(DTS) 파일에 추가하여 하드웨어 정보를 기술합니다. Mini2440 보드의 경우 LED1~LED4는 GPB5~GPB8 핀에 연결되어 있습니다. LED1을 켜기 위한 GPBCON 레지스터(0x56000010)는 출력 모드로 설정하고, GPBDAT 레지스터(0x56000014)의 해당 비트를 조작하여 LED를 제어합니다.

s3c2440-smdk2440.dts 파일에 다음과 같은 LED 노드를 추가합니다:

led {
    compatible = "mini2440-led"; /* 드라이버와 호환성을 나타내는 문자열 */
    status = "okay";             /* 장치 활성화 상태 */
    reg = <0x56000010 0x04 /* GPBCON */
          0x56000014 0x04>; /* GPBDATA */
};
  • compatible: 특정 드라이버와 장치를 연결하는 데 사용됩니다.
  • status = "okay": 장치가 활성화되었음을 나타냅니다.
  • reg: 레지스터의 시작 주소와 크기 정보를 정의합니다. 여기서는 GPBCON과 GPBDATA 레지스터의 주소와 4바이트 크기를 지정합니다.

2. LED 드라이버 개발

드라이버 코드는 /work/sambashare/drivers/23.led_dev_dts 디렉토리에 작성합니다. led_dev.c 파일에는 LED 장치를 위한 구조체 정의, open, write 등의 파일 연산 함수, 그리고 드라이버 모듈 초기화 및 종료 함수가 포함됩니다.

2.1 LED 파일 연산 함수

  • led_open: LED 제어에 사용될 GPIO 핀(GPB5~GPB8)을 출력 모드로 설정합니다.
  • led_switch: LED를 켜거나 끄는 기능을 수행합니다.
  • led_write: 사용자 공간으로부터 데이터를 받아 LED를 제어합니다. copy_from_user를 사용하여 사용자 버퍼의 데이터를 커널 공간으로 복사하고, 값을 기반으로 led_switch 함수를 호출합니다.

2.2 LED 드라이버 등록

led_init 함수는 다음과 같은 주요 단계를 수행합니다:

  1. alloc_chrdev_region: 동적으로 문자 장치 번호를 할당받습니다.
  2. cdev_initcdev_add: 문자 장치를 초기화하고 커널에 등록합니다.
  3. class_create: /sys/class 아래에 장치 클래스를 생성합니다.
  4. device_create: /dev 아래에 장치 노드를 생성합니다.
  5. of_find_node_by_path: 디바이스 트리에서 "/led" 노드를 찾습니다.
  6. of_iomap: 디바이스 트리에서 찾은 노드의 레지스터 주소를 커널 가상 주소 공간으로 매핑합니다. GPBCON과 GPBDATA 레지스터에 대한 가상 주소를 얻습니다.

2.3 LED 드라이버 제거

led_exit 함수는 드라이버가 언로드될 때 호출되며, iounmap을 사용하여 of_iomap으로 매핑된 가상 주소를 해제하고, 생성된 장치 노드, 클래스, 문자 장치 등록 등을 해제합니다.

2.4 led_dev.c 전체 코드

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/errno.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <asm/io.h>

#define DTSLED_CNT      1
#define DTSLED_NAME     "dtsled"

struct led_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
    void __iomem *gpbcon; /* GPBCON 레지스터 가상 주소 */
    void __iomem *gpbdat; /* GPBDATA 레지스터 가상 주소 */
};

struct led_dev dts_led;

static int led_open(struct inode *inode, struct file *file) {
    void __iomem *gpbcon_reg = dts_led.gpbcon;
    unsigned int val;

    /* GPB5~GPB8를 출력 모드로 설정 */
    val = readl(gpbcon_reg);
    val &= ~((0x03 << 10) | (0x03 << 12) | (0x03 << 14) | (0x03 << 16)); /* Clear bits */
    val |= ((0x01 << 10) | (0x01 << 12) | (0x01 << 14) | (0x01 << 16)); /* Set output mode */
    writel(val, gpbcon_reg);
    return 0;
}

static void led_switch(int on_off) {
    void __iomem *gpbdat_reg = dts_led.gpbdat;
    unsigned int val = readl(gpbdat_reg);

    if (on_off == 1) { /* LED 켜기 */
        val &= ~((0x01 << 5) | (0x01 << 6) | (0x01 << 7) | (0x01 << 8)); /* Turn LEDs ON */
    } else { /* LED 끄기 */
        val |= ((0x01 << 5) | (0x01 << 6) | (0x01 << 7) | (0x01 << 8)); /* Turn LEDs OFF */
    }
    writel(val, gpbdat_reg);
}

static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    int led_state;
    if (copy_from_user(&led_state, buf, sizeof(int))) {
        return -EFAULT;
    }

    led_switch(led_state);
    return count;
}

static struct file_operations led_fops = {
    .owner  = THIS_MODULE,
    .open   = led_open,
    .write  = led_write,
};

static int __init led_init(void) {
    int ret;

    ret = alloc_chrdev_region(&dts_led.devid, 0, DTSLED_CNT, DTSLED_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate device region\n");
        return ret;
    }

    cdev_init(&dts_led.cdev, &led_fops);
    ret = cdev_add(&dts_led.cdev, dts_led.devid, DTSLED_CNT);
    if (ret < 0) {
        pr_err("Failed to add character device\n");
        goto fail_cdev_add;
    }

    dts_led.class = class_create(THIS_MODULE, DTSLED_NAME);
    if (IS_ERR(dts_led.class)) {
        pr_err("Failed to create device class\n");
        ret = PTR_ERR(dts_led.class);
        goto fail_class_create;
    }

    dts_led.device = device_create(dts_led.class, NULL, dts_led.devid, NULL, DTSLED_NAME);
    if (IS_ERR(dts_led.device)) {
        pr_err("Failed to create device node\n");
        ret = PTR_ERR(dts_led.device);
        goto fail_device_create;
    }

    dts_led.nd = of_find_node_by_path("/led");
    if (!dts_led.nd) {
        pr_err("LED device node not found\n");
        ret = -EINVAL;
        goto fail_find_node;
    }

    /* 0번 리소스(GPBCON) 및 1번 리소스(GPBDATA) 매핑 */
    dts_led.gpbcon = of_iomap(dts_led.nd, 0);
    dts_led.gpbdat = of_iomap(dts_led.nd, 1);
    if (!dts_led.gpbcon || !dts_led.gpbdat) {
        pr_err("Failed to iomap registers\n");
        ret = -ENOMEM;
        goto fail_iomap;
    }

    pr_info("LED driver initialized\n");
    return 0;

fail_iomap:
    if (dts_led.gpbcon) iounmap(dts_led.gpbcon);
    if (dts_led.gpbdat) iounmap(dts_led.gpbdat);
fail_find_node:
    device_destroy(dts_led.class, dts_led.devid);
fail_device_create:
    class_destroy(dts_led.class);
fail_class_create:
    cdev_del(&dts_led.cdev);
fail_cdev_add:
    unregister_chrdev_region(dts_led.devid, DTSLED_CNT);
    return ret;
}

static void __exit led_exit(void) {
    if (dts_led.gpbcon) iounmap(dts_led.gpbcon);
    if (dts_led.gpbdat) iounmap(dts_led.gpbdat);

    device_destroy(dts_led.class, dts_led.devid);
    class_destroy(dts_led.class);
    cdev_del(&dts_led.cdev);
    unregister_chrdev_region(dts_led.devid, DTSLED_CNT);
    pr_info("LED driver exited\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Device Tree LED Driver");

2.5 Makefile

커널 모듈 빌드를 위한 Makefile입니다.

KERN_DIR := /work/sambashare/linux-5.2.8-dt
all:
    make -C $(KERN_DIR) M=$(PWD) modules
clean:
    make -C $(KERN_DIR) M=$(PWD) modules clean
    rm -rf modules.order

obj-m += led_dev.o

3. LED 드라이버 테스트 애플리케이션

test 디렉토리에 main.c 파일을 생성하여 드라이버를 테스트합니다.

3.1 main.c

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

void print_usage(char *file) {
    fprintf(stderr, "Usage: %s <device_path> <on|off>\n", file);
    fprintf(stderr, "Example: %s /dev/dtsled on\n", file);
}

int main(int argc, char *argv[]) {
    int fd;
    int state;
    char *device_path;

    if (argc != 3) {
        print_usage(argv[0]);
        return 1;
    }

    device_path = argv[1];

    if (strcmp(argv[2], "on") == 0) {
        state = 1; /* LED 켜기 */
    } else if (strcmp(argv[2], "off") == 0) {
        state = 0; /* LED 끄기 */
    } else {
        print_usage(argv[0]);
        return 1;
    }

    fd = open(device_path, O_WRONLY);
    if (fd < 0) {
        perror("Failed to open device");
        return 1;
    }

    if (write(fd, &state, sizeof(int)) < 0) {
        perror("Failed to write to device");
        close(fd);
        return 1;
    }

    printf("Set LED to %s\n", argv[2]);
    close(fd);
    return 0;
}

3.2 Makefile

테스트 애플리케이션 빌드를 위한 Makefile입니다.

all:
    arm-linux-gnueabihf-gcc -o led_test main.c
clean:
    rm -f led_test

4. 보드에서 테스트

  1. 디바이스 트리 컴파일: make dtbs 명령으로 DTS 파일을 DTB 파일로 컴파일합니다.
  2. 커널 및 DTB 다운로드: U-Boot를 통해 컴파일된 DTB 파일과 커널 이미지를 보드의 메모리로 로드합니다.
  3. 커널 부팅: DTB 파일을 포함하여 커널을 부팅합니다.
  4. 드라이버 설치: NFS 또는 다른 방법으로 컴파일된 led_dev.ko 파일을 보드로 옮기고 insmod led_dev.ko 명령으로 설치합니다.
  5. 장치 노드 확인: ls /dev/dtsled 명령으로 생성된 장치 노드를 확인합니다.
  6. 테스트 애플리케이션 실행: 컴파일된 led_test 애플리케이션을 보드로 옮겨 ./led_test /dev/dtsled on 또는 ./led_test /dev/dtsled off 명령으로 LED를 제어합니다.
  7. 드라이버 제거: rmmod led_dev 명령으로 드라이버를 제거합니다.

5. 코드 다운로드

소스 코드는 GitHub 저장소에서 다운로드할 수 있습니다. (실제 링크는 제공되지 않음)

태그: linux 디바이스 트리 임베디드 LED 문자 장치

8월 22일 11:43에 게시됨