I2C 서브시스템 — I2C / SMBus / I3C
임베디드 Linux에서 가장 많이 쓰이는 I2C 서브시스템을 보드 초기화부터 드라이버 운영까지 실무 관점으로 정리합니다. I2C 전송 모델과 버스(Bus) arbitration, SMBus 프로토콜과 PEC, I3C 차세대 직렬 버스, Device Tree 바인딩, regmap을 활용한 레지스터(Register) 추상화, IRQ 처리, 전원관리와 슬립(Sleep) 복귀 시 상태 복원, 로직 분석기와 tracepoint를 이용한 타이밍 문제 진단까지 I2C 기반 드라이버 개발에 필요한 핵심을 다룹니다.
핵심 요약
- 2-wire 직렬 버스 — SCL(클럭)과 SDA(데이터) 두 선만으로 다수의 디바이스를 연결합니다.
- 주소 기반 통신 — 7비트/10비트 주소로 슬레이브를 식별하여 데이터를 송수신합니다.
- SMBus 호환 — PC 시스템 관리용 SMBus 프로토콜은 I2C 기반이며, PEC 오류 검출을 지원합니다.
- I3C 차세대 — I2C 호환을 유지하면서 최대 33 Mbps, IBI(In-Band Interrupt), 동적 주소 할당을 제공합니다.
- regmap 추상화 — 버스별 전송 함수 호출 없이 통합된 레지스터 접근을 제공합니다.
단계별 이해
- I2C 프로토콜 이해
START/STOP 조건, 주소 체계, ACK/NACK 흐름을 파악합니다. - 커널 서브시스템 구조
i2c_adapter, i2c_client, i2c_driver 관계를 정리합니다. - 드라이버 작성
probe/remove, SMBus API, Device Tree 바인딩을 구현합니다. - 디버깅과 검증
i2c-tools, tracepoint, regmap debugfs로 동작을 확인합니다.
I2C 프로토콜 기초
I2C (Inter-Integrated Circuit)는 Philips(현 NXP)가 1982년 개발한 2-wire 직렬 버스입니다. 센서, EEPROM, RTC, PMIC 등 저속 주변장치 연결에 널리 사용됩니다.
| 신호선 | 역할 | 특성 |
|---|---|---|
| SCL | Serial Clock | 마스터가 생성, 오픈 드레인 |
| SDA | Serial Data | 양방향 데이터, 오픈 드레인 |
신호 프로토콜
I2C 통신의 기본 단위는 START 조건으로 시작하여 STOP 조건으로 끝나는 트랜잭션(Transaction)입니다:
| 조건 | SDA 상태 | SCL 상태 | 의미 |
|---|---|---|---|
| START (S) | HIGH → LOW | HIGH | 트랜잭션 시작 |
| STOP (P) | LOW → HIGH | HIGH | 트랜잭션 종료 |
| Repeated START (Sr) | HIGH → LOW | HIGH | STOP 없이 재시작(Reboot) |
| 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 | 고속 메모리 |
I2C 버스 타이밍 파형
I2C 프로토콜의 완전한 트랜잭션 타이밍을 이해하는 것은 하드웨어 디버깅(Debugging)과 드라이버 개발에 필수적입니다. 아래 다이어그램은 마스터가 슬레이브에 1바이트를 쓰는 전형적인 Write 트랜잭션의 SCL/SDA 파형을 보여줍니다.
Multi-Master Arbitration과 Clock Stretching
I2C는 multi-master 버스이므로, 두 마스터가 동시에 전송을 시작할 수 있습니다. Arbitration은 SDA 라인에서 비트 단위로 수행됩니다. 오픈 드레인 특성상, 어떤 마스터가 HIGH를 출력하지만 다른 마스터가 LOW를 구동하면 버스는 LOW가 됩니다. HIGH를 출력한 마스터가 SDA를 모니터링하여 LOW를 감지하면 arbitration에 패배한 것이므로 즉시 전송을 중단합니다.
Clock Stretching은 슬레이브가 SCL 라인을 LOW로 유지하여 마스터의 클럭을 늦추는 메커니즘입니다. 슬레이브가 데이터를 준비할 시간이 필요할 때 사용합니다. 마스터는 SCL을 HIGH로 전환하려 할 때 실제 SCL 레벨을 확인하여, 슬레이브가 여전히 LOW로 잡고 있으면 대기합니다.
i2c-scl-internal-delay-ns와 i2c-scl-clk-low-timeout-us 속성으로 타임아웃을 설정할 수 있습니다.
Linux I2C 서브시스템
커널 I2C 서브시스템은 drivers/i2c/ 디렉토리에 구현되어 있으며, 다음 핵심 구조체(Struct)로 구성됩니다:
| 구조체 | 역할 | 헤더 |
|---|---|---|
| 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> |
위 아키텍처에서 핵심 데이터 흐름은 다음과 같습니다: Device Tree(또는 ACPI)가 어댑터와 클라이언트 정보를 제공하면, I2C Core가 i2c_adapter를 등록하고 하위 i2c_client를 열거합니다. i2c_driver의 of_match_table 또는 id_table로 매칭이 이루어지면 probe가 호출됩니다.
i2c_adapter와 i2c_algorithm
i2c_adapter는 물리적 I2C 컨트롤러를 나타내며, i2c_algorithm을 통해 실제 하드웨어 전송을 수행합니다:
struct i2c_algorithm {
int (*master_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
int (*master_xfer_atomic)(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
int (*smbus_xfer)(struct i2c_adapter *adap,
u16 addr, unsigned short flags,
char read_write, u8 command,
int size, union i2c_smbus_data *data);
u32 (*functionality)(struct i2c_adapter *adap);
};
master_xfer는 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)를 사용하세요.
SMBus (System Management Bus)
SMBus (System Management Bus)는 Intel이 1995년 정의한 I2C 기반의 2-wire 직렬 버스 규격입니다. PC 시스템 관리를 위해 설계되었으며, PMIC(전원 관리(Power Management) IC), DIMM SPD EEPROM, 팬 컨트롤러, 온도 센서, 배터리 관리 IC 등 PC 메인보드 주변장치 통신에 표준적으로 사용됩니다. SMBus는 I2C를 기반으로 하되, 전기적 특성과 프로토콜 규칙을 더 엄격하게 제한하여 상호운용성과 신뢰성을 강화한 것이 특징입니다.
SMBus와 I2C 차이점
SMBus는 I2C의 상위집합이 아니라 별도의 규격이며, 여러 측면에서 I2C와 다릅니다:
| 항목 | I2C (표준) | SMBus 2.0/3.x |
|---|---|---|
| 전압 레벨 | VDD 자유 (1.8V~5V) | VDD = 3.3V (기본), 고정된 VLOW/VHIGH 임계값 |
| 클럭 속도 | 100 kHz ~ 3.4 MHz | 10 kHz ~ 100 kHz (2.0), 최대 1 MHz (3.x) |
| 최저 클럭 | 제한 없음 (DC 가능) | 10 kHz 하한 (timeout 때문) |
| Clock Stretching | 무제한 허용 | 슬레이브 25 ms, 마스터 Cumulative 25 ms 제한 |
| Timeout | 없음 | 35 ms timeout (SCL LOW 유지 시 버스 리셋) |
| PEC | 없음 | CRC-8 Packet Error Checking (선택) |
| Address Resolution | 없음 | ARP (Address Resolution Protocol) 지원 |
| Alert 메커니즘 | 별도 GPIO 인터럽트 | SMBALERT# 전용 라인 |
| 트랜잭션 크기 | 제한 없음 | 최대 32바이트 블록 (SMBus 2.0), 255바이트 (3.x) |
| Host Notify | 없음 | 슬레이브→호스트 알림 프로토콜 |
SMBus 프로토콜 타이밍
SMBus의 Write Byte 트랜잭션은 I2C와 유사하지만, Command 코드 바이트가 추가되고 선택적으로 PEC(Packet Error Checking) 바이트가 뒤따릅니다. 아래는 PEC를 포함한 SMBus Write Byte 트랜잭션의 구조를 보여줍니다.
SMBus 트랜잭션 유형
SMBus는 사전 정의된 트랜잭션 유형을 명시하여 디바이스 간 상호운용성을 보장합니다:
| 트랜잭션 | 데이터 흐름 | 바이트 수 | 커널 함수 | 용도 |
|---|---|---|---|---|
| Quick Command | R/W 비트만 | 0 | i2c_smbus_write_quick() | 디바이스 존재 확인, ON/OFF 토글 |
| Send Byte | Host → Device 1바이트 | 1 | i2c_smbus_write_byte() | 단순 명령 전송 |
| Receive Byte | Device → Host 1바이트 | 1 | i2c_smbus_read_byte() | 상태 읽기 |
| Write Byte | Cmd + 1바이트 | 2 | i2c_smbus_write_byte_data() | 레지스터 쓰기 |
| Read Byte | Cmd → 1바이트 | 2 | i2c_smbus_read_byte_data() | 레지스터 읽기 |
| Write Word | Cmd + 2바이트 | 3 | i2c_smbus_write_word_data() | 16비트 레지스터 쓰기 |
| Read Word | Cmd → 2바이트 | 3 | i2c_smbus_read_word_data() | 16비트 레지스터 읽기 |
| Process Call | Cmd + 2바이트 → 2바이트 | 4 | i2c_smbus_process_call() | 명령-응답 쌍 |
| Block Write | Cmd + N바이트 (N≤32) | 2+N | i2c_smbus_write_block_data() | 다중 바이트 쓰기 |
| Block Read | Cmd → N바이트 | 2+N | i2c_smbus_read_block_data() | 다중 바이트 읽기 |
| Block Process Call | Cmd + N바이트 → M바이트 | 3+N+M | i2c_smbus_block_process_call() | 블록 명령-응답 |
| Host Notify | 슬레이브→호스트 알림 | 3 | i2c_smbus_host_notify 콜백(Callback) | 슬레이브 이벤트 통보 |
SMBus 시스템 토폴로지(Topology)
일반적인 PC 시스템에서 SMBus는 사우스브릿지(PCH)의 SMBus 컨트롤러가 마스터 역할을 하며, 다양한 시스템 관리 디바이스가 슬레이브로 연결됩니다:
PEC (Packet Error Checking)
SMBus PEC는 CRC-8 알고리즘(다항식: x8 + x2 + x + 1, 초기값 0x00)을 사용하여 전송 데이터의 무결성(Integrity)을 검증합니다. PEC 바이트는 트랜잭션의 마지막에 추가되며, 주소 바이트(R/W 비트 포함)부터 마지막 데이터 바이트까지 모든 전송 바이트에 대해 CRC를 계산합니다.
/* 커널의 SMBus PEC 계산 (drivers/i2c/i2c-core-smbus.c) */
static u8 i2c_smbus_pec(u8 crc, u8 *p, size_t count)
{
/* CRC-8 다항식: x^8 + x^2 + x + 1 = 0x107 */
for (int i = 0; i < count; i++)
crc = crc8(crc8_table, p[i], crc);
return crc;
}
/* PEC 사용을 위한 클라이언트 플래그 설정 */
client->flags |= I2C_CLIENT_PEC;
/* PEC가 활성화된 SMBus 읽기 - 자동으로 PEC 검증 */
int val = i2c_smbus_read_byte_data(client, reg);
/* PEC 불일치 시 -EBADMSG 반환 */
SMBus ARP (Address Resolution Protocol)
SMBus ARP는 디바이스에 동적으로 주소를 할당하는 프로토콜입니다. 여러 동일 디바이스가 같은 버스에 연결될 때 주소 충돌 없이 식별할 수 있습니다. 각 디바이스는 고유한 128비트 UDID (Unique Device Identifier)를 가지며, 호스트가 이를 기반으로 7비트 주소를 할당합니다.
0x61 (SMBus Device Default Address)과 0x6C (ARP Controller) 입니다. 호스트는 General Call(0x00)로 ARP 명령을 브로드캐스트하고, 할당되지 않은 디바이스는 0x61로 응답합니다.
Linux 커널 SMBus 구현
커널의 SMBus 지원은 drivers/i2c/i2c-core-smbus.c에 구현되어 있습니다. i2c_smbus_xfer()가 핵심 함수이며, 어댑터가 네이티브 SMBus를 지원하면 i2c_algorithm.smbus_xfer를 호출하고, 그렇지 않으면 i2c_smbus_xfer_emulated()를 통해 일반 I2C 메시지로 에뮬레이션합니다.
/* SMBus functionality 플래그 확인 */
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_READ_BYTE_DATA |
I2C_FUNC_SMBUS_WRITE_BYTE_DATA)) {
dev_err(&client->dev, "SMBus byte data not supported\\n");
return -ENODEV;
}
/* SMBus 트랜잭션 - 어댑터가 네이티브 지원하면 HW 경로, 아니면 에뮬레이션 */
s32 val = i2c_smbus_read_word_data(client, 0x00);
if (val < 0)
return val;
dev_info(&client->dev, "reg 0x00 = 0x%04x\\n", val);
/* Block Read: 가변 길이 데이터 읽기 */
u8 buf[I2C_SMBUS_BLOCK_MAX]; /* 최대 32바이트 */
s32 len = i2c_smbus_read_block_data(client, 0x10, buf);
if (len < 0)
return len;
dev_info(&client->dev, "block read %d bytes\\n", len);
SMBus Alert 처리
SMBus Alert는 슬레이브 디바이스가 호스트에 이벤트를 알리는 메커니즘입니다. 슬레이브가 SMBALERT# 라인을 LOW로 구동하면, 호스트가 Alert Response Address(0x0C)로 Read 트랜잭션을 수행하여 알림 발생 디바이스의 주소를 얻습니다.
#include <linux/i2c.h>
#include <linux/i2c-smbus.h>
/* SMBus Alert 핸들러 콜백 */
static void my_smbus_alert(struct i2c_client *client,
enum i2c_alert_protocol type,
unsigned int data)
{
struct my_device *priv = i2c_get_clientdata(client);
dev_info(&client->dev, "SMBus alert received, data=0x%x\\n", data);
/* 알림 원인 확인 후 처리 */
schedule_work(&priv->alert_work);
}
static struct i2c_driver my_driver = {
.driver = {
.name = "my-smbus-device",
.of_match_table = my_of_ids,
},
.probe = my_probe,
.alert = my_smbus_alert, /* Alert 핸들러 등록 */
.id_table = my_ids,
};
SMBus 3.x 신규 기능
| 기능 | SMBus 2.0 | SMBus 3.0+ |
|---|---|---|
| 최대 클럭 | 100 kHz | 1 MHz |
| Block 최대 크기 | 32 바이트 | 255 바이트 |
| Zone Read/Write | 미지원 | 지원 (다중 디바이스 동시 설정) |
| 32-bit Process Call | 미지원 | 지원 (4바이트 데이터) |
| High Power | 350 μA max | 4 mA max (high-power class) |
SMBus/I2C 디버깅 도구
i2c-tools 패키지는 SMBus/I2C 버스 디버깅에 필수적인 명령행 도구를 제공합니다:
/* i2cdetect: 버스 스캔 (SMBus Quick Command 사용) */
$ i2cdetect -y 0 /* i2c-0 버스의 모든 주소 스캔 */
$ i2cdetect -l /* 시스템의 I2C 어댑터 목록 */
/* i2cdump: 디바이스 전체 레지스터 덤프 */
$ i2cdump -y 0 0x48 /* i2c-0 버스, 주소 0x48 전체 읽기 */
$ i2cdump -y 0 0x48 b /* SMBus byte read 모드 */
$ i2cdump -y 0 0x48 w /* SMBus word read 모드 */
/* i2cget: 특정 레지스터 읽기 */
$ i2cget -y 0 0x48 0x00 b /* byte read: 온도값 */
$ i2cget -y 0 0x48 0x00 w /* word read */
/* i2cset: 특정 레지스터 쓰기 */
$ i2cset -y 0 0x48 0x01 0x60 b /* config 레지스터 설정 */
/* i2ctransfer: raw I2C 메시지 전송 */
$ i2ctransfer -y 0 w2@0x48 0x00 0x01 r2 /* 2바이트 쓰고 2바이트 읽기 */
trace-cmd record -e i2c로 I2C/SMBus 트랜잭션을 커널 레벨에서 추적할 수 있습니다. i2c_read, i2c_write, i2c_result, smbus_read, smbus_write, smbus_result tracepoint가 제공됩니다.
I3C 서브시스템 개요
I3C (Improved Inter-Integrated Circuit)는 MIPI Alliance가 표준화한 차세대 직렬 버스로, I2C와의 하위 호환성을 유지하면서 성능과 기능을 대폭 개선했습니다.
| 특성 | I2C (Fm+) | I3C (SDR) | I3C (HDR-DDR) |
|---|---|---|---|
| 최대 클럭 | 1 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);
I3C 버스 토폴로지
I3C 버스는 I3C 마스터(Main Master), I3C 타겟 디바이스, 그리고 레거시 I2C 디바이스가 동일한 2-wire 버스에 공존할 수 있습니다. I3C 디바이스는 푸시풀(push-pull) 출력을 사용하고, I2C 레거시 디바이스는 오픈 드레인 모드에서 동작합니다.
DAA (Dynamic Address Assignment)
I3C의 핵심 기능 중 하나인 DAA는 마스터가 버스 상의 모든 I3C 디바이스에 동적으로 7비트 주소를 할당하는 절차입니다. 각 I3C 디바이스는 48비트 Provisional ID (PID)를 가지며, 마스터는 ENTDAA CCC를 사용하여 디바이스를 열거합니다.
| 단계 | 동작 | 설명 |
|---|---|---|
| 1 | ENTDAA CCC 브로드캐스트 | 마스터가 ENTDAA(0x07) Common Command Code를 전송 |
| 2 | 디바이스 PID 응답 | 할당되지 않은 디바이스가 48-bit PID + BCR + DCR 전송 |
| 3 | Arbitration | 여러 디바이스가 동시 응답 시, PID 기반 arbitration (낮은 PID 우선) |
| 4 | 주소 할당 | 마스터가 승리한 디바이스에 7-bit 동적 주소 할당 |
| 5 | 반복 | 모든 디바이스에 주소가 할당될 때까지 2-4 반복 |
IBI (In-Band Interrupt)
I3C의 IBI는 별도의 인터럽트 라인 없이 SDA 버스를 통해 타겟 디바이스가 마스터에 인터럽트를 요청하는 메커니즘입니다. 타겟은 버스가 유휴 상태(Idle State)일 때 START 조건을 발생시키고 자신의 동적 주소를 전송합니다. 마스터는 이를 감지하여 ACK/NACK으로 응답합니다.
/* I3C IBI 핸들러 등록 */
static void my_i3c_ibi_handler(struct i3c_device *dev,
const struct i3c_ibi_payload *payload)
{
/* MDB (Mandatory Data Byte) 확인 */
if (payload->len > 0) {
u8 mdb = ((u8 *)payload->data)[0];
dev_dbg(i3cdev_to_dev(dev),
"IBI received, MDB=0x%02x\\n", mdb);
}
/* 인터럽트 처리 로직 */
}
/* probe에서 IBI 요청 */
struct i3c_ibi_setup ibi_setup = {
.handler = my_i3c_ibi_handler,
.max_payload_len = 2,
.num_slots = 4,
};
ret = i3c_device_request_ibi(i3cdev, &ibi_setup);
if (ret)
return ret;
i3c_device_enable_ibi(i3cdev);
HDR (High Data Rate) 모드
I3C는 SDR(Standard Data Rate) 외에 3가지 HDR 모드를 지원하여 더 높은 데이터 전송률을 달성합니다:
| 모드 | 최대 데이터율 | 특징 | 용도 |
|---|---|---|---|
| HDR-DDR | 25 Mbps | Double Data Rate, SCL 양 에지에서 데이터 전송, CRC-5 오류 검출 | 고속 센서 데이터 스트리밍 |
| HDR-TSP | 33 Mbps | Ternary Symbol Pure, 3-level 신호 (SDA만 사용), SCL 불필요 | 짧은 배선, 최고 속도 요구 |
| HDR-TSL | 33 Mbps | Ternary Symbol Legacy, TSP + I2C 레거시 디바이스 호환 | 혼합 버스 환경 고속 전송 |
/* HDR-DDR 전송 */
struct i3c_priv_xfer hdr_xfer = {
.rnw = 0,
.len = 64,
.data.out = tx_data,
};
/* HDR 모드는 i3c_device_do_priv_xfers()에서
컨트롤러가 지원하면 자동 선택될 수 있음 */
ret = i3c_device_do_priv_xfers(i3cdev, &hdr_xfer, 1);
I3C Hot-Join 프로토콜
Hot-Join은 버스가 이미 동작 중일 때 새로운 I3C 디바이스가 동적으로 참여하는 메커니즘입니다. 새 디바이스가 버스에 연결되면 IBI 유사한 방식으로 마스터에 알리고, 마스터가 DAA를 수행하여 주소를 할당합니다.
| 단계 | 주체 | 동작 |
|---|---|---|
| 1 | 새 디바이스 | 버스가 유휴일 때 SDA를 LOW로 구동 (Hot-Join 요청) |
| 2 | 마스터 | Hot-Join 요청 감지, 주소 0x02 + R=1로 응답 |
| 3 | 마스터 | ENTDAA CCC 발행하여 새 디바이스에 동적 주소 할당 |
| 4 | 마스터 | 할당 완료 후 새 디바이스의 드라이버 바인딩 |
CCC (Common Command Codes)
I3C CCC는 마스터가 모든 디바이스(브로드캐스트) 또는 특정 디바이스(다이렉트)에 보내는 표준 명령입니다:
| CCC | 코드 | 유형 | 설명 |
|---|---|---|---|
| ENEC | 0x00 | Broadcast/Direct | 이벤트 활성화 (IBI, MR, HJ 허용) |
| DISEC | 0x01 | Broadcast/Direct | 이벤트 비활성화 |
| ENTAS0~3 | 0x02~0x05 | Broadcast/Direct | Activity State 진입 (저전력 모드) |
| RSTDAA | 0x06 | Broadcast | 모든 동적 주소 리셋 |
| ENTDAA | 0x07 | Broadcast | 동적 주소 할당 시작 |
| SETDASA | 0x87 | Direct | 정적→동적 주소 매핑(Mapping) |
| SETNEWDA | 0x88 | Direct | 새 동적 주소 설정 |
| GETPID | 0x8D | Direct | Provisional ID 조회 (48-bit) |
| GETBCR | 0x8E | Direct | Bus Characteristics Register 조회 |
| GETDCR | 0x8F | Direct | Device Characteristics Register 조회 |
| GETSTATUS | 0x90 | Direct | 디바이스 상태 조회 (IBI pending 등) |
| GETMXDS | 0x94 | Direct | 최대 데이터 속도 조회 |
| ENTHDR0~7 | 0x20~0x27 | Broadcast | HDR 모드 진입 |
I2C에서 I3C으로 마이그레이션
| 항목 | 변경 사항 | 호환성 |
|---|---|---|
| 물리 계층 | Push-Pull 출력 (I3C), 풀업 저항 제거 가능 | I2C 디바이스는 여전히 오픈 드레인으로 동작 |
| 주소 할당 | 정적 → DAA 동적 할당 | I2C 디바이스는 SETDASA로 정적→동적 매핑 |
| 인터럽트 | GPIO IRQ → SDA IBI | I2C 디바이스는 여전히 별도 IRQ 라인 필요 |
| 속도 | 12.5 MHz SDR, 최대 33 Mbps HDR | 레거시 모드에서 I2C Fm+ 속도 지원 |
| 커널 API | i2c_driver → i3c_driver | i3c_device_do_priv_xfers() 사용 |
| Device Tree | reg 대신 PID/BCR/DCR 기반 | I2C 디바이스는 i3c-i2c-dev 노드로 기술 |
drivers/i3c/에 Cadence, DW(DesignWare), SVC(Silvaco) 등의 컨트롤러 드라이버가 포함되어 있습니다. I3C Target 모드 지원은 v6.x에서 추가되고 있으며, HDR 모드 지원은 컨트롤러별로 다릅니다.
regmap: 레지스터 추상화 API
regmap은 다양한 버스(I2C, SPI, MMIO) 위의 레지스터 접근을 통합 추상화하는 프레임워크입니다. I2C 드라이버에서는 devm_regmap_init_i2c()를 통해 버스별 전송 함수 호출을 제거하고, 캐싱, 범위 검사, endian 변환 등 공통 기능을 투명하게 제공합니다. SPI에서의 regmap 사용은 SPI 서브시스템 문서를 참조하세요.
regmap 설정
#include <linux/regmap.h>
/* 레지스터 기본값 (캐시 초기화용) */
static const struct reg_default my_reg_defaults[] = {
{ 0x00, 0x0000 }, /* STATUS */
{ 0x01, 0x001F }, /* CONFIG */
{ 0x02, 0x0000 }, /* DATA */
};
/* 읽기 가능 레지스터 범위 */
static bool my_readable_reg(struct device *dev, unsigned int reg)
{
return reg <= 0x10;
}
/* 휘발성 레지스터 (캐시하지 않음) */
static bool my_volatile_reg(struct device *dev, unsigned int reg)
{
return reg == 0x00 || reg == 0x02; /* STATUS, DATA */
}
static const struct regmap_config my_regmap_config = {
.reg_bits = 8, /* 레지스터 주소 비트 수 */
.val_bits = 16, /* 레지스터 값 비트 수 */
.max_register = 0x10,
.readable_reg = my_readable_reg,
.volatile_reg = my_volatile_reg,
.cache_type = REGCACHE_RBTREE,
.reg_defaults = my_reg_defaults,
.num_reg_defaults = ARRAY_SIZE(my_reg_defaults),
};
버스별 regmap 생성
/* I2C regmap */
static int my_i2c_probe(struct i2c_client *client)
{
struct regmap *regmap;
regmap = devm_regmap_init_i2c(client, &my_regmap_config);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
/* ... */
}
/* SPI regmap (참고: SPI 서브시스템 문서 참조) */
static int my_spi_probe(struct spi_device *spi)
{
struct regmap *regmap;
regmap = devm_regmap_init_spi(spi, &my_regmap_config);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
/* ... */
}
/* MMIO regmap */
static int my_platform_probe(struct platform_device *pdev)
{
void __iomem *base;
struct regmap *regmap;
base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);
regmap = devm_regmap_init_mmio(&pdev->dev, base, &my_regmap_config);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
/* ... */
}
regmap 사용
unsigned int val;
int ret;
/* 단일 레지스터 읽기 */
ret = regmap_read(regmap, 0x00, &val);
if (ret)
return ret;
/* 단일 레지스터 쓰기 */
ret = regmap_write(regmap, 0x01, 0x1234);
/* 비트 필드 업데이트 (read-modify-write 원자적 수행) */
ret = regmap_update_bits(regmap, 0x01,
0x00FF, /* mask */
0x0042); /* value */
/* 벌크 읽기 */
u16 buf[4];
ret = regmap_bulk_read(regmap, 0x00, buf, 4);
/* 폴링: 비트가 설정될 때까지 대기 */
ret = regmap_read_poll_timeout(regmap, 0x00, val,
val & BIT(0), /* condition */
1000, /* sleep_us */
100000); /* timeout_us */
REGCACHE_NONE (캐시 없음), REGCACHE_RBTREE (희소 레지스터용, RB 트리), REGCACHE_FLAT (연속 레지스터용, 배열), REGCACHE_MAPLE (v6.4+, maple tree 기반). 휘발성(volatile) 레지스터는 항상 하드웨어에서 직접 읽습니다.
regmap IRQ 프레임워크
regmap-irq는 레지스터 기반 인터럽트 컨트롤러(Interrupt Controller)를 위한 generic IRQ chip을 제공합니다:
static const struct regmap_irq my_irqs[] = {
REGMAP_IRQ_REG(0, 0, BIT(0)), /* IRQ 0: bit 0 of reg 0 */
REGMAP_IRQ_REG(1, 0, BIT(1)), /* IRQ 1: bit 1 of reg 0 */
REGMAP_IRQ_REG(2, 0, BIT(2)), /* IRQ 2: bit 2 of reg 0 */
};
static const struct regmap_irq_chip my_irq_chip = {
.name = "my-device",
.irqs = my_irqs,
.num_irqs = ARRAY_SIZE(my_irqs),
.num_regs = 1,
.status_base = 0x08, /* 인터럽트 상태 레지스터 */
.mask_base = 0x09, /* 인터럽트 마스크 레지스터 */
.ack_base = 0x08, /* ACK = 상태 레지스터에 W1C */
};
/* probe에서 등록 */
struct regmap_irq_chip_data *irq_data;
ret = devm_regmap_add_irq_chip(&client->dev, regmap,
client->irq, IRQF_ONESHOT, 0,
&my_irq_chip, &irq_data);
/* 하위 디바이스에서 가상 IRQ 사용 */
int virq = regmap_irq_get_virq(irq_data, 0);
regmap 내부 아키텍처
regmap 프레임워크는 버스 추상화 계층(bus abstraction layer)과 캐시 백엔드로 구성됩니다. 핵심 구조체 struct regmap은 드라이버에 불투명(opaque)하며, 내부적으로 버스 콜백, 캐시 상태, 잠금(Lock) 메커니즘을 관리합니다.
/* include/linux/regmap.h - regmap 핵심 구조 (개념도) */
/* struct regmap 내부 주요 필드(위 SVG 참고) */
/* 버스 추상화: 각 버스 타입이 이 콜백을 구현 */
struct regmap_bus {
bool fast_io; /* true면 spinlock 사용 */
int (*write)(void *context, /* 레지스터 쓰기 */
const void *data, size_t count);
int (*read)(void *context, /* 레지스터 읽기 */
const void *reg_buf, size_t reg_size,
void *val_buf, size_t val_size);
int (*reg_write)(void *context, /* 단일 reg 쓰기 (선택) */
unsigned int reg, unsigned int val);
int (*reg_read)(void *context, /* 단일 reg 읽기 (선택) */
unsigned int reg, unsigned int *val);
int (*reg_update_bits)(void *context, /* RMW 최적화 (선택) */
unsigned int reg,
unsigned int mask, unsigned int val);
enum regmap_endian val_format_endian_default; /* 버스 기본 엔디안 */
};
devm_regmap_init_i2c(), devm_regmap_init_spi(), devm_regmap_init_mmio() 외에도 devm_regmap_init_spi_avmm() (SPI Avalon-MM), devm_regmap_init_spmi_base/ext() (SPMI), devm_regmap_init_w1() (1-Wire), devm_regmap_init_sdw() (SoundWire), devm_regmap_init_slimbus() (SLIMbus) 등 다양한 버스를 지원합니다. fast_io = true인 버스(MMIO 등)는 mutex 대신 spinlock을 사용하여 원자적(Atomic) 컨텍스트에서도 접근 가능합니다.
regmap_field: 비트 필드 추상화
regmap_field는 레지스터 내 특정 비트 필드를 독립적인 객체로 추상화합니다. 비트 마스크/시프트 연산을 캡슐화(Encapsulation)하여 드라이버 코드의 가독성과 유지보수성을 높입니다.
#include <linux/regmap.h>
/* 레지스터 필드 정의 매크로 */
/* REG_FIELD(reg, lsb, msb) - 레지스터 주소와 비트 범위 지정 */
/* 예: 0x04 레지스터 레이아웃 (위 SVG 참고) */
static const struct reg_field enable_field = REG_FIELD(0x04, 15, 15);
static const struct reg_field mode_field = REG_FIELD(0x04, 12, 14);
static const struct reg_field gain_field = REG_FIELD(0x04, 8, 11);
static const struct reg_field offset_field = REG_FIELD(0x04, 0, 7);
struct my_device {
struct regmap *regmap;
struct regmap_field *f_enable;
struct regmap_field *f_mode;
struct regmap_field *f_gain;
struct regmap_field *f_offset;
};
static int my_probe(struct i2c_client *client)
{
struct my_device *dev;
struct regmap *regmap;
dev = devm_kzalloc(&client->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
regmap = devm_regmap_init_i2c(client, &my_regmap_config);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
dev->regmap = regmap;
/* 필드 객체 할당 */
dev->f_enable = devm_regmap_field_alloc(&client->dev, regmap, enable_field);
if (IS_ERR(dev->f_enable))
return PTR_ERR(dev->f_enable);
dev->f_mode = devm_regmap_field_alloc(&client->dev, regmap, mode_field);
dev->f_gain = devm_regmap_field_alloc(&client->dev, regmap, gain_field);
dev->f_offset = devm_regmap_field_alloc(&client->dev, regmap, offset_field);
/* 필드 접근: 비트 마스크/시프트 불필요 */
regmap_field_write(dev->f_enable, 1); /* bit 15 = 1 */
regmap_field_write(dev->f_mode, 0x5); /* bits 14:12 = 5 */
regmap_field_write(dev->f_gain, 0xA); /* bits 11:8 = 0xA */
unsigned int val;
regmap_field_read(dev->f_offset, &val); /* bits 7:0 읽기 */
/* 필드 비트 업데이트 (field 범위 내에서 RMW) */
regmap_field_update_bits(dev->f_mode, 0x3, 0x2); /* mode 하위 2비트만 수정 */
return 0;
}
/* 벌크 필드 할당 (v5.3+): 여러 필드를 한 번에 할당 */
static const struct reg_field my_fields[] = {
[F_ENABLE] = REG_FIELD(0x04, 15, 15),
[F_MODE] = REG_FIELD(0x04, 12, 14),
[F_GAIN] = REG_FIELD(0x04, 8, 11),
[F_OFFSET] = REG_FIELD(0x04, 0, 7),
};
struct regmap_field *fields[F_MAX];
ret = devm_regmap_field_bulk_alloc(&client->dev, regmap,
fields, my_fields, F_MAX);
regmap_update_bits(regmap, 0x04, 0x7000, 0x5000)로 작성하면 매직 넘버가 코드 전체에 흩어집니다. regmap_field를 사용하면 필드 정의가 한곳에 집중되고, 드라이버 코드는 regmap_field_write(f_mode, 5)처럼 의미가 명확해집니다. IIO, regulator, clock 등 커널 서브시스템 드라이버에서 널리 사용됩니다.
regmap 캐시
regmap 캐시는 레지스터 값의 로컬 복사본을 유지하여 불필요한 버스 트랜잭션을 줄입니다. 전원 관리와 긴밀하게 연동되며, 캐시 상태 관리를 통해 resume 시 효율적인 레지스터 복원을 수행합니다.
/*
* regmap 캐시 상태 머신 (위 SVG 참고)
*/
/* 캐시 제어 API */
/* 캐시 전용 모드: 버스 접근 차단, 캐시만 갱신 */
regcache_cache_only(regmap, true); /* suspend 시 */
regmap_write(regmap, 0x01, 0x42); /* 캐시에만 기록, dirty 마킹 */
regcache_cache_only(regmap, false); /* resume 시 */
/* 캐시 → HW 동기화: dirty 레지스터만 하드웨어에 기록 */
ret = regcache_sync(regmap);
/* 특정 범위만 동기화 */
ret = regcache_sync_region(regmap, 0x00, 0x0F);
/* 캐시 전체를 dirty로 마킹 (resume 시 전체 복원 강제) */
regcache_mark_dirty(regmap);
/* 캐시 무효화: 캐시된 값 폐기, 다음 읽기 시 HW 접근 */
ret = regcache_drop_region(regmap, 0x00, 0x0F);
/* 캐시 바이패스: 일시적으로 캐시 우회, HW 직접 접근 */
regcache_cache_bypass(regmap, true);
regmap_read(regmap, 0x00, &val); /* HW에서 직접 읽기 */
regcache_cache_bypass(regmap, false);
| 캐시 타입 | 자료구조 | 적합한 경우 | 메모리 사용 |
|---|---|---|---|
REGCACHE_NONE | 없음 | 캐시 불필요 (volatile 위주) | 0 |
REGCACHE_FLAT | 배열 | 연속/조밀한 레지스터 맵 | O(max_register) |
REGCACHE_RBTREE | Red-Black Tree | 희소(sparse) 레지스터 맵 | O(사용 레지스터 수) |
REGCACHE_MAPLE | Maple Tree | v6.4+, 범위 기반 최적화 | O(사용 레지스터 수) |
regmap 전원 관리 연동
regmap 캐시는 시스템 suspend/resume과 runtime PM에서 핵심적인 역할을 합니다. 전원 차단 시 캐시 전용 모드로 전환하고, 복원 시 dirty 레지스터만 하드웨어에 동기화하여 resume 시간을 최소화합니다.
/* 시스템 suspend/resume */
static int my_suspend(struct device *dev)
{
struct my_device *mydev = dev_get_drvdata(dev);
/* 디바이스 비활성화 */
regmap_update_bits(mydev->regmap, 0x01, BIT(0), 0);
/* 캐시 전용 모드: 이후 접근은 캐시에만 기록 */
regcache_cache_only(mydev->regmap, true);
/* 전체 캐시를 dirty로 마킹: resume 시 전체 복원 */
regcache_mark_dirty(mydev->regmap);
return 0;
}
static int my_resume(struct device *dev)
{
struct my_device *mydev = dev_get_drvdata(dev);
int ret;
/* 캐시 전용 모드 해제: 버스 접근 재개 */
regcache_cache_only(mydev->regmap, false);
/* dirty 레지스터만 HW에 동기화 */
ret = regcache_sync(mydev->regmap);
if (ret)
dev_err(dev, "regcache sync failed: %d\\n", ret);
return ret;
}
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
# 레지스터 값 확인
$ cat /sys/kernel/debug/regmap/0-001a/registers
00: 0042
01: 001f
02: 0000
03: abcd
...
# 접근 권한 확인
$ cat /sys/kernel/debug/regmap/0-001a/access
00: RV # R=readable, V=volatile
01: RW # R=readable, W=writable
02: RWv # v=volatile (소문자 = 캐시 안 함)
0a: RWP # P=precious (debugfs dump 제외)
# 캐시 상태 확인
$ cat /sys/kernel/debug/regmap/0-001a/cache_only
N # Y=캐시 전용 모드 활성
registers 파일에 값을 쓰면 런타임에 레지스터를 변경할 수 있습니다: echo "01 abcd" > registers. 이는 프로토타이핑 시 매우 유용하지만, precious 레지스터는 dump에서 자동 제외되므로 FIFO 데이터가 의도치 않게 소비되는 문제를 방지합니다.
MFD 디바이스와 regmap 공유
Multi-Function Device(MFD)에서는 하나의 regmap을 여러 서브 디바이스가 공유합니다. 부모 MFD 드라이버가 regmap을 생성하고, 자식 드라이버가 dev_get_regmap()으로 접근합니다.
/* ===== MFD 부모 드라이버 (PMIC 예시) ===== */
static const struct regmap_config pmic_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0xFF,
.cache_type = REGCACHE_RBTREE,
};
/* MFD 셀 정의 */
static const struct mfd_cell pmic_cells[] = {
{ .name = "pmic-regulator" },
{ .name = "pmic-gpio" },
{ .name = "pmic-rtc" },
{ .name = "pmic-charger" },
};
static int pmic_i2c_probe(struct i2c_client *client)
{
struct regmap *regmap;
/* regmap 생성: dev에 자동 연결됨 */
regmap = devm_regmap_init_i2c(client, &pmic_regmap_config);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
/* MFD 서브디바이스 등록 */
return devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE,
pmic_cells, ARRAY_SIZE(pmic_cells),
NULL, 0, NULL);
}
/* ===== MFD 자식 드라이버 (regulator 예시) ===== */
static int pmic_regulator_probe(struct platform_device *pdev)
{
struct regmap *regmap;
/* 부모 디바이스의 regmap 획득 */
regmap = dev_get_regmap(pdev->dev.parent, NULL);
if (!regmap) {
dev_err(&pdev->dev, "parent regmap not found\\n");
return -ENODEV;
}
/* 이제 공유 regmap으로 레지스터 접근 */
regmap_write(regmap, 0x30, 0x01); /* regulator 제어 레지스터 */
return 0;
}
Device Tree 통합: I2C 바인딩 패턴
I2C 서브시스템의 Device Tree 바인딩에서 공통적으로 사용되는 패턴을 정리합니다.
공통 프로퍼티
| 프로퍼티 | 적용 대상 | 설명 |
|---|---|---|
compatible | 모든 디바이스 | 드라이버 매칭 문자열 (vendor,device) |
reg | I2C 슬레이브 주소 | 7비트 I2C 주소 |
interrupts | 인터럽트 사용 디바이스 | IRQ 스펙 |
interrupt-parent | 인터럽트 사용 디바이스 | IRQ 컨트롤러 phandle |
status | 모든 노드 | "okay", "disabled" |
*-gpios | GPIO 사용 디바이스 | GPIO specifier |
*-supply | 전원 사용 디바이스 | regulator phandle |
clock-frequency | I2C 컨트롤러 | 버스 클럭 속도 (100000, 400000 등) |
pinctrl-* | 핀 설정 필요 디바이스 | pinctrl 상태 |
종합 예제: I2C 디바이스 구성
실제 임베디드 보드에서 I2C 센서와 GPIO expander를 함께 사용하는 Device Tree 예제:
&i2c1 {
status = "okay";
clock-frequency = <400000>;
pinctrl-names = "default", "sleep";
pinctrl-0 = <&i2c1_default>;
pinctrl-1 = <&i2c1_sleep>;
/* 온습도 센서 */
htu21d@40 {
compatible = "meas,htu21";
reg = <0x40>;
};
/* 가속도계 */
accelerometer@1d {
compatible = "st,lis3dh";
reg = <0x1D>;
interrupt-parent = <&gpiob>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;
vdd-supply = <®_3v3>;
};
/* GPIO expander */
gpio_exp: gpio@20 {
compatible = "nxp,pca9555";
reg = <0x20>;
gpio-controller;
#gpio-cells = <2>;
interrupt-parent = <&gpiob>;
interrupts = <8 IRQ_TYPE_EDGE_FALLING>;
interrupt-controller;
#interrupt-cells = <2>;
};
};
Device Tree 디버깅
I2C 관련 Device Tree 문제를 디버깅하는 방법:
# I2C 버스 및 디바이스 확인
i2cdetect -l # 시스템의 I2C 어댑터 목록
i2cdetect -y 1 # I2C bus 1의 디바이스 스캔
i2cget -y 1 0x48 0x00 # 0x48 디바이스의 레지스터 0x00 읽기
i2cdump -y 1 0x48 # 전체 레지스터 덤프
# Device Tree 런타임 확인
ls /proc/device-tree/ # DT 노드 트리
dtc -I fs /proc/device-tree/ # 런타임 DT를 DTS로 디컴파일
# pinctrl 핀 매핑 확인
cat /sys/kernel/debug/pinctrl/*/pins
i2cdetect로 주소 응답 확인,
(2) dmesg | grep i2c로 어댑터 등록 확인,
(3) Device Tree의 reg 속성이 실제 하드웨어 주소와 일치하는지 확인,
(4) pinctrl 설정이 올바른지 확인 (SDA/SCL 핀이 I2C 기능으로 mux 되었는지),
(5) 풀업 저항이 있는지 확인 (오픈 드레인 버스에 외부 풀업 필요).
참고자료
커널 공식 문서
- Linux I2C Subsystem — Linux I2C 서브시스템 문서
- Implementing I2C device drivers — I2C 디바이스 드라이버 작성법
- How to instantiate I2C devices — I2C 디바이스 인스턴스 생성 방법
- SMBus Protocol — SMBus 프로토콜 상세
- I2C /dev interface — I2C /dev 인터페이스
- I2C bus topology — I2C 버스 토폴로지
커널 소스 코드
drivers/i2c/i2c-core-base.c— I2C 코어 구현drivers/i2c/i2c-core-smbus.c— SMBus 프로토콜 구현include/linux/i2c.h— i2c_client, i2c_driver, i2c_adapter 정의include/linux/i2c-smbus.h— SMBus 관련 정의drivers/i2c/busses/— I2C 버스 어댑터 드라이버drivers/i2c/muxes/— I2C 멀티플렉서 드라이버drivers/i2c/i2c-dev.c— /dev/i2c-* 유저스페이스 인터페이스drivers/i3c/master.c— I3C 마스터 코어
규격 문서
- I2C-bus specification (NXP UM10204) — I2C 버스 공식 규격 (PDF)
- MIPI I3C Specification — MIPI I3C 규격
외부 자료
- I3C support in the Linux kernel — 커널의 I3C 지원 소개
- I2C drivers, part I — LWN I2C 드라이버 해설 (1부)
- I2C drivers, part II — LWN I2C 드라이버 해설 (2부)
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.