안녕하세요.
오늘은 PCF8591 디바이스의 또 다른 기능인 DAC 를 드라이버에 추가하고 실제 출력을 확인해보는 시간을 가지도록 하겠습니다.
#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" /* i2c driver name (/sys/bus/i2c/drivers/pcf8591) */
#define CLASS_NAME "adc" /* /sys/class/adc/... (원하면 pcf8591로 바꿔도 됨) */
#define ADC_DEV_NAME "pcf8591" /* /dev/pcf8591 (minor 0) */
#define DAC_DEV_NAME "pcf8591_dac" /* /dev/pcf8591_dac (minor 1) */
#define NUM_MINORS 2
#define BUF_SIZE 32
/* I2C handle + shared state */
static struct i2c_client *g_client;
static DEFINE_MUTEX(g_lock);
static u8 g_channel = 0; /* ADC channel: 0~3 */
/* char dev */
static dev_t g_devt;
static struct cdev g_cdev;
static struct class *g_class;
static struct device *g_adc_node;
static struct device *g_dac_node;
/* -------------------------
* PCF8591 low-level helpers
* ------------------------- */
/* ADC read: select channel -> dummy read -> real read */
static int pcf8591_read_adc(struct i2c_client *client, u8 channel)
{
int ret, dummy, adc;
u8 ctrl = 0x40 | (channel & 0x03); /* 0x40: analog input enable, single-ended, channel select */
ret = i2c_smbus_write_byte(client, ctrl);
if (ret < 0)
return ret;
dummy = i2c_smbus_read_byte(client); /* dummy read (PCF8591 특징) */
if (dummy < 0)
return dummy;
adc = i2c_smbus_read_byte(client); /* real read */
if (adc < 0)
return adc;
return adc & 0xFF; /* 하위 8비트만 값으로 취급 */
}
/* DAC write: send [ctrl][value] */
static int pcf8591_write_dac(struct i2c_client *client, u8 value)
{
u8 data[2];
/*
* 보드/설정에 따라 ctrl 구성은 달라질 수 있지만,
* 학습/기본은 0x40 기반으로 시작해도 보통 동작함.
* (필요하면 "DAC enable" 비트를 더 명확히 반영)
*/
u8 ctrl = 0x40;
data[0] = ctrl;
data[1] = value;
return (i2c_master_send(client, data, 2) == 2) ? 0 : -EIO;
}
/* -------------------------
* char device file ops
* minor 0: ADC node
* minor 1: DAC node
* ------------------------- */
static ssize_t pcf_chr_read(struct file *file, char __user *buf,
size_t len, loff_t *off)
{
unsigned int minor = iminor(file_inode(file));
char kbuf[BUF_SIZE];
int adc, n;
/* DAC 노드는 read 의미 없음 */
if (minor != 0)
return -EINVAL;
/* cat 무한루프 방지: 첫 read만 데이터 반환, 그 다음은 EOF(0) */
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)
{
unsigned int minor = iminor(file_inode(file));
char kbuf[BUF_SIZE];
long val;
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, &val) < 0)
return -EINVAL;
mutex_lock(&g_lock);
if (!g_client) {
mutex_unlock(&g_lock);
return -ENODEV;
}
if (minor == 0) {
/* ADC node: write = 채널 변경(0~3) */
if (val < 0 || val > 3) {
mutex_unlock(&g_lock);
return -EINVAL;
}
g_channel = (u8)val;
mutex_unlock(&g_lock);
return len;
} else if (minor == 1) {
/* DAC node: write = 출력값 설정(0~255) */
int ret;
if (val < 0 || val > 255) {
mutex_unlock(&g_lock);
return -EINVAL;
}
ret = pcf8591_write_dac(g_client, (u8)val);
mutex_unlock(&g_lock);
return (ret < 0) ? ret : len;
}
mutex_unlock(&g_lock);
return -EINVAL;
}
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)
{
dev_t adc_dev = MKDEV(MAJOR(g_devt), 0);
dev_t dac_dev = MKDEV(MAJOR(g_devt), 1);
mutex_lock(&g_lock);
g_client = client;
mutex_unlock(&g_lock);
/* /dev nodes create (once per module) */
if (!g_adc_node) {
g_adc_node = device_create(g_class, &client->dev, adc_dev, NULL, ADC_DEV_NAME);
if (IS_ERR(g_adc_node)) {
g_adc_node = NULL;
dev_err(&client->dev, "device_create ADC failed\n");
return -ENOMEM;
}
}
if (!g_dac_node) {
g_dac_node = device_create(g_class, &client->dev, dac_dev, NULL, DAC_DEV_NAME);
if (IS_ERR(g_dac_node)) {
device_destroy(g_class, adc_dev);
g_adc_node = NULL;
g_dac_node = NULL;
dev_err(&client->dev, "device_create DAC failed\n");
return -ENOMEM;
}
}
dev_info(&client->dev,
"bound. /dev/%s (minor0), /dev/%s (minor1) created. channel=%u\n",
ADC_DEV_NAME, DAC_DEV_NAME, g_channel);
return 0;
}
static void pcf8591_remove(struct i2c_client *client)
{
dev_t adc_dev = MKDEV(MAJOR(g_devt), 0);
dev_t dac_dev = MKDEV(MAJOR(g_devt), 1);
mutex_lock(&g_lock);
g_client = NULL;
mutex_unlock(&g_lock);
if (g_dac_node) {
device_destroy(g_class, dac_dev);
g_dac_node = NULL;
}
if (g_adc_node) {
device_destroy(g_class, adc_dev);
g_adc_node = NULL;
}
dev_info(&client->dev, "removed. /dev nodes destroyed\n");
}
static const struct i2c_device_id pcf8591_id[] = {
{ "pcf8591", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, pcf8591_id);
/* (DT로 갈 때 추가할 예정) */
static const struct of_device_id pcf8591_of_match[] = {
{ .compatible = "nxp,pcf8591" },
{ }
};
MODULE_DEVICE_TABLE(of, pcf8591_of_match);
static struct i2c_driver pcf8591_driver = {
.driver = {
.name = DRV_NAME,
.of_match_table = pcf8591_of_match,
},
.probe = pcf8591_probe,
.remove = pcf8591_remove,
.id_table = pcf8591_id,
};
/* -------------------------
* module init/exit
* ------------------------- */
static int __init pcf_mod_init(void)
{
int ret;
/* Allocate 2 minors (ADC/DAC) */
ret = alloc_chrdev_region(&g_devt, 0, NUM_MINORS, ADC_DEV_NAME);
if (ret < 0)
return ret;
/* Register cdev (minor 0~1 share same fops; we branch by iminor()) */
cdev_init(&g_cdev, &pcf_fops);
ret = cdev_add(&g_cdev, g_devt, NUM_MINORS);
if (ret < 0)
goto err_cdev;
/* Create class (/sys/class/adc) */
g_class = class_create(CLASS_NAME);
if (IS_ERR(g_class)) {
ret = PTR_ERR(g_class);
g_class = NULL;
goto err_class;
}
/* Register I2C driver -> triggers match/probe */
ret = i2c_add_driver(&pcf8591_driver);
if (ret < 0)
goto err_i2c;
pr_info("%s: loaded (major=%d, minors=%d)\n",
ADC_DEV_NAME, MAJOR(g_devt), NUM_MINORS);
return 0;
err_i2c:
class_destroy(g_class);
g_class = NULL;
err_class:
cdev_del(&g_cdev);
err_cdev:
unregister_chrdev_region(g_devt, NUM_MINORS);
return ret;
}
static void __exit pcf_mod_exit(void)
{
dev_t adc_dev = MKDEV(MAJOR(g_devt), 0);
dev_t dac_dev = MKDEV(MAJOR(g_devt), 1);
i2c_del_driver(&pcf8591_driver);
/* In case remove didn't run (safety) */
if (g_dac_node) {
device_destroy(g_class, dac_dev);
g_dac_node = NULL;
}
if (g_adc_node) {
device_destroy(g_class, adc_dev);
g_adc_node = NULL;
}
if (g_class) {
class_destroy(g_class);
g_class = NULL;
}
cdev_del(&g_cdev);
unregister_chrdev_region(g_devt, NUM_MINORS);
pr_info("%s: unloaded\n", ADC_DEV_NAME);
}
module_init(pcf_mod_init);
module_exit(pcf_mod_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("sihwan");
MODULE_DESCRIPTION("PCF8591 I2C + ADC/DAC char device driver (2 nodes)");
코드에서 보이는 바와 같이, 본 드라이버에서는 pcf8591 디바이스 노드뿐만 아니라 pcf8591_dac이라는 별도의 디바이스 노드를 추가로 생성합니다. 이는 하나의 PCF8591 장치 내에 ADC와 DAC라는 서로 다른 기능이 존재하기 때문에, 각 기능을 독립적인 인터페이스로 분리하여 처리하기 위함입니다.
이에 따라 하나의 메이저 번호를 공유하되, 두 개의 마이너 번호를 할당하여 각 마이너 번호에 따라 서로 다른 기능을 수행하도록 구현하였습니다. 결과적으로 /dev/pcf8591은 ADC 기능을, /dev/pcf8591_dac은 DAC 기능을 담당하는 캐릭터 디바이스로 동작합니다.
이러한 구조를 통해 사용자 공간에서는 각 기능에 맞는 디바이스 노드에 명령을 전달함으로써, ADC와 DAC 동작을 명확하게 구분하여 테스트할 수 있습니다.
위 코드에서 추가된 부분에 대해서만 간략히 코멘트하고 테스트를 진행해보도록 하겠습니다.




(각각 DAC 입력값으로 255, 128, 0 을 입력 후 직접 전압값을 측정해본 결과)
/* DAC write: send [ctrl][value] */
static int pcf8591_write_dac(struct i2c_client *client, u8 value)
{
u8 data[2];
/*
* 보드/설정에 따라 ctrl 구성은 달라질 수 있음
*/
u8 ctrl = 0x40;
data[0] = ctrl;
data[1] = value;
return (i2c_master_send(client, data, 2) == 2) ? 0 : -EIO;
}
이 함수는 PCF8591의 AOUT에 아날로그 전압을 설정하기 위한 함수입니다. I2C를 통해 PCF8591 칩에 control byte와 DAC 값을 연속으로 전송하여, AOUT 핀에 출력되는 아날로그 전압을 설정하는 역할을 수행합니다.
struct i2c_client *client : i2c 연결된 디바이스를 의미(PCF8591)u8 value : 전송하게 될 DAC 값을 의미함.여기서 u8은 unsigned 8 bit를 의미하고, 0~255의 값을 가짐.
PCF8591 DAC 제어는 2바이트 연속 전송 구조로,
를 각각 한 바이트 씩 사용하므로, 총 2바이트 배열을 사용하여 관리하게 하였음.
차례로 Control byte - DAC value 를 i2c로 전송하게 되는데,
컨트롤 바이트에서 “DAC enable 상태로 PCF8591의 동작 모드를 설정한다”
라고 이해하면 된다. 그리고 그 다음에 오는 value 바이트가 실제 DAC 출력값이다.
앞서 PCF8591과 데이터를 주고 받는 과정에서 사용한 함수는
가 있었다.
이들은 모두 리눅스 커널에서 제공하는 I2C API로서,<linux/i2c.h> 라는 헤더파일에 존재하는 함수들이다.
각각 무슨 차이가 있고 또 다른 함수들에는 무엇이 있는지 알아보기 위해 별도의 챕터를 파게 되었다.
우선 I2C 트랜잭션이란 무엇인지에 대해 이해하자.
I2C 트랜잭션이란,
하나의 START부터 STOP까지 이어지는 통신 단위이며, 슬레이브가 하나의 의미 있는 명령으로 인식하는 최소 통신 흐름이다.
I2C 트랜잭션이란, 하나의 START부터 STOP까지 이어지는 “의미 있는 통신 단위”. 단순히 바이트 몇 개를 보내는 게 아니라, 연결되어있는 슬레이브가 “아, 이건 하나의 요청이구나”라고 이해하는 최소 단위를 말함
가장 단순한 형태의 트랜잭션인 1바이트 쓰기 형태를 예로 들어보자.
START
[Slave Address + Write] // 어느 디바이스에 어떤 명령할지 요청서
[Data Byte] // 1 바이트
STOP
이 전체가 1개의 I2C 트랜잭션으로 사용된다.
실제 우리가 사용한 PCF8591에 사용되는 트랜잭션은 아래와 같은 형태를 가진다.
Control Byte 쓰고 ADC 읽기한 과정은,
START
[Addr + W]
[Control Byte]
REPEATED START
[Addr + R]
[Dummy Read]
[ADC Data]
STOP
👉 이 전체가 하나의 I2C 트랜잭션 구성
이와 같은 트랜잭션을 전송하기 위해서 리눅스에서 제공하는 I2C API를 활용한다.
그렇다면 여러가지 API가 있던데 이들은 무슨 차이이며, 왜 이 API를 사용하는 걸지에 대해 생각해보았다.
우선 이 함수들 간에는 '추상화 수준(Abstraction Level)'과 '유연성' 에서 큰 차이가 있었다.
i2c_smbus_* 시리즈 (규격화된 통신)SMBus(System Management Bus)는 I2C를 기반으로 하지만, "명령어(Command) + 데이터(Data)"라는 엄격한 형식을 미리 정해둔 프로토콜이다.
Read Byte Data 기준):하드웨어(센서)가 SMBus 규격을 따를 때 사용 가능하다.
i2c_master_send/recv (단순 흐름 통신)이 함수들은 "프로토콜"보다는 "데이터 덩어리(Stream)"를 보내거나 받는 데 집중한다.
i2c_master_send: [주소] + [데이터1] + [데이터2] + ... + [데이터N] + STOPi2c_master_recv: [주소] + [수신1] + [수신2] + ... + [수신N] + STOPi2c_transfer (자유 설계 방식)가장 강력하고 자유로운 API. 여러 개의 struct i2c_msg 객체를 배열로 묶어서 한 번에 던진다.
i2c_msg 구조체를 통해 각 메시지의 주소, 방향(Read/Write), 길이, 데이터 버퍼를 직접 설정합니다.
struct i2c_msg {
__u16 addr; /* 장치의 7비트 주소 */
__u16 flags; /* 메시지의 특성 (읽기/쓰기, 10비트 주소 등) */
__u16 len; /* 데이터 버퍼의 길이 (바이트 단위) */
__u8 *buf; /* 데이터가 저장된 버퍼의 포인터 */
};
struct i2c_msg msgs[2]; // 2개의 메시지를 하나의 트랜잭션으로 설계
// 첫 번째 메시지: 쓰기 (보통 레지스터 주소를 알림)
msgs[0].addr = client->addr;
msgs[0].flags = 0; // 0은 Write를 의미
msgs[0].len = 1; // 1바이트 보냄
msgs[0].buf = ®_addr;
// 두 번째 메시지: 읽기 (실제 데이터를 가져옴)
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD; // 읽기 모드 설정
msgs[1].len = 2; // 2바이트 읽음
msgs[1].buf = data_buffer;
// 실행: 이 함수는 msgs[0]과 msgs[1] 사이에 'Repeated START'를 자동으로 넣습니다.
i2c_transfer(client->adapter, msgs, 2);
i2c_transfer의 핵심 장점: Repeated START일반적인 master_send 후 master_recv를 쓰면 중간에 버스 소유권을 놓치는 STOP 신호가 발생한다.
i2c_transfer는 메시지들 사이에 STOP 없이 Repeated START를 사용하여, 통신 중간에 다른 장치가 끼어들지 못하게 원자적(Atomic)인 통신을 보장함.
| 구분 | SMBus API | Master API | Transfer API |
|---|---|---|---|
| 유연성 | 낮음 (정해진 틀) | 중간 (단순 송수신) | 매우 높음 (사용자 정의) |
| 트랜잭션 | 커맨드/데이터 구조 | 단순 바이트 스트림 | 복합 메시지 (Repeated START) |
| 복잡도 | 매우 쉬움 | 쉬움 | 조금 어려움 |
| 주요 용도 | 표준 센서 제어 | EEPROM, 펌웨어 전송 | 특수한 타이밍/복합 동작 필요 시 |
하나의 하드웨어 장치에 여러 기능이 존재하는 경우, 메이저 번호는 공유하되 마이너 번호를 분리하여 기능별 캐릭터 디바이스를 제공함으로써 사용자 공간 인터페이스를 명확하게 설계할 수 있다.
✅ ADC / DAC처럼 “기능적으로 독립된 인터페이스”라면
→ 마이너 번호를 나눠 별도 디바이스 노드로 관리함.
👉 /dev/pcf8591
👉 /dev/pcf8591_dac
이렇게 나누면 읽기 쓰기 동작의 의미가 명확해지고 오용을 구조적으로 방지할 수 있음.
⸻
만약 하나의 노드였다면:
echo "dac 128" > /dev/pcf8591
echo "adc 0" > /dev/pcf8591
cat /dev/pcf8591
해당 문자열 처리를 위한 파싱이 별도로 존재해야함.
하지만 마이너 분리를 통해
echo 0 > /dev/pcf8591
cat /dev/pcf8591
echo 128 > /dev/pcf8591_dac
기능적인 구분을 해주면 좀 더 단순하고 직관적인 사용이 가능하였음.
이렇게 마이너를 통해 기능을 구분해주면, 또 좋은 점이 "권한 및 보안 분리가 가능하다" 는 점이다.
예를 들어, ADC는 읽기 허용, DAC는 특정 프로세스만 쓰기 허용하는 상태라면,
chmod 444 /dev/pcf8591
chmod 220 /dev/pcf8591_dac
각 장치노드에 대한 권한 설정을 통해 정책을 설정해줄 수 있음.
이번 게시물에서는 DAC 기능을 추가하였습니다.
다음에는 DT overlay를 통해 PCF8591이 라즈베리파이 상에 기본적으로 등록되어있을 수 있도록 지원해보도록 하겠습니다.