I2C / SPI / GPIO 서브시스템

임베디드 Linux 시스템의 핵심 저속 버스인 I2C, SPI와 범용 입출력 GPIO 서브시스템의 커널 내부 구조, 드라이버 작성법, Device Tree 바인딩, regmap 추상화, pinctrl 연동까지 종합적으로 다룹니다.

관련 표준: I2C-bus specification (NXP UM10204), SPI (Motorola/de facto), MIPI I3C (MIPI Alliance HCI 1.0) — 이 문서에서 다루는 버스 프로토콜의 기반 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

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고속 메모리

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>

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)를 사용하세요.

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

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선으로 다수 디바이스를 연결할 수 있지만 속도가 제한됩니다.

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 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 칩 지원은 벤더별 파일에 파라미터만 추가하면 됩니다.

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를 사용하는 새 드라이버를 받아들이지 않습니다.

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 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 상태입니다. 새 프로젝트에서는 반드시 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()을 사용해야 합니다.

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)하며, 내부적으로 버스 콜백, 캐시 상태, 잠금 메커니즘을 관리합니다.

/* include/linux/regmap.h - regmap 핵심 구조 (개념도) */

/*
 * struct regmap 내부 주요 필드:
 *
 *   ┌─────────────────────────────────────────────┐
 *   │ struct regmap                                │
 *   ├─────────────────────────────────────────────┤
 *   │ const struct regmap_bus *bus     ← 버스 콜백 │
 *   │ void *bus_context               ← 버스 컨텍스트│
 *   │ const struct regmap_config *config           │
 *   │ struct regcache_ops *cache_ops  ← 캐시 백엔드│
 *   │ void *cache                     ← 캐시 데이터│
 *   │ struct mutex mutex / spinlock   ← 동기화     │
 *   │ regmap_lock / regmap_unlock     ← 잠금 콜백  │
 *   │ struct reg_default *patch       ← 패치 테이블│
 *   │ int num_patch                                │
 *   │ struct list_head async_list     ← 비동기 I/O │
 *   └─────────────────────────────────────────────┘
 */

/* 버스 추상화: 각 버스 타입이 이 콜백을 구현 */
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는 레지스터 내 특정 비트 필드를 독립적인 객체로 추상화합니다. 비트 마스크/시프트 연산을 캡슐화하여 드라이버 코드의 가독성과 유지보수성을 높입니다.

#include <linux/regmap.h>

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

/*
 * 예: 0x04 레지스터 레이아웃
 * ┌─────┬──────┬───────┬──────────┐
 * │ 15  │14:12 │ 11:8  │   7:0    │
 * │ EN  │ MODE │ GAIN  │ OFFSET   │
 * └─────┴──────┴───────┴──────────┘
 */
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는 이러한 페이지 기반 레지스터 접근을 투명하게 처리합니다.

/*
 * 디바이스 레지스터 구조:
 *
 *   물리 주소 공간 (8비트):
 *   ┌──────────────────────────┐
 *   │ 0x00-0x7F: 글로벌 레지스터 │  ← 항상 접근 가능
 *   │ 0x80:      페이지 선택     │  ← 페이지 번호 기록
 *   │ 0x81-0xFF: 페이지 윈도우   │  ← 선택된 페이지의 레지스터
 *   └──────────────────────────┘
 *
 *   가상 주소 공간 (regmap이 추상화):
 *   ┌──────────────────────────┐
 *   │ 0x000-0x07F: 글로벌       │
 *   │ 0x081-0x0FF: 페이지 0     │
 *   │ 0x181-0x1FF: 페이지 1     │
 *   │ 0x281-0x2FF: 페이지 2     │
 *   │ ...                       │
 *   └──────────────────────────┘
 */

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 캐시 상태 머신:
 *
 *   ┌─────────┐  regmap_write()  ┌─────────┐
 *   │  CLEAN  │ ───────────────→ │  DIRTY  │
 *   │ (HW=SW) │                  │ (HW≠SW) │
 *   └────┬────┘                  └────┬────┘
 *        │                            │
 *   cache_only=1                 cache_sync()
 *   (suspend)                    (resume)
 *        │                            │
 *   ┌────▼────┐                  ┌────▼────┐
 *   │  CLEAN  │  regmap_write()  │  CLEAN  │
 *   │cache_only│ → mark dirty →  │ synced  │
 *   └─────────┘                  └─────────┘
 */

/* 캐시 제어 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
├── name                                # regmap 이름
├── range                               # 레지스터 주소 범위
├── registers                           # 모든 레지스터 덤프 (precious 제외)
├── access                              # 접근 권한 테이블
└── cache_only                          # 캐시 전용 모드 상태

# 레지스터 값 확인
$ 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=캐시 전용 모드 활성
/* 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, 버튼, 리셋