[Device Driver] 4편. I2C + Char dev.

나우히즈·2025년 12월 21일

안녕하세요.

전편에서까지는 I2C 드라이버 설계를 진행해보았습니다.
하지만, probe() 함수에서 ADC 값을 하나 받아오는 것에 그치고, 이후 추가적인 사용이 불가능해서 불편한 부분이 있었습니다.

오늘은 이 I2C 드라이버를 캐릭터 디바이스 드라이버에 결합시켜서, 장치파일로 사용자가 값을 read/write 할 수 있는 단계까지 발전시켜보도록 하겠습니다.

바로 코드를 보면서 실습을 진행해보겠습니다.


1. 소스코드

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/i2c.h>

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>

#define DRV_NAME  "pcf8591"   /* 디바이스 이름과 맞춰 자동 매칭 */
#define DEV_NAME  "pcf8591"   /* /dev/pcf8591 */
#define BUF_SIZE  32

static struct i2c_client *g_client;
static DEFINE_MUTEX(g_lock);
static u8 g_channel = 0;

/* chardev */
static dev_t g_devt;
static struct cdev g_cdev;
static struct class *g_class;
static struct device *g_devnode;

static int pcf8591_read_adc(struct i2c_client *client, u8 channel)
{
    int ret, dummy, adc;
    u8 ctrl = 0x40 | (channel & 0x03); /* single-ended + AIN(channel) */

    ret = i2c_smbus_write_byte(client, ctrl);
    if (ret < 0)
        return ret;

    dummy = i2c_smbus_read_byte(client); /* dummy read */
    if (dummy < 0)
        return dummy;

    adc = i2c_smbus_read_byte(client);   /* real read */
    if (adc < 0)
        return adc;

    return adc & 0xFF;
}

static ssize_t pcf_chr_read(struct file *file, char __user *buf,
                            size_t len, loff_t *off)
{
    char kbuf[BUF_SIZE];
    int adc, n;

    /* cat이 무한루프 안 돌게 EOF 처리 */
    if (*off > 0)
        return 0;

    mutex_lock(&g_lock);
    if (!g_client) {
        mutex_unlock(&g_lock);
        return -ENODEV;
    }
    adc = pcf8591_read_adc(g_client, g_channel);
    mutex_unlock(&g_lock);

    if (adc < 0)
        return adc;

    n = scnprintf(kbuf, sizeof(kbuf), "%d\n", adc);
    if (len < n)
        return -EINVAL;

    if (copy_to_user(buf, kbuf, n))
        return -EFAULT;

    *off += n;
    return n;
}

static ssize_t pcf_chr_write(struct file *file, const char __user *buf,
                             size_t len, loff_t *off)
{
    char kbuf[BUF_SIZE];
    long ch;

    if (len == 0)
        return 0;
    if (len >= sizeof(kbuf))
        return -EINVAL;

    if (copy_from_user(kbuf, buf, len))
        return -EFAULT;
    kbuf[len] = '\0';

    if (kstrtol(kbuf, 10, &ch) < 0)
        return -EINVAL;
    if (ch < 0 || ch > 3)
        return -EINVAL;

    mutex_lock(&g_lock);
    g_channel = (u8)ch;
    mutex_unlock(&g_lock);

    return len;
}

static const struct file_operations pcf_fops = {
    .owner = THIS_MODULE,
    .read  = pcf_chr_read,
    .write = pcf_chr_write,
};

/* I2C driver */
static int pcf8591_probe(struct i2c_client *client)
{
    mutex_lock(&g_lock);
    g_client = client;
    mutex_unlock(&g_lock);

    if (!g_devnode) {
        g_devnode = device_create(g_class, &client->dev, g_devt, NULL, DEV_NAME);
        if (IS_ERR(g_devnode)) {
            g_devnode = NULL;
            dev_err(&client->dev, "device_create failed\n");
            return -ENOMEM;
        }
    }

    dev_info(&client->dev, "bound. /dev/%s created (channel=%u)\n",
             DEV_NAME, g_channel);
    return 0;
}

static void pcf8591_remove(struct i2c_client *client)
{
    mutex_lock(&g_lock);
    g_client = NULL;
    mutex_unlock(&g_lock);

    if (g_devnode) {
        device_destroy(g_class, g_devt);
        g_devnode = NULL;
    }

    dev_info(&client->dev, "removed. /dev/%s destroyed\n", DEV_NAME);
}

static const struct i2c_device_id pcf8591_id[] = {
    { "pcf8591", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, pcf8591_id);

static struct i2c_driver pcf8591_driver = {
    .driver = {
        .name = DRV_NAME,
    },
    .probe    = pcf8591_probe,
    .remove   = pcf8591_remove,
    .id_table = pcf8591_id,
};

static int __init pcf_mod_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&g_devt, 0, 1, DEV_NAME);
    if (ret < 0)
        return ret;

    cdev_init(&g_cdev, &pcf_fops);
    ret = cdev_add(&g_cdev, g_devt, 1);
    if (ret < 0)
        goto err_cdev;

    g_class = class_create(DEV_NAME);
    if (IS_ERR(g_class)) {
        ret = PTR_ERR(g_class);
        g_class = NULL;
        goto err_class;
    }

    ret = i2c_add_driver(&pcf8591_driver);
    if (ret < 0)
        goto err_i2c;

    pr_info(DEV_NAME ": loaded (major=%d)\n", MAJOR(g_devt));
    return 0;

err_i2c:
    class_destroy(g_class);
    g_class = NULL;
err_class:
    cdev_del(&g_cdev);
err_cdev:
    unregister_chrdev_region(g_devt, 1);
    return ret;
}

static void __exit pcf_mod_exit(void)
{
    i2c_del_driver(&pcf8591_driver);

    if (g_devnode) {
        device_destroy(g_class, g_devt);
        g_devnode = NULL;
    }
    if (g_class) {
        class_destroy(g_class);
        g_class = NULL;
    }

    cdev_del(&g_cdev);
    unregister_chrdev_region(g_devt, 1);

    pr_info(DEV_NAME ": unloaded\n");
}

module_init(pcf_mod_init);
module_exit(pcf_mod_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("sihwan");
MODULE_DESCRIPTION("PCF8591 I2C + char device driver");

코드 상으로 보면 기존 I2C 드라이버와 캐릭터 디바이스 드라이버를 합한 것 처럼 보인다. 실제로 특별하게 추가된 개념은 없었고, 그 둘 간의 이음새 부분만 살짝 추가가 되었다. 요소들을 하나씩 확인해보도록 하자.

1) 전역변수 세팅

추가된 전역변수들이 몇 있었다. 이 코드에서 전역변수들은 크게 3개 묶음(하드웨어 상태 / 동기화·설정 / char device 인프라)으로 나뉘어 역할을 한다.

(1) I2C 디바이스(PCF8591) 상태 / 핸들

static struct i2c_client *g_client;

역할: “현재 내 드라이버가 붙어있는 PCF8591 디바이스 핸들”

왜 전역변수로 쓰는가?

  • read()/write()는 file_operations 콜백이라 probe()의 지역변수 client에 접근 못 함
    → 전역에 저장해두고 사용.

언제 설정/해제?

  • probe()에서 g_client = client;
  • remove()에서 g_client = NULL;

remove 이후 userspace가 /dev/pcf8591 접근하면 -ENODEV로 깔끔하게 처리 가능.

(2) 동기화 & 현재 설정(채널)

static DEFINE_MUTEX(g_lock);

역할: g_client와 g_channel 같은 공유 상태를 보호하는 락

왜 필요?

  • userspace가 동시에 cat과 echo를 치거나, 드라이버가 remove 되는 타이밍과 read 타이밍이 겹칠 수 있어서 보호하기 위함.
    • g_client (NULL로 바뀌는 순간을 안전하게)
    • g_channel (채널 변경 중 read가 들어오는 경쟁 방지)
static u8 g_channel = 0;

역할: 현재 ADC 읽기에 사용할 입력 채널(0~3)

  • pcf_chr_write() 가 숫자를 받아 g_channel 변경
  • pcf_chr_read() 에서 pcf8591_read_adc(g_client, g_channel)로 사용

3) Char device 인프라(장치번호/등록/노드 생성)

static dev_t g_devt;

역할: 이 char device의 장치번호(major/minor가 인코딩된 dev_t)

  • alloc_chrdev_region()이 장치번호 할당하여 g_devt 초기화
  • device_create(..., g_devt, ...) : /dev 노드 생성 시 major/minor 제공해야 하므로 이용
  • unregister_chrdev_region(g_devt, 1) : 해제 시 변수 이용
static struct cdev g_cdev;

역할: VFS에 “이 dev_t는 이 file_operations로 처리한다”를 등록하는 커널 객체

  • file_operations(pcf_fops) ↔ dev_t(g_devt)를 묶어서 커널에 올려줌(cdev_add(&g_cdev, g_devt, 1))
static struct class *g_class;

Linux의 class는 디바이스를 사용자 공간에 노출하기 위한 분류 단위로,
sysfs와 udev를 통해 /dev 노드를 자동 생성하고 장치의 성격을 전달하는 역할을 한다.

역할: sysfs에 /sys/class/pcf8591/ 같은 클래스 디렉토리를 만들고, udev가 /dev/pcf8591 노드를 자동 생성하도록 돕는 “분류(그룹)”

  • class_create() / class_destroy() 을 통해 클래스 관리
static struct device *g_devnode;

역할: device_create()가 만든 “device 객체(디바이스 노드에 대응)” 핸들

  • sysfs에 /sys/class/pcf8591/pcf8591 같은 엔트리 생성 후, udev가 /dev/pcf8591를 만들 수 있게 이벤트 발생
  • remove/exit에서 device_destroy()하려면 핸들(struct device)이 필요
  • 중복 생성 방지 (if (!g_devnode))

2. 함수 진행

  1. pcf_mod_init()
    → chardev 인프라(dev_t/cdev/class) 준비 + I2C 드라이버 등록

  2. I2C core가 디바이스와 매칭되면 pcf8591_probe() 호출
    → g_client 저장 + /dev/pcf8591 생성

  3. 유저가 cat /dev/pcf8591 하면 pcf_chr_read()
    → I2C로 ADC 읽어서 문자열로 반환

  4. 유저가 echo 2 > /dev/pcf8591 하면 pcf_chr_write()
    → 채널 설정 변경

  5. rmmod 시 pcf_mod_exit()
    → I2C 드라이버 제거 + /dev 제거 + chardev 해제


3. 함수 세부사항

1) pcf8591_read_adc(struct i2c_client *client, u8 channel)

역할: PCF8591에서 지정 채널(0~3)의 ADC 값을 1회 읽어오는 “순수 기능 함수”

  • i2c_smbus_write_byte(client, ctrl)
    → PCF8591에 “이 채널 읽을게”라고 control byte 전송

  • dummy = i2c_smbus_read_byte(client)
    → PCF8591 특성: 첫 read는 이전 변환값(쓰레기)일 수 있어 버림

  • adc = i2c_smbus_read_byte(client)
    → 실제 변환 결과 8-bit(0~255)를 읽음

2) pcf_chr_read(struct file *file, char __user *buf, size_t len, loff_t *off)

역할: /dev/pcf8591에 대한 read 시스템콜 처리
사용자에게 텍스트 형태로 ADC 값을 돌려줌.

(1) EOF 처리(중요)

if (*off > 0) return 0;
  • cat은 read를 여러 번 호출함(EOF를 만날때까지)
  • 여기서 0(EOF)을 반환해야 cat이 종료됨.
  • 이 줄이 없으면 cat /dev/pcf8591이 무한히 block/loop 패턴이 될 수 있음.

(2) 유저로 반환할 데이터는 문자열

n = scnprintf(kbuf, sizeof(kbuf), "%d\n", adc);
copy_to_user(...)
*off += n;
return n;
  • 이 드라이버는 “바이너리 데이터”가 아니라 “사용자 보기 좋은 CLI 출력”을 의도함.
  • *off 증가로 EOF 흐름과 맞물림.

설계 의미
• 드라이버가 ADC 값을 “기능적으로” 제공하되,
• 유저가 cat으로 바로 확인 가능한 형태(개발/학습 친화)를 만든 것.

3) pcf_chr_write(struct file *file, const char __user *buf, size_t len, loff_t *off)

역할: /dev/pcf8591에 대한 write 시스템콜 처리
현재 ADC 읽기 채널(g_channel)을 바꿈.

흐름
(1) 유저 버퍼 → 커널 버퍼 복사

copy_from_user(kbuf, buf, len);
kbuf[len] = '\0';

(2) 문자열을 숫자로 파싱

kstrtol(kbuf, 10, &ch)

(3) 채널 범위 검증 + 설정 적용

if (ch < 0 || ch > 3) return -EINVAL;
g_channel = (u8)ch;

4) pcf_fops (struct file_operations)

역할: VFS가 /dev/pcf8591에 대해 호출할 콜백 테이블
• .read = pcf_chr_read
• .write = pcf_chr_write
• .owner = THIS_MODULE : 모듈이 사용 중일 때 언로드 방지(참조카운트)

즉, 장치파일 ↔ 드라이버 함수 연결표.

5) pcf8591_probe(struct i2c_client *client)

역할: I2C core가 디바이스(PCF8591)를 드라이버에 붙일 때 호출되는 “바인딩 지점”

(1) g_client = client 저장
→ 이후 read/write에서 I2C 통신 가능해짐
(2) /dev/pcf8591 생성

g_devnode = device_create(g_class, &client->dev, g_devt, NULL, DEV_NAME);

실제로 디바이스 - 드라이버 간 연결이 매칭되었을 때 장치파일을 생성하도록 probe() 에 위치시켰음

6) pcf8591_remove(struct i2c_client *client)

역할: 디바이스가 드라이버에서 떨어질 때 호출되는 “언바인딩 지점”

(1) g_client = NULL
→ 이후 read/write는 -ENODEV
(2) /dev/pcf8591 제거
→device_destroy(g_class, g_devt);

7) pcf8591_id[] + MODULE_DEVICE_TABLE(i2c, ...)

역할: “이 드라이버는 이름이 pcf8591인 I2C 디바이스와 매칭 가능”이라는 매칭 테이블
→ new_device로 만든 디바이스 이름이 "pcf8591"이면 매칭됨

8) pcf8591_driver (struct i2c_driver)

역할: I2C core에 등록되는 “드라이버 설명서”

  • .driver.name : 드라이버 이름(매칭/표시용)
  • .probe.remove : 붙고 떨어질 때 호출할 함수
  • .id_table : 이름 기반 매칭 테이블

9) pcf_mod_init(void) (module_init)

역할: 모듈 로드시 초기화. 크게 2파트.

Part A: char device 기반 마련

  • alloc_chrdev_region(&g_devt, ...) : dev_t 확보
  • cdev_init, cdev_add : VFS에 등록
  • class_create : sysfs/udev 연동 기반

Part B: I2C 드라이버 등록

  • i2c_add_driver(&pcf8591_driver)
    → 등록되자마자 I2C core가 버스 스캔하여, 이미 존재하던 디바이스와 매칭되면 probe() 호출됨

10) pcf_mod_exit(void) (module_exit)

역할: 모듈 언로드 시 정리. init의 역순.
(1) i2c_del_driver() : remove 호출 유발 + 매칭 해제
(2) device_destroy(있으면) : /dev 제거
(3) class_destroy : sysfs에 분류 제거
(4) cdev_del : VFS에 등록된 device - File_operations 연결 해제
(5) unregister_chrdev_region : 장치번호 반납


정리

이렇게 작성한 모듈을 cat, echo를 통해 디바이스 드라이버의 read/write를 호출할 수 있다.

cat을 통해 측정된 ADC 값을 읽어오고, echo를 통해 읽고자 하는 채널값을 수정할 수 있었다.

하지만 PCF8591 디바이스에는 DAC 기능이 존재한다. 라즈베리파이에서 전압값을 설정하여 아날로그로 출력할 수 있다. 이 기능을 다음시간에 추가하고 디바이스트리를 통해 디바이스를 라즈베리파이가 부팅과 함께 체크하도록 해보자.

0개의 댓글