I2C / SPI / GPIO 서브시스템
임베디드 Linux에서 가장 많이 쓰이는 I2C/SPI/GPIO 서브시스템을 보드 초기화부터 드라이버 운영까지 실무 관점으로 정리합니다. I2C/SPI 전송 모델과 버스 arbitration, GPIO descriptor 기반 안전한 제어 패턴, Device Tree 바인딩 및 pinctrl 상호작용, regmap을 활용한 레지스터 추상화, IRQ-capable GPIO 처리, 전원관리와 슬립 복귀 시 상태 복원, 로직 분석기와 tracepoint를 이용한 타이밍 문제 진단까지 하드웨어 근접 드라이버 개발에 필요한 핵심을 다룹니다.
핵심 요약
- 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
- 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
- IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
- 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
- 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.
단계별 이해
- 장치 수명주기 확인
probe부터 remove까지 흐름을 점검합니다. - 비동기 경로 설계
IRQ, 워크큐, 타이머 역할을 분리합니다. - 자원 정합성 검증
DMA/클록/전원 참조를 교차 확인합니다. - 현장 조건 테스트
연결 끊김/복구/부하 상황을 재현합니다.
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 | 고속 메모리 |
I2C 버스 타이밍 파형
I2C 프로토콜의 완전한 트랜잭션 타이밍을 이해하는 것은 하드웨어 디버깅과 드라이버 개발에 필수적입니다. 아래 다이어그램은 마스터가 슬레이브에 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/ 디렉토리에 구현되어 있으며, 다음 핵심 구조체로 구성됩니다:
| 구조체 | 역할 | 헤더 |
|---|---|---|
| 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(전원 관리 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 콜백 | 슬레이브 이벤트 통보 |
SMBus 시스템 토폴로지
일반적인 PC 시스템에서 SMBus는 사우스브릿지(PCH)의 SMBus 컨트롤러가 마스터 역할을 하며, 다양한 시스템 관리 디바이스가 슬레이브로 연결됩니다:
PEC (Packet Error Checking)
SMBus PEC는 CRC-8 알고리즘(다항식: x8 + x2 + x + 1, 초기값 0x00)을 사용하여 전송 데이터의 무결성을 검증합니다. PEC 바이트는 트랜잭션의 마지막에 추가되며, 주소 바이트(R/W 비트 포함)부터 마지막 데이터 바이트까지 모든 전송 바이트에 대해 CRC를 계산합니다.
/* 커널의 SMBus PEC 계산 (drivers/i2c/i2c-core-smbus.c) */
static u8 i2c_smbus_pec(u8 crc, u8 *p, size_t count)
{
/* CRC-8 다항식: x^8 + x^2 + x + 1 = 0x107 */
for (int i = 0; i < count; i++)
crc = crc8(crc8_table, p[i], crc);
return crc;
}
/* PEC 사용을 위한 클라이언트 플래그 설정 */
client->flags |= I2C_CLIENT_PEC;
/* PEC가 활성화된 SMBus 읽기 - 자동으로 PEC 검증 */
int val = i2c_smbus_read_byte_data(client, reg);
/* PEC 불일치 시 -EBADMSG 반환 */
SMBus ARP (Address Resolution Protocol)
SMBus ARP는 디바이스에 동적으로 주소를 할당하는 프로토콜입니다. 여러 동일 디바이스가 같은 버스에 연결될 때 주소 충돌 없이 식별할 수 있습니다. 각 디바이스는 고유한 128비트 UDID (Unique Device Identifier)를 가지며, 호스트가 이를 기반으로 7비트 주소를 할당합니다.
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 버스를 통해 타겟 디바이스가 마스터에 인터럽트를 요청하는 메커니즘입니다. 타겟은 버스가 유휴 상태일 때 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 | 정적→동적 주소 매핑 |
| 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 모드 지원은 컨트롤러별로 다릅니다.
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 | 상승 에지 |
SPI 모드별 파형과 클럭 에지 동작
SPI의 4가지 모드는 CPOL(Clock Polarity)과 CPHA(Clock Phase) 비트 조합으로 결정됩니다. 데이터 시트에서 "상승 에지에서 샘플링"이라고 적혀 있다면 Mode 0 또는 Mode 3에 해당합니다. 아래 다이어그램은 각 모드의 SCK/MOSI/MISO 타이밍 관계를 보여줍니다.
Setup Time과 Hold Time
SPI 통신의 신뢰성은 setup time(tSU)과 hold time(tHD)에 의존합니다. Setup time은 샘플링 에지 전에 데이터가 안정되어야 하는 최소 시간이고, hold time은 샘플링 에지 후에 데이터가 유지되어야 하는 최소 시간입니다:
| 파라미터 | 기호 | 일반 범위 | 설명 |
|---|---|---|---|
| Setup Time (데이터) | tSU | 5~15 ns | 샘플링 에지 전 데이터 안정 시간 |
| Hold Time (데이터) | tHD | 5~10 ns | 샘플링 에지 후 데이터 유지 시간 |
| CS Setup | tCSS | 10~50 ns | CS 활성화 후 첫 클럭까지 대기 |
| CS Hold | tCSH | 10~50 ns | 마지막 클럭 후 CS 비활성화까지 |
| CS 비활성 간격 | tCSI | 50~100 ns | 연속 트랜잭션 사이 CS 최소 비활성 시간 |
전이중(Full Duplex) vs 반이중(Half Duplex)
SPI는 기본적으로 전이중(Full Duplex) 통신을 지원합니다. 마스터가 MOSI로 데이터를 송신하는 동시에 MISO로 데이터를 수신합니다. 그러나 일부 디바이스는 단방향 또는 반이중 모드로 동작합니다:
| 통신 방식 | 데이터 라인 | 동작 | Linux 커널 플래그 |
|---|---|---|---|
| Full Duplex | MOSI + MISO | 동시 양방향 전송 | (기본값) |
| Half Duplex | 단일 양방향 라인 | 교대 송수신 (3-wire SPI) | SPI_3WIRE |
| Simplex TX | MOSI만 | 송신 전용 (예: LED 드라이버) | SPI_NO_RX |
| Simplex RX | MISO만 | 수신 전용 (예: ADC) | SPI_NO_TX |
전기적 특성과 신호 무결성
고속 SPI(10 MHz 이상)에서는 신호 무결성이 중요합니다. PCB 설계 시 다음 사항을 고려해야 합니다:
| 항목 | 권장 값 | 설명 |
|---|---|---|
| 배선 길이 | < 10 cm (50 MHz 기준) | 짧을수록 반사와 크로스톡 감소 |
| 임피던스 매칭 | 50 Ω (단선) / 100 Ω (차동) | 고속에서 반사 최소화 |
| 풀업 저항 (CS) | 10 kΩ | 미사용 CS 라인은 풀업하여 비활성 유지 |
| 바이패스 커패시터 | 100 nF + 10 μF | VCC 핀 가까이 배치하여 노이즈 감소 |
| Ground 플레인 | 연속 GND 레이어 | 신호 리턴 경로 확보, EMI 감소 |
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 컨트롤러 드라이버는 하드웨어 레지스터를 직접 프로그래밍하여 클럭 분주, FIFO 관리, DMA 채널 설정을 수행합니다. 아래 다이어그램은 SPI 컨트롤러 내부의 전송 데이터 경로를 보여줍니다.
클럭 분주(Divider) 계산
SPI 컨트롤러의 SCK 클럭 주파수는 부모 클럭(PCLK 또는 APB 클럭)을 분주하여 생성합니다. 대부분의 컨트롤러는 짝수 분주를 사용하며, 실제 속도가 요청 속도를 초과하지 않도록 올림 처리해야 합니다:
/* SPI 클럭 분주 계산: f_sck = f_pclk / (2 * (div + 1)) */
static u32 calc_spi_clk_div(u32 pclk_hz, u32 target_hz)
{
u32 div;
if (target_hz >= pclk_hz / 2)
return 0; /* 최소 분주 = 2배 */
/* div = ceil(pclk / (2 * target)) - 1
* 실제 속도가 target을 넘지 않도록 올림 처리 */
div = DIV_ROUND_UP(pclk_hz, 2 * target_hz) - 1;
/* 하드웨어 최대값 클램프 (예: 8-bit divider → 0~255) */
return min_t(u32, div, 0xFF);
}
FIFO 관리와 Watermark 설정
TX/RX FIFO의 watermark 레벨은 인터럽트 빈도와 전송 효율의 균형을 결정합니다. Watermark가 너무 낮으면 인터럽트가 과다 발생하고, 너무 높으면 RX FIFO 오버플로 위험이 있습니다:
| Watermark 전략 | TX Watermark | RX Watermark | 특성 |
|---|---|---|---|
| 낮은 지연 | FIFO_DEPTH / 4 | 1 | 인터럽트 빈번, 응답 빠름 |
| 균형 | FIFO_DEPTH / 2 | FIFO_DEPTH / 2 | 범용 설정 |
| 높은 처리량 | FIFO_DEPTH * 3/4 | FIFO_DEPTH * 3/4 | DMA 연동 시 최적 |
SPI 컨트롤러 드라이버 스켈레톤
#include <linux/spi/spi.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <linux/dmaengine.h>
#define SPI_CR1 0x00 /* Control Register 1 */
#define SPI_CR2 0x04 /* Control Register 2 */
#define SPI_SR 0x08 /* Status Register */
#define SPI_DR 0x0C /* Data Register */
#define SPI_TXFR 0x10 /* TX FIFO Threshold */
#define SPI_RXFR 0x14 /* RX FIFO Threshold */
#define SPI_CR1_SPE BIT(0) /* SPI Enable */
#define SPI_CR1_MSTR BIT(2) /* Master Mode */
#define SPI_CR1_CPOL BIT(3) /* Clock Polarity */
#define SPI_CR1_CPHA BIT(4) /* Clock Phase */
#define SPI_SR_TXE BIT(1) /* TX FIFO Empty */
#define SPI_SR_RXNE BIT(0) /* RX FIFO Not Empty */
#define SPI_SR_BSY BIT(7) /* Busy */
struct my_spi_priv {
void __iomem *base;
struct clk *clk;
int irq;
struct completion xfer_done;
const u8 *tx_buf;
u8 *rx_buf;
int tx_len, rx_len;
};
static int my_spi_transfer_one(struct spi_controller *ctlr,
struct spi_device *spi,
struct spi_transfer *xfer)
{
struct my_spi_priv *priv = spi_controller_get_devdata(ctlr);
u32 cr1, div;
/* 클럭 분주 설정 */
div = calc_spi_clk_div(clk_get_rate(priv->clk), xfer->speed_hz);
cr1 = readl(priv->base + SPI_CR1);
cr1 &= ~(0xFF << 8); /* divider 필드 클리어 */
cr1 |= (div << 8); /* 새 divider 설정 */
/* SPI 모드 설정 (CPOL/CPHA) */
if (spi->mode & SPI_CPOL)
cr1 |= SPI_CR1_CPOL;
else
cr1 &= ~SPI_CR1_CPOL;
if (spi->mode & SPI_CPHA)
cr1 |= SPI_CR1_CPHA;
else
cr1 &= ~SPI_CR1_CPHA;
writel(cr1 | SPI_CR1_SPE, priv->base + SPI_CR1);
/* FIFO watermark 설정 (깊이의 절반) */
writel(8, priv->base + SPI_TXFR); /* TX threshold */
writel(8, priv->base + SPI_RXFR); /* RX threshold */
/* 전송 시작: PIO 또는 DMA */
priv->tx_buf = xfer->tx_buf;
priv->rx_buf = xfer->rx_buf;
priv->tx_len = xfer->len;
priv->rx_len = xfer->len;
reinit_completion(&priv->xfer_done);
/* IRQ 기반 PIO: TX empty 인터럽트 활성화 */
writel(readl(priv->base + SPI_CR2) | BIT(0),
priv->base + SPI_CR2);
return 1; /* 비동기: completion 대기 */
}
SPI DMA 통합
대용량 SPI 전송(수백 바이트 이상)에서는 DMA를 사용하여 CPU 부하를 줄입니다. DMA 버퍼는 캐시 라인 정렬이 필수이며, scatter-gather 리스트로 비연속 메모리를 처리할 수 있습니다:
____cacheline_aligned 속성이나 kmalloc()/dma_alloc_coherent()를 사용하세요.
/* SPI DMA 전송 설정 */
static int my_spi_dma_xfer(struct my_spi_priv *priv,
struct spi_transfer *xfer)
{
struct dma_async_tx_descriptor *tx_desc, *rx_desc;
dma_addr_t tx_dma, rx_dma;
struct device *dev = priv->dev;
/* TX: 메모리 → 주변장치 (MEM_TO_DEV) */
tx_dma = dma_map_single(dev, (void *)xfer->tx_buf,
xfer->len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, tx_dma))
return -ENOMEM;
/* RX: 주변장치 → 메모리 (DEV_TO_MEM) */
rx_dma = dma_map_single(dev, xfer->rx_buf,
xfer->len, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, rx_dma)) {
dma_unmap_single(dev, tx_dma, xfer->len, DMA_TO_DEVICE);
return -ENOMEM;
}
/* RX DMA 먼저 준비 (데이터 유실 방지) */
rx_desc = dmaengine_prep_slave_single(
priv->rx_chan, rx_dma, xfer->len,
DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
rx_desc->callback = my_spi_dma_rx_done;
rx_desc->callback_param = priv;
dmaengine_submit(rx_desc);
dma_async_issue_pending(priv->rx_chan);
/* TX DMA 시작 → SCK 생성 → 수신 자동 진행 */
tx_desc = dmaengine_prep_slave_single(
priv->tx_chan, tx_dma, xfer->len,
DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
tx_desc->callback = my_spi_dma_tx_done;
tx_desc->callback_param = priv;
dmaengine_submit(tx_desc);
dma_async_issue_pending(priv->tx_chan);
return 0;
}
칩 셀렉트(CS) 관리
SPI 컨트롤러의 CS(Chip Select) 관리는 하드웨어 CS와 GPIO CS 두 가지 방식이 있습니다. 다중 슬레이브 환경에서는 CS 타이밍이 정확해야 버스 충돌을 방지할 수 있습니다:
| 방식 | 장점 | 단점 | 용도 |
|---|---|---|---|
| HW CS | 정확한 타이밍, 자동 토글 | 제한된 CS 수 (보통 2~4개) | 고속 전송, 정밀 타이밍 |
| GPIO CS | 무제한 CS, 유연한 핀 배치 | 소프트웨어 지연, 약간의 오버헤드 | 다수 슬레이브, 비표준 핀 |
spi_transfer 구조체의 cs_change 필드는 전송 간 CS 동작을 제어합니다. 이 필드가 설정되면 해당 transfer 종료 후 CS를 잠시 비활성화(deassert)했다가 다시 활성화합니다. 이는 일부 슬레이브 디바이스가 명령 사이에 CS 토글을 요구하는 경우에 필요합니다:
/* cs_change를 활용한 다중 명령 전송 */
static int spi_multi_cmd(struct spi_device *spi,
u8 *cmd1, size_t len1,
u8 *cmd2, size_t len2)
{
struct spi_transfer xfer[2] = { };
struct spi_message msg;
/* Transfer 1: 첫 번째 명령 후 CS 토글 */
xfer[0].tx_buf = cmd1;
xfer[0].len = len1;
xfer[0].cs_change = 1; /* 이 transfer 후 CS deassert→reassert */
xfer[0].cs_change_delay.value = 10;
xfer[0].cs_change_delay.unit = SPI_DELAY_UNIT_USECS;
/* Transfer 2: 두 번째 명령 (마지막이므로 CS 자동 해제) */
xfer[1].tx_buf = cmd2;
xfer[1].len = len2;
spi_message_init_with_transfers(&msg, xfer, ARRAY_SIZE(xfer));
return spi_sync(spi, &msg);
}
cs-gpios 프로퍼티로 GPIO 기반 CS를 지정합니다: cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;. SPI 프레임워크가 자동으로 GPIO를 assert/deassert하며, 컨트롤러 드라이버에서 set_cs 콜백을 구현하지 않아도 됩니다.
SPI 드라이버 작성
SPI ADC (Analog-to-Digital Converter) 드라이버 예제:
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/iio/iio.h>
#define ADC_CMD_READ_CH0 0x06
#define ADC_CMD_READ_CH1 0x07
struct my_adc_data {
struct spi_device *spi;
struct mutex lock;
u8 tx_buf[3] ____cacheline_aligned;
u8 rx_buf[3];
};
static int my_adc_read_channel(struct my_adc_data *data, int channel)
{
struct spi_transfer xfer = {
.tx_buf = data->tx_buf,
.rx_buf = data->rx_buf,
.len = 3,
.speed_hz = 1000000, /* 1 MHz */
};
int ret;
data->tx_buf[0] = (channel == 0) ? ADC_CMD_READ_CH0 : ADC_CMD_READ_CH1;
data->tx_buf[1] = 0x00;
data->tx_buf[2] = 0x00;
ret = spi_sync_transfer(data->spi, &xfer, 1);
if (ret)
return ret;
/* 12비트 ADC: 상위 4비트 버림 */
return ((data->rx_buf[1] & 0x0F) << 8) | data->rx_buf[2];
}
static int my_adc_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
struct my_adc_data *data = iio_priv(indio_dev);
int ret;
switch (mask) {
case IIO_CHAN_INFO_RAW:
mutex_lock(&data->lock);
ret = my_adc_read_channel(data, chan->channel);
mutex_unlock(&data->lock);
if (ret < 0)
return ret;
*val = ret;
return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
/* Vref=3.3V, 12-bit: 3300/4096 = 0.805664 mV/LSB */
*val = 3300;
*val2 = 12;
return IIO_VAL_FRACTIONAL_LOG2;
default:
return -EINVAL;
}
}
static const struct iio_chan_spec my_adc_channels[] = {
{
.type = IIO_VOLTAGE,
.channel = 0,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
},
{
.type = IIO_VOLTAGE,
.channel = 1,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
},
};
static const struct iio_info my_adc_info = {
.read_raw = my_adc_read_raw,
};
static int my_adc_probe(struct spi_device *spi)
{
struct iio_dev *indio_dev;
struct my_adc_data *data;
indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*data));
if (!indio_dev)
return -ENOMEM;
data = iio_priv(indio_dev);
data->spi = spi;
mutex_init(&data->lock);
indio_dev->name = "my-adc";
indio_dev->info = &my_adc_info;
indio_dev->channels = my_adc_channels;
indio_dev->num_channels = ARRAY_SIZE(my_adc_channels);
indio_dev->modes = INDIO_DIRECT_MODE;
return devm_iio_device_register(&spi->dev, indio_dev);
}
static const struct spi_device_id my_adc_spi_id[] = {
{ "my-adc", 0 },
{ }
};
MODULE_DEVICE_TABLE(spi, my_adc_spi_id);
static const struct of_device_id my_adc_of_match[] = {
{ .compatible = "vendor,my-adc" },
{ }
};
MODULE_DEVICE_TABLE(of, my_adc_of_match);
static struct spi_driver my_adc_driver = {
.driver = {
.name = "my-adc",
.of_match_table = my_adc_of_match,
},
.probe = my_adc_probe,
.id_table = my_adc_spi_id,
};
module_spi_driver(my_adc_driver);
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 칩 지원은 벤더별 파일에 파라미터만 추가하면 됩니다.
Quad SPI 심화: DDR 모드와 XIP
고성능 SPI NOR Flash는 기본 Quad I/O를 넘어 DDR (Double Data Rate) 모드, Data Strobe (DQS), XIP (eXecute-In-Place) 등 고급 기능을 지원합니다. 아래 다이어그램은 Quad SPI 명령의 4단계 구조를 보여줍니다.
DDR (Double Data Rate) 모드
DDR 모드에서는 SCK의 상승 에지와 하강 에지 양쪽에서 데이터를 전송하여 SDR(Single Data Rate) 대비 2배 대역폭을 달성합니다. Octal DDR (8D-8D-8D) 모드에서는 8개 데이터 라인 x DDR로 사이클당 16비트를 전송합니다:
| 전송 모드 | 데이터 라인 | 클럭 에지 | 사이클당 비트 | 200 MHz 시 대역폭 |
|---|---|---|---|---|
| 1-1-1 SDR | 1 | 단일 | 1 | 25 MB/s |
| 1-1-4 SDR | 4 | 단일 | 4 | 100 MB/s |
| 1-4-4 DDR | 4 | 양쪽 | 8 | 200 MB/s |
| 4-4-4 DDR | 4 | 양쪽 | 8 | 200 MB/s |
| 8D-8D-8D | 8 | 양쪽 | 16 | 400 MB/s |
Data Strobe (DQS)
DDR 모드에서 고주파 클럭의 정확한 샘플링 타이밍을 보장하기 위해 DQS (Data Strobe) 신호가 사용됩니다. DQS는 슬레이브 디바이스가 데이터와 함께 출력하는 스트로브 신호로, 컨트롤러가 이 신호의 에지에서 데이터를 캡처합니다. 이는 SDR에서는 불필요하지만, DDR에서 클럭-데이터 스큐 보상에 필수적입니다.
XIP (eXecute-In-Place) 모드
XIP 모드는 SPI NOR Flash의 내용을 CPU 메모리 맵에 직접 매핑하여, 별도의 복사 없이 Flash에서 직접 코드를 실행할 수 있게 합니다. QSPI 컨트롤러가 CPU의 메모리 접근을 자동으로 SPI 읽기 명령으로 변환합니다:
/* XIP 모드 활성화: QSPI 컨트롤러 메모리 매핑 예시 */
static int qspi_enable_xip(struct qspi_priv *priv)
{
struct spi_mem_op op = SPI_MEM_OP(
SPI_MEM_OP_CMD(0xEB, 1), /* Fast Read Quad I/O */
SPI_MEM_OP_ADDR(3, 0, 4), /* 24-bit addr, 4-wire */
SPI_MEM_OP_DUMMY(6, 4), /* 6 dummy cycles */
SPI_MEM_OP_DATA_IN(0, NULL, 4) /* 4-wire data */
);
u32 cr;
/* 메모리 매핑 윈도우 설정 */
writel(priv->mmap_phys, priv->base + QSPI_MMAP_BASE);
writel(priv->flash_size - 1, priv->base + QSPI_MMAP_SIZE);
/* Continuous Read 모드: 반복 CMD 전송 생략 */
cr = readl(priv->base + QSPI_CR);
cr |= QSPI_CR_XIP_EN | QSPI_CR_CONT_READ;
writel(cr, priv->base + QSPI_CR);
/* XIP 후 Flash는 메모리처럼 접근 가능:
* void *data = ioremap(priv->mmap_phys, priv->flash_size);
* memcpy(dst, data + offset, len); */
return 0;
}
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를 사용하는 새 드라이버를 받아들이지 않습니다.
GPIO 서브시스템 아키텍처
Linux GPIO 서브시스템은 하드웨어 GPIO 컨트롤러부터 유저스페이스 접근까지 여러 계층으로 구성됩니다. gpiolib이 핵심 프레임워크 역할을 하며, gpio_chip이 하드웨어 추상화를, gpiod가 소비자(consumer) API를 제공합니다.
GPIO 하드웨어 내부 구조
물리적 GPIO 핀은 여러 전기적 설정을 지원하며, 드라이버에서 이를 올바르게 구성하는 것이 중요합니다:
| 설정 | 설명 | 커널 API / DT 속성 | 용도 |
|---|---|---|---|
| Push-Pull | HIGH/LOW 모두 능동적으로 구동 | 기본 출력 모드 | LED 제어, 리셋 라인 |
| Open-Drain | LOW만 능동 구동, HIGH는 풀업 의존 | GPIO_OPEN_DRAIN / drive-open-drain | I2C SDA/SCL, 인터럽트 라인 |
| Open-Source | HIGH만 능동 구동, LOW는 풀다운 의존 | GPIO_OPEN_SOURCE / drive-open-source | 특수 전원 제어 |
| Pull-Up | 내장 풀업 저항 활성화 | GPIO_PULL_UP / bias-pull-up | 버튼 입력 (active-low) |
| Pull-Down | 내장 풀다운 저항 활성화 | GPIO_PULL_DOWN / bias-pull-down | 기본 LOW 유지 필요 시 |
| Schmitt Trigger | 히스테리시스 입력 (노이즈 내성) | input-schmitt-enable | 느린 신호 에지, 노이즈 환경 |
| Debounce | 글리치 필터링 (HW/SW) | input-debounce = <usec> | 기계식 버튼/스위치 |
pinctrl 서브시스템과 밀접하게 연관됩니다. 대부분의 SoC에서 GPIO 핀은 pinctrl 핀과 1:1 매핑되며, pinctrl_gpio_set_config()를 통해 gpiolib에서 pinctrl로 설정이 전달됩니다.
gpiod API (Descriptor-based)
현대 Linux 커널의 표준 GPIO 인터페이스인 gpiod API를 사용합니다:
GPIO 획득과 해제
#include <linux/gpio/consumer.h>
/* Device Tree에서 "reset-gpios" 속성을 참조하여 GPIO 획득 */
struct gpio_desc *reset_gpio;
reset_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(reset_gpio))
return PTR_ERR(reset_gpio);
/* 선택적(optional) GPIO: 없어도 에러 아님 */
struct gpio_desc *led_gpio;
led_gpio = devm_gpiod_get_optional(&pdev->dev, "led", GPIOD_OUT_LOW);
/* 인덱스로 여러 GPIO 획득 */
struct gpio_desc *cs_gpio;
cs_gpio = devm_gpiod_get_index(&pdev->dev, "cs", 0, GPIOD_OUT_HIGH);
GPIO 동작
/* 출력 값 설정 (active-low 자동 처리) */
gpiod_set_value(reset_gpio, 1); /* active (논리적 1) */
gpiod_set_value(reset_gpio, 0); /* inactive (논리적 0) */
/* sleepable context에서 사용 (I2C/SPI GPIO expander 등) */
gpiod_set_value_cansleep(reset_gpio, 1);
/* 입력 값 읽기 */
int val = gpiod_get_value(button_gpio);
/* 방향 변경 */
gpiod_direction_input(gpio);
gpiod_direction_output(gpio, 1);
/* GPIO → IRQ 번호 변환 */
int irq = gpiod_to_irq(button_gpio);
if (irq < 0)
return irq;
ret = devm_request_threaded_irq(&pdev->dev, irq, NULL,
my_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"my-button", data);
gpiod_set_value()는 Device Tree의 GPIO_ACTIVE_LOW 플래그를 자동 반영합니다. gpiod_set_raw_value()는 물리적 라인 레벨을 직접 제어합니다. 일반적으로 gpiod_set_value()를 사용하세요.
GPIO 인터럽트 흐름
GPIO를 인터럽트 소스로 사용하는 경우, GPIO 서브시스템과 IRQ 서브시스템이 협력하여 핀 상태 변화를 커널 인터럽트로 변환합니다. 아래 다이어그램은 GPIO 인터럽트의 전체 처리 흐름을 보여줍니다.
GPIO Debounce (글리치 필터링)
기계식 버튼이나 스위치는 접점 바운싱으로 인해 한 번의 누름에 여러 번의 에지 변화가 발생합니다. Debounce는 이러한 글리치를 필터링하여 깨끗한 신호를 제공합니다.
| 방식 | 구현 | 지연 | 장점 | 단점 |
|---|---|---|---|---|
| HW Debounce | SoC GPIO 컨트롤러 내장 필터 | HW에서 설정 (수십 us ~ 수 ms) | CPU 부하 없음, 정밀한 타이밍 | 모든 SoC 지원 아님 |
| SW Debounce | gpiolib hrtimer 기반 | gpiod_set_debounce() | 모든 GPIO에 적용 가능 | CPU 오버헤드, hrtimer 정밀도 의존 |
/* HW debounce 설정 (지원하는 컨트롤러만) */
ret = gpiod_set_debounce(button_gpio, 50000); /* 50ms */
if (ret == -ENOTSUPP)
dev_warn(dev, "HW debounce not supported, using SW\\n");
/* gpio_chip에서 HW debounce 구현 */
static int my_gpio_set_config(struct gpio_chip *gc,
unsigned int offset,
unsigned long config)
{
if (pinconf_to_config_param(config) == PIN_CONFIG_INPUT_DEBOUNCE) {
u32 debounce_us = pinconf_to_config_argument(config);
/* HW 디바운스 레지스터 설정 */
writel(debounce_us / 31, priv->base + DEBOUNCE_REG(offset));
return 0;
}
return -ENOTSUPP;
}
GPIO Aggregator
gpio-aggregator는 여러 물리적 GPIO 라인을 하나의 가상 GPIO 컨트롤러로 묶어 유저스페이스에 노출하는 기능입니다. 보안 제한이나 권한 분리가 필요한 환경에서 특정 GPIO 라인만 선택적으로 컨테이너나 VM에 전달할 때 유용합니다.
/* GPIO Aggregator 사용 (sysfs 인터페이스) */
$ echo "gpiochip0 3,5,7" > /sys/bus/platform/drivers/gpio-aggregator/new_device
/* 새로운 /dev/gpiochipN 생성 (3개 라인: 0=pin3, 1=pin5, 2=pin7) */
$ echo "gpiochip0 3,5,7" > /sys/bus/platform/drivers/gpio-aggregator/delete_device
/* 가상 GPIO 컨트롤러 제거 */
gpio-sim 모듈을 사용하여 실제 하드웨어 없이 가상 GPIO 컨트롤러를 생성할 수 있습니다. configfs를 통해 라인 수, 라벨, 초기 값 등을 설정합니다. CI/CD 파이프라인이나 유닛 테스트에 유용합니다.
GPIO Device Tree 바인딩
my_device: my-device@0 {
compatible = "vendor,my-device";
/* 프로퍼티 이름: <con-id>-gpios */
reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>;
led-gpios = <&gpio2 3 GPIO_ACTIVE_HIGH>;
cs-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>,
<&gpio1 5 GPIO_ACTIVE_LOW>;
};
gpio_chip 구현
GPIO 컨트롤러 드라이버를 작성하려면 gpio_chip 구조체를 구현하고 등록합니다:
#include <linux/gpio/driver.h>
struct my_gpio {
struct gpio_chip gc;
void __iomem *base;
struct mutex lock;
};
static int my_gpio_get(struct gpio_chip *gc, unsigned int offset)
{
struct my_gpio *priv = gpiochip_get_data(gc);
u32 reg;
reg = readl(priv->base + 0x10); /* Data Input Register */
return !!(reg & BIT(offset));
}
static void my_gpio_set(struct gpio_chip *gc,
unsigned int offset, int value)
{
struct my_gpio *priv = gpiochip_get_data(gc);
u32 reg;
mutex_lock(&priv->lock);
reg = readl(priv->base + 0x14); /* Data Output Register */
if (value)
reg |= BIT(offset);
else
reg &= ~BIT(offset);
writel(reg, priv->base + 0x14);
mutex_unlock(&priv->lock);
}
static int my_gpio_direction_input(struct gpio_chip *gc,
unsigned int offset)
{
struct my_gpio *priv = gpiochip_get_data(gc);
u32 reg;
mutex_lock(&priv->lock);
reg = readl(priv->base + 0x04); /* Direction Register */
reg &= ~BIT(offset); /* 0 = input */
writel(reg, priv->base + 0x04);
mutex_unlock(&priv->lock);
return 0;
}
static int my_gpio_direction_output(struct gpio_chip *gc,
unsigned int offset, int value)
{
my_gpio_set(gc, offset, value);
struct my_gpio *priv = gpiochip_get_data(gc);
u32 reg;
mutex_lock(&priv->lock);
reg = readl(priv->base + 0x04);
reg |= BIT(offset); /* 1 = output */
writel(reg, priv->base + 0x04);
mutex_unlock(&priv->lock);
return 0;
}
static int my_gpio_probe(struct platform_device *pdev)
{
struct my_gpio *priv;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
mutex_init(&priv->lock);
priv->gc.label = "my-gpio";
priv->gc.parent = &pdev->dev;
priv->gc.owner = THIS_MODULE;
priv->gc.base = -1; /* 동적 번호 할당 */
priv->gc.ngpio = 32;
priv->gc.get = my_gpio_get;
priv->gc.set = my_gpio_set;
priv->gc.direction_input = my_gpio_direction_input;
priv->gc.direction_output = my_gpio_direction_output;
return devm_gpiochip_add_data(&pdev->dev, &priv->gc, priv);
}
libgpiod 유저스페이스
libgpiod는 Linux GPIO character device (/dev/gpiochipN)를 통한 유저스페이스 GPIO 접근 라이브러리입니다. 기존의 /sys/class/gpio/ sysfs 인터페이스를 대체합니다.
/sys/class/gpio/export 인터페이스는 deprecated 상태입니다. 새 프로젝트에서는 chardev 기반(/dev/gpiochipN) 접근을 권장하며, 가능한 경우 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()을 사용해야 합니다.
GPIO IRQ 컨트롤러 (irqchip)
GPIO 컨트롤러가 인터럽트를 지원하려면 gpio_chip에 IRQ chip 기능을 통합해야 합니다. Linux 커널은 GPIOLIB_IRQCHIP 인프라를 통해 이 과정을 크게 단순화합니다. gpio_irq_chip 구조체를 gpio_chip에 내장하여 등록하면, gpiolib이 자동으로 irq_domain을 생성하고 관리합니다.
IRQ Domain 유형
| 유형 | 설명 | 적용 대상 |
|---|---|---|
| Flat (Linear) | GPIO 번호가 직접 HW IRQ 번호로 매핑 | 일반 SoC GPIO 컨트롤러, GPIO expander |
| Hierarchical | GPIO IRQ → 상위 IRQ 컨트롤러(GIC 등)에 계층적 매핑 | 부모 IRQ 컨트롤러가 별도 존재하는 경우 |
gpio_chip irqchip 구현
현대 커널(v5.10+)에서는 gpio_irq_chip을 gpio_chip 내에 설정하고 devm_gpiochip_add_data()로 한 번에 등록하는 것이 권장 패턴입니다:
#include <linux/gpio/driver.h>
#include <linux/interrupt.h>
struct my_gpio_irq {
struct gpio_chip gc;
void __iomem *base;
struct mutex lock;
u32 irq_mask; /* 소프트웨어 IRQ 마스크 상태 */
u32 irq_type; /* 에지/레벨 타입 비트맵 */
};
/* irq_chip 콜백: 인터럽트 마스크/언마스크 */
static void my_gpio_irq_mask(struct irq_data *d)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
struct my_gpio_irq *priv = gpiochip_get_data(gc);
u32 mask = BIT(irqd_to_hwirq(d));
priv->irq_mask &= ~mask;
writel(priv->irq_mask, priv->base + IRQ_MASK_REG);
gpiochip_disable_irq(gc, irqd_to_hwirq(d));
}
static void my_gpio_irq_unmask(struct irq_data *d)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
struct my_gpio_irq *priv = gpiochip_get_data(gc);
u32 mask = BIT(irqd_to_hwirq(d));
gpiochip_enable_irq(gc, irqd_to_hwirq(d));
priv->irq_mask |= mask;
writel(priv->irq_mask, priv->base + IRQ_MASK_REG);
}
/* 인터럽트 타입 설정 (에지/레벨) */
static int my_gpio_irq_set_type(struct irq_data *d,
unsigned int type)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
struct my_gpio_irq *priv = gpiochip_get_data(gc);
u32 bit = BIT(irqd_to_hwirq(d));
switch (type & IRQ_TYPE_SENSE_MASK) {
case IRQ_TYPE_EDGE_RISING:
priv->irq_type |= bit;
writel(priv->irq_type, priv->base + IRQ_EDGE_REG);
break;
case IRQ_TYPE_EDGE_FALLING:
priv->irq_type &= ~bit;
writel(priv->irq_type, priv->base + IRQ_EDGE_REG);
break;
case IRQ_TYPE_EDGE_BOTH:
/* 하드웨어가 지원하면 both-edge 설정 */
break;
default:
return -EINVAL;
}
return 0;
}
/* IMMUTABLE irq_chip: 런타임 수정 불가 (v6.0+ 필수) */
static const struct irq_chip my_gpio_irqchip = {
.name = "my-gpio-irq",
.irq_mask = my_gpio_irq_mask,
.irq_unmask = my_gpio_irq_unmask,
.irq_set_type = my_gpio_irq_set_type,
.flags = IRQCHIP_IMMUTABLE,
GPIOCHIP_IRQ_RESOURCE_HELPERS,
};
/* Chained IRQ handler: 부모 IRQ에서 호출 */
static void my_gpio_irq_handler(struct irq_desc *desc)
{
struct gpio_chip *gc = irq_desc_get_handler_data(desc);
struct my_gpio_irq *priv = gpiochip_get_data(gc);
struct irq_chip *irqchip = irq_desc_get_chip(desc);
u32 pending;
chained_irq_enter(irqchip, desc);
pending = readl(priv->base + IRQ_STATUS_REG);
pending &= priv->irq_mask;
while (pending) {
int hwirq = __ffs(pending);
generic_handle_domain_irq(gc->irq.domain, hwirq);
pending &= ~BIT(hwirq);
}
/* 인터럽트 상태 클리어 (W1C) */
writel(pending, priv->base + IRQ_STATUS_REG);
chained_irq_exit(irqchip, desc);
}
/* probe에서 GPIO + IRQ chip 등록 */
static int my_gpio_irq_probe(struct platform_device *pdev)
{
struct my_gpio_irq *priv;
struct gpio_irq_chip *girq;
int parent_irq;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
parent_irq = platform_get_irq(pdev, 0);
if (parent_irq < 0)
return parent_irq;
mutex_init(&priv->lock);
/* GPIO chip 기본 설정 */
priv->gc.label = "my-gpio-irq";
priv->gc.parent = &pdev->dev;
priv->gc.owner = THIS_MODULE;
priv->gc.base = -1;
priv->gc.ngpio = 32;
priv->gc.get = my_gpio_get;
priv->gc.set = my_gpio_set;
priv->gc.direction_input = my_gpio_direction_input;
priv->gc.direction_output = my_gpio_direction_output;
/* IRQ chip 내장 설정 (권장 패턴) */
girq = &priv->gc.irq;
gpio_irq_chip_set_chip(girq, &my_gpio_irqchip);
girq->parent_handler = my_gpio_irq_handler;
girq->num_parents = 1;
girq->parents = devm_kcalloc(&pdev->dev, 1,
sizeof(*girq->parents), GFP_KERNEL);
if (!girq->parents)
return -ENOMEM;
girq->parents[0] = parent_irq;
girq->default_type = IRQ_TYPE_NONE;
girq->handler = handle_edge_irq;
/* GPIO chip + IRQ chip 동시 등록 */
return devm_gpiochip_add_data(&pdev->dev, &priv->gc, priv);
}
irq_chip 구조체에 IRQCHIP_IMMUTABLE 플래그를 설정해야 합니다. 이는 런타임에 irq_chip이 수정되는 것을 방지합니다. 또한 gpiochip_enable_irq()와 gpiochip_disable_irq()를 unmask/mask 콜백에서 호출하고, GPIOCHIP_IRQ_RESOURCE_HELPERS 매크로를 포함해야 합니다.
Nested (Threaded) IRQ 패턴
I2C/SPI GPIO expander처럼 슬립 가능한 버스 뒤에 있는 GPIO 컨트롤러는 chained handler를 사용할 수 없습니다(인터럽트 컨텍스트에서 I2C/SPI 전송 불가). 이 경우 nested (threaded) IRQ 패턴을 사용합니다:
/* I2C GPIO expander의 threaded IRQ 패턴 */
girq = &priv->gc.irq;
gpio_irq_chip_set_chip(girq, &my_expander_irqchip);
/* parent_handler를 NULL로 설정하면 nested(threaded) IRQ 사용 */
girq->parent_handler = NULL;
girq->num_parents = 0;
girq->parents = NULL;
girq->default_type = IRQ_TYPE_NONE;
girq->handler = handle_bad_irq; /* 직접 호출되면 안 됨 */
girq->threaded = true; /* 핵심: threaded IRQ 사용 */
/* 부모 IRQ를 threaded handler로 직접 등록 */
ret = devm_request_threaded_irq(&client->dev, client->irq,
NULL, my_expander_irq_thread,
IRQF_ONESHOT | IRQF_SHARED,
"my-expander", priv);
/* threaded IRQ handler에서 I2C 통신으로 상태 확인 */
static irqreturn_t my_expander_irq_thread(int irq, void *data)
{
struct my_gpio_irq *priv = data;
u32 pending;
/* I2C/SPI 통신으로 인터럽트 상태 레지스터 읽기 (sleep 가능) */
pending = i2c_smbus_read_byte_data(priv->client, INT_STATUS_REG);
while (pending) {
int hwirq = __ffs(pending);
handle_nested_irq(irq_find_mapping(
priv->gc.irq.domain, hwirq));
pending &= ~BIT(hwirq);
}
return IRQ_HANDLED;
}
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 내부 주요 필드(위 SVG 참고) */
/* 버스 추상화: 각 버스 타입이 이 콜백을 구현 */
struct regmap_bus {
bool fast_io; /* true면 spinlock 사용 */
int (*write)(void *context, /* 레지스터 쓰기 */
const void *data, size_t count);
int (*read)(void *context, /* 레지스터 읽기 */
const void *reg_buf, size_t reg_size,
void *val_buf, size_t val_size);
int (*reg_write)(void *context, /* 단일 reg 쓰기 (선택) */
unsigned int reg, unsigned int val);
int (*reg_read)(void *context, /* 단일 reg 읽기 (선택) */
unsigned int reg, unsigned int *val);
int (*reg_update_bits)(void *context, /* RMW 최적화 (선택) */
unsigned int reg,
unsigned int mask, unsigned int val);
enum regmap_endian val_format_endian_default; /* 버스 기본 엔디안 */
};
devm_regmap_init_i2c(), devm_regmap_init_spi(), devm_regmap_init_mmio() 외에도 devm_regmap_init_spi_avmm() (SPI Avalon-MM), devm_regmap_init_spmi_base/ext() (SPMI), devm_regmap_init_w1() (1-Wire), devm_regmap_init_sdw() (SoundWire), devm_regmap_init_slimbus() (SLIMbus) 등 다양한 버스를 지원합니다. fast_io = true인 버스(MMIO 등)는 mutex 대신 spinlock을 사용하여 원자적 컨텍스트에서도 접근 가능합니다.
regmap_field: 비트 필드 추상화
regmap_field는 레지스터 내 특정 비트 필드를 독립적인 객체로 추상화합니다. 비트 마스크/시프트 연산을 캡슐화하여 드라이버 코드의 가독성과 유지보수성을 높입니다.
#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_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는 이러한 페이지 기반 레지스터 접근을 투명하게 처리합니다.
/*
* 디바이스 레지스터 구조 (위 SVG 참고):
*/
static const struct regmap_range_cfg my_range_cfg[] = {
{
.name = "pages",
.range_min = 0x81, /* 윈도우 시작 (가상) */
.range_max = 0x2FF, /* 윈도우 끝 (가상) */
.selector_reg = 0x80, /* 페이지 선택 레지스터 */
.selector_mask = 0xFF, /* 선택 비트 마스크 */
.selector_shift = 0, /* 선택 비트 시프트 */
.window_start = 0x81, /* 물리 윈도우 시작 */
.window_len = 0x7F, /* 윈도우 크기 (바이트) */
},
};
static const struct regmap_config my_paged_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0x2FF, /* 가상 주소 공간 최대 */
.ranges = my_range_cfg,
.num_ranges = ARRAY_SIZE(my_range_cfg),
};
/* 드라이버에서는 가상 주소로 투명하게 접근 */
regmap_read(regmap, 0x185, &val); /* 자동으로: page=1 선택 → 0x85 읽기 */
regmap_write(regmap, 0x281, 0x42); /* 자동으로: page=2 선택 → 0x81 쓰기 */
다중 레지스터 연산
여러 레지스터를 원자적으로 읽거나 쓸 때 사용하는 고급 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 캐시 상태 머신 (위 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(사용 레지스터 수) |
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
# 레지스터 값 확인
$ 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=캐시 전용 모드 활성
| 항목 | 의미 |
|---|---|
name | regmap 이름 |
range | 레지스터 주소 범위 |
registers | 레지스터 덤프 (precious 제외) |
access | 읽기/쓰기/volatile/precious 권한 정보 |
cache_only | 캐시 전용 모드 상태 |
/* debugfs에서 regmap 이름 지정 */
static const struct regmap_config my_config = {
.name = "main", /* debugfs 디렉토리에 이름 표시 */
.reg_bits = 8,
.val_bits = 16,
/* ... */
};
/* MFD 등에서 여러 regmap이 있을 때 구분에 유용:
* /sys/kernel/debug/regmap/0-001a-main/
* /sys/kernel/debug/regmap/0-001a-gpio/
*/
registers 파일에 값을 쓰면 런타임에 레지스터를 변경할 수 있습니다: echo "01 abcd" > registers. 이는 프로토타이핑 시 매우 유용하지만, precious 레지스터는 dump에서 자동 제외되므로 FIFO 데이터가 의도치 않게 소비되는 문제를 방지합니다. regmap 이름을 지정하면 ftrace의 regmap 이벤트에서도 구분하여 필터링할 수 있습니다: echo 'name == "main"' > /sys/kernel/debug/tracing/events/regmap/filter
MFD 디바이스와 regmap 공유
Multi-Function Device(MFD)에서는 하나의 regmap을 여러 서브 디바이스가 공유합니다. 부모 MFD 드라이버가 regmap을 생성하고, 자식 드라이버가 dev_get_regmap()으로 접근합니다.
/* ===== MFD 부모 드라이버 (PMIC 예시) ===== */
static const struct regmap_config pmic_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0xFF,
.cache_type = REGCACHE_RBTREE,
};
/* MFD 셀 정의 */
static const struct mfd_cell pmic_cells[] = {
{ .name = "pmic-regulator" },
{ .name = "pmic-gpio" },
{ .name = "pmic-rtc" },
{ .name = "pmic-charger" },
};
static int pmic_i2c_probe(struct i2c_client *client)
{
struct regmap *regmap;
/* regmap 생성: dev에 자동 연결됨 */
regmap = devm_regmap_init_i2c(client, &pmic_regmap_config);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
/* MFD 서브디바이스 등록 */
return devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE,
pmic_cells, ARRAY_SIZE(pmic_cells),
NULL, 0, NULL);
}
/* ===== MFD 자식 드라이버 (regulator 예시) ===== */
static int pmic_regulator_probe(struct platform_device *pdev)
{
struct regmap *regmap;
/* 부모 디바이스의 regmap 획득 */
regmap = dev_get_regmap(pdev->dev.parent, NULL);
if (!regmap) {
dev_err(&pdev->dev, "parent regmap not found\\n");
return -ENODEV;
}
/* 이제 공유 regmap으로 레지스터 접근 */
regmap_write(regmap, 0x30, 0x01); /* regulator 제어 레지스터 */
/* 또는 regmap_field로 특정 필드만 관리 */
/* ... */
return 0;
}
/* ===== 여러 regmap을 가진 MFD (이름으로 구분) ===== */
/* 부모 드라이버: 두 개의 I2C 주소를 사용하는 PMIC */
static int pmic_dual_probe(struct i2c_client *client)
{
struct regmap *regmap_main, *regmap_gpio;
struct i2c_client *gpio_client;
struct regmap_config cfg = pmic_regmap_config;
/* 메인 regmap (이름 없음 = 기본) */
regmap_main = devm_regmap_init_i2c(client, &cfg);
/* 보조 I2C 주소의 regmap */
gpio_client = devm_i2c_new_dummy_device(&client->dev,
client->adapter, client->addr + 1);
cfg.name = "gpio";
regmap_gpio = devm_regmap_init_i2c(gpio_client, &cfg);
/* ... */
}
/* 자식 드라이버: 이름으로 특정 regmap 획득 */
regmap = dev_get_regmap(pdev->dev.parent, "gpio");
regmap 고급 설정
regmap_config의 다양한 고급 옵션으로 복잡한 하드웨어 특성을 처리할 수 있습니다.
static const struct regmap_config advanced_config = {
.reg_bits = 16, /* 16비트 레지스터 주소 */
.val_bits = 32, /* 32비트 레지스터 값 */
.reg_stride = 4, /* 레지스터 주소 간격 (MMIO 워드 정렬) */
.max_register = 0x1000,
/* 엔디안 설정 */
.reg_format_endian = REGMAP_ENDIAN_BIG, /* 주소 바이트 순서 */
.val_format_endian = REGMAP_ENDIAN_LITTLE, /* 값 바이트 순서 */
/* SPI 읽기 시 상위 비트 설정 (SPI 프로토콜 관례) */
.read_flag_mask = 0x80, /* 레지스터 주소에 OR */
.write_flag_mask = 0x00,
/* 레지스터 주소 패딩 (일부 SPI 디바이스 요구) */
.pad_bits = 8, /* 주소 뒤 패딩 비트 */
/* 읽기 전 지연 (슬로우 디바이스) */
.read_delay_us = 10, /* 주소 전송 후 읽기 전 지연 */
/* 캐시 설정 */
.cache_type = REGCACHE_MAPLE,
.reg_defaults = my_defaults,
.num_reg_defaults = ARRAY_SIZE(my_defaults),
/* 동기화 비활성 레지스터: cache_sync 시 기록하지 않을 범위 */
.disable_locking = false, /* true면 외부 잠금 사용 */
/* 커스텀 잠금 (기존 잠금과 통합 시) */
.lock = my_lock_fn,
.unlock = my_unlock_fn,
.lock_arg = &my_mutex,
/* 레지스터 값 비트 후처리 */
.use_single_read = true, /* bulk를 단일 읽기로 분해 */
.use_single_write = true, /* bulk를 단일 쓰기로 분해 */
.can_multi_write = true, /* multi_reg_write 최적화 허용 */
/* 비동기 쓰기 지원 */
.use_hwlock = false, /* HW spinlock 사용 여부 */
};
reg_stride = 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, 버튼, 리셋 |
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.