I2C / SPI / GPIO 서브시스템
임베디드 Linux 시스템의 핵심 저속 버스인 I2C, SPI와 범용 입출력 GPIO 서브시스템의 커널 내부 구조, 드라이버 작성법, Device Tree 바인딩, regmap 추상화, pinctrl 연동까지 종합적으로 다룹니다.
I2C 프로토콜 기초
I2C (Inter-Integrated Circuit)는 Philips(현 NXP)가 1982년 개발한 2-wire 직렬 버스입니다. 센서, EEPROM, RTC, PMIC 등 저속 주변장치 연결에 널리 사용됩니다.
| 신호선 | 역할 | 특성 |
|---|---|---|
| SCL | Serial Clock | 마스터가 생성, 오픈 드레인 |
| SDA | Serial Data | 양방향 데이터, 오픈 드레인 |
신호 프로토콜
I2C 통신의 기본 단위는 START 조건으로 시작하여 STOP 조건으로 끝나는 트랜잭션입니다:
| 조건 | SDA 상태 | SCL 상태 | 의미 |
|---|---|---|---|
| START (S) | HIGH → LOW | HIGH | 트랜잭션 시작 |
| STOP (P) | LOW → HIGH | HIGH | 트랜잭션 종료 |
| Repeated START (Sr) | HIGH → LOW | HIGH | STOP 없이 재시작 |
| ACK | LOW (수신측) | 9번째 클럭 | 수신 확인 |
| NACK | HIGH (수신측) | 9번째 클럭 | 수신 거부 / 마지막 바이트 |
주소 체계
I2C는 7비트(표준)와 10비트(확장) 주소를 지원합니다. 7비트 주소의 경우 첫 번째 바이트는 [A6:A0 | R/W] 형식으로, 최하위 비트가 방향을 나타냅니다 (0 = Write, 1 = Read).
| 속도 모드 | 클럭 주파수 | 용도 |
|---|---|---|
| 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_adapter | I2C 버스 컨트롤러 (마스터) | <linux/i2c.h> |
| i2c_algorithm | 전송 알고리즘 (HW 접근 방법) | <linux/i2c.h> |
| i2c_client | I2C 버스 상의 슬레이브 디바이스 | <linux/i2c.h> |
| i2c_driver | I2C 디바이스 드라이버 | <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는 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 = ®,
},
{ /* 읽기: 데이터 수신 */
.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-mux)를 사용하세요.
I3C 서브시스템 개요
I3C (Improved Inter-Integrated Circuit)는 MIPI Alliance가 표준화한 차세대 직렬 버스로, I2C와의 하위 호환성을 유지하면서 성능과 기능을 대폭 개선했습니다.
| 특성 | I2C (Fm+) | I3C (SDR) | I3C (HDR-DDR) |
|---|---|---|---|
| 최대 클럭 | 1 MHz | 12.5 MHz | 12.5 MHz |
| 최대 데이터율 | 1 Mbps | 12.5 Mbps | 25 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 메모리 등에 사용됩니다.
| 신호선 | 별칭 | 역할 |
|---|---|---|
| MOSI | SDO, COPI, DI | Master Out Slave In |
| MISO | SDI, CIPO, DO | Master In Slave Out |
| SCK | SCLK, CLK | Serial Clock (마스터 생성) |
| CS/SS | NSS, CE | Chip Select (Active Low) |
SPI 모드 (CPOL/CPHA)
SPI는 클럭 극성(CPOL)과 클럭 위상(CPHA) 조합으로 4가지 동작 모드를 정의합니다:
| 모드 | CPOL | CPHA | 유휴 클럭 | 데이터 샘플링 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | LOW | 상승 에지 |
| Mode 1 | 0 | 1 | LOW | 하강 에지 |
| Mode 2 | 1 | 0 | HIGH | 하강 에지 |
| Mode 3 | 1 | 1 | HIGH | 상승 에지 |
Linux SPI 서브시스템
SPI 서브시스템은 drivers/spi/에 구현되어 있으며 다음 핵심 구조체를 사용합니다:
| 구조체 | 역할 | 헤더 |
|---|---|---|
| spi_controller | SPI 마스터 (호스트) 컨트롤러 | <linux/spi/spi.h> |
| spi_device | SPI 버스 상의 슬레이브 디바이스 | <linux/spi/spi.h> |
| spi_driver | SPI 디바이스 드라이버 | <linux/spi/spi.h> |
| spi_message | SPI 트랜잭션 (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);
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 SPI | 1-bit (MOSI/MISO) | 1-bit | 일반 SPI Flash |
| Dual Output | 2-bit | 1-bit | W25Q series |
| Quad Output | 4-bit | 1-bit | W25Q series |
| QPI (Quad I/O) | 4-bit | 4-bit | 고속 NOR Flash |
| Octal (8D-8D-8D) | 8-bit DDR | 8-bit DDR | Macronix 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);
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> | Deprecated | gpio_request(), gpio_direction_input() |
| Descriptor-based (gpiod) | <linux/gpio/consumer.h> | 현재 표준 | gpiod_get(), gpiod_set_value() |
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()는 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 인터페이스를 대체합니다.
/sys/class/gpio/export 인터페이스는 deprecated 상태입니다. 새 프로젝트에서는 반드시 libgpiod (v2)를 사용하세요.
libgpiod 명령행 도구
| 도구 | 용도 | 예시 |
|---|---|---|
gpiodetect | 시스템의 GPIO 칩 목록 | gpiodetect |
gpioinfo | GPIO 라인 상세 정보 | gpioinfo gpiochip0 |
gpioget | GPIO 입력 값 읽기 | gpioget gpiochip0 7 |
gpioset | GPIO 출력 값 설정 | gpioset gpiochip0 7=1 |
gpiomon | GPIO 이벤트 모니터링 | 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 수 | 인터럽트 | 커널 드라이버 |
|---|---|---|---|---|
| MCP23017 | I2C | 16 | 지원 | gpio-mcp23s08 |
| MCP23S17 | SPI | 16 | 지원 | gpio-mcp23s08 |
| PCA9555 | I2C | 16 | 지원 | gpio-pca953x |
| PCA9535 | I2C | 16 | 지원 | gpio-pca953x |
| PCF8574 | I2C | 8 | 지원 | gpio-pcf857x |
| TCA6424A | I2C | 24 | 지원 | 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";
};
};
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 */
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_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 쓰기 */
다중 레지스터 연산
여러 레지스터를 원자적으로 읽거나 쓸 때 사용하는 고급 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));
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_RBTREE | Red-Black Tree | 희소(sparse) 레지스터 맵 | O(사용 레지스터 수) |
REGCACHE_MAPLE | Maple Tree | v6.4+, 범위 기반 최적화 | O(사용 레지스터 수) |
max_register가 작고 대부분의 레지스터를 사용하면 REGCACHE_FLAT이 가장 빠릅니다 (O(1) 접근). 레지스터 주소가 넓게 분산되어 있으면 REGCACHE_RBTREE나 REGCACHE_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);
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_FS와 CONFIG_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 = 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";
};
pm_runtime_suspend()에 들어가면 커널이 자동으로 "sleep" 상태의 핀 설정을 적용하고, resume 시 "default"로 복원합니다. 이 동작은 pinctrl-names에 "default"와 "sleep"이 정의되어 있을 때 활성화됩니다.
Device Tree 통합: 공통 바인딩 패턴
I2C, SPI, GPIO 서브시스템의 Device Tree 바인딩에서 공통적으로 사용되는 패턴을 정리합니다.
공통 프로퍼티
| 프로퍼티 | 적용 대상 | 설명 |
|---|---|---|
compatible | 모든 디바이스 | 드라이버 매칭 문자열 (vendor,device) |
reg | I2C: 슬레이브 주소, SPI: CS 번호 | 버스별 주소/식별자 |
interrupts | 인터럽트 사용 디바이스 | IRQ 스펙 |
interrupt-parent | 인터럽트 사용 디바이스 | IRQ 컨트롤러 phandle |
status | 모든 노드 | "okay", "disabled" |
*-gpios | GPIO 사용 디바이스 | 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 = <®_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 = <®_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로 디컴파일
i2cdetect로 주소 응답 확인,
(2) dmesg | grep i2c로 어댑터 등록 확인,
(3) Device Tree의 reg 속성이 실제 하드웨어 주소와 일치하는지 확인,
(4) pinctrl 설정이 올바른지 확인 (SDA/SCL 핀이 I2C 기능으로 mux 되었는지),
(5) 풀업 저항이 있는지 확인 (오픈 드레인 버스에 외부 풀업 필요).
I2C / SPI / GPIO 비교 요약
| 특성 | I2C | SPI | GPIO |
|---|---|---|---|
| 신호선 | 2 (SCL, SDA) | 4+ (MOSI, MISO, SCK, CS) | 1/핀 |
| 통신 방식 | 반이중 | 전이중 | 단방향 (입력 또는 출력) |
| 최대 속도 | 3.4 MHz (Hs) | 수백 MHz | N/A |
| 어드레싱 | 7/10비트 주소 | CS 라인 | 컨트롤러+오프셋 |
| 커널 헤더 | <linux/i2c.h> | <linux/spi/spi.h> | <linux/gpio/consumer.h> |
| DT reg 의미 | 슬레이브 주소 | CS 번호 | base + ngpio |
| regmap 지원 | regmap_init_i2c | regmap_init_spi | N/A |
| 대표 디바이스 | 센서, EEPROM, RTC | Flash, ADC, 디스플레이 | LED, 버튼, 리셋 |