안녕하세요.
전편에서까지는 I2C 드라이버 설계를 진행해보았습니다.
하지만, probe() 함수에서 ADC 값을 하나 받아오는 것에 그치고, 이후 추가적인 사용이 불가능해서 불편한 부분이 있었습니다.
오늘은 이 I2C 드라이버를 캐릭터 디바이스 드라이버에 결합시켜서, 장치파일로 사용자가 값을 read/write 할 수 있는 단계까지 발전시켜보도록 하겠습니다.
바로 코드를 보면서 실습을 진행해보겠습니다.
#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 드라이버와 캐릭터 디바이스 드라이버를 합한 것 처럼 보인다. 실제로 특별하게 추가된 개념은 없었고, 그 둘 간의 이음새 부분만 살짝 추가가 되었다. 요소들을 하나씩 확인해보도록 하자.
추가된 전역변수들이 몇 있었다. 이 코드에서 전역변수들은 크게 3개 묶음(하드웨어 상태 / 동기화·설정 / char device 인프라)으로 나뉘어 역할을 한다.
static struct i2c_client *g_client;
역할: “현재 내 드라이버가 붙어있는 PCF8591 디바이스 핸들”
remove 이후 userspace가 /dev/pcf8591 접근하면 -ENODEV로 깔끔하게 처리 가능.
static DEFINE_MUTEX(g_lock);
역할: g_client와 g_channel 같은 공유 상태를 보호하는 락
static u8 g_channel = 0;
역할: 현재 ADC 읽기에 사용할 입력 채널(0~3)
pcf_chr_write() 가 숫자를 받아 g_channel 변경pcf_chr_read() 에서 pcf8591_read_adc(g_client, g_channel)로 사용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로 처리한다”를 등록하는 커널 객체
cdev_add(&g_cdev, g_devt, 1))static struct class *g_class;
Linux의 class는 디바이스를 사용자 공간에 노출하기 위한 분류 단위로,
sysfs와 udev를 통해 /dev 노드를 자동 생성하고 장치의 성격을 전달하는 역할을 한다.
역할: sysfs에 /sys/class/pcf8591/ 같은 클래스 디렉토리를 만들고, udev가 /dev/pcf8591 노드를 자동 생성하도록 돕는 “분류(그룹)”
static struct device *g_devnode;
역할: device_create()가 만든 “device 객체(디바이스 노드에 대응)” 핸들
pcf_mod_init()
→ chardev 인프라(dev_t/cdev/class) 준비 + I2C 드라이버 등록
I2C core가 디바이스와 매칭되면 pcf8591_probe() 호출
→ g_client 저장 + /dev/pcf8591 생성
유저가 cat /dev/pcf8591 하면 pcf_chr_read()
→ I2C로 ADC 읽어서 문자열로 반환
유저가 echo 2 > /dev/pcf8591 하면 pcf_chr_write()
→ 채널 설정 변경
rmmod 시 pcf_mod_exit()
→ I2C 드라이버 제거 + /dev 제거 + chardev 해제
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;
(2) 유저로 반환할 데이터는 문자열
n = scnprintf(kbuf, sizeof(kbuf), "%d\n", adc);
copy_to_user(...)
*off += n;
return n;
설계 의미
• 드라이버가 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에 등록되는 “드라이버 설명서”
9) pcf_mod_init(void) (module_init)
역할: 모듈 로드시 초기화. 크게 2파트.
Part A: char device 기반 마련
Part B: I2C 드라이버 등록
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 기능이 존재한다. 라즈베리파이에서 전압값을 설정하여 아날로그로 출력할 수 있다. 이 기능을 다음시간에 추가하고 디바이스트리를 통해 디바이스를 라즈베리파이가 부팅과 함께 체크하도록 해보자.