[Device Driver] 3편. I2C driver

나우히즈·2025년 12월 17일

안녕하세요.

오늘은 I2C driver 실습을 진행해보도록 하겠습니다.


1. 보드 세팅

우선 교육을 진행하며 사용한 PCF8591 모듈(이하 PCF)을 이용하여 라즈베리파이와 I2C 통신 드라이버를 구현할 것임을 말씀드립니다.

PCF8591 = I2C 기반 ADC(4채널) + DAC(1채널) 복합 디바이스

두 보드 간 연결은 아래와 같이 진행.

PCF8591 Raspberry Pi 4
GND GND - (Pin 6)
VCC 3.3V - (Pin 1)
SDA GPIO2 / SDA - (Pin 3)
SCL GPIO3 / SCL - (Pin 5)

이렇게 해주면, 두 보드 간 물리적인 연결은 완료된다.
라즈베리파이에서 PCF를 잘 인식하고 있는지 확인하기 위해서,

sudo raspi-config

를 진행 후 부팅되는 GUI 창에서,

•	Interface Options
•	I2C → Enable
•	재부팅

이후 두 보드가 잘 연결되었는지 확인하기 위해 아래 명령어로 I2C 주소가 잘 배정되었는지 확인한다.

sudo apt install -y i2c-tools
i2cdetect -y 1

I2C 주소

1. I2C 주소란 무엇인가? (주소 = 동/호수)

I2C 통신은 두 가닥의 선(SDA, SCL)만 사용하여 여러 개의 센서를 고속도로처럼 공유하는 구조이다.

라즈베리파이(Master)가 "지금 데이터를 줄게!"라고 외치면, 연결된 모든 센서가 그 소리를 듣습니다. 이때 누구에게 주는 데이터인지 구분하기 위해 붙이는 고유 번호가 바로 I2C Slave 주소입니다.

  • 라즈베리파이: "주소 0x48번 센서, 지금 조도값 얼마야?"
  • PCF8591: (자기 번호를 확인하고) "저 여기 있어요! 지금 값은 120입니다."
  • 다른 센서: (자기 번호가 아니므로 무시함)

2. 왜 하필 '48(0x48)'이 나오는 걸까?

이것은 PCF8591 칩을 만든 설계자가 정해놓은 약속 때문입니다.

I2C 주소는 보통 7비트로 구성됩니다. PCF8591의 주소 구조를 뜯어보면 다음과 같습니다:

  1. 고정 비트 (상위 4비트): 1001 (이건 칩 제조사가 공장에서 박아 넣은 값이라 못 바꿉니다.)
  2. 가변 비트 (하위 3비트): A2, A1, A0 (우리가 점퍼로 바꿀 수 있는 부분입니다.)

PCF의 기본 세팅 상 0x48이 나오게 되는 것이 바로 이 이유입니다.

3. 왜 이게 중요할까? (드라이버 개발의 핵심)

우리가 나중에 작성할 Device Tree 코드에 이 주소를 명시해야 합니다.

pcf8591@48 {           // @48이 바로 I2C 주소입니다.
    compatible = "philips,pcf8591";
    reg = <0x48>;      // 커널에게 "0x48번지로 대화해!"라고 알려주는 설정
};

만약 실제 하드웨어 주소는 0x48인데 코드에 0x49라고 적으면, 커널은 엉뚱한 집 문을 두드리는 꼴이 되어 "Device not found" 에러를 뱉게 됩니다.


I2C 디바이스 드라이버

앞서 진행한 실습의 캐릭터 디바이스 드라이버와는 다르다.

char device driver → VFS ↔ file_operations ↔ driver
I2C client driver → I2C core ↔ I2C controller ↔ 실제 I2C 디바이스

👉 중요한 차이
• char driver: “파일 인터페이스 중심”
• I2C driver: “버스 + 디바이스 모델 중심”

따라서, 별도의 장치파일을 생성하지 않고 통신 드라이버를 구현하게 된다.

I2C 드라이버는 2단 구조

[1] I2C controller driver (이미 있음)

  • BCM2711 I2C controller
  • Raspberry Pi 커널에 이미 포함

[2] I2C client driver (우리가 작성)

  • PCF8591

I2C 타이밍 / SDA / SCL 제어는 커널에서 관리하는 부분이고,
우리는 PCF가 I2C 통신에 대한 동작을 어떻게 진행할지 만들면 된다.

커널에서 I2C 디바이스는 어떻게 인식될까?

라즈베리파이에는 디바이스 트리라는 하드웨어를 총망라하는 개념이 존재한다.
디바이스 트리는 마찬가지로 I2C 디바이스도 관리하게 된다.

I2C 버스에 PCF가 있음을 확인하면, 이를 I2C Core가 PCF에 대응되는 드라이버를 매칭시킨다. 이 과정에서 I2C 장치를 감지했다는 의미로, 드라이버의 probe() 함수가 호출된다.

PCF8591 드라이버가 할 일

그렇다면 우리가 구현할 I2C PCF8591 드라이버는 어떤 역할을 수행해야하는지 정리해보자.

  • ADC 값 읽기
  • DAC 값 쓰기
  • 채널 선택
  • mode 설정

그리고 이걸:

  • sysfs
  • or character device

를 통해 유저에게 노출하여 활용하게 된다.

연결확인

위의 명령어는 i2c 의 값을 가져오는 명령어로서, 0x48 번지 i2c주소에서 0x40 레지스터 값(0b0100 0000)을 읽어오라는 명령입니다.

주변이 어두울 땐 0xc7 이라는 값이, 밝게 후레쉬를 비추었을 때 0x26으로 값이 내려왔음을 볼 수 있습니다. 이를 통해 현재 PCF보드와 라즈베리파이가 잘 연결되었음을 확인했습니다.


2. PCF8591

PCF8591은:

  • I2C slave 디바이스
  • 내부 레지스터 구조가 거의 없음
  • 모든 제어는 Control Byte 하나로 함
  • ADC / DAC 모두 지원

Control Byte 비트 구조

라즈베리파이 - PCF8591 간의 I2C 통신은 Control byte라는 1 바이트 크기의 데이터로 진행된다.

Bit 7 : ADC Enable
Bit 6 : DAC 출력 enable
Bit 5~4 : Analog Input Mode (Single / Differential)
Bit 3~0 : Channel Select (ADC Channel)

예를 들어 앞서 진행한 i2cget 명령에서 우리는 0x40 이라는 컨트롤 비트를 전송했다.
0x48 번지에 위치한 PCF8591은 0x40(0b 0100 0000)을 받고, AIN0에 대한 아날로그 값을 디지털 값으로 변환하여 전달한다.

PCF8591의 역할

PCF8591 보드는 뭘 가능하게 해주냐?

✔ ADC로 가능한 것

•	가변저항 값 읽기
•	조도 센서 읽기
•	온도 센서 전압 읽기
•	아날로그 센서 드라이버 연습

✔ DAC로 가능한 것

•	LED 밝기 아날로그 제어
•	기준 전압 생성
•	ADC 테스트용 피드백 루프

이번 실습에서는 ADC 로 AIN0 (조도센서 값) 을 받아오는 드라이버를 구현해보겠습니다.


3. 기본 뼈대 코드

I2C 드라이버 동작 개요

캐릭터 디바이스 드라이버는 VFS에 의해 탐색되고 관리되었던 것과 달리, I2C 드라이버는 I2C 코어에서 관리를 하게 된다.

코어는 PCF8591에 대한 통신을 필요로 할 때, 어떤식으로 진행할 것인지를 우리가 작성할 PCF8591 client driver를 통해 소통해야한다.
따라서, I2C 드라이버 모듈을 I2C 코어에 등록해야한다.

성공적으로 등록되면, probe() 함수가 발동한다. 코어가 PCF8591 에 대한 I2C 통신을 위해 드라이버를 찾게되고, 해당 드라이버가 다룰 장치(PCF8591)가 탐지가 되었을 때 동작하는 함수이다. 기본 뼈대 코드에서는 이 부분에 집중하여 구현해보도록 하자.

I2C core
   ↓
i2c_driver (우리 코드)
   ↓
probe() ← PCF8591 발견 시 호출

소스코드 구현

디렉토리 생성

작업할 pcf8591 드라이버에 대한 디렉토리를 별도로 생성한다.

mkdir -p ~/pcf8591_driver
cd ~/pcf8591_driver

pcf8591.c

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

#define DRV_NAME "pcf8591"

/* probe */
static int pcf8591_probe(struct i2c_client *client)
{
    dev_info(&client->dev, "PCF8591 probe called (addr=0x%02x)\n",
             client->addr);
    return 0;
}

/* remove */
static void pcf8591_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "PCF8591 removed\n");
}

/* I2C device id table */
static const struct i2c_device_id pcf8591_id[] = {
    { "pcf8591", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, pcf8591_id);

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

/* module init/exit */
static int __init pcf8591_init(void)
{
    return i2c_add_driver(&pcf8591_driver);
}

static void __exit pcf8591_exit(void)
{
    i2c_del_driver(&pcf8591_driver);
}

module_init(pcf8591_init);
module_exit(pcf8591_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("sihwan");
MODULE_DESCRIPTION("Minimal PCF8591 I2C driver");

Makefile

obj-m += pcf8591.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

앞서 진행했던 메이크파일들과 동일합니다.

빌드 & 로드

make
sudo insmod pcf8591.ko

이후 dmesg | tail 을 통해 커널 메시지가 잘 찍혔는지를 확인해본 결과, i2c 드라이버에 의한 메시지는 찍히지 않았다.

그 이유는 라즈베리파이가 디바이스(PCF8591) 를 인식하지 못한 상태이므로 디바이스와 드라이버가 매칭되지 못했기 때문 이다.

임베디드 리눅스에서 디바이스들은 디바이스 트리에 등록되어야, 현재 보드 상에 연결되었다고 인식된다. 하지만 지금 내 라즈베리파이는 디바이스트리에 PCF8591 를 등록한 상태가 아닌 기본 그 자체의 상태이므로 드라이버와 디바이스 간 매칭 과정이 이루어지지 않았다.

이를 해결하는 방법으로는,

  1. 라즈베리파이에 디바이스트리로 PCF8591 등록하기 (정석)
  2. userspace에서 new_device로 수동 등록하기

중 2번을 선택해서 일단 만들고 있는 I2C 드라이버가 잘 동작하는지 테스트해보도록 하자.

여기서 new_device란, /sys/bus/i2c/devices/i2c-1/new_device 를 의미한다. 이 파일은 일반 파일이 아닌 커널 코드와 연결된 인터페이스 로서, 해당 파일에 write 하게 되면, 커널 내부에서는

i2c_new_client_device(...)

과 같은 로직이 실행된다. 그 결과 디바이스 객체가 생성된다. 이를 이용해 매칭을 진행해보자.

디바이스-드라이버 매칭 강제

현재 우리가 사용하는 I2C 디바이스 PCF8591가 있는지 확인해보자.

ls /sys/bus/i2c/devices/

진행해보면 현재 존재하는 디바이스 명들이 쭉 나타난다.
우리가 사용하는 PCF8591은 i2c 1번 버스에 48번 주소로 위치하고 있으므로,
1-0048 이라는 파일명으로 나타나야한다.

지금은 내가 디바이스 등록 과정을 진행해서 나타났는데, 원래는 안떴었다.
이를 수동으로 등록하는 과정은 아래와 같다.

echo pcf8591 0x48 | sudo tee /sys/bus/i2c/devices/i2c-1/new_device

위 경로에서 new_device 가 i2c 버스에 새로운 디바이스를 등록하는 창구의 역할을 한다. 해당 경로에 tee 명령을 요청하면,

디바이스 디렉토리에 요청한대로 1번 버스 - 48번지 주소에 디바이스가 등록된다.

이후 커널에서는 디바이스와 드라이버의 매칭에 성공하고, 성공적으로 probe(), remove() 동작이 이루어진다.

정상적으로 probe()가 호출되는 것을 볼 수 있다.(그 아래 ADC value는 이후 코드에서 추가할 내용임)

위 수행 내용을 통해 정상적으로 디바이스, 드라이버, 매칭이 잘 되었는지 확인 가능하다.

ls /sys/bus/i2c/devices/1-0048 			// 버스 1 - 48번지에 디바이스 등록 확인
ls /sys/bus/i2c/drivers/pcf8591 		// pcf8591의 드라이버 등록 확인
ls -l /sys/bus/i2c/devices/1-0048/driver // 디바이스가 드라이버와 매칭되었는지 확인

그런데 이 디바이스 수동 등록 과정은 라즈베리파이를 재부팅하면 초기화되는 것으로 보인다. 추후 디바이스트리에 직접 등록해보는 방식을 이용해보도록 하겠다.


4. ADC 값 읽기

드라이버와 디바이스 연동이 성공적으로 되었으니, probe() 가 실행될 때, PCF8591의 0채널(AIN0)의 값을 읽어오기를 구현해보도록 하자.

목표 : probe() 안에서 PCF8591 ADC 값을 실제로 읽고, dmesg로 확인

PCF8591 ADC 읽기 순서

PCF8591은 아래 순서로 읽을 수 있다.

  1. control byte write
  2. dummy read (버림)
  3. real read (ADC 값)

control 비트는 위에서 소개한 대로, 한 바이트 크기의 비트를 통해 읽어올 채널과 설정을 적어 보내게 된다. 그러면 PCF8591 은 받은 control byte에 따라 해당하는 데이터를 보내게 된다. 그 과정에서 기존에 저장되어 있던 값인 dummy data가 포함될 수 있으므로, 먼저 읽은 값은 버리는 과정이 있어야한다.

우리가 사용할 I2C API는 SMBus API 이다.

SMBus(System Management Bus) API 함수들은 리눅스 I2C 드라이버 개발에서 가장 표준적이고 안전한 도구들로, 단순히 데이터를 보내는 것을 넘어, I2C 프로토콜의 복잡한 타이밍과 신호 제어(Start, Stop, Ack 등)를 커널이 대신 처리해 준다.


1. i2c_smbus_write_byte(client, value)

이 함수는 대상 장치(Client)에 딱 1바이트의 데이터만 던질 때 사용한다.

  • 동작 방식: I2C 버스에 [Start] -> [7비트 주소 + Write 비트] -> [데이터(value) 1바이트] -> [Stop] 신호를 순서대로 보낸다.
  • PCF8591에서의 역할: 여기서 value컨트롤 바이트(0x40 등) 가 된다. 칩에게 "이제부터 너는 0번 채널을 ADC 해라"라고 명령만 내리고 통신을 종료하는 단계.
  • 특징: 레지스터 주소를 지정하지 않고 데이터만 보내는 가장 단순한 쓰기 방식입니다.

2. i2c_smbus_read_byte(client)

이 함수는 장치로부터 현재 포인터가 가리키는 위치의 데이터 1바이트를 읽어온다.

  • 동작 방식: [Start] -> [7비트 주소 + Read 비트] -> [장치가 보내주는 1바이트 수신] -> [NACK] -> [Stop] 과정을 거침.
  • PCF8591에서의 역할:
    • 첫 번째 호출 (Dummy Read): 칩은 명령을 받았으니 데이터를 내보내야 하는데, 아직 새로운 ADC 변환이 완료되지 않아서, 내부 버퍼에 있던 이전 값(또는 초기값 0x80) 을 내보냄.
    • 두 번째 호출 (Real Read): 첫 번째 읽기가 끝나는 동안 칩은 열심히 새로운 값을 변환해 둡니다. 이때 비로소 최신 ADC 값이 우리 손에 들어옵니다.
  • 특징: 별도의 명령(Command) 없이 "너 지금 들고 있는 거 하나 줘봐"라고 요청하는 방식이다.

이 두 함수들은 실패 시 음수 값을 반환한다.


수정된 probe() 예제

static int pcf8591_probe(struct i2c_client *client)
{
    int ret;
    int dummy;
    int adc;

    dev_info(&client->dev, "PCF8591 probe called (addr=0x%02x)\n",
             client->addr);

    /* 1. Control byte write
     * 0x40 : ADC enable, AIN0 선택
     */
    ret = i2c_smbus_write_byte(client, 0x40);
    if (ret < 0) {
        dev_err(&client->dev, "Failed to write control byte\n");
        return ret;
    }

    /* 2. Dummy read */
    dummy = i2c_smbus_read_byte(client);
    if (dummy < 0) {
        dev_err(&client->dev, "Failed to read dummy value\n");
        return dummy;
    }

    /* 3. Real ADC read */
    adc = i2c_smbus_read_byte(client);
    if (adc < 0) {
        dev_err(&client->dev, "Failed to read ADC value\n");
        return adc;
    }

    dev_info(&client->dev, "PCF8591 ADC value = %d (0x%02x)\n",
             adc, adc);

    return 0;
}

수정된 probe 함수이다.

  • i2c_smbus_write_byte() 를 통해 0x40 control byte를 PCF8591로 write 한다.
  • i2c_smbus_read_byte()를 통해 처음 받은 더미 데이터를 무시한다.
  • 다시 i2c_smbus_read_byte() 하여 AIN0의 ADC 값을 전달받아, 커널 메시지로 기록한다.

모듈을 빌드하여 올려보면 성공적으로 ADC 값을 받아옴을 알 수 있다. 참고로 AIN0은 조도센서의 값을 의미한다.

1개의 댓글

comment-user-thumbnail
2025년 12월 20일

글 잘보고 있습니다 감사합니다!

답글 달기