I2C 서브시스템 — I2C / SMBus / I3C

임베디드 Linux에서 가장 많이 쓰이는 I2C 서브시스템을 보드 초기화부터 드라이버 운영까지 실무 관점으로 정리합니다. I2C 전송 모델과 버스(Bus) arbitration, SMBus 프로토콜과 PEC, I3C 차세대 직렬 버스, Device Tree 바인딩, regmap을 활용한 레지스터(Register) 추상화, IRQ 처리, 전원관리와 슬립(Sleep) 복귀 시 상태 복원, 로직 분석기와 tracepoint를 이용한 타이밍 문제 진단까지 I2C 기반 드라이버 개발에 필요한 핵심을 다룹니다.

전제 조건: 디바이스 드라이버인터럽트(Interrupt) 문서를 먼저 읽으세요. 버스/열거/프로브(Probe) 경로는 초기화 순서와 자원 등록 규칙이 핵심이므로, 장치 발견부터 바인딩까지 흐름을 먼저 고정해야 합니다.
일상 비유: I2C 버스는 공유 전화선과 비슷합니다. 여러 장치(슬레이브)가 같은 두 가닥 선(SCL/SDA)에 연결되어 있고, 마스터가 주소를 불러 특정 장치와 대화합니다. SPI는 SPI 서브시스템, GPIO는 GPIO 서브시스템 문서를 참조하세요.

핵심 요약

  • 2-wire 직렬 버스 — SCL(클럭)과 SDA(데이터) 두 선만으로 다수의 디바이스를 연결합니다.
  • 주소 기반 통신 — 7비트/10비트 주소로 슬레이브를 식별하여 데이터를 송수신합니다.
  • SMBus 호환 — PC 시스템 관리용 SMBus 프로토콜은 I2C 기반이며, PEC 오류 검출을 지원합니다.
  • I3C 차세대 — I2C 호환을 유지하면서 최대 33 Mbps, IBI(In-Band Interrupt), 동적 주소 할당을 제공합니다.
  • regmap 추상화 — 버스별 전송 함수 호출 없이 통합된 레지스터 접근을 제공합니다.

단계별 이해

  1. I2C 프로토콜 이해
    START/STOP 조건, 주소 체계, ACK/NACK 흐름을 파악합니다.
  2. 커널 서브시스템 구조
    i2c_adapter, i2c_client, i2c_driver 관계를 정리합니다.
  3. 드라이버 작성
    probe/remove, SMBus API, Device Tree 바인딩을 구현합니다.
  4. 디버깅과 검증
    i2c-tools, tracepoint, regmap debugfs로 동작을 확인합니다.
관련 표준: I2C-bus specification (NXP UM10204), MIPI I3C (MIPI Alliance HCI 1.0) — 이 문서에서 다루는 버스 프로토콜의 기반 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
I2C Adapter SCL / SDA (Open-Drain) i2c_adapter + i2c_algorithm i2c_client (0x48) 온도 센서 i2c_client (0x50) EEPROM i2c_driver .probe / .remove of_match / id_table

I2C 프로토콜 기초

I2C (Inter-Integrated Circuit)는 Philips(현 NXP)가 1982년 개발한 2-wire 직렬 버스입니다. 센서, EEPROM, RTC, PMIC 등 저속 주변장치 연결에 널리 사용됩니다.

신호선역할특성
SCLSerial Clock마스터가 생성, 오픈 드레인
SDASerial Data양방향 데이터, 오픈 드레인

신호 프로토콜

I2C 통신의 기본 단위는 START 조건으로 시작하여 STOP 조건으로 끝나는 트랜잭션(Transaction)입니다:

조건SDA 상태SCL 상태의미
START (S)HIGH → LOWHIGH트랜잭션 시작
STOP (P)LOW → HIGHHIGH트랜잭션 종료
Repeated START (Sr)HIGH → LOWHIGHSTOP 없이 재시작(Reboot)
ACKLOW (수신측)9번째 클럭수신 확인
NACKHIGH (수신측)9번째 클럭수신 거부 / 마지막 바이트

주소 체계

I2C는 7비트(표준)와 10비트(확장) 주소를 지원합니다. 7비트 주소의 경우 첫 번째 바이트는 [A6:A0 | R/W] 형식으로, 최하위 비트가 방향을 나타냅니다 (0 = Write, 1 = Read).

예약 주소: 0x00 (General Call), 0x01 (CBUS), 0x02 (다른 버스 형식), 0x03 (미래 용도), 0x04-0x07 (Hs-mode 마스터 코드), 0x78-0x7B (10비트 주소 prefix), 0x7C-0x7F (미래 용도)는 슬레이브 주소로 사용할 수 없습니다.
속도 모드클럭 주파수용도
Standard Mode (Sm)100 kHz일반 센서, EEPROM
Fast Mode (Fm)400 kHz가속도계, 터치 컨트롤러
Fast Mode Plus (Fm+)1 MHz고속 센서
High Speed Mode (Hs)3.4 MHz고속 메모리

I2C 버스 타이밍 파형

I2C 프로토콜의 완전한 트랜잭션 타이밍을 이해하는 것은 하드웨어 디버깅(Debugging)과 드라이버 개발에 필수적입니다. 아래 다이어그램은 마스터가 슬레이브에 1바이트를 쓰는 전형적인 Write 트랜잭션의 SCL/SDA 파형을 보여줍니다.

I2C Write Transaction: START + Address(7-bit) + W + ACK + Data + ACK + STOP SDA SCL S A6 A5 A4 A3 A2 A1 A0 R/W ACK D7 D6 D5 D4 D3 D2 D1 D0 ACK P tHD:STA tHIGH tLOW tSU:STO 7-bit Address + R/W 8-bit Data
타이밍 파라미터 (Standard Mode 100 kHz 기준): tHD:STA (START hold) ≥ 4.0 μs, tSU:STA (Repeated START setup) ≥ 4.7 μs, tLOW (SCL low) ≥ 4.7 μs, tHIGH (SCL high) ≥ 4.0 μs, tSU:STO (STOP setup) ≥ 4.0 μs, tSU:DAT (Data setup) ≥ 250 ns. Fast Mode에서는 각 값이 크게 줄어듭니다.

Multi-Master Arbitration과 Clock Stretching

I2C는 multi-master 버스이므로, 두 마스터가 동시에 전송을 시작할 수 있습니다. Arbitration은 SDA 라인에서 비트 단위로 수행됩니다. 오픈 드레인 특성상, 어떤 마스터가 HIGH를 출력하지만 다른 마스터가 LOW를 구동하면 버스는 LOW가 됩니다. HIGH를 출력한 마스터가 SDA를 모니터링하여 LOW를 감지하면 arbitration에 패배한 것이므로 즉시 전송을 중단합니다.

I2C Multi-Master Arbitration Master A SDA Arbitration Lost! Master B SDA Arbitration Won - 전송 계속 SDA Bus (Wire-AND) SCL b3=1 b2=0 b1=0 b0=1 충돌! A=LOW, B=HIGH Bus=LOW Arbitration 규칙 1. 두 마스터 동시 START 발생 2. 각 비트마다 SDA 모니터링 3. HIGH 출력 시 LOW 감지 → 패배 4. 패배한 마스터는 즉시 중단

Clock Stretching은 슬레이브가 SCL 라인을 LOW로 유지하여 마스터의 클럭을 늦추는 메커니즘입니다. 슬레이브가 데이터를 준비할 시간이 필요할 때 사용합니다. 마스터는 SCL을 HIGH로 전환하려 할 때 실제 SCL 레벨을 확인하여, 슬레이브가 여전히 LOW로 잡고 있으면 대기합니다.

Clock Stretching 주의: 모든 I2C 마스터 컨트롤러가 clock stretching을 올바르게 지원하는 것은 아닙니다. 특히 비트뱅(bit-bang) I2C 구현에서는 반드시 SCL 상태를 읽어 확인해야 합니다. Device Tree에서 i2c-scl-internal-delay-nsi2c-scl-clk-low-timeout-us 속성으로 타임아웃을 설정할 수 있습니다.

Linux I2C 서브시스템

커널 I2C 서브시스템은 drivers/i2c/ 디렉토리에 구현되어 있으며, 다음 핵심 구조체(Struct)로 구성됩니다:

구조체역할헤더
i2c_adapterI2C 버스 컨트롤러 (마스터)<linux/i2c.h>
i2c_algorithm전송 알고리즘 (HW 접근 방법)<linux/i2c.h>
i2c_clientI2C 버스 상의 슬레이브 디바이스<linux/i2c.h>
i2c_driverI2C 디바이스 드라이버<linux/i2c.h>
i2c_msg단일 I2C 메시지 (주소+데이터)<linux/i2c.h>
Linux I2C 서브시스템 아키텍처 User Space: /dev/i2c-N (i2c-dev), i2c-tools, hwmon sysfs, IIO sysfs i2c-dev (chardev) I2C Core (drivers/i2c/i2c-core-*.c) i2c_driver .probe / .remove .id_table / of_match i2c_client .addr / .flags 슬레이브 디바이스 표현 i2c_adapter 버스 컨트롤러 (마스터) .algo / .algo_data i2c_algorithm .master_xfer .smbus_xfer / .functionality bind Device Tree / ACPI / Board Info Platform Bus Driver (i2c-imx, i2c-designware, i2c-bcm2835 ...) Hardware I2C Controller SCL / SDA (Open-Drain Bus)

위 아키텍처에서 핵심 데이터 흐름은 다음과 같습니다: Device Tree(또는 ACPI)가 어댑터와 클라이언트 정보를 제공하면, I2C Core가 i2c_adapter를 등록하고 하위 i2c_client를 열거합니다. i2c_driverof_match_table 또는 id_table로 매칭이 이루어지면 probe가 호출됩니다.

i2c_adapter와 i2c_algorithm

i2c_adapter는 물리적 I2C 컨트롤러를 나타내며, i2c_algorithm을 통해 실제 하드웨어 전송을 수행합니다:

struct i2c_algorithm {
    int (*master_xfer)(struct i2c_adapter *adap,
                        struct i2c_msg *msgs, int num);
    int (*master_xfer_atomic)(struct i2c_adapter *adap,
                              struct i2c_msg *msgs, int num);
    int (*smbus_xfer)(struct i2c_adapter *adap,
                       u16 addr, unsigned short flags,
                       char read_write, u8 command,
                       int size, union i2c_smbus_data *data);
    u32 (*functionality)(struct i2c_adapter *adap);
};
master_xfer vs smbus_xfer: master_xfer는 raw I2C 메시지를 전송하며, smbus_xfer는 SMBus 프로토콜에 최적화된 전송을 수행합니다. 대부분의 어댑터는 master_xfer만 구현하고, 커널이 SMBus 호출을 I2C 메시지로 에뮬레이션합니다.

i2c_client와 i2c_driver

i2c_client는 특정 어댑터의 특정 주소에 위치한 디바이스를 나타내며, i2c_driver가 이를 제어합니다:

struct i2c_driver {
    int (*probe)(struct i2c_client *client);
    void (*remove)(struct i2c_client *client);
    void (*shutdown)(struct i2c_client *client);
    struct device_driver driver;
    const struct i2c_device_id *id_table;
};

I2C 드라이버 작성

실제 I2C 센서 드라이버 예제를 통해 작성 방법을 살펴봅니다. 아래는 가상의 온도 센서 드라이버입니다:

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/iio/iio.h>

#define TEMP_REG_VALUE    0x00
#define TEMP_REG_CONFIG   0x01
#define TEMP_REG_ID       0xFF

struct my_temp_data {
    struct i2c_client *client;
    struct mutex lock;
    u8 config;
};

static int my_temp_read_reg(struct my_temp_data *data, u8 reg)
{
    int ret;

    ret = i2c_smbus_read_byte_data(data->client, reg);
    if (ret < 0)
        dev_err(&data->client->dev,
                "failed to read reg 0x%02x: %d\\n", reg, ret);
    return ret;
}

static int my_temp_read_raw(struct iio_dev *indio_dev,
                            struct iio_chan_spec const *chan,
                            int *val, int *val2, long mask)
{
    struct my_temp_data *data = iio_priv(indio_dev);
    int ret;

    switch (mask) {
    case IIO_CHAN_INFO_RAW:
        mutex_lock(&data->lock);
        ret = my_temp_read_reg(data, TEMP_REG_VALUE);
        mutex_unlock(&data->lock);
        if (ret < 0)
            return ret;
        *val = (s8)ret;  /* 부호 확장 */
        return IIO_VAL_INT;
    case IIO_CHAN_INFO_SCALE:
        *val = 1000;  /* milli-degrees */
        return IIO_VAL_INT;
    default:
        return -EINVAL;
    }
}

static const struct iio_chan_spec my_temp_channels[] = {
    {
        .type = IIO_TEMP,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
                              BIT(IIO_CHAN_INFO_SCALE),
    },
};

static const struct iio_info my_temp_info = {
    .read_raw = my_temp_read_raw,
};

static int my_temp_probe(struct i2c_client *client)
{
    struct iio_dev *indio_dev;
    struct my_temp_data *data;
    int chip_id;

    /* 디바이스 ID 확인 */
    chip_id = i2c_smbus_read_byte_data(client, TEMP_REG_ID);
    if (chip_id < 0)
        return chip_id;
    if (chip_id != 0xA1) {
        dev_err(&client->dev, "unexpected chip id: 0x%02x\\n", chip_id);
        return -ENODEV;
    }

    indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
    if (!indio_dev)
        return -ENOMEM;

    data = iio_priv(indio_dev);
    data->client = client;
    mutex_init(&data->lock);

    indio_dev->name = "my_temp";
    indio_dev->info = &my_temp_info;
    indio_dev->channels = my_temp_channels;
    indio_dev->num_channels = ARRAY_SIZE(my_temp_channels);
    indio_dev->modes = INDIO_DIRECT_MODE;

    /* 센서 활성화 */
    i2c_smbus_write_byte_data(client, TEMP_REG_CONFIG, 0x01);

    return devm_iio_device_register(&client->dev, indio_dev);
}

static const struct i2c_device_id my_temp_id[] = {
    { "my-temp-sensor", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, my_temp_id);

static const struct of_device_id my_temp_of_match[] = {
    { .compatible = "vendor,my-temp-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, my_temp_of_match);

static struct i2c_driver my_temp_driver = {
    .driver = {
        .name = "my-temp-sensor",
        .of_match_table = my_temp_of_match,
    },
    .probe = my_temp_probe,
    .id_table = my_temp_id,
};
module_i2c_driver(my_temp_driver);

SMBus API

대부분의 I2C 디바이스 드라이버는 raw i2c_transfer() 대신 SMBus 래퍼를 사용합니다:

함수동작데이터 크기
i2c_smbus_read_byte()커맨드 없이 1바이트 읽기1 byte
i2c_smbus_write_byte()커맨드 없이 1바이트 쓰기1 byte
i2c_smbus_read_byte_data()레지스터에서 1바이트 읽기1 byte
i2c_smbus_write_byte_data()레지스터에 1바이트 쓰기1 byte
i2c_smbus_read_word_data()레지스터에서 2바이트 읽기2 bytes
i2c_smbus_write_word_data()레지스터에 2바이트 쓰기2 bytes
i2c_smbus_read_block_data()레지스터에서 블록 읽기최대 32 bytes
i2c_smbus_read_i2c_block_data()I2C 블록 읽기 (길이 지정)지정 길이

i2c_transfer (Raw API)

복잡한 다중 메시지 트랜잭션에는 i2c_transfer()를 직접 사용합니다:

/* 레지스터 주소 쓰기 후 데이터 읽기 (Repeated START) */
static int read_reg16(struct i2c_client *client,
                       u8 reg, u16 *val)
{
    u8 buf[2];
    struct i2c_msg msgs[2] = {
        {   /* 쓰기: 레지스터 주소 전송 */
            .addr  = client->addr,
            .flags = 0,
            .len   = 1,
            .buf   = &reg,
        },
        {   /* 읽기: 데이터 수신 */
            .addr  = client->addr,
            .flags = I2C_M_RD,
            .len   = 2,
            .buf   = buf,
        },
    };
    int ret;

    ret = i2c_transfer(client->adapter, msgs, 2);
    if (ret != 2)
        return ret < 0 ? ret : -EIO;

    *val = (u16)(buf[0] << 8) | buf[1];
    return 0;
}

I2C Device Tree 바인딩

Device Tree에서 I2C 버스와 슬레이브 디바이스를 선언하는 패턴:

/* SoC dtsi: I2C 컨트롤러 노드 */
i2c1: i2c@40005400 {
    compatible = "st,stm32f7-i2c";
    reg = <0x40005400 0x400>;
    interrupts = <31>, <32>;
    clocks = <&rcc 0x40 21>;
    #address-cells = <1>;
    #size-cells = <0>;
    status = "disabled";
};

/* 보드 dts: 슬레이브 디바이스 추가 */
&i2c1 {
    status = "okay";
    clock-frequency = <400000>;  /* Fast mode 400kHz */

    temp_sensor: temperature@48 {
        compatible = "vendor,my-temp-sensor";
        reg = <0x48>;  /* 7비트 I2C 주소 */
        interrupt-parent = <&gpio1>;
        interrupts = <7 IRQ_TYPE_EDGE_FALLING>;
    };

    eeprom@50 {
        compatible = "atmel,24c256";
        reg = <0x50>;
        pagesize = <64>;
    };
};
주소 충돌 주의: 같은 I2C 버스에 동일 주소의 디바이스가 두 개 이상 존재하면 커널이 경고를 출력하고 두 번째 디바이스 등록이 실패합니다. 주소가 겹치는 경우 I2C 멀티플렉서(i2c-mux)를 사용하세요.

SMBus (System Management Bus)

SMBus (System Management Bus)는 Intel이 1995년 정의한 I2C 기반의 2-wire 직렬 버스 규격입니다. PC 시스템 관리를 위해 설계되었으며, PMIC(전원 관리(Power Management) IC), DIMM SPD EEPROM, 팬 컨트롤러, 온도 센서, 배터리 관리 IC 등 PC 메인보드 주변장치 통신에 표준적으로 사용됩니다. SMBus는 I2C를 기반으로 하되, 전기적 특성과 프로토콜 규칙을 더 엄격하게 제한하여 상호운용성과 신뢰성을 강화한 것이 특징입니다.

SMBus와 I2C 차이점

SMBus는 I2C의 상위집합이 아니라 별도의 규격이며, 여러 측면에서 I2C와 다릅니다:

항목I2C (표준)SMBus 2.0/3.x
전압 레벨VDD 자유 (1.8V~5V)VDD = 3.3V (기본), 고정된 VLOW/VHIGH 임계값
클럭 속도100 kHz ~ 3.4 MHz10 kHz ~ 100 kHz (2.0), 최대 1 MHz (3.x)
최저 클럭제한 없음 (DC 가능)10 kHz 하한 (timeout 때문)
Clock Stretching무제한 허용슬레이브 25 ms, 마스터 Cumulative 25 ms 제한
Timeout없음35 ms timeout (SCL LOW 유지 시 버스 리셋)
PEC없음CRC-8 Packet Error Checking (선택)
Address Resolution없음ARP (Address Resolution Protocol) 지원
Alert 메커니즘별도 GPIO 인터럽트SMBALERT# 전용 라인
트랜잭션 크기제한 없음최대 32바이트 블록 (SMBus 2.0), 255바이트 (3.x)
Host Notify없음슬레이브→호스트 알림 프로토콜

SMBus 프로토콜 타이밍

SMBus의 Write Byte 트랜잭션은 I2C와 유사하지만, Command 코드 바이트가 추가되고 선택적으로 PEC(Packet Error Checking) 바이트가 뒤따릅니다. 아래는 PEC를 포함한 SMBus Write Byte 트랜잭션의 구조를 보여줍니다.

SMBus Write Byte with PEC S Slave Address [A6:A0] + Wr(0) A Command Code 레지스터 주소 A Data Byte 8-bit value A PEC (CRC-8) 선택적 오류 검출 A P 마스터 → 슬레이브 마스터 → 슬레이브 마스터 → 슬레이브 마스터 → 슬레이브 전체 트랜잭션: 35ms timeout (SCL LOW 누적) PEC 계산 범위: Address + W + Command + Data CRC-8 다항식: x⁸ + x² + x + 1 (C2h)

SMBus 트랜잭션 유형

SMBus는 사전 정의된 트랜잭션 유형을 명시하여 디바이스 간 상호운용성을 보장합니다:

트랜잭션데이터 흐름바이트 수커널 함수용도
Quick CommandR/W 비트만0i2c_smbus_write_quick()디바이스 존재 확인, ON/OFF 토글
Send ByteHost → Device 1바이트1i2c_smbus_write_byte()단순 명령 전송
Receive ByteDevice → Host 1바이트1i2c_smbus_read_byte()상태 읽기
Write ByteCmd + 1바이트2i2c_smbus_write_byte_data()레지스터 쓰기
Read ByteCmd → 1바이트2i2c_smbus_read_byte_data()레지스터 읽기
Write WordCmd + 2바이트3i2c_smbus_write_word_data()16비트 레지스터 쓰기
Read WordCmd → 2바이트3i2c_smbus_read_word_data()16비트 레지스터 읽기
Process CallCmd + 2바이트 → 2바이트4i2c_smbus_process_call()명령-응답 쌍
Block WriteCmd + N바이트 (N≤32)2+Ni2c_smbus_write_block_data()다중 바이트 쓰기
Block ReadCmd → N바이트2+Ni2c_smbus_read_block_data()다중 바이트 읽기
Block Process CallCmd + N바이트 → M바이트3+N+Mi2c_smbus_block_process_call()블록 명령-응답
Host Notify슬레이브→호스트 알림3i2c_smbus_host_notify 콜백(Callback)슬레이브 이벤트 통보

SMBus 시스템 토폴로지(Topology)

일반적인 PC 시스템에서 SMBus는 사우스브릿지(PCH)의 SMBus 컨트롤러가 마스터 역할을 하며, 다양한 시스템 관리 디바이스가 슬레이브로 연결됩니다:

SMBus 시스템 토폴로지 (PC 메인보드) CPU / PCH (SMBus Controller) i2c-i801 / i2c-piix4 드라이버 SMBCLK SMBDAT SMBALERT# PMIC 전원 관리 IC Addr: 0x08 DIMM SPD 메모리 정보 EEPROM Addr: 0x50~0x57 Fan Controller 팬 속도 제어/모니터 Addr: 0x2E Temp Sensor 보드 온도 감지 Addr: 0x48~0x4F Battery Ctrl SBS 배터리 관리 Addr: 0x0B (SBS) VDD Pull-up (1k~10kΩ) SMBALERT#: 슬레이브가 호스트에 주의를 요청하는 인터럽트 라인 (active low, wired-OR) 호스트는 Alert Response Address(0x0C)로 Read하여 알림 발생 디바이스 식별

PEC (Packet Error Checking)

SMBus PEC는 CRC-8 알고리즘(다항식: x8 + x2 + x + 1, 초기값 0x00)을 사용하여 전송 데이터의 무결성(Integrity)을 검증합니다. PEC 바이트는 트랜잭션의 마지막에 추가되며, 주소 바이트(R/W 비트 포함)부터 마지막 데이터 바이트까지 모든 전송 바이트에 대해 CRC를 계산합니다.

/* 커널의 SMBus PEC 계산 (drivers/i2c/i2c-core-smbus.c) */
static u8 i2c_smbus_pec(u8 crc, u8 *p, size_t count)
{
    /* CRC-8 다항식: x^8 + x^2 + x + 1 = 0x107 */
    for (int i = 0; i < count; i++)
        crc = crc8(crc8_table, p[i], crc);
    return crc;
}

/* PEC 사용을 위한 클라이언트 플래그 설정 */
client->flags |= I2C_CLIENT_PEC;

/* PEC가 활성화된 SMBus 읽기 - 자동으로 PEC 검증 */
int val = i2c_smbus_read_byte_data(client, reg);
/* PEC 불일치 시 -EBADMSG 반환 */

SMBus ARP (Address Resolution Protocol)

SMBus ARP는 디바이스에 동적으로 주소를 할당하는 프로토콜입니다. 여러 동일 디바이스가 같은 버스에 연결될 때 주소 충돌 없이 식별할 수 있습니다. 각 디바이스는 고유한 128비트 UDID (Unique Device Identifier)를 가지며, 호스트가 이를 기반으로 7비트 주소를 할당합니다.

SMBus ARP (Address Resolution Protocol) 흐름 SMBus Host Device A Device B 1. Prepare to ARP (General Call) 2. Reset Device ARP (0x02) 3. Get UDID (General) → 0x61 Rd Device A: UDID (128-bit) + PEC Device B: UDID (arb 경쟁) UDID arbitration 4. Assign Address (UDID + 새 주소 0x2A) ACK - Device A = 0x2A 주소 할당 완료 5. Get UDID (General) → Device B 응답 6. Assign Address (UDID + 새 주소 0x2B) ACK - Device B = 0x2B 주소 할당 완료
ARP 주소: SMBus ARP에서 사용하는 고정 주소는 0x61 (SMBus Device Default Address)과 0x6C (ARP Controller) 입니다. 호스트는 General Call(0x00)로 ARP 명령을 브로드캐스트하고, 할당되지 않은 디바이스는 0x61로 응답합니다.

Linux 커널 SMBus 구현

커널의 SMBus 지원은 drivers/i2c/i2c-core-smbus.c에 구현되어 있습니다. i2c_smbus_xfer()가 핵심 함수이며, 어댑터가 네이티브 SMBus를 지원하면 i2c_algorithm.smbus_xfer를 호출하고, 그렇지 않으면 i2c_smbus_xfer_emulated()를 통해 일반 I2C 메시지로 에뮬레이션합니다.

/* SMBus functionality 플래그 확인 */
if (!i2c_check_functionality(client->adapter,
        I2C_FUNC_SMBUS_READ_BYTE_DATA |
        I2C_FUNC_SMBUS_WRITE_BYTE_DATA)) {
    dev_err(&client->dev, "SMBus byte data not supported\\n");
    return -ENODEV;
}

/* SMBus 트랜잭션 - 어댑터가 네이티브 지원하면 HW 경로, 아니면 에뮬레이션 */
s32 val = i2c_smbus_read_word_data(client, 0x00);
if (val < 0)
    return val;
dev_info(&client->dev, "reg 0x00 = 0x%04x\\n", val);

/* Block Read: 가변 길이 데이터 읽기 */
u8 buf[I2C_SMBUS_BLOCK_MAX];  /* 최대 32바이트 */
s32 len = i2c_smbus_read_block_data(client, 0x10, buf);
if (len < 0)
    return len;
dev_info(&client->dev, "block read %d bytes\\n", len);

SMBus Alert 처리

SMBus Alert는 슬레이브 디바이스가 호스트에 이벤트를 알리는 메커니즘입니다. 슬레이브가 SMBALERT# 라인을 LOW로 구동하면, 호스트가 Alert Response Address(0x0C)로 Read 트랜잭션을 수행하여 알림 발생 디바이스의 주소를 얻습니다.

#include <linux/i2c.h>
#include <linux/i2c-smbus.h>

/* SMBus Alert 핸들러 콜백 */
static void my_smbus_alert(struct i2c_client *client,
                            enum i2c_alert_protocol type,
                            unsigned int data)
{
    struct my_device *priv = i2c_get_clientdata(client);

    dev_info(&client->dev, "SMBus alert received, data=0x%x\\n", data);

    /* 알림 원인 확인 후 처리 */
    schedule_work(&priv->alert_work);
}

static struct i2c_driver my_driver = {
    .driver = {
        .name = "my-smbus-device",
        .of_match_table = my_of_ids,
    },
    .probe    = my_probe,
    .alert    = my_smbus_alert,  /* Alert 핸들러 등록 */
    .id_table = my_ids,
};

SMBus 3.x 신규 기능

기능SMBus 2.0SMBus 3.0+
최대 클럭100 kHz1 MHz
Block 최대 크기32 바이트255 바이트
Zone Read/Write미지원지원 (다중 디바이스 동시 설정)
32-bit Process Call미지원지원 (4바이트 데이터)
High Power350 μA max4 mA max (high-power class)

SMBus/I2C 디버깅 도구

i2c-tools 패키지는 SMBus/I2C 버스 디버깅에 필수적인 명령행 도구를 제공합니다:

/* i2cdetect: 버스 스캔 (SMBus Quick Command 사용) */
$ i2cdetect -y 0       /* i2c-0 버스의 모든 주소 스캔 */
$ i2cdetect -l          /* 시스템의 I2C 어댑터 목록 */

/* i2cdump: 디바이스 전체 레지스터 덤프 */
$ i2cdump -y 0 0x48    /* i2c-0 버스, 주소 0x48 전체 읽기 */
$ i2cdump -y 0 0x48 b  /* SMBus byte read 모드 */
$ i2cdump -y 0 0x48 w  /* SMBus word read 모드 */

/* i2cget: 특정 레지스터 읽기 */
$ i2cget -y 0 0x48 0x00 b  /* byte read: 온도값 */
$ i2cget -y 0 0x48 0x00 w  /* word read */

/* i2cset: 특정 레지스터 쓰기 */
$ i2cset -y 0 0x48 0x01 0x60 b  /* config 레지스터 설정 */

/* i2ctransfer: raw I2C 메시지 전송 */
$ i2ctransfer -y 0 w2@0x48 0x00 0x01 r2  /* 2바이트 쓰고 2바이트 읽기 */
커널 tracepoint: trace-cmd record -e i2c로 I2C/SMBus 트랜잭션을 커널 레벨에서 추적할 수 있습니다. i2c_read, i2c_write, i2c_result, smbus_read, smbus_write, smbus_result tracepoint가 제공됩니다.

I3C 서브시스템 개요

I3C (Improved Inter-Integrated Circuit)는 MIPI Alliance가 표준화한 차세대 직렬 버스로, I2C와의 하위 호환성을 유지하면서 성능과 기능을 대폭 개선했습니다.

특성I2C (Fm+)I3C (SDR)I3C (HDR-DDR)
최대 클럭1 MHz12.5 MHz12.5 MHz
최대 데이터율1 Mbps12.5 Mbps25 Mbps
주소 할당정적동적 (DAA)동적 (DAA)
In-Band Interrupt별도 IRQ 라인 필요SDA로 IBI 지원SDA로 IBI 지원
핫조인미지원지원지원

Linux I3C 서브시스템은 drivers/i3c/에 위치하며, i3c_master_controller, i3c_device, i3c_driver 구조체를 사용합니다:

#include <linux/i3c/device.h>
#include <linux/i3c/master.h>

static int my_i3c_probe(struct i3c_device *i3cdev)
{
    struct device *dev = i3cdev_to_dev(i3cdev);
    struct i3c_priv_xfer xfer;
    u8 tx_buf = 0x00;
    u8 rx_buf[2];

    /* I3C private transfer: 레지스터 읽기 */
    xfer.rnw = 0;
    xfer.len = 1;
    xfer.data.out = &tx_buf;
    i3c_device_do_priv_xfers(i3cdev, &xfer, 1);

    xfer.rnw = 1;
    xfer.len = 2;
    xfer.data.in = rx_buf;
    i3c_device_do_priv_xfers(i3cdev, &xfer, 1);

    dev_info(dev, "sensor value: 0x%02x%02x\\n", rx_buf[0], rx_buf[1]);
    return 0;
}

static const struct i3c_device_id my_i3c_ids[] = {
    I3C_DEVICE(0x0123, 0x4567, NULL),
    { }
};

static struct i3c_driver my_i3c_driver = {
    .driver.name = "my-i3c-sensor",
    .probe = my_i3c_probe,
    .id_table = my_i3c_ids,
};
module_i3c_driver(my_i3c_driver);

I3C 버스 토폴로지

I3C 버스는 I3C 마스터(Main Master), I3C 타겟 디바이스, 그리고 레거시 I2C 디바이스가 동일한 2-wire 버스에 공존할 수 있습니다. I3C 디바이스는 푸시풀(push-pull) 출력을 사용하고, I2C 레거시 디바이스는 오픈 드레인 모드에서 동작합니다.

I3C 버스 토폴로지: I3C + Legacy I2C 공존 I3C Main Master i3c_master_controller (Push-Pull) SCL SDA I3C Bus (Push-Pull / Open-Drain 혼합) I3C Target A 가속도계 (IBI 지원) DAA 동적 주소: 0x09 I3C Target B 온도 센서 (HDR-DDR) DAA 동적 주소: 0x0A I3C Secondary Master 승격 가능 DAA 동적 주소: 0x0B I2C Device EEPROM (Open-Drain) 정적 주소: 0x50 I2C Device RTC (Open-Drain) 정적 주소: 0x68 IBI 발생 가능 I3C 디바이스 (Push-Pull, DAA) Legacy I2C 디바이스 (Open-Drain, 정적 주소) Secondary Master (Master 요청 가능)

DAA (Dynamic Address Assignment)

I3C의 핵심 기능 중 하나인 DAA는 마스터가 버스 상의 모든 I3C 디바이스에 동적으로 7비트 주소를 할당하는 절차입니다. 각 I3C 디바이스는 48비트 Provisional ID (PID)를 가지며, 마스터는 ENTDAA CCC를 사용하여 디바이스를 열거합니다.

단계동작설명
1ENTDAA CCC 브로드캐스트마스터가 ENTDAA(0x07) Common Command Code를 전송
2디바이스 PID 응답할당되지 않은 디바이스가 48-bit PID + BCR + DCR 전송
3Arbitration여러 디바이스가 동시 응답 시, PID 기반 arbitration (낮은 PID 우선)
4주소 할당마스터가 승리한 디바이스에 7-bit 동적 주소 할당
5반복모든 디바이스에 주소가 할당될 때까지 2-4 반복
PID 구성: 48-bit Provisional ID는 MIPI Manufacturer ID (16-bit), Part ID (16-bit), Instance ID (4-bit), JEDEC MFG ID 유무 (1-bit) 등으로 구성됩니다. BCR (Bus Characteristics Register)은 디바이스의 IBI 지원, HDR 모드 지원, 오프라인 가능 여부를 나타내며, DCR (Device Characteristics Register)은 디바이스 타입(가속도계, 자이로, 온도 등)을 식별합니다.

IBI (In-Band Interrupt)

I3C의 IBI는 별도의 인터럽트 라인 없이 SDA 버스를 통해 타겟 디바이스가 마스터에 인터럽트를 요청하는 메커니즘입니다. 타겟은 버스가 유휴 상태(Idle State)일 때 START 조건을 발생시키고 자신의 동적 주소를 전송합니다. 마스터는 이를 감지하여 ACK/NACK으로 응답합니다.

I3C In-Band Interrupt (IBI) 흐름 I3C Master I3C Target 버스 유휴 (IDLE) 1. Target: SDA LOW (START) 2. 동적 주소 + R=1 + IBI 플래그 3. Master: ACK (IBI 수락) 4. MDB (Mandatory Data Byte) 전송 5. 추가 데이터 (선택적) 6. STOP - IBI 처리 완료 IBI 핵심 특징 - 별도 IRQ 라인 불필요 (SDA 사용) - ENEC/DISEC CCC로 IBI 활성화/비활성화 - MDB: 인터럽트 원인 코드 - 주소 arbitration으로 우선순위 결정 - Master NACK 시 Target 재시도 - Hot-Join도 IBI 메커니즘 활용
/* I3C IBI 핸들러 등록 */
static void my_i3c_ibi_handler(struct i3c_device *dev,
                                const struct i3c_ibi_payload *payload)
{
    /* MDB (Mandatory Data Byte) 확인 */
    if (payload->len > 0) {
        u8 mdb = ((u8 *)payload->data)[0];
        dev_dbg(i3cdev_to_dev(dev),
                "IBI received, MDB=0x%02x\\n", mdb);
    }
    /* 인터럽트 처리 로직 */
}

/* probe에서 IBI 요청 */
struct i3c_ibi_setup ibi_setup = {
    .handler = my_i3c_ibi_handler,
    .max_payload_len = 2,
    .num_slots = 4,
};
ret = i3c_device_request_ibi(i3cdev, &ibi_setup);
if (ret)
    return ret;
i3c_device_enable_ibi(i3cdev);

HDR (High Data Rate) 모드

I3C는 SDR(Standard Data Rate) 외에 3가지 HDR 모드를 지원하여 더 높은 데이터 전송률을 달성합니다:

모드최대 데이터율특징용도
HDR-DDR25 MbpsDouble Data Rate, SCL 양 에지에서 데이터 전송, CRC-5 오류 검출고속 센서 데이터 스트리밍
HDR-TSP33 MbpsTernary Symbol Pure, 3-level 신호 (SDA만 사용), SCL 불필요짧은 배선, 최고 속도 요구
HDR-TSL33 MbpsTernary Symbol Legacy, TSP + I2C 레거시 디바이스 호환혼합 버스 환경 고속 전송
/* HDR-DDR 전송 */
struct i3c_priv_xfer hdr_xfer = {
    .rnw = 0,
    .len = 64,
    .data.out = tx_data,
};

/* HDR 모드는 i3c_device_do_priv_xfers()에서
   컨트롤러가 지원하면 자동 선택될 수 있음 */
ret = i3c_device_do_priv_xfers(i3cdev, &hdr_xfer, 1);

I3C Hot-Join 프로토콜

Hot-Join은 버스가 이미 동작 중일 때 새로운 I3C 디바이스가 동적으로 참여하는 메커니즘입니다. 새 디바이스가 버스에 연결되면 IBI 유사한 방식으로 마스터에 알리고, 마스터가 DAA를 수행하여 주소를 할당합니다.

단계주체동작
1새 디바이스버스가 유휴일 때 SDA를 LOW로 구동 (Hot-Join 요청)
2마스터Hot-Join 요청 감지, 주소 0x02 + R=1로 응답
3마스터ENTDAA CCC 발행하여 새 디바이스에 동적 주소 할당
4마스터할당 완료 후 새 디바이스의 드라이버 바인딩

CCC (Common Command Codes)

I3C CCC는 마스터가 모든 디바이스(브로드캐스트) 또는 특정 디바이스(다이렉트)에 보내는 표준 명령입니다:

CCC코드유형설명
ENEC0x00Broadcast/Direct이벤트 활성화 (IBI, MR, HJ 허용)
DISEC0x01Broadcast/Direct이벤트 비활성화
ENTAS0~30x02~0x05Broadcast/DirectActivity State 진입 (저전력 모드)
RSTDAA0x06Broadcast모든 동적 주소 리셋
ENTDAA0x07Broadcast동적 주소 할당 시작
SETDASA0x87Direct정적→동적 주소 매핑(Mapping)
SETNEWDA0x88Direct새 동적 주소 설정
GETPID0x8DDirectProvisional ID 조회 (48-bit)
GETBCR0x8EDirectBus Characteristics Register 조회
GETDCR0x8FDirectDevice Characteristics Register 조회
GETSTATUS0x90Direct디바이스 상태 조회 (IBI pending 등)
GETMXDS0x94Direct최대 데이터 속도 조회
ENTHDR0~70x20~0x27BroadcastHDR 모드 진입

I2C에서 I3C으로 마이그레이션

항목변경 사항호환성
물리 계층Push-Pull 출력 (I3C), 풀업 저항 제거 가능I2C 디바이스는 여전히 오픈 드레인으로 동작
주소 할당정적 → DAA 동적 할당I2C 디바이스는 SETDASA로 정적→동적 매핑
인터럽트GPIO IRQ → SDA IBII2C 디바이스는 여전히 별도 IRQ 라인 필요
속도12.5 MHz SDR, 최대 33 Mbps HDR레거시 모드에서 I2C Fm+ 속도 지원
커널 APIi2c_driveri3c_driveri3c_device_do_priv_xfers() 사용
Device Treereg 대신 PID/BCR/DCR 기반I2C 디바이스는 i3c-i2c-dev 노드로 기술
I3C 커널 지원 현황: Linux I3C 서브시스템은 v5.0에서 도입되었으며, drivers/i3c/에 Cadence, DW(DesignWare), SVC(Silvaco) 등의 컨트롤러 드라이버가 포함되어 있습니다. I3C Target 모드 지원은 v6.x에서 추가되고 있으며, HDR 모드 지원은 컨트롤러별로 다릅니다.

regmap: 레지스터 추상화 API

regmap은 다양한 버스(I2C, SPI, MMIO) 위의 레지스터 접근을 통합 추상화하는 프레임워크입니다. I2C 드라이버에서는 devm_regmap_init_i2c()를 통해 버스별 전송 함수 호출을 제거하고, 캐싱, 범위 검사, endian 변환 등 공통 기능을 투명하게 제공합니다. SPI에서의 regmap 사용은 SPI 서브시스템 문서를 참조하세요.

regmap 설정

#include <linux/regmap.h>

/* 레지스터 기본값 (캐시 초기화용) */
static const struct reg_default my_reg_defaults[] = {
    { 0x00, 0x0000 },  /* STATUS */
    { 0x01, 0x001F },  /* CONFIG */
    { 0x02, 0x0000 },  /* DATA */
};

/* 읽기 가능 레지스터 범위 */
static bool my_readable_reg(struct device *dev, unsigned int reg)
{
    return reg <= 0x10;
}

/* 휘발성 레지스터 (캐시하지 않음) */
static bool my_volatile_reg(struct device *dev, unsigned int reg)
{
    return reg == 0x00 || reg == 0x02;  /* STATUS, DATA */
}

static const struct regmap_config my_regmap_config = {
    .reg_bits      = 8,         /* 레지스터 주소 비트 수 */
    .val_bits      = 16,        /* 레지스터 값 비트 수 */
    .max_register  = 0x10,
    .readable_reg  = my_readable_reg,
    .volatile_reg  = my_volatile_reg,
    .cache_type    = REGCACHE_RBTREE,
    .reg_defaults  = my_reg_defaults,
    .num_reg_defaults = ARRAY_SIZE(my_reg_defaults),
};

버스별 regmap 생성

/* I2C regmap */
static int my_i2c_probe(struct i2c_client *client)
{
    struct regmap *regmap;

    regmap = devm_regmap_init_i2c(client, &my_regmap_config);
    if (IS_ERR(regmap))
        return PTR_ERR(regmap);
    /* ... */
}

/* SPI regmap (참고: SPI 서브시스템 문서 참조) */
static int my_spi_probe(struct spi_device *spi)
{
    struct regmap *regmap;

    regmap = devm_regmap_init_spi(spi, &my_regmap_config);
    if (IS_ERR(regmap))
        return PTR_ERR(regmap);
    /* ... */
}

/* MMIO regmap */
static int my_platform_probe(struct platform_device *pdev)
{
    void __iomem *base;
    struct regmap *regmap;

    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    regmap = devm_regmap_init_mmio(&pdev->dev, base, &my_regmap_config);
    if (IS_ERR(regmap))
        return PTR_ERR(regmap);
    /* ... */
}

regmap 사용

unsigned int val;
int ret;

/* 단일 레지스터 읽기 */
ret = regmap_read(regmap, 0x00, &val);
if (ret)
    return ret;

/* 단일 레지스터 쓰기 */
ret = regmap_write(regmap, 0x01, 0x1234);

/* 비트 필드 업데이트 (read-modify-write 원자적 수행) */
ret = regmap_update_bits(regmap, 0x01,
                         0x00FF,    /* mask */
                         0x0042);   /* value */

/* 벌크 읽기 */
u16 buf[4];
ret = regmap_bulk_read(regmap, 0x00, buf, 4);

/* 폴링: 비트가 설정될 때까지 대기 */
ret = regmap_read_poll_timeout(regmap, 0x00, val,
                               val & BIT(0),     /* condition */
                               1000,              /* sleep_us */
                               100000);           /* timeout_us */
regmap 캐시(Cache) 타입: REGCACHE_NONE (캐시 없음), REGCACHE_RBTREE (희소 레지스터용, RB 트리), REGCACHE_FLAT (연속 레지스터용, 배열), REGCACHE_MAPLE (v6.4+, maple tree 기반). 휘발성(volatile) 레지스터는 항상 하드웨어에서 직접 읽습니다.

regmap IRQ 프레임워크

regmap-irq는 레지스터 기반 인터럽트 컨트롤러(Interrupt Controller)를 위한 generic IRQ chip을 제공합니다:

static const struct regmap_irq my_irqs[] = {
    REGMAP_IRQ_REG(0, 0, BIT(0)),  /* IRQ 0: bit 0 of reg 0 */
    REGMAP_IRQ_REG(1, 0, BIT(1)),  /* IRQ 1: bit 1 of reg 0 */
    REGMAP_IRQ_REG(2, 0, BIT(2)),  /* IRQ 2: bit 2 of reg 0 */
};

static const struct regmap_irq_chip my_irq_chip = {
    .name           = "my-device",
    .irqs           = my_irqs,
    .num_irqs       = ARRAY_SIZE(my_irqs),
    .num_regs       = 1,
    .status_base    = 0x08,  /* 인터럽트 상태 레지스터 */
    .mask_base      = 0x09,  /* 인터럽트 마스크 레지스터 */
    .ack_base       = 0x08,  /* ACK = 상태 레지스터에 W1C */
};

/* probe에서 등록 */
struct regmap_irq_chip_data *irq_data;

ret = devm_regmap_add_irq_chip(&client->dev, regmap,
        client->irq, IRQF_ONESHOT, 0,
        &my_irq_chip, &irq_data);

/* 하위 디바이스에서 가상 IRQ 사용 */
int virq = regmap_irq_get_virq(irq_data, 0);

regmap 내부 아키텍처

regmap 프레임워크는 버스 추상화 계층(bus abstraction layer)과 캐시 백엔드로 구성됩니다. 핵심 구조체 struct regmap은 드라이버에 불투명(opaque)하며, 내부적으로 버스 콜백, 캐시 상태, 잠금(Lock) 메커니즘을 관리합니다.

struct regmap 개념 구조 - bus / bus_context: 버스 콜백과 컨텍스트 - config: reg_bits, val_bits, stride, volatile/writeable/readable 정책 - cache_ops / cache: 레지스터 캐시 백엔드와 데이터 - mutex/spinlock + lock/unlock callback: 동기화 - patch / async_list: 초기 패치와 비동기 I/O 관리
/* include/linux/regmap.h - regmap 핵심 구조 (개념도) */

/* struct regmap 내부 주요 필드(위 SVG 참고) */

/* 버스 추상화: 각 버스 타입이 이 콜백을 구현 */
struct regmap_bus {
    bool fast_io;                   /* true면 spinlock 사용 */
    int (*write)(void *context,      /* 레지스터 쓰기 */
                 const void *data, size_t count);
    int (*read)(void *context,       /* 레지스터 읽기 */
                const void *reg_buf, size_t reg_size,
                void *val_buf, size_t val_size);
    int (*reg_write)(void *context,  /* 단일 reg 쓰기 (선택) */
                     unsigned int reg, unsigned int val);
    int (*reg_read)(void *context,   /* 단일 reg 읽기 (선택) */
                    unsigned int reg, unsigned int *val);
    int (*reg_update_bits)(void *context, /* RMW 최적화 (선택) */
                          unsigned int reg,
                          unsigned int mask, unsigned int val);
    enum regmap_endian val_format_endian_default; /* 버스 기본 엔디안 */
};
버스 타입별 초기화 함수: devm_regmap_init_i2c(), devm_regmap_init_spi(), devm_regmap_init_mmio() 외에도 devm_regmap_init_spi_avmm() (SPI Avalon-MM), devm_regmap_init_spmi_base/ext() (SPMI), devm_regmap_init_w1() (1-Wire), devm_regmap_init_sdw() (SoundWire), devm_regmap_init_slimbus() (SLIMbus) 등 다양한 버스를 지원합니다. fast_io = true인 버스(MMIO 등)는 mutex 대신 spinlock을 사용하여 원자적(Atomic) 컨텍스트에서도 접근 가능합니다.

regmap_field: 비트 필드 추상화

regmap_field는 레지스터 내 특정 비트 필드를 독립적인 객체로 추상화합니다. 비트 마스크/시프트 연산을 캡슐화(Encapsulation)하여 드라이버 코드의 가독성과 유지보수성을 높입니다.

0x04 레지스터 비트 필드 bit15 EN bit14:12 MODE bit11:8 GAIN bit7:0 OFFSET
#include <linux/regmap.h>

/* 레지스터 필드 정의 매크로 */
/* REG_FIELD(reg, lsb, msb) - 레지스터 주소와 비트 범위 지정 */

/* 예: 0x04 레지스터 레이아웃 (위 SVG 참고) */
static const struct reg_field enable_field  = REG_FIELD(0x04, 15, 15);
static const struct reg_field mode_field    = REG_FIELD(0x04, 12, 14);
static const struct reg_field gain_field    = REG_FIELD(0x04, 8,  11);
static const struct reg_field offset_field  = REG_FIELD(0x04, 0,  7);

struct my_device {
    struct regmap *regmap;
    struct regmap_field *f_enable;
    struct regmap_field *f_mode;
    struct regmap_field *f_gain;
    struct regmap_field *f_offset;
};

static int my_probe(struct i2c_client *client)
{
    struct my_device *dev;
    struct regmap *regmap;

    dev = devm_kzalloc(&client->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    regmap = devm_regmap_init_i2c(client, &my_regmap_config);
    if (IS_ERR(regmap))
        return PTR_ERR(regmap);
    dev->regmap = regmap;

    /* 필드 객체 할당 */
    dev->f_enable = devm_regmap_field_alloc(&client->dev, regmap, enable_field);
    if (IS_ERR(dev->f_enable))
        return PTR_ERR(dev->f_enable);

    dev->f_mode = devm_regmap_field_alloc(&client->dev, regmap, mode_field);
    dev->f_gain = devm_regmap_field_alloc(&client->dev, regmap, gain_field);
    dev->f_offset = devm_regmap_field_alloc(&client->dev, regmap, offset_field);

    /* 필드 접근: 비트 마스크/시프트 불필요 */
    regmap_field_write(dev->f_enable, 1);           /* bit 15 = 1 */
    regmap_field_write(dev->f_mode, 0x5);           /* bits 14:12 = 5 */
    regmap_field_write(dev->f_gain, 0xA);           /* bits 11:8 = 0xA */

    unsigned int val;
    regmap_field_read(dev->f_offset, &val);         /* bits 7:0 읽기 */

    /* 필드 비트 업데이트 (field 범위 내에서 RMW) */
    regmap_field_update_bits(dev->f_mode, 0x3, 0x2); /* mode 하위 2비트만 수정 */

    return 0;
}

/* 벌크 필드 할당 (v5.3+): 여러 필드를 한 번에 할당 */
static const struct reg_field my_fields[] = {
    [F_ENABLE] = REG_FIELD(0x04, 15, 15),
    [F_MODE]   = REG_FIELD(0x04, 12, 14),
    [F_GAIN]   = REG_FIELD(0x04, 8,  11),
    [F_OFFSET] = REG_FIELD(0x04, 0,  7),
};

struct regmap_field *fields[F_MAX];
ret = devm_regmap_field_bulk_alloc(&client->dev, regmap,
                                   fields, my_fields, F_MAX);
regmap_field vs regmap_update_bits: 직접 regmap_update_bits(regmap, 0x04, 0x7000, 0x5000)로 작성하면 매직 넘버가 코드 전체에 흩어집니다. regmap_field를 사용하면 필드 정의가 한곳에 집중되고, 드라이버 코드는 regmap_field_write(f_mode, 5)처럼 의미가 명확해집니다. IIO, regulator, clock 등 커널 서브시스템 드라이버에서 널리 사용됩니다.

regmap 캐시

regmap 캐시는 레지스터 값의 로컬 복사본을 유지하여 불필요한 버스 트랜잭션을 줄입니다. 전원 관리와 긴밀하게 연동되며, 캐시 상태 관리를 통해 resume 시 효율적인 레지스터 복원을 수행합니다.

regmap 캐시 상태 머신 CLEAN (HW=SW) cache_only=0 DIRTY (HW!=SW) cache_only=1 또는 write 누적 regmap_write()로 DIRTY 전이, resume 시 cache_sync()로 CLEAN 복귀
/*
 * regmap 캐시 상태 머신 (위 SVG 참고)
 */

/* 캐시 제어 API */

/* 캐시 전용 모드: 버스 접근 차단, 캐시만 갱신 */
regcache_cache_only(regmap, true);   /* suspend 시 */
regmap_write(regmap, 0x01, 0x42);    /* 캐시에만 기록, dirty 마킹 */
regcache_cache_only(regmap, false);  /* resume 시 */

/* 캐시 → HW 동기화: dirty 레지스터만 하드웨어에 기록 */
ret = regcache_sync(regmap);

/* 특정 범위만 동기화 */
ret = regcache_sync_region(regmap, 0x00, 0x0F);

/* 캐시 전체를 dirty로 마킹 (resume 시 전체 복원 강제) */
regcache_mark_dirty(regmap);

/* 캐시 무효화: 캐시된 값 폐기, 다음 읽기 시 HW 접근 */
ret = regcache_drop_region(regmap, 0x00, 0x0F);

/* 캐시 바이패스: 일시적으로 캐시 우회, HW 직접 접근 */
regcache_cache_bypass(regmap, true);
regmap_read(regmap, 0x00, &val);      /* HW에서 직접 읽기 */
regcache_cache_bypass(regmap, false);
캐시 타입자료구조적합한 경우메모리 사용
REGCACHE_NONE없음캐시 불필요 (volatile 위주)0
REGCACHE_FLAT배열연속/조밀한 레지스터 맵O(max_register)
REGCACHE_RBTREERed-Black Tree희소(sparse) 레지스터 맵O(사용 레지스터 수)
REGCACHE_MAPLEMaple Treev6.4+, 범위 기반 최적화O(사용 레지스터 수)

regmap 전원 관리 연동

regmap 캐시는 시스템 suspend/resume과 runtime PM에서 핵심적인 역할을 합니다. 전원 차단 시 캐시 전용 모드로 전환하고, 복원 시 dirty 레지스터만 하드웨어에 동기화하여 resume 시간을 최소화합니다.

/* 시스템 suspend/resume */
static int my_suspend(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    /* 디바이스 비활성화 */
    regmap_update_bits(mydev->regmap, 0x01, BIT(0), 0);

    /* 캐시 전용 모드: 이후 접근은 캐시에만 기록 */
    regcache_cache_only(mydev->regmap, true);

    /* 전체 캐시를 dirty로 마킹: resume 시 전체 복원 */
    regcache_mark_dirty(mydev->regmap);

    return 0;
}

static int my_resume(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);
    int ret;

    /* 캐시 전용 모드 해제: 버스 접근 재개 */
    regcache_cache_only(mydev->regmap, false);

    /* dirty 레지스터만 HW에 동기화 */
    ret = regcache_sync(mydev->regmap);
    if (ret)
        dev_err(dev, "regcache sync failed: %d\\n", ret);

    return ret;
}
PM 연동 주의사항:
  • regcache_mark_dirty()는 반드시 regcache_cache_only(true) 이후에 호출해야 합니다. 순서가 바뀌면 dirty 마킹 후 HW 동기화가 시도되어 전원 차단된 디바이스에 버스 접근이 발생할 수 있습니다
  • resume 시 regcache_cache_only(false)는 반드시 HW가 준비된 이후에 호출하세요
  • 디바이스가 소프트 리셋이 아닌 완전 전원 차단을 거치면, POR(Power-On Reset) 기본값이 적용되므로 mark_dirty로 전체 복원이 필요합니다

regmap debugfs 디버깅

regmap은 자동으로 debugfs 인터페이스를 생성하여 런타임에 레지스터 값을 검사할 수 있습니다. CONFIG_DEBUG_FSCONFIG_REGMAP이 활성화되어야 합니다.

# regmap debugfs 위치
/sys/kernel/debug/regmap/

# 디바이스별 디렉토리 예시
/sys/kernel/debug/regmap/0-001a/        # I2C bus 0, addr 0x1a

# 레지스터 값 확인
$ cat /sys/kernel/debug/regmap/0-001a/registers
00: 0042
01: 001f
02: 0000
03: abcd
...

# 접근 권한 확인
$ cat /sys/kernel/debug/regmap/0-001a/access
00: RV      # R=readable, V=volatile
01: RW      # R=readable, W=writable
02: RWv     # v=volatile (소문자 = 캐시 안 함)
0a: RWP     # P=precious (debugfs dump 제외)

# 캐시 상태 확인
$ cat /sys/kernel/debug/regmap/0-001a/cache_only
N           # Y=캐시 전용 모드 활성
디버깅 팁: registers 파일에 값을 쓰면 런타임에 레지스터를 변경할 수 있습니다: echo "01 abcd" > registers. 이는 프로토타이핑 시 매우 유용하지만, precious 레지스터는 dump에서 자동 제외되므로 FIFO 데이터가 의도치 않게 소비되는 문제를 방지합니다.

MFD 디바이스와 regmap 공유

Multi-Function Device(MFD)에서는 하나의 regmap을 여러 서브 디바이스가 공유합니다. 부모 MFD 드라이버가 regmap을 생성하고, 자식 드라이버가 dev_get_regmap()으로 접근합니다.

/* ===== MFD 부모 드라이버 (PMIC 예시) ===== */

static const struct regmap_config pmic_regmap_config = {
    .reg_bits      = 8,
    .val_bits      = 8,
    .max_register  = 0xFF,
    .cache_type    = REGCACHE_RBTREE,
};

/* MFD 셀 정의 */
static const struct mfd_cell pmic_cells[] = {
    { .name = "pmic-regulator" },
    { .name = "pmic-gpio" },
    { .name = "pmic-rtc" },
    { .name = "pmic-charger" },
};

static int pmic_i2c_probe(struct i2c_client *client)
{
    struct regmap *regmap;

    /* regmap 생성: dev에 자동 연결됨 */
    regmap = devm_regmap_init_i2c(client, &pmic_regmap_config);
    if (IS_ERR(regmap))
        return PTR_ERR(regmap);

    /* MFD 서브디바이스 등록 */
    return devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE,
                               pmic_cells, ARRAY_SIZE(pmic_cells),
                               NULL, 0, NULL);
}

/* ===== MFD 자식 드라이버 (regulator 예시) ===== */

static int pmic_regulator_probe(struct platform_device *pdev)
{
    struct regmap *regmap;

    /* 부모 디바이스의 regmap 획득 */
    regmap = dev_get_regmap(pdev->dev.parent, NULL);
    if (!regmap) {
        dev_err(&pdev->dev, "parent regmap not found\\n");
        return -ENODEV;
    }

    /* 이제 공유 regmap으로 레지스터 접근 */
    regmap_write(regmap, 0x30, 0x01);  /* regulator 제어 레지스터 */

    return 0;
}

Device Tree 통합: I2C 바인딩 패턴

I2C 서브시스템의 Device Tree 바인딩에서 공통적으로 사용되는 패턴을 정리합니다.

공통 프로퍼티

프로퍼티적용 대상설명
compatible모든 디바이스드라이버 매칭 문자열 (vendor,device)
regI2C 슬레이브 주소7비트 I2C 주소
interrupts인터럽트 사용 디바이스IRQ 스펙
interrupt-parent인터럽트 사용 디바이스IRQ 컨트롤러 phandle
status모든 노드"okay", "disabled"
*-gpiosGPIO 사용 디바이스GPIO specifier
*-supply전원 사용 디바이스regulator phandle
clock-frequencyI2C 컨트롤러버스 클럭 속도 (100000, 400000 등)
pinctrl-*핀 설정 필요 디바이스pinctrl 상태

종합 예제: I2C 디바이스 구성

실제 임베디드 보드에서 I2C 센서와 GPIO expander를 함께 사용하는 Device Tree 예제:

&i2c1 {
    status = "okay";
    clock-frequency = <400000>;
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&i2c1_default>;
    pinctrl-1 = <&i2c1_sleep>;

    /* 온습도 센서 */
    htu21d@40 {
        compatible = "meas,htu21";
        reg = <0x40>;
    };

    /* 가속도계 */
    accelerometer@1d {
        compatible = "st,lis3dh";
        reg = <0x1D>;
        interrupt-parent = <&gpiob>;
        interrupts = <5 IRQ_TYPE_EDGE_RISING>;
        vdd-supply = <&reg_3v3>;
    };

    /* GPIO expander */
    gpio_exp: gpio@20 {
        compatible = "nxp,pca9555";
        reg = <0x20>;
        gpio-controller;
        #gpio-cells = <2>;
        interrupt-parent = <&gpiob>;
        interrupts = <8 IRQ_TYPE_EDGE_FALLING>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
};

Device Tree 디버깅

I2C 관련 Device Tree 문제를 디버깅하는 방법:

# I2C 버스 및 디바이스 확인
i2cdetect -l                      # 시스템의 I2C 어댑터 목록
i2cdetect -y 1                    # I2C bus 1의 디바이스 스캔
i2cget -y 1 0x48 0x00             # 0x48 디바이스의 레지스터 0x00 읽기
i2cdump -y 1 0x48                 # 전체 레지스터 덤프

# Device Tree 런타임 확인
ls /proc/device-tree/             # DT 노드 트리
dtc -I fs /proc/device-tree/      # 런타임 DT를 DTS로 디컴파일

# pinctrl 핀 매핑 확인
cat /sys/kernel/debug/pinctrl/*/pins
I2C 디바이스가 감지되지 않을 때 체크리스트: (1) i2cdetect로 주소 응답 확인, (2) dmesg | grep i2c로 어댑터 등록 확인, (3) Device Tree의 reg 속성이 실제 하드웨어 주소와 일치하는지 확인, (4) pinctrl 설정이 올바른지 확인 (SDA/SCL 핀이 I2C 기능으로 mux 되었는지), (5) 풀업 저항이 있는지 확인 (오픈 드레인 버스에 외부 풀업 필요).

참고자료

커널 공식 문서

커널 소스 코드

규격 문서

외부 자료

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.