I2C / SPI / GPIO 서브시스템

임베디드 Linux에서 가장 많이 쓰이는 I2C/SPI/GPIO 서브시스템을 보드 초기화부터 드라이버 운영까지 실무 관점으로 정리합니다. I2C/SPI 전송 모델과 버스 arbitration, GPIO descriptor 기반 안전한 제어 패턴, Device Tree 바인딩 및 pinctrl 상호작용, regmap을 활용한 레지스터 추상화, IRQ-capable GPIO 처리, 전원관리와 슬립 복귀 시 상태 복원, 로직 분석기와 tracepoint를 이용한 타이밍 문제 진단까지 하드웨어 근접 드라이버 개발에 필요한 핵심을 다룹니다.

전제 조건: 디바이스 드라이버인터럽트 문서를 먼저 읽으세요. 버스/열거/프로브 경로는 초기화 순서와 자원 등록 규칙이 핵심이므로, 장치 발견부터 바인딩까지 흐름을 먼저 고정해야 합니다.
일상 비유: 이 주제는 터미널 입출고 게이트 운영과 비슷합니다. 차량(디바이스)이 들어오면 게이트 규칙(버스 규약)에 맞춰 배정하고 점검하듯이, 드라이버도 바인딩 규약을 정확히 따라야 합니다.

핵심 요약

  • 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
  • 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
  • IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
  • 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
  • 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.

단계별 이해

  1. 장치 수명주기 확인
    probe부터 remove까지 흐름을 점검합니다.
  2. 비동기 경로 설계
    IRQ, 워크큐, 타이머 역할을 분리합니다.
  3. 자원 정합성 검증
    DMA/클록/전원 참조를 교차 확인합니다.
  4. 현장 조건 테스트
    연결 끊김/복구/부하 상황을 재현합니다.
관련 표준: I2C-bus specification (NXP UM10204), SPI (Motorola/de facto), MIPI I3C (MIPI Alliance HCI 1.0) — 이 문서에서 다루는 버스 프로토콜의 기반 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
I2C Adapter SCL/SDA SPI Controller MOSI/MISO/SCLK/CS GPIO / Pinctrl 라인 제어/멀티플렉스

I2C 프로토콜 기초

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

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

신호 프로토콜

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

조건SDA 상태SCL 상태의미
START (S)HIGH → LOWHIGH트랜잭션 시작
STOP (P)LOW → HIGHHIGH트랜잭션 종료
Repeated START (Sr)HIGH → LOWHIGHSTOP 없이 재시작
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 프로토콜의 완전한 트랜잭션 타이밍을 이해하는 것은 하드웨어 디버깅과 드라이버 개발에 필수적입니다. 아래 다이어그램은 마스터가 슬레이브에 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 bit 3: 1 bit 2: 0 bit 1: 0 bit 0: 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/ 디렉토리에 구현되어 있으며, 다음 핵심 구조체로 구성됩니다:

구조체역할헤더
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(전원 관리 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 콜백슬레이브 이벤트 통보

SMBus 시스템 토폴로지

일반적인 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)을 사용하여 전송 데이터의 무결성을 검증합니다. 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 I3C 디바이스 (Push-Pull, DAA) Legacy I2C 디바이스 (Open-Drain, 정적 주소) Secondary Master (Master 요청 가능) IBI 발생 가능

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 버스를 통해 타겟 디바이스가 마스터에 인터럽트를 요청하는 메커니즘입니다. 타겟은 버스가 유휴 상태일 때 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정적→동적 주소 매핑
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 모드 지원은 컨트롤러별로 다릅니다.

SPI 프로토콜 기초

SPI (Serial Peripheral Interface)는 Motorola가 개발한 전이중(full-duplex) 동기 직렬 버스입니다. I2C보다 빠른 속도가 필요한 ADC, DAC, 디스플레이, Flash 메모리 등에 사용됩니다.

신호선별칭역할
MOSISDO, COPI, DIMaster Out Slave In
MISOSDI, CIPO, DOMaster In Slave Out
SCKSCLK, CLKSerial Clock (마스터 생성)
CS/SSNSS, CEChip Select (Active Low)

SPI 모드 (CPOL/CPHA)

SPI는 클럭 극성(CPOL)과 클럭 위상(CPHA) 조합으로 4가지 동작 모드를 정의합니다:

모드CPOLCPHA유휴 클럭데이터 샘플링
Mode 000LOW상승 에지
Mode 101LOW하강 에지
Mode 210HIGH하강 에지
Mode 311HIGH상승 에지
SPI vs I2C 선택 기준: SPI는 전이중 통신, 높은 클럭 속도(수십 MHz), 단순한 프로토콜이 장점이지만, 디바이스당 전용 CS 라인이 필요하여 핀 수가 증가합니다. I2C는 2선으로 다수 디바이스를 연결할 수 있지만 속도가 제한됩니다.

SPI 모드별 파형과 클럭 에지 동작

SPI의 4가지 모드는 CPOL(Clock Polarity)과 CPHA(Clock Phase) 비트 조합으로 결정됩니다. 데이터 시트에서 "상승 에지에서 샘플링"이라고 적혀 있다면 Mode 0 또는 Mode 3에 해당합니다. 아래 다이어그램은 각 모드의 SCK/MOSI/MISO 타이밍 관계를 보여줍니다.

SPI Mode 0~3: SCK / MOSI / MISO Timing Mode 0 (CPOL=0, CPHA=0) 유휴=LOW, 상승 에지에서 샘플링, 하강 에지에서 전환 CS SCK MOSI ● = 샘플링 포인트 (상승 에지) Mode 1 (CPOL=0, CPHA=1) 유휴=LOW, 하강 에지에서 샘플링 CS SCK MOSI ● = 샘플링 포인트 (하강 에지) Mode 2 (CPOL=1, CPHA=0) 유휴=HIGH, 하강 에지에서 샘플링, 상승 에지에서 전환 CS SCK MOSI ● = 샘플링 포인트 (하강 에지) Mode 3 (CPOL=1, CPHA=1) 유휴=HIGH, 상승 에지에서 샘플링 CS SCK MOSI ● = 샘플링 포인트 (상승 에지) 모드별 에지 동작 정리 Mode 유휴 SCK 데이터 출력(전환) 데이터 샘플링 대표 디바이스 Mode 0 LOW 하강 에지 상승 에지 대부분의 ADC, 센서 Mode 1 LOW 상승 에지 하강 에지 일부 DAC, 디스플레이 Mode 2 HIGH 상승 에지 하강 에지 일부 SPI Flash Mode 3 HIGH 하강 에지 상승 에지 SPI NOR Flash, SD 카드 핵심: CPHA=0이면 CS 활성화 직후 첫 에지에서 샘플링, CPHA=1이면 두 번째 에지에서 샘플링 Mode 0과 Mode 3이 가장 널리 사용됨 (둘 다 상승 에지에서 샘플링하지만 유휴 클럭 극성이 다름) Linux 커널: spi-cpol = CPOL=1, spi-cpha = CPHA=1 (Device Tree 프로퍼티)

Setup Time과 Hold Time

SPI 통신의 신뢰성은 setup time(tSU)과 hold time(tHD)에 의존합니다. Setup time은 샘플링 에지 전에 데이터가 안정되어야 하는 최소 시간이고, hold time은 샘플링 에지 후에 데이터가 유지되어야 하는 최소 시간입니다:

파라미터기호일반 범위설명
Setup Time (데이터)tSU5~15 ns샘플링 에지 전 데이터 안정 시간
Hold Time (데이터)tHD5~10 ns샘플링 에지 후 데이터 유지 시간
CS SetuptCSS10~50 nsCS 활성화 후 첫 클럭까지 대기
CS HoldtCSH10~50 ns마지막 클럭 후 CS 비활성화까지
CS 비활성 간격tCSI50~100 ns연속 트랜잭션 사이 CS 최소 비활성 시간

전이중(Full Duplex) vs 반이중(Half Duplex)

SPI는 기본적으로 전이중(Full Duplex) 통신을 지원합니다. 마스터가 MOSI로 데이터를 송신하는 동시에 MISO로 데이터를 수신합니다. 그러나 일부 디바이스는 단방향 또는 반이중 모드로 동작합니다:

통신 방식데이터 라인동작Linux 커널 플래그
Full DuplexMOSI + MISO동시 양방향 전송(기본값)
Half Duplex단일 양방향 라인교대 송수신 (3-wire SPI)SPI_3WIRE
Simplex TXMOSI만송신 전용 (예: LED 드라이버)SPI_NO_RX
Simplex RXMISO만수신 전용 (예: ADC)SPI_NO_TX

전기적 특성과 신호 무결성

고속 SPI(10 MHz 이상)에서는 신호 무결성이 중요합니다. PCB 설계 시 다음 사항을 고려해야 합니다:

항목권장 값설명
배선 길이< 10 cm (50 MHz 기준)짧을수록 반사와 크로스톡 감소
임피던스 매칭50 Ω (단선) / 100 Ω (차동)고속에서 반사 최소화
풀업 저항 (CS)10 kΩ미사용 CS 라인은 풀업하여 비활성 유지
바이패스 커패시터100 nF + 10 μFVCC 핀 가까이 배치하여 노이즈 감소
Ground 플레인연속 GND 레이어신호 리턴 경로 확보, EMI 감소
클럭 속도 한계: SPI는 공식 속도 규격이 없지만, 실무에서 일반 SPI Flash는 50~133 MHz, ADC/DAC는 1~50 MHz, 디스플레이 컨트롤러는 10~80 MHz 범위로 동작합니다. 실제 최대 속도는 슬레이브 디바이스의 데이터 시트, PCB 배선 품질, 그리고 SPI 컨트롤러의 하드웨어 능력에 의해 결정됩니다.

Linux SPI 서브시스템

SPI 서브시스템은 drivers/spi/에 구현되어 있으며 다음 핵심 구조체를 사용합니다:

구조체역할헤더
spi_controllerSPI 마스터 (호스트) 컨트롤러<linux/spi/spi.h>
spi_deviceSPI 버스 상의 슬레이브 디바이스<linux/spi/spi.h>
spi_driverSPI 디바이스 드라이버<linux/spi/spi.h>
spi_messageSPI 트랜잭션 (transfer 묶음)<linux/spi/spi.h>
spi_transfer단일 전이중 전송 단위<linux/spi/spi.h>

spi_controller 구조

SPI 컨트롤러 드라이버의 핵심 콜백:

struct spi_controller {
    int (*setup)(struct spi_device *spi);
    int (*transfer_one_message)(struct spi_controller *ctlr,
                                 struct spi_message *msg);
    int (*transfer_one)(struct spi_controller *ctlr,
                         struct spi_device *spi,
                         struct spi_transfer *xfer);
    void (*set_cs)(struct spi_device *spi, bool enable);
    u32 min_speed_hz;
    u32 max_speed_hz;
    u16 num_chipselect;
    /* ... */
};

SPI 컨트롤러 레지스터 프로그래밍

SPI 컨트롤러 드라이버는 하드웨어 레지스터를 직접 프로그래밍하여 클럭 분주, FIFO 관리, DMA 채널 설정을 수행합니다. 아래 다이어그램은 SPI 컨트롤러 내부의 전송 데이터 경로를 보여줍니다.

SPI Controller Transfer Flow (TX/RX Data Path) CPU (PIO) 레지스터 Write DMA Engine TX DMA Channel TX FIFO Watermark: N entries FIFO 깊이: 16~256 TXFIFOLVL IRQ Shift Register bits_per_word (8/16/32 bit) Clock Divider f_sck = f_pclk / (2*(div+1)) MOSI Pad 직렬 출력 MISO Pad 직렬 입력 SCK Pad SPI Slave Device RX FIFO Watermark: M entries Overflow 보호 RXFIFOLVL IRQ DMA Engine RX DMA Channel CPU (PIO) 레지스터 Read RX data RX Shift Reg 병렬→FIFO 저장 TX 경로 RX 경로 Clock 분배 DMA

클럭 분주(Divider) 계산

SPI 컨트롤러의 SCK 클럭 주파수는 부모 클럭(PCLK 또는 APB 클럭)을 분주하여 생성합니다. 대부분의 컨트롤러는 짝수 분주를 사용하며, 실제 속도가 요청 속도를 초과하지 않도록 올림 처리해야 합니다:

/* SPI 클럭 분주 계산: f_sck = f_pclk / (2 * (div + 1)) */
static u32 calc_spi_clk_div(u32 pclk_hz, u32 target_hz)
{
    u32 div;

    if (target_hz >= pclk_hz / 2)
        return 0;  /* 최소 분주 = 2배 */

    /* div = ceil(pclk / (2 * target)) - 1
     * 실제 속도가 target을 넘지 않도록 올림 처리 */
    div = DIV_ROUND_UP(pclk_hz, 2 * target_hz) - 1;

    /* 하드웨어 최대값 클램프 (예: 8-bit divider → 0~255) */
    return min_t(u32, div, 0xFF);
}

FIFO 관리와 Watermark 설정

TX/RX FIFO의 watermark 레벨은 인터럽트 빈도와 전송 효율의 균형을 결정합니다. Watermark가 너무 낮으면 인터럽트가 과다 발생하고, 너무 높으면 RX FIFO 오버플로 위험이 있습니다:

Watermark 전략TX WatermarkRX Watermark특성
낮은 지연FIFO_DEPTH / 41인터럽트 빈번, 응답 빠름
균형FIFO_DEPTH / 2FIFO_DEPTH / 2범용 설정
높은 처리량FIFO_DEPTH * 3/4FIFO_DEPTH * 3/4DMA 연동 시 최적

SPI 컨트롤러 드라이버 스켈레톤

#include <linux/spi/spi.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <linux/dmaengine.h>

#define SPI_CR1       0x00   /* Control Register 1 */
#define SPI_CR2       0x04   /* Control Register 2 */
#define SPI_SR        0x08   /* Status Register */
#define SPI_DR        0x0C   /* Data Register */
#define SPI_TXFR      0x10   /* TX FIFO Threshold */
#define SPI_RXFR      0x14   /* RX FIFO Threshold */

#define SPI_CR1_SPE   BIT(0)  /* SPI Enable */
#define SPI_CR1_MSTR  BIT(2)  /* Master Mode */
#define SPI_CR1_CPOL  BIT(3)  /* Clock Polarity */
#define SPI_CR1_CPHA  BIT(4)  /* Clock Phase */
#define SPI_SR_TXE    BIT(1)  /* TX FIFO Empty */
#define SPI_SR_RXNE   BIT(0)  /* RX FIFO Not Empty */
#define SPI_SR_BSY    BIT(7)  /* Busy */

struct my_spi_priv {
    void __iomem *base;
    struct clk *clk;
    int irq;
    struct completion xfer_done;
    const u8 *tx_buf;
    u8 *rx_buf;
    int tx_len, rx_len;
};

static int my_spi_transfer_one(struct spi_controller *ctlr,
                                struct spi_device *spi,
                                struct spi_transfer *xfer)
{
    struct my_spi_priv *priv = spi_controller_get_devdata(ctlr);
    u32 cr1, div;

    /* 클럭 분주 설정 */
    div = calc_spi_clk_div(clk_get_rate(priv->clk), xfer->speed_hz);
    cr1 = readl(priv->base + SPI_CR1);
    cr1 &= ~(0xFF << 8);      /* divider 필드 클리어 */
    cr1 |= (div << 8);          /* 새 divider 설정 */

    /* SPI 모드 설정 (CPOL/CPHA) */
    if (spi->mode & SPI_CPOL)
        cr1 |= SPI_CR1_CPOL;
    else
        cr1 &= ~SPI_CR1_CPOL;
    if (spi->mode & SPI_CPHA)
        cr1 |= SPI_CR1_CPHA;
    else
        cr1 &= ~SPI_CR1_CPHA;

    writel(cr1 | SPI_CR1_SPE, priv->base + SPI_CR1);

    /* FIFO watermark 설정 (깊이의 절반) */
    writel(8, priv->base + SPI_TXFR);  /* TX threshold */
    writel(8, priv->base + SPI_RXFR);  /* RX threshold */

    /* 전송 시작: PIO 또는 DMA */
    priv->tx_buf = xfer->tx_buf;
    priv->rx_buf = xfer->rx_buf;
    priv->tx_len = xfer->len;
    priv->rx_len = xfer->len;
    reinit_completion(&priv->xfer_done);

    /* IRQ 기반 PIO: TX empty 인터럽트 활성화 */
    writel(readl(priv->base + SPI_CR2) | BIT(0),
           priv->base + SPI_CR2);

    return 1;  /* 비동기: completion 대기 */
}

SPI DMA 통합

대용량 SPI 전송(수백 바이트 이상)에서는 DMA를 사용하여 CPU 부하를 줄입니다. DMA 버퍼는 캐시 라인 정렬이 필수이며, scatter-gather 리스트로 비연속 메모리를 처리할 수 있습니다:

DMA 버퍼 정렬: DMA 전송에 사용하는 버퍼는 반드시 캐시 라인에 정렬되어야 합니다. 스택 변수나 구조체 중간의 버퍼를 DMA에 사용하면 캐시 일관성 문제로 데이터 손상이 발생할 수 있습니다. ____cacheline_aligned 속성이나 kmalloc()/dma_alloc_coherent()를 사용하세요.
/* SPI DMA 전송 설정 */
static int my_spi_dma_xfer(struct my_spi_priv *priv,
                             struct spi_transfer *xfer)
{
    struct dma_async_tx_descriptor *tx_desc, *rx_desc;
    dma_addr_t tx_dma, rx_dma;
    struct device *dev = priv->dev;

    /* TX: 메모리 → 주변장치 (MEM_TO_DEV) */
    tx_dma = dma_map_single(dev, (void *)xfer->tx_buf,
                             xfer->len, DMA_TO_DEVICE);
    if (dma_mapping_error(dev, tx_dma))
        return -ENOMEM;

    /* RX: 주변장치 → 메모리 (DEV_TO_MEM) */
    rx_dma = dma_map_single(dev, xfer->rx_buf,
                             xfer->len, DMA_FROM_DEVICE);
    if (dma_mapping_error(dev, rx_dma)) {
        dma_unmap_single(dev, tx_dma, xfer->len, DMA_TO_DEVICE);
        return -ENOMEM;
    }

    /* RX DMA 먼저 준비 (데이터 유실 방지) */
    rx_desc = dmaengine_prep_slave_single(
        priv->rx_chan, rx_dma, xfer->len,
        DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
    rx_desc->callback = my_spi_dma_rx_done;
    rx_desc->callback_param = priv;
    dmaengine_submit(rx_desc);
    dma_async_issue_pending(priv->rx_chan);

    /* TX DMA 시작 → SCK 생성 → 수신 자동 진행 */
    tx_desc = dmaengine_prep_slave_single(
        priv->tx_chan, tx_dma, xfer->len,
        DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
    tx_desc->callback = my_spi_dma_tx_done;
    tx_desc->callback_param = priv;
    dmaengine_submit(tx_desc);
    dma_async_issue_pending(priv->tx_chan);

    return 0;
}

칩 셀렉트(CS) 관리

SPI 컨트롤러의 CS(Chip Select) 관리는 하드웨어 CS와 GPIO CS 두 가지 방식이 있습니다. 다중 슬레이브 환경에서는 CS 타이밍이 정확해야 버스 충돌을 방지할 수 있습니다:

방식장점단점용도
HW CS정확한 타이밍, 자동 토글제한된 CS 수 (보통 2~4개)고속 전송, 정밀 타이밍
GPIO CS무제한 CS, 유연한 핀 배치소프트웨어 지연, 약간의 오버헤드다수 슬레이브, 비표준 핀

spi_transfer 구조체의 cs_change 필드는 전송 간 CS 동작을 제어합니다. 이 필드가 설정되면 해당 transfer 종료 후 CS를 잠시 비활성화(deassert)했다가 다시 활성화합니다. 이는 일부 슬레이브 디바이스가 명령 사이에 CS 토글을 요구하는 경우에 필요합니다:

/* cs_change를 활용한 다중 명령 전송 */
static int spi_multi_cmd(struct spi_device *spi,
                          u8 *cmd1, size_t len1,
                          u8 *cmd2, size_t len2)
{
    struct spi_transfer xfer[2] = { };
    struct spi_message msg;

    /* Transfer 1: 첫 번째 명령 후 CS 토글 */
    xfer[0].tx_buf = cmd1;
    xfer[0].len = len1;
    xfer[0].cs_change = 1;  /* 이 transfer 후 CS deassert→reassert */
    xfer[0].cs_change_delay.value = 10;
    xfer[0].cs_change_delay.unit = SPI_DELAY_UNIT_USECS;

    /* Transfer 2: 두 번째 명령 (마지막이므로 CS 자동 해제) */
    xfer[1].tx_buf = cmd2;
    xfer[1].len = len2;

    spi_message_init_with_transfers(&msg, xfer, ARRAY_SIZE(xfer));
    return spi_sync(spi, &msg);
}
GPIO CS 설정: Device Tree에서 cs-gpios 프로퍼티로 GPIO 기반 CS를 지정합니다: cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;. SPI 프레임워크가 자동으로 GPIO를 assert/deassert하며, 컨트롤러 드라이버에서 set_cs 콜백을 구현하지 않아도 됩니다.

SPI 드라이버 작성

SPI ADC (Analog-to-Digital Converter) 드라이버 예제:

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

#define ADC_CMD_READ_CH0  0x06
#define ADC_CMD_READ_CH1  0x07

struct my_adc_data {
    struct spi_device *spi;
    struct mutex lock;
    u8 tx_buf[3] ____cacheline_aligned;
    u8 rx_buf[3];
};

static int my_adc_read_channel(struct my_adc_data *data, int channel)
{
    struct spi_transfer xfer = {
        .tx_buf = data->tx_buf,
        .rx_buf = data->rx_buf,
        .len = 3,
        .speed_hz = 1000000,  /* 1 MHz */
    };
    int ret;

    data->tx_buf[0] = (channel == 0) ? ADC_CMD_READ_CH0 : ADC_CMD_READ_CH1;
    data->tx_buf[1] = 0x00;
    data->tx_buf[2] = 0x00;

    ret = spi_sync_transfer(data->spi, &xfer, 1);
    if (ret)
        return ret;

    /* 12비트 ADC: 상위 4비트 버림 */
    return ((data->rx_buf[1] & 0x0F) << 8) | data->rx_buf[2];
}

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

    switch (mask) {
    case IIO_CHAN_INFO_RAW:
        mutex_lock(&data->lock);
        ret = my_adc_read_channel(data, chan->channel);
        mutex_unlock(&data->lock);
        if (ret < 0)
            return ret;
        *val = ret;
        return IIO_VAL_INT;
    case IIO_CHAN_INFO_SCALE:
        /* Vref=3.3V, 12-bit: 3300/4096 = 0.805664 mV/LSB */
        *val = 3300;
        *val2 = 12;
        return IIO_VAL_FRACTIONAL_LOG2;
    default:
        return -EINVAL;
    }
}

static const struct iio_chan_spec my_adc_channels[] = {
    {
        .type = IIO_VOLTAGE,
        .channel = 0,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
    },
    {
        .type = IIO_VOLTAGE,
        .channel = 1,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
    },
};

static const struct iio_info my_adc_info = {
    .read_raw = my_adc_read_raw,
};

static int my_adc_probe(struct spi_device *spi)
{
    struct iio_dev *indio_dev;
    struct my_adc_data *data;

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

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

    indio_dev->name = "my-adc";
    indio_dev->info = &my_adc_info;
    indio_dev->channels = my_adc_channels;
    indio_dev->num_channels = ARRAY_SIZE(my_adc_channels);
    indio_dev->modes = INDIO_DIRECT_MODE;

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

static const struct spi_device_id my_adc_spi_id[] = {
    { "my-adc", 0 },
    { }
};
MODULE_DEVICE_TABLE(spi, my_adc_spi_id);

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

static struct spi_driver my_adc_driver = {
    .driver = {
        .name = "my-adc",
        .of_match_table = my_adc_of_match,
    },
    .probe = my_adc_probe,
    .id_table = my_adc_spi_id,
};
module_spi_driver(my_adc_driver);
DMA 정렬: SPI 전송 버퍼가 DMA를 사용할 수 있으므로, tx_buf/rx_buf를 스택에 할당하면 안 됩니다. 구조체 멤버로 선언하거나 kmalloc()으로 할당하고, ____cacheline_aligned 속성을 적용하세요.

spi_message와 spi_transfer

복잡한 SPI 트랜잭션은 spi_message에 여러 spi_transfer를 연결하여 구성합니다:

/* 명령어 전송 후 데이터 수신 (CS 유지) */
static int spi_flash_read(struct spi_device *spi,
                           u32 addr, u8 *buf, size_t len)
{
    struct spi_message msg;
    struct spi_transfer xfer[2];
    u8 cmd[4];

    cmd[0] = 0x03;  /* READ 명령 */
    cmd[1] = (addr >> 16) & 0xFF;
    cmd[2] = (addr >> 8)  & 0xFF;
    cmd[3] = addr & 0xFF;

    memset(xfer, 0, sizeof(xfer));

    /* Transfer 1: 명령 + 주소 전송 */
    xfer[0].tx_buf = cmd;
    xfer[0].len = 4;

    /* Transfer 2: 데이터 수신 */
    xfer[1].rx_buf = buf;
    xfer[1].len = len;

    spi_message_init(&msg);
    spi_message_add_tail(&xfer[0], &msg);
    spi_message_add_tail(&xfer[1], &msg);

    return spi_sync(spi, &msg);
}

QSPI / Dual / Quad SPI

고속 SPI NOR Flash 등은 표준 SPI 외에 Dual (2-bit), Quad (4-bit), Octal (8-bit) I/O를 지원합니다. Linux 커널은 spi-mem 레이어를 통해 이를 추상화합니다.

모드데이터 라인명령 전송대표 디바이스
Standard SPI1-bit (MOSI/MISO)1-bit일반 SPI Flash
Dual Output2-bit1-bitW25Q series
Quad Output4-bit1-bitW25Q series
QPI (Quad I/O)4-bit4-bit고속 NOR Flash
Octal (8D-8D-8D)8-bit DDR8-bit DDRMacronix MX25

spi-mem 프레임워크

spi-mem은 메모리형 SPI 디바이스를 위한 표준화된 인터페이스입니다:

#include <linux/spi/spi-mem.h>

/* spi_mem_op: 명령/주소/더미/데이터 단계를 분리하여 기술 */
struct spi_mem_op op = SPI_MEM_OP(
    SPI_MEM_OP_CMD(0xEB, 1),           /* Quad I/O Fast Read, 1-wire cmd */
    SPI_MEM_OP_ADDR(3, addr, 4),       /* 3-byte addr, 4-wire */
    SPI_MEM_OP_DUMMY(6, 4),            /* 6 dummy cycles, 4-wire */
    SPI_MEM_OP_DATA_IN(len, buf, 4)   /* 데이터 수신, 4-wire */
);

ret = spi_mem_exec_op(spi_mem, &op);
SPI NOR 프레임워크: drivers/mtd/spi-nor/의 SPI NOR 프레임워크는 spi-mem 위에서 동작하며, JEDEC 표준 명령어 셋을 자동으로 처리합니다. 새 Flash 칩 지원은 벤더별 파일에 파라미터만 추가하면 됩니다.

Quad SPI 심화: DDR 모드와 XIP

고성능 SPI NOR Flash는 기본 Quad I/O를 넘어 DDR (Double Data Rate) 모드, Data Strobe (DQS), XIP (eXecute-In-Place) 등 고급 기능을 지원합니다. 아래 다이어그램은 Quad SPI 명령의 4단계 구조를 보여줍니다.

Quad SPI Command Phases (1-1-4 Read Example: 0xEB) 시간 CS Command 1-wire (MOSI only) 0xEB (8 cycles) IO0: CMD[7:0] Address 4-wire (IO0~IO3) 24-bit (6 cycles) IO0~3: ADDR[23:0] Dummy 4-wire (대기) 2~10 cycles IO: Hi-Z 또는 고정 Data 4-wire (IO0~IO3 수신) N bytes (N/2 cycles each) IO0~3: DATA[N*8-1:0] (연속 수신) Standard SPI vs Quad SPI vs QPI 비교 모드 CMD 전송 ADDR 전송 DATA 전송 표기법 유효 대역폭 Standard SPI 1-wire 1-wire 1-wire 1-1-1 f_sck x 1 bit Quad Output 1-wire 1-wire 4-wire 1-1-4 f_sck x 4 bit (data) Quad I/O 1-wire 4-wire 4-wire 1-4-4 f_sck x 4 bit (addr+data) QPI 4-wire 4-wire 4-wire 4-4-4 f_sck x 4 bit (전체)

DDR (Double Data Rate) 모드

DDR 모드에서는 SCK의 상승 에지와 하강 에지 양쪽에서 데이터를 전송하여 SDR(Single Data Rate) 대비 2배 대역폭을 달성합니다. Octal DDR (8D-8D-8D) 모드에서는 8개 데이터 라인 x DDR로 사이클당 16비트를 전송합니다:

전송 모드데이터 라인클럭 에지사이클당 비트200 MHz 시 대역폭
1-1-1 SDR1단일125 MB/s
1-1-4 SDR4단일4100 MB/s
1-4-4 DDR4양쪽8200 MB/s
4-4-4 DDR4양쪽8200 MB/s
8D-8D-8D8양쪽16400 MB/s

Data Strobe (DQS)

DDR 모드에서 고주파 클럭의 정확한 샘플링 타이밍을 보장하기 위해 DQS (Data Strobe) 신호가 사용됩니다. DQS는 슬레이브 디바이스가 데이터와 함께 출력하는 스트로브 신호로, 컨트롤러가 이 신호의 에지에서 데이터를 캡처합니다. 이는 SDR에서는 불필요하지만, DDR에서 클럭-데이터 스큐 보상에 필수적입니다.

XIP (eXecute-In-Place) 모드

XIP 모드는 SPI NOR Flash의 내용을 CPU 메모리 맵에 직접 매핑하여, 별도의 복사 없이 Flash에서 직접 코드를 실행할 수 있게 합니다. QSPI 컨트롤러가 CPU의 메모리 접근을 자동으로 SPI 읽기 명령으로 변환합니다:

/* XIP 모드 활성화: QSPI 컨트롤러 메모리 매핑 예시 */
static int qspi_enable_xip(struct qspi_priv *priv)
{
    struct spi_mem_op op = SPI_MEM_OP(
        SPI_MEM_OP_CMD(0xEB, 1),          /* Fast Read Quad I/O */
        SPI_MEM_OP_ADDR(3, 0, 4),         /* 24-bit addr, 4-wire */
        SPI_MEM_OP_DUMMY(6, 4),           /* 6 dummy cycles */
        SPI_MEM_OP_DATA_IN(0, NULL, 4)   /* 4-wire data */
    );
    u32 cr;

    /* 메모리 매핑 윈도우 설정 */
    writel(priv->mmap_phys, priv->base + QSPI_MMAP_BASE);
    writel(priv->flash_size - 1, priv->base + QSPI_MMAP_SIZE);

    /* Continuous Read 모드: 반복 CMD 전송 생략 */
    cr = readl(priv->base + QSPI_CR);
    cr |= QSPI_CR_XIP_EN | QSPI_CR_CONT_READ;
    writel(cr, priv->base + QSPI_CR);

    /* XIP 후 Flash는 메모리처럼 접근 가능:
     * void *data = ioremap(priv->mmap_phys, priv->flash_size);
     * memcpy(dst, data + offset, len);  */

    return 0;
}
XIP 제한사항: XIP 모드에서는 Flash 쓰기/지우기 작업 시 반드시 XIP를 비활성화해야 합니다. 쓰기 중 XIP 읽기가 발생하면 데이터 손상이 발생합니다. 또한 XIP 모드의 읽기 지연 시간은 DDR RAM 대비 10~100배 느리므로, 성능이 중요한 코드는 RAM에 복사 후 실행하는 것이 좋습니다.

SPI Device Tree 바인딩

&spi1 {
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;

    adc@0 {
        compatible = "vendor,my-adc";
        reg = <0>;          /* chip select 0 */
        spi-max-frequency = <10000000>;  /* 10 MHz */
        spi-cpol;           /* CPOL=1 (Mode 2 or 3) */
        spi-cpha;           /* CPHA=1 (Mode 1 or 3) */
        /* spi-cpol + spi-cpha = Mode 3 */
    };

    flash@1 {
        compatible = "jedec,spi-nor";
        reg = <1>;
        spi-max-frequency = <50000000>;
        spi-rx-bus-width = <4>;  /* quad read */
        spi-tx-bus-width = <4>;  /* quad write */
        m25p,fast-read;
    };
};

GPIO 개요

GPIO (General-Purpose Input/Output)는 소프트웨어로 제어 가능한 범용 디지털 핀입니다. LED, 버튼, 리셋 라인, 칩 셀렉트, 인터럽트 입력 등 다양한 용도로 사용됩니다.

Linux GPIO 서브시스템은 drivers/gpio/에 구현되며, 크게 두 가지 API가 있습니다:

API헤더상태특징
Legacy (integer-based)<linux/gpio.h>Deprecatedgpio_request(), gpio_direction_input()
Descriptor-based (gpiod)<linux/gpio/consumer.h>현재 표준gpiod_get(), gpiod_set_value()
Legacy API 사용 금지: 새 코드에서 gpio_request(), gpio_free(), gpio_get_value() 등 정수 기반 legacy API를 사용하지 마세요. 커널 메인라인에서는 legacy GPIO API를 사용하는 새 드라이버를 받아들이지 않습니다.

GPIO 서브시스템 아키텍처

Linux GPIO 서브시스템은 하드웨어 GPIO 컨트롤러부터 유저스페이스 접근까지 여러 계층으로 구성됩니다. gpiolib이 핵심 프레임워크 역할을 하며, gpio_chip이 하드웨어 추상화를, gpiod가 소비자(consumer) API를 제공합니다.

Linux GPIO 서브시스템 아키텍처 User Space /dev/gpiochipN (chardev) libgpiod (v2 API) gpioget / gpioset / gpiomon gpio-cdev (chardev 드라이버) sysfs GPIO (deprecated) gpiod Consumer API devm_gpiod_get() / gpiod_set_value() / gpiod_to_irq() 커널 드라이버 (GPIO 소비자) SPI CS, I2C recovery, LED, Key, Reset, Regulator Enable... gpiolib Core (drivers/gpio/gpiolib.c) gpio_desc 관리, 라인 요청/해제, 값 읽기/쓰기, IRQ 매핑, 이벤트 관리 gpio_chip Provider Interface (devm_gpiochip_add_data) SoC GPIO Controller gpio-mxc, gpio-tegra, gpio-rcar, gpio-stm32 I2C/SPI GPIO Expander gpio-pca953x, gpio-mcp23s08, gpio-pcf857x ACPI GPIO gpiolib-acpi.c, ACPI _DSD GPIO 매핑 GPIO Aggregator gpio-aggregator, gpio-sim (테스트용) pinctrl 서브시스템과 연동: 핀 멀티플렉싱, 풀업/풀다운, 드라이브 강도 설정

GPIO 하드웨어 내부 구조

물리적 GPIO 핀은 여러 전기적 설정을 지원하며, 드라이버에서 이를 올바르게 구성하는 것이 중요합니다:

설정설명커널 API / DT 속성용도
Push-PullHIGH/LOW 모두 능동적으로 구동기본 출력 모드LED 제어, 리셋 라인
Open-DrainLOW만 능동 구동, HIGH는 풀업 의존GPIO_OPEN_DRAIN / drive-open-drainI2C SDA/SCL, 인터럽트 라인
Open-SourceHIGH만 능동 구동, LOW는 풀다운 의존GPIO_OPEN_SOURCE / drive-open-source특수 전원 제어
Pull-Up내장 풀업 저항 활성화GPIO_PULL_UP / bias-pull-up버튼 입력 (active-low)
Pull-Down내장 풀다운 저항 활성화GPIO_PULL_DOWN / bias-pull-down기본 LOW 유지 필요 시
Schmitt Trigger히스테리시스 입력 (노이즈 내성)input-schmitt-enable느린 신호 에지, 노이즈 환경
Debounce글리치 필터링 (HW/SW)input-debounce = <usec>기계식 버튼/스위치
핀 설정과 pinctrl: GPIO 하드웨어 설정(풀업/풀다운, 드라이브 강도, 슬루율 등)은 pinctrl 서브시스템과 밀접하게 연관됩니다. 대부분의 SoC에서 GPIO 핀은 pinctrl 핀과 1:1 매핑되며, pinctrl_gpio_set_config()를 통해 gpiolib에서 pinctrl로 설정이 전달됩니다.

gpiod API (Descriptor-based)

현대 Linux 커널의 표준 GPIO 인터페이스인 gpiod API를 사용합니다:

GPIO 획득과 해제

#include <linux/gpio/consumer.h>

/* Device Tree에서 "reset-gpios" 속성을 참조하여 GPIO 획득 */
struct gpio_desc *reset_gpio;

reset_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(reset_gpio))
    return PTR_ERR(reset_gpio);

/* 선택적(optional) GPIO: 없어도 에러 아님 */
struct gpio_desc *led_gpio;
led_gpio = devm_gpiod_get_optional(&pdev->dev, "led", GPIOD_OUT_LOW);

/* 인덱스로 여러 GPIO 획득 */
struct gpio_desc *cs_gpio;
cs_gpio = devm_gpiod_get_index(&pdev->dev, "cs", 0, GPIOD_OUT_HIGH);

GPIO 동작

/* 출력 값 설정 (active-low 자동 처리) */
gpiod_set_value(reset_gpio, 1);  /* active (논리적 1) */
gpiod_set_value(reset_gpio, 0);  /* inactive (논리적 0) */

/* sleepable context에서 사용 (I2C/SPI GPIO expander 등) */
gpiod_set_value_cansleep(reset_gpio, 1);

/* 입력 값 읽기 */
int val = gpiod_get_value(button_gpio);

/* 방향 변경 */
gpiod_direction_input(gpio);
gpiod_direction_output(gpio, 1);

/* GPIO → IRQ 번호 변환 */
int irq = gpiod_to_irq(button_gpio);
if (irq < 0)
    return irq;

ret = devm_request_threaded_irq(&pdev->dev, irq, NULL,
        my_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
        "my-button", data);
gpiod_set_value vs gpiod_set_raw_value: gpiod_set_value()는 Device Tree의 GPIO_ACTIVE_LOW 플래그를 자동 반영합니다. gpiod_set_raw_value()는 물리적 라인 레벨을 직접 제어합니다. 일반적으로 gpiod_set_value()를 사용하세요.

GPIO 인터럽트 흐름

GPIO를 인터럽트 소스로 사용하는 경우, GPIO 서브시스템과 IRQ 서브시스템이 협력하여 핀 상태 변화를 커널 인터럽트로 변환합니다. 아래 다이어그램은 GPIO 인터럽트의 전체 처리 흐름을 보여줍니다.

GPIO 인터럽트 처리 흐름 GPIO Pin 에지/레벨 변화 gpio_chip irqchip 콜백 irq_mask/unmask/set_type irq_domain HW IRQ → Linux virq 매핑 Generic IRQ irq_desc / action handle_edge_irq() Threaded IRQ request_threaded_irq IRQF_ONESHOT Driver handler() gpiod_to_irq(gpio_desc) → irq_find_mapping(domain, hwirq) → Linux virq 반환 드라이버의 GPIO IRQ 설정 절차 1. gpio = devm_gpiod_get(dev, "alert", GPIOD_IN); 2. irq = gpiod_to_irq(gpio); 3. devm_request_threaded_irq(dev, irq, NULL, handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, ...);

GPIO Debounce (글리치 필터링)

기계식 버튼이나 스위치는 접점 바운싱으로 인해 한 번의 누름에 여러 번의 에지 변화가 발생합니다. Debounce는 이러한 글리치를 필터링하여 깨끗한 신호를 제공합니다.

방식구현지연장점단점
HW DebounceSoC GPIO 컨트롤러 내장 필터HW에서 설정 (수십 us ~ 수 ms)CPU 부하 없음, 정밀한 타이밍모든 SoC 지원 아님
SW Debouncegpiolib hrtimer 기반gpiod_set_debounce()모든 GPIO에 적용 가능CPU 오버헤드, hrtimer 정밀도 의존
/* HW debounce 설정 (지원하는 컨트롤러만) */
ret = gpiod_set_debounce(button_gpio, 50000);  /* 50ms */
if (ret == -ENOTSUPP)
    dev_warn(dev, "HW debounce not supported, using SW\\n");

/* gpio_chip에서 HW debounce 구현 */
static int my_gpio_set_config(struct gpio_chip *gc,
                              unsigned int offset,
                              unsigned long config)
{
    if (pinconf_to_config_param(config) == PIN_CONFIG_INPUT_DEBOUNCE) {
        u32 debounce_us = pinconf_to_config_argument(config);
        /* HW 디바운스 레지스터 설정 */
        writel(debounce_us / 31, priv->base + DEBOUNCE_REG(offset));
        return 0;
    }
    return -ENOTSUPP;
}

GPIO Aggregator

gpio-aggregator는 여러 물리적 GPIO 라인을 하나의 가상 GPIO 컨트롤러로 묶어 유저스페이스에 노출하는 기능입니다. 보안 제한이나 권한 분리가 필요한 환경에서 특정 GPIO 라인만 선택적으로 컨테이너나 VM에 전달할 때 유용합니다.

/* GPIO Aggregator 사용 (sysfs 인터페이스) */
$ echo "gpiochip0 3,5,7" > /sys/bus/platform/drivers/gpio-aggregator/new_device
/* 새로운 /dev/gpiochipN 생성 (3개 라인: 0=pin3, 1=pin5, 2=pin7) */

$ echo "gpiochip0 3,5,7" > /sys/bus/platform/drivers/gpio-aggregator/delete_device
/* 가상 GPIO 컨트롤러 제거 */
gpio-sim: 테스트 환경에서는 gpio-sim 모듈을 사용하여 실제 하드웨어 없이 가상 GPIO 컨트롤러를 생성할 수 있습니다. configfs를 통해 라인 수, 라벨, 초기 값 등을 설정합니다. CI/CD 파이프라인이나 유닛 테스트에 유용합니다.

GPIO Device Tree 바인딩

my_device: my-device@0 {
    compatible = "vendor,my-device";
    /* 프로퍼티 이름: <con-id>-gpios */
    reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>;
    led-gpios = <&gpio2 3 GPIO_ACTIVE_HIGH>;
    cs-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>,
               <&gpio1 5 GPIO_ACTIVE_LOW>;
};

gpio_chip 구현

GPIO 컨트롤러 드라이버를 작성하려면 gpio_chip 구조체를 구현하고 등록합니다:

#include <linux/gpio/driver.h>

struct my_gpio {
    struct gpio_chip gc;
    void __iomem *base;
    struct mutex lock;
};

static int my_gpio_get(struct gpio_chip *gc, unsigned int offset)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 reg;

    reg = readl(priv->base + 0x10);  /* Data Input Register */
    return !!(reg & BIT(offset));
}

static void my_gpio_set(struct gpio_chip *gc,
                         unsigned int offset, int value)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 reg;

    mutex_lock(&priv->lock);
    reg = readl(priv->base + 0x14);  /* Data Output Register */
    if (value)
        reg |= BIT(offset);
    else
        reg &= ~BIT(offset);
    writel(reg, priv->base + 0x14);
    mutex_unlock(&priv->lock);
}

static int my_gpio_direction_input(struct gpio_chip *gc,
                                    unsigned int offset)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 reg;

    mutex_lock(&priv->lock);
    reg = readl(priv->base + 0x04);  /* Direction Register */
    reg &= ~BIT(offset);            /* 0 = input */
    writel(reg, priv->base + 0x04);
    mutex_unlock(&priv->lock);
    return 0;
}

static int my_gpio_direction_output(struct gpio_chip *gc,
                                     unsigned int offset, int value)
{
    my_gpio_set(gc, offset, value);

    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 reg;

    mutex_lock(&priv->lock);
    reg = readl(priv->base + 0x04);
    reg |= BIT(offset);              /* 1 = output */
    writel(reg, priv->base + 0x04);
    mutex_unlock(&priv->lock);
    return 0;
}

static int my_gpio_probe(struct platform_device *pdev)
{
    struct my_gpio *priv;

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

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

    mutex_init(&priv->lock);

    priv->gc.label            = "my-gpio";
    priv->gc.parent           = &pdev->dev;
    priv->gc.owner            = THIS_MODULE;
    priv->gc.base             = -1;  /* 동적 번호 할당 */
    priv->gc.ngpio            = 32;
    priv->gc.get              = my_gpio_get;
    priv->gc.set              = my_gpio_set;
    priv->gc.direction_input  = my_gpio_direction_input;
    priv->gc.direction_output = my_gpio_direction_output;

    return devm_gpiochip_add_data(&pdev->dev, &priv->gc, priv);
}

libgpiod 유저스페이스

libgpiod는 Linux GPIO character device (/dev/gpiochipN)를 통한 유저스페이스 GPIO 접근 라이브러리입니다. 기존의 /sys/class/gpio/ sysfs 인터페이스를 대체합니다.

sysfs GPIO 폐기: /sys/class/gpio/export 인터페이스는 deprecated 상태입니다. 새 프로젝트에서는 chardev 기반(/dev/gpiochipN) 접근을 권장하며, 가능한 경우 libgpiod(v2 이상)를 사용하세요.

libgpiod 명령행 도구

도구용도예시
gpiodetect시스템의 GPIO 칩 목록gpiodetect
gpioinfoGPIO 라인 상세 정보gpioinfo gpiochip0
gpiogetGPIO 입력 값 읽기gpioget gpiochip0 7
gpiosetGPIO 출력 값 설정gpioset gpiochip0 7=1
gpiomonGPIO 이벤트 모니터링gpiomon gpiochip0 7

libgpiod C API (v2)

#include <gpiod.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    struct gpiod_chip *chip;
    struct gpiod_line_settings *settings;
    struct gpiod_line_config *line_cfg;
    struct gpiod_request_config *req_cfg;
    struct gpiod_line_request *request;
    unsigned int offsets[] = { 7 };
    enum gpiod_line_value value;

    chip = gpiod_chip_open("/dev/gpiochip0");
    settings = gpiod_line_settings_new();
    gpiod_line_settings_set_direction(settings,
                                       GPIOD_LINE_DIRECTION_INPUT);
    gpiod_line_settings_set_bias(settings,
                                  GPIOD_LINE_BIAS_PULL_UP);

    line_cfg = gpiod_line_config_new();
    gpiod_line_config_add_line_settings(line_cfg, offsets, 1, settings);

    req_cfg = gpiod_request_config_new();
    gpiod_request_config_set_consumer(req_cfg, "my-app");

    request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);

    value = gpiod_line_request_get_value(request, 7);
    printf("GPIO 7 = %d\\n", value);

    gpiod_line_request_release(request);
    gpiod_request_config_free(req_cfg);
    gpiod_line_config_free(line_cfg);
    gpiod_line_settings_free(settings);
    gpiod_chip_close(chip);
    return 0;
}

GPIO Expander

GPIO expander는 I2C 또는 SPI를 통해 GPIO 핀 수를 확장하는 디바이스입니다. 커널에서는 일반 GPIO 컨트롤러와 동일한 gpio_chip 인터페이스로 통합됩니다.

디바이스인터페이스GPIO 수인터럽트커널 드라이버
MCP23017I2C16지원gpio-mcp23s08
MCP23S17SPI16지원gpio-mcp23s08
PCA9555I2C16지원gpio-pca953x
PCA9535I2C16지원gpio-pca953x
PCF8574I2C8지원gpio-pcf857x
TCA6424AI2C24지원gpio-pca953x

GPIO Expander Device Tree 예시

&i2c1 {
    gpio_exp: gpio-expander@20 {
        compatible = "nxp,pca9555";
        reg = <0x20>;
        gpio-controller;
        #gpio-cells = <2>;
        interrupt-parent = <&gpio1>;
        interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
};

/* GPIO expander의 핀을 다른 디바이스에서 참조 */
my_led: led-controller {
    compatible = "gpio-leds";
    led-status {
        gpios = <&gpio_exp 3 GPIO_ACTIVE_HIGH>;
        label = "status";
        linux,default-trigger = "heartbeat";
    };
};
can_sleep 플래그: I2C/SPI 기반 GPIO expander는 버스 전송이 필요하므로 gpio_chip.can_sleep = true로 설정됩니다. 이 경우 인터럽트 컨텍스트에서 gpiod_get_value()를 호출할 수 없으며, 반드시 gpiod_get_value_cansleep()을 사용해야 합니다.

GPIO IRQ 컨트롤러 (irqchip)

GPIO 컨트롤러가 인터럽트를 지원하려면 gpio_chip에 IRQ chip 기능을 통합해야 합니다. Linux 커널은 GPIOLIB_IRQCHIP 인프라를 통해 이 과정을 크게 단순화합니다. gpio_irq_chip 구조체를 gpio_chip에 내장하여 등록하면, gpiolib이 자동으로 irq_domain을 생성하고 관리합니다.

IRQ Domain 유형

유형설명적용 대상
Flat (Linear)GPIO 번호가 직접 HW IRQ 번호로 매핑일반 SoC GPIO 컨트롤러, GPIO expander
HierarchicalGPIO IRQ → 상위 IRQ 컨트롤러(GIC 등)에 계층적 매핑부모 IRQ 컨트롤러가 별도 존재하는 경우
계층적 GPIO IRQ Domain vs Flat IRQ Domain Flat IRQ Domain GPIO 0..31 gpio_chip + irq_chip chained_irq_handler irq_domain (linear) hwirq 0..31 → virq Parent IRQ (GIC) 단일 parent IRQ line chained handler가 부모 IRQ에서 호출되어 개별 GPIO IRQ를 dispatch Hierarchical IRQ Domain GPIO 0 GPIO 1 GPIO 2 ... GPIO irq_domain (child) gpio_irq_chip + irq_domain_ops Parent irq_domain (GIC/INTC) 각 GPIO가 독립된 parent HW IRQ에 매핑 HW IRQ A HW IRQ B HW IRQ C ... 각 GPIO → 독립된 parent HW IRQ

gpio_chip irqchip 구현

현대 커널(v5.10+)에서는 gpio_irq_chipgpio_chip 내에 설정하고 devm_gpiochip_add_data()로 한 번에 등록하는 것이 권장 패턴입니다:

#include <linux/gpio/driver.h>
#include <linux/interrupt.h>

struct my_gpio_irq {
    struct gpio_chip gc;
    void __iomem *base;
    struct mutex lock;
    u32 irq_mask;     /* 소프트웨어 IRQ 마스크 상태 */
    u32 irq_type;     /* 에지/레벨 타입 비트맵 */
};

/* irq_chip 콜백: 인터럽트 마스크/언마스크 */
static void my_gpio_irq_mask(struct irq_data *d)
{
    struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
    struct my_gpio_irq *priv = gpiochip_get_data(gc);
    u32 mask = BIT(irqd_to_hwirq(d));

    priv->irq_mask &= ~mask;
    writel(priv->irq_mask, priv->base + IRQ_MASK_REG);
    gpiochip_disable_irq(gc, irqd_to_hwirq(d));
}

static void my_gpio_irq_unmask(struct irq_data *d)
{
    struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
    struct my_gpio_irq *priv = gpiochip_get_data(gc);
    u32 mask = BIT(irqd_to_hwirq(d));

    gpiochip_enable_irq(gc, irqd_to_hwirq(d));
    priv->irq_mask |= mask;
    writel(priv->irq_mask, priv->base + IRQ_MASK_REG);
}

/* 인터럽트 타입 설정 (에지/레벨) */
static int my_gpio_irq_set_type(struct irq_data *d,
                                 unsigned int type)
{
    struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
    struct my_gpio_irq *priv = gpiochip_get_data(gc);
    u32 bit = BIT(irqd_to_hwirq(d));

    switch (type & IRQ_TYPE_SENSE_MASK) {
    case IRQ_TYPE_EDGE_RISING:
        priv->irq_type |= bit;
        writel(priv->irq_type, priv->base + IRQ_EDGE_REG);
        break;
    case IRQ_TYPE_EDGE_FALLING:
        priv->irq_type &= ~bit;
        writel(priv->irq_type, priv->base + IRQ_EDGE_REG);
        break;
    case IRQ_TYPE_EDGE_BOTH:
        /* 하드웨어가 지원하면 both-edge 설정 */
        break;
    default:
        return -EINVAL;
    }
    return 0;
}

/* IMMUTABLE irq_chip: 런타임 수정 불가 (v6.0+ 필수) */
static const struct irq_chip my_gpio_irqchip = {
    .name         = "my-gpio-irq",
    .irq_mask     = my_gpio_irq_mask,
    .irq_unmask   = my_gpio_irq_unmask,
    .irq_set_type = my_gpio_irq_set_type,
    .flags        = IRQCHIP_IMMUTABLE,
    GPIOCHIP_IRQ_RESOURCE_HELPERS,
};

/* Chained IRQ handler: 부모 IRQ에서 호출 */
static void my_gpio_irq_handler(struct irq_desc *desc)
{
    struct gpio_chip *gc = irq_desc_get_handler_data(desc);
    struct my_gpio_irq *priv = gpiochip_get_data(gc);
    struct irq_chip *irqchip = irq_desc_get_chip(desc);
    u32 pending;

    chained_irq_enter(irqchip, desc);

    pending = readl(priv->base + IRQ_STATUS_REG);
    pending &= priv->irq_mask;

    while (pending) {
        int hwirq = __ffs(pending);
        generic_handle_domain_irq(gc->irq.domain, hwirq);
        pending &= ~BIT(hwirq);
    }

    /* 인터럽트 상태 클리어 (W1C) */
    writel(pending, priv->base + IRQ_STATUS_REG);

    chained_irq_exit(irqchip, desc);
}

/* probe에서 GPIO + IRQ chip 등록 */
static int my_gpio_irq_probe(struct platform_device *pdev)
{
    struct my_gpio_irq *priv;
    struct gpio_irq_chip *girq;
    int parent_irq;

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

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

    parent_irq = platform_get_irq(pdev, 0);
    if (parent_irq < 0)
        return parent_irq;

    mutex_init(&priv->lock);

    /* GPIO chip 기본 설정 */
    priv->gc.label            = "my-gpio-irq";
    priv->gc.parent           = &pdev->dev;
    priv->gc.owner            = THIS_MODULE;
    priv->gc.base             = -1;
    priv->gc.ngpio            = 32;
    priv->gc.get              = my_gpio_get;
    priv->gc.set              = my_gpio_set;
    priv->gc.direction_input  = my_gpio_direction_input;
    priv->gc.direction_output = my_gpio_direction_output;

    /* IRQ chip 내장 설정 (권장 패턴) */
    girq = &priv->gc.irq;
    gpio_irq_chip_set_chip(girq, &my_gpio_irqchip);
    girq->parent_handler = my_gpio_irq_handler;
    girq->num_parents = 1;
    girq->parents = devm_kcalloc(&pdev->dev, 1,
                                  sizeof(*girq->parents), GFP_KERNEL);
    if (!girq->parents)
        return -ENOMEM;
    girq->parents[0] = parent_irq;
    girq->default_type = IRQ_TYPE_NONE;
    girq->handler = handle_edge_irq;

    /* GPIO chip + IRQ chip 동시 등록 */
    return devm_gpiochip_add_data(&pdev->dev, &priv->gc, priv);
}
IRQCHIP_IMMUTABLE 필수: v6.0 이후 커널에서는 irq_chip 구조체에 IRQCHIP_IMMUTABLE 플래그를 설정해야 합니다. 이는 런타임에 irq_chip이 수정되는 것을 방지합니다. 또한 gpiochip_enable_irq()gpiochip_disable_irq()를 unmask/mask 콜백에서 호출하고, GPIOCHIP_IRQ_RESOURCE_HELPERS 매크로를 포함해야 합니다.

Nested (Threaded) IRQ 패턴

I2C/SPI GPIO expander처럼 슬립 가능한 버스 뒤에 있는 GPIO 컨트롤러는 chained handler를 사용할 수 없습니다(인터럽트 컨텍스트에서 I2C/SPI 전송 불가). 이 경우 nested (threaded) IRQ 패턴을 사용합니다:

/* I2C GPIO expander의 threaded IRQ 패턴 */
girq = &priv->gc.irq;
gpio_irq_chip_set_chip(girq, &my_expander_irqchip);

/* parent_handler를 NULL로 설정하면 nested(threaded) IRQ 사용 */
girq->parent_handler = NULL;
girq->num_parents = 0;
girq->parents = NULL;
girq->default_type = IRQ_TYPE_NONE;
girq->handler = handle_bad_irq;  /* 직접 호출되면 안 됨 */
girq->threaded = true;           /* 핵심: threaded IRQ 사용 */

/* 부모 IRQ를 threaded handler로 직접 등록 */
ret = devm_request_threaded_irq(&client->dev, client->irq,
        NULL, my_expander_irq_thread,
        IRQF_ONESHOT | IRQF_SHARED,
        "my-expander", priv);

/* threaded IRQ handler에서 I2C 통신으로 상태 확인 */
static irqreturn_t my_expander_irq_thread(int irq, void *data)
{
    struct my_gpio_irq *priv = data;
    u32 pending;

    /* I2C/SPI 통신으로 인터럽트 상태 레지스터 읽기 (sleep 가능) */
    pending = i2c_smbus_read_byte_data(priv->client, INT_STATUS_REG);

    while (pending) {
        int hwirq = __ffs(pending);
        handle_nested_irq(irq_find_mapping(
            priv->gc.irq.domain, hwirq));
        pending &= ~BIT(hwirq);
    }
    return IRQ_HANDLED;
}
chained vs nested IRQ 선택 기준: SoC 내장 GPIO 컨트롤러(MMIO 접근)는 chained handler를 사용합니다. I2C/SPI GPIO expander처럼 슬립 가능한 버스 뒤의 컨트롤러는 nested (threaded) handler를 사용합니다. chained handler는 하드 IRQ 컨텍스트에서 실행되므로 더 빠르지만, 슬립이 불가능합니다.

regmap: 레지스터 추상화 API

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

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 */
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 캐시 타입: REGCACHE_NONE (캐시 없음), REGCACHE_RBTREE (희소 레지스터용, RB 트리), REGCACHE_FLAT (연속 레지스터용, 배열), REGCACHE_MAPLE (v6.4+, maple tree 기반). 휘발성(volatile) 레지스터는 항상 하드웨어에서 직접 읽습니다.

regmap IRQ 프레임워크

regmap-irq는 레지스터 기반 인터럽트 컨트롤러를 위한 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)하며, 내부적으로 버스 콜백, 캐시 상태, 잠금 메커니즘을 관리합니다.

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을 사용하여 원자적 컨텍스트에서도 접근 가능합니다.

regmap_field: 비트 필드 추상화

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

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_access_table을 사용하면 테이블 기반으로 레지스터 접근 권한을 정의할 수 있습니다. 레지스터가 많은 디바이스에서 더 간결합니다.

/* 레지스터 범위 정의 */
static const struct regmap_range my_readable_ranges[] = {
    regmap_reg_range(0x00, 0x0F),   /* 0x00 ~ 0x0F 읽기 가능 */
    regmap_reg_range(0x20, 0x2F),   /* 0x20 ~ 0x2F 읽기 가능 */
    regmap_reg_range(0x80, 0x80),   /* 0x80 단일 레지스터 */
};

static const struct regmap_range my_writeable_ranges[] = {
    regmap_reg_range(0x01, 0x0F),   /* 0x00(STATUS)은 읽기 전용 */
    regmap_reg_range(0x20, 0x2F),
};

static const struct regmap_range my_volatile_ranges[] = {
    regmap_reg_range(0x00, 0x00),   /* STATUS: 항상 HW 읽기 */
    regmap_reg_range(0x08, 0x09),   /* IRQ 상태/마스크 */
};

static const struct regmap_range my_precious_ranges[] = {
    regmap_reg_range(0x0A, 0x0A),   /* FIFO: 읽으면 값 소비됨 */
};

static const struct regmap_access_table my_rd_table = {
    .yes_ranges = my_readable_ranges,
    .n_yes_ranges = ARRAY_SIZE(my_readable_ranges),
};

static const struct regmap_access_table my_wr_table = {
    .yes_ranges = my_writeable_ranges,
    .n_yes_ranges = ARRAY_SIZE(my_writeable_ranges),
};

static const struct regmap_access_table my_volatile_table = {
    .yes_ranges = my_volatile_ranges,
    .n_yes_ranges = ARRAY_SIZE(my_volatile_ranges),
};

static const struct regmap_access_table my_precious_table = {
    .yes_ranges = my_precious_ranges,
    .n_yes_ranges = ARRAY_SIZE(my_precious_ranges),
};

static const struct regmap_config my_table_config = {
    .reg_bits      = 8,
    .val_bits      = 16,
    .max_register  = 0x80,
    .rd_table      = &my_rd_table,       /* 읽기 가능 범위 */
    .wr_table      = &my_wr_table,       /* 쓰기 가능 범위 */
    .volatile_table = &my_volatile_table, /* 캐시 안 함 */
    .precious_table = &my_precious_table, /* debugfs에서 읽지 않음 */
    .cache_type    = REGCACHE_RBTREE,
};
접근 제어 우선순위: *_table*_reg() 콜백을 동시에 설정할 수 없습니다. precious 레지스터는 FIFO 포트처럼 읽기 자체가 부작용을 유발하는 레지스터입니다. debugfs의 register dump에서 자동 제외되어 디버깅 시 의도치 않은 데이터 손실을 방지합니다. no_ranges 필드를 사용하면 "이 범위를 제외한 나머지 전부"와 같은 역전 논리도 표현할 수 있습니다.

레지스터 윈도우와 페이지 매핑

일부 디바이스는 레지스터 공간이 커서 페이지 레지스터를 통해 윈도우 방식으로 접근합니다. regmap_range_cfg는 이러한 페이지 기반 레지스터 접근을 투명하게 처리합니다.

regmap 페이지 윈도우 매핑 물리 주소 공간 (8-bit) 0x00-0x7F: 글로벌 레지스터 0x80: 페이지 선택 레지스터 0x81-0xFF: 페이지 윈도우 가상 주소 공간 (regmap) 0x000-0x07F: 글로벌 0x081-0x0FF: 페이지 0 0x181-0x1FF: 페이지 1 ...
/*
 * 디바이스 레지스터 구조 (위 SVG 참고):
 */

static const struct regmap_range_cfg my_range_cfg[] = {
    {
        .name         = "pages",
        .range_min    = 0x81,        /* 윈도우 시작 (가상) */
        .range_max    = 0x2FF,       /* 윈도우 끝 (가상) */
        .selector_reg = 0x80,        /* 페이지 선택 레지스터 */
        .selector_mask = 0xFF,       /* 선택 비트 마스크 */
        .selector_shift = 0,         /* 선택 비트 시프트 */
        .window_start = 0x81,        /* 물리 윈도우 시작 */
        .window_len   = 0x7F,        /* 윈도우 크기 (바이트) */
    },
};

static const struct regmap_config my_paged_config = {
    .reg_bits      = 8,
    .val_bits      = 8,
    .max_register  = 0x2FF,        /* 가상 주소 공간 최대 */
    .ranges        = my_range_cfg,
    .num_ranges    = ARRAY_SIZE(my_range_cfg),
};

/* 드라이버에서는 가상 주소로 투명하게 접근 */
regmap_read(regmap, 0x185, &val);   /* 자동으로: page=1 선택 → 0x85 읽기 */
regmap_write(regmap, 0x281, 0x42);  /* 자동으로: page=2 선택 → 0x81 쓰기 */
실제 사용 사례: TI TAS2770/TAS2781 오디오 앰프, Maxim MAX77686 PMIC, NXP PCA9685 PWM 컨트롤러 등 레지스터 수가 256개를 초과하는 I2C/SPI 디바이스에서 활용됩니다. regmap이 페이지 전환을 자동 관리하므로 드라이버 코드에서 페이지 선택 로직이 완전히 제거됩니다.

다중 레지스터 연산

여러 레지스터를 원자적으로 읽거나 쓸 때 사용하는 고급 API입니다.

/* 다중 레지스터 쓰기 (시퀀스 보장) */
static const struct reg_sequence init_seq[] = {
    { 0x01, 0x0000 },                  /* CONFIG = 0 (리셋) */
    REG_SEQ0(0x01, 0x0000),              /* 동일, delay_us = 0 */
    { 0x02, 0x1234, 1000 },             /* DATA = 0x1234, 1ms 지연 후 다음 */
    { 0x01, 0x001F },                  /* CONFIG = 0x1F (활성화) */
};

/* 시퀀스를 한 번에 실행 */
ret = regmap_multi_reg_write(regmap, init_seq, ARRAY_SIZE(init_seq));

/* 레지스터 패치: regmap 생성 시 자동 적용되는 초기화 시퀀스 */
static const struct reg_sequence my_patch[] = {
    { 0x10, 0xABCD },
    { 0x11, 0x1234 },
};
ret = regmap_register_patch(regmap, my_patch, ARRAY_SIZE(my_patch));

/* raw 읽기/쓰기: 포맷 변환 없이 바이트 스트림 전송 */
u8 raw_buf[16];
ret = regmap_raw_read(regmap, 0x00, raw_buf, sizeof(raw_buf));
ret = regmap_raw_write(regmap, 0x00, raw_buf, sizeof(raw_buf));

/* noinc 읽기/쓰기: 주소 증가 없이 같은 레지스터 반복 접근 (FIFO) */
u8 fifo_buf[64];
ret = regmap_noinc_read(regmap, 0x0A, fifo_buf, sizeof(fifo_buf));
ret = regmap_noinc_write(regmap, 0x0A, fifo_buf, sizeof(fifo_buf));
bulk vs raw vs noinc 차이:
  • regmap_bulk_read/write: 연속 레지스터를 val_bits 단위로 읽기/쓰기. 엔디안 변환 적용
  • regmap_raw_read/write: 연속 레지스터를 바이트 스트림으로 전송. 엔디안 변환 없음. val_bits > 8일 때만 사용 가능
  • regmap_noinc_read/write: 주소 증가 없이 동일 레지스터에 반복 접근. FIFO 포트 등에 사용. regmap_config.read_flag_mask 설정 필요할 수 있음

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(사용 레지스터 수)
캐시 타입 선택 가이드: max_register가 작고 대부분의 레지스터를 사용하면 REGCACHE_FLAT이 가장 빠릅니다 (O(1) 접근). 레지스터 주소가 넓게 분산되어 있으면 REGCACHE_RBTREEREGCACHE_MAPLE이 메모리 효율적입니다. REGCACHE_MAPLE은 v6.4에서 추가되었으며, 연속 범위 탐색이 rbtree보다 캐시 친화적입니다.

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;
}

/* Runtime PM과 regmap 연동 */
static int my_runtime_suspend(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    regcache_cache_only(mydev->regmap, true);
    regcache_mark_dirty(mydev->regmap);

    /* 레귤레이터/클록 비활성화 */
    regulator_disable(mydev->vdd);
    clk_disable_unprepare(mydev->clk);

    return 0;
}

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

    /* 레귤레이터/클록 활성화 */
    ret = clk_prepare_enable(mydev->clk);
    if (ret)
        return ret;

    ret = regulator_enable(mydev->vdd);
    if (ret) {
        clk_disable_unprepare(mydev->clk);
        return ret;
    }

    /* HW 안정화 대기 */
    usleep_range(1000, 1500);

    regcache_cache_only(mydev->regmap, false);
    ret = regcache_sync(mydev->regmap);

    return ret;
}

static DEFINE_RUNTIME_DEV_PM_OPS(my_pm_ops,
    my_runtime_suspend, my_runtime_resume, NULL);
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=캐시 전용 모드 활성
항목의미
nameregmap 이름
range레지스터 주소 범위
registers레지스터 덤프 (precious 제외)
access읽기/쓰기/volatile/precious 권한 정보
cache_only캐시 전용 모드 상태
/* debugfs에서 regmap 이름 지정 */
static const struct regmap_config my_config = {
    .name     = "main",    /* debugfs 디렉토리에 이름 표시 */
    .reg_bits = 8,
    .val_bits = 16,
    /* ... */
};

/* MFD 등에서 여러 regmap이 있을 때 구분에 유용:
 * /sys/kernel/debug/regmap/0-001a-main/
 * /sys/kernel/debug/regmap/0-001a-gpio/
 */
디버깅 팁: registers 파일에 값을 쓰면 런타임에 레지스터를 변경할 수 있습니다: echo "01 abcd" > registers. 이는 프로토타이핑 시 매우 유용하지만, precious 레지스터는 dump에서 자동 제외되므로 FIFO 데이터가 의도치 않게 소비되는 문제를 방지합니다. regmap 이름을 지정하면 ftrace의 regmap 이벤트에서도 구분하여 필터링할 수 있습니다: echo 'name == "main"' > /sys/kernel/debug/tracing/events/regmap/filter

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 제어 레지스터 */

    /* 또는 regmap_field로 특정 필드만 관리 */
    /* ... */

    return 0;
}

/* ===== 여러 regmap을 가진 MFD (이름으로 구분) ===== */

/* 부모 드라이버: 두 개의 I2C 주소를 사용하는 PMIC */
static int pmic_dual_probe(struct i2c_client *client)
{
    struct regmap *regmap_main, *regmap_gpio;
    struct i2c_client *gpio_client;
    struct regmap_config cfg = pmic_regmap_config;

    /* 메인 regmap (이름 없음 = 기본) */
    regmap_main = devm_regmap_init_i2c(client, &cfg);

    /* 보조 I2C 주소의 regmap */
    gpio_client = devm_i2c_new_dummy_device(&client->dev,
                    client->adapter, client->addr + 1);
    cfg.name = "gpio";
    regmap_gpio = devm_regmap_init_i2c(gpio_client, &cfg);

    /* ... */
}

/* 자식 드라이버: 이름으로 특정 regmap 획득 */
regmap = dev_get_regmap(pdev->dev.parent, "gpio");

regmap 고급 설정

regmap_config의 다양한 고급 옵션으로 복잡한 하드웨어 특성을 처리할 수 있습니다.

static const struct regmap_config advanced_config = {
    .reg_bits        = 16,          /* 16비트 레지스터 주소 */
    .val_bits        = 32,          /* 32비트 레지스터 값 */
    .reg_stride      = 4,           /* 레지스터 주소 간격 (MMIO 워드 정렬) */
    .max_register    = 0x1000,

    /* 엔디안 설정 */
    .reg_format_endian = REGMAP_ENDIAN_BIG,     /* 주소 바이트 순서 */
    .val_format_endian = REGMAP_ENDIAN_LITTLE,  /* 값 바이트 순서 */

    /* SPI 읽기 시 상위 비트 설정 (SPI 프로토콜 관례) */
    .read_flag_mask  = 0x80,        /* 레지스터 주소에 OR */
    .write_flag_mask = 0x00,

    /* 레지스터 주소 패딩 (일부 SPI 디바이스 요구) */
    .pad_bits        = 8,           /* 주소 뒤 패딩 비트 */

    /* 읽기 전 지연 (슬로우 디바이스) */
    .read_delay_us   = 10,          /* 주소 전송 후 읽기 전 지연 */

    /* 캐시 설정 */
    .cache_type      = REGCACHE_MAPLE,
    .reg_defaults    = my_defaults,
    .num_reg_defaults = ARRAY_SIZE(my_defaults),

    /* 동기화 비활성 레지스터: cache_sync 시 기록하지 않을 범위 */
    .disable_locking = false,       /* true면 외부 잠금 사용 */

    /* 커스텀 잠금 (기존 잠금과 통합 시) */
    .lock            = my_lock_fn,
    .unlock          = my_unlock_fn,
    .lock_arg        = &my_mutex,

    /* 레지스터 값 비트 후처리 */
    .use_single_read  = true,       /* bulk를 단일 읽기로 분해 */
    .use_single_write = true,       /* bulk를 단일 쓰기로 분해 */
    .can_multi_write  = true,       /* multi_reg_write 최적화 허용 */

    /* 비동기 쓰기 지원 */
    .use_hwlock      = false,       /* HW spinlock 사용 여부 */
};
reg_stride 활용: MMIO 레지스터가 4바이트 정렬일 때 reg_stride = 4로 설정하면 regmap_read(regmap, 0x04, &val)이 실제 오프셋 0x04에 접근합니다. stride가 없으면 레지스터 번호와 바이트 오프셋을 혼동하기 쉽습니다. reg_stride의 배수가 아닌 주소로의 접근은 자동으로 거부(-EINVAL)됩니다.

regmap ftrace 이벤트

regmap은 ftrace 이벤트를 통해 모든 레지스터 접근을 추적할 수 있습니다. 버스 트랜잭션 디버깅과 성능 분석에 활용됩니다.

# regmap 관련 ftrace 이벤트
$ ls /sys/kernel/debug/tracing/events/regmap/
regmap_reg_read/         # 레지스터 읽기
regmap_reg_write/        # 레지스터 쓰기
regmap_bulk_read/        # 벌크 읽기
regmap_bulk_write/       # 벌크 쓰기
regmap_hw_read_start/    # HW 읽기 시작
regmap_hw_read_done/     # HW 읽기 완료
regmap_hw_write_start/   # HW 쓰기 시작
regmap_hw_write_done/    # HW 쓰기 완료
regmap_cache_only/       # 캐시 전용 모드 전환
regmap_cache_sync/       # 캐시 동기화
regmap_cache_bypass/     # 캐시 바이패스 전환

# 특정 regmap만 필터링
$ cd /sys/kernel/debug/tracing
$ echo 1 > events/regmap/regmap_reg_write/enable
$ cat trace
# 출력 예:
#    my_driver-1234  [002] ....  1.234567: regmap_reg_write:
#        0-001a reg=01 val=1234

# 특정 디바이스만 추적
$ echo 'name == "0-001a"' > events/regmap/regmap_reg_write/filter

# HW 접근 시간 측정 (start/done 이벤트 페어)
$ echo 1 > events/regmap/regmap_hw_read_start/enable
$ echo 1 > events/regmap/regmap_hw_read_done/enable
$ cat trace_pipe
# hw_read_start와 hw_read_done 타임스탬프 차이 = 실제 버스 지연
성능 분석 패턴: regmap_cache_sync 이벤트로 resume 시 동기화되는 레지스터 수를 확인하고, regmap_hw_read_start/done 페어로 버스 지연을 측정할 수 있습니다. 캐시 적중률이 낮다면 volatile_reg 설정을 검토하세요. 불필요하게 volatile로 마킹된 레지스터가 성능 병목을 유발할 수 있습니다.

pinctrl: 핀 멀티플렉싱

pinctrl 서브시스템은 SoC의 핀 멀티플렉싱(pinmux)과 핀 설정(pinconf)을 관리합니다. GPIO 서브시스템과 밀접하게 연동되며, 하나의 물리 핀이 GPIO, I2C SDA, SPI MOSI 등 여러 기능 중 하나로 설정될 수 있습니다.

pinctrl 핵심 개념

개념설명예시
Pin Group함께 설정되는 핀 그룹i2c1_pins: {SDA, SCL}
Function핀 그룹이 수행하는 기능i2c, spi, gpio, uart
pinmux핀과 기능의 매핑PA9 → I2C1_SDA
pinconf핀 전기적 특성 설정풀업, 드라이브 강도, 슬루율
State디바이스 상태별 핀 설정default, sleep, idle

Device Tree pinctrl 바인딩

/* SoC pinctrl 노드에서 핀 설정 정의 */
&pinctrl {
    i2c1_default: i2c1-default-pins {
        pins = "PA9", "PA10";
        function = "i2c1";
        bias-pull-up;
        drive-open-drain;
    };

    i2c1_sleep: i2c1-sleep-pins {
        pins = "PA9", "PA10";
        function = "gpio";
        bias-high-impedance;
    };

    spi1_default: spi1-default-pins {
        mosi-sck-pins {
            pins = "PB3", "PB5";
            function = "spi1";
            bias-disable;
            drive-push-pull;
            slew-rate = <1>;  /* high speed */
        };
        miso-pin {
            pins = "PB4";
            function = "spi1";
            bias-pull-down;
        };
    };

    user_led_pin: user-led-pin {
        pins = "PC13";
        function = "gpio";
        drive-push-pull;
        output-low;
    };
};

/* 디바이스 노드에서 pinctrl 상태 참조 */
&i2c1 {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&i2c1_default>;
    pinctrl-1 = <&i2c1_sleep>;
    status = "okay";
};

&spi1 {
    pinctrl-names = "default";
    pinctrl-0 = <&spi1_default>;
    status = "okay";
};
pinctrl 자동 전환: 디바이스가 pm_runtime_suspend()에 들어가면 커널이 자동으로 "sleep" 상태의 핀 설정을 적용하고, resume 시 "default"로 복원합니다. 이 동작은 pinctrl-names에 "default"와 "sleep"이 정의되어 있을 때 활성화됩니다.

Device Tree 통합: 공통 바인딩 패턴

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

공통 프로퍼티

프로퍼티적용 대상설명
compatible모든 디바이스드라이버 매칭 문자열 (vendor,device)
regI2C: 슬레이브 주소, SPI: CS 번호버스별 주소/식별자
interrupts인터럽트 사용 디바이스IRQ 스펙
interrupt-parent인터럽트 사용 디바이스IRQ 컨트롤러 phandle
status모든 노드"okay", "disabled"
*-gpiosGPIO 사용 디바이스GPIO specifier
*-supply전원 사용 디바이스regulator phandle
pinctrl-*핀 설정 필요 디바이스pinctrl 상태

종합 예제: I2C + SPI + GPIO 연동

실제 임베디드 보드에서 I2C 센서, SPI Flash, GPIO LED/버튼을 함께 사용하는 Device Tree 예제:

/ {
    model = "My Custom Board";
    compatible = "vendor,my-board";

    leds {
        compatible = "gpio-leds";
        pinctrl-names = "default";
        pinctrl-0 = <&user_led_pin>;

        led-status {
            gpios = <&gpioc 13 GPIO_ACTIVE_LOW>;
            label = "board:green:status";
            linux,default-trigger = "heartbeat";
        };
    };

    gpio-keys {
        compatible = "gpio-keys";

        button-user {
            label = "User Button";
            gpios = <&gpioa 0 GPIO_ACTIVE_LOW>;
            linux,code = <KEY_ENTER>;
            debounce-interval = <20>;
        };
    };
};

&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>;
    };
};

&spi1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi1_default>;

    /* SPI NOR Flash */
    flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <50000000>;
        spi-rx-bus-width = <4>;
        m25p,fast-read;

        partitions {
            compatible = "fixed-partitions";
            #address-cells = <1>;
            #size-cells = <1>;

            bootloader@0 {
                label = "bootloader";
                reg = <0x0 0x40000>;
                read-only;
            };

            firmware@40000 {
                label = "firmware";
                reg = <0x40000 0x3C0000>;
            };
        };
    };

    /* SPI ADC (GPIO expander의 핀을 CS로 사용) */
    adc@1 {
        compatible = "vendor,my-adc";
        reg = <1>;
        spi-max-frequency = <5000000>;
        vref-supply = <&reg_3v3>;
    };
};

Device Tree 디버깅

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

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

# SPI 디바이스 확인
ls /sys/bus/spi/devices/          # 등록된 SPI 디바이스
ls /sys/class/spi_master/         # SPI 컨트롤러

# GPIO 상태 확인
gpiodetect                        # GPIO 칩 목록
gpioinfo                          # 모든 GPIO 라인 정보
cat /sys/kernel/debug/gpio        # debugfs GPIO 상태
cat /sys/kernel/debug/pinctrl/*/pins  # pinctrl 핀 매핑

# Device Tree 런타임 확인
ls /proc/device-tree/             # DT 노드 트리
dtc -I fs /proc/device-tree/      # 런타임 DT를 DTS로 디컴파일
I2C 디바이스가 감지되지 않을 때 체크리스트: (1) i2cdetect로 주소 응답 확인, (2) dmesg | grep i2c로 어댑터 등록 확인, (3) Device Tree의 reg 속성이 실제 하드웨어 주소와 일치하는지 확인, (4) pinctrl 설정이 올바른지 확인 (SDA/SCL 핀이 I2C 기능으로 mux 되었는지), (5) 풀업 저항이 있는지 확인 (오픈 드레인 버스에 외부 풀업 필요).

I2C / SPI / GPIO 비교 요약

특성I2CSPIGPIO
신호선2 (SCL, SDA)4+ (MOSI, MISO, SCK, CS)1/핀
통신 방식반이중전이중단방향 (입력 또는 출력)
최대 속도3.4 MHz (Hs)수백 MHzN/A
어드레싱7/10비트 주소CS 라인컨트롤러+오프셋
커널 헤더<linux/i2c.h><linux/spi/spi.h><linux/gpio/consumer.h>
DT reg 의미슬레이브 주소CS 번호base + ngpio
regmap 지원regmap_init_i2cregmap_init_spiN/A
대표 디바이스센서, EEPROM, RTCFlash, ADC, 디스플레이LED, 버튼, 리셋

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