Industrial I/O (IIO)

IIO(Industrial I/O) 서브시스템을 센서, ADC/DAC, IMU, DAQ 데이터 경로의 표준화 계층으로 정리합니다. iio_dev, iio_chan_spec, iio_info, trigger, buffer, 이벤트, scan layout, /dev/iio:deviceX 문자 디바이스, in-kernel consumer API, Device Tree의 io-channels, DMA/DMABUF 기반 고속 수집, 그리고 hwmon·input·power_supply와의 역할 경계를 실무 관점에서 최대한 자세히 설명합니다.

전제 조건: 디바이스 드라이버, I2C/SPI/GPIO, Device Tree, hwmon 문서를 먼저 읽으세요. IIO는 단순 파일 몇 개를 읽는 수준이 아니라, 채널 정의, 메타데이터, 트리거, 버퍼 ABI, 샘플 타이밍까지 함께 설계해야 이해가 됩니다.
일상 비유: 멀티미터가 순간값 하나를 보는 도구라면 IIO는 다채널 데이터 로거 + 오실로스코프 + 트리거 장치에 가깝습니다. 어떤 축을 어떤 순서로, 어떤 시점에, 어떤 형식으로 잡았는지까지 함께 저장해야 나중에 데이터가 의미를 갖습니다.

핵심 요약

  • 채널(channel) — 전압, 전류, 가속도 X축, 각속도 Z축 같은 개별 측정 단위를 iio_chan_spec로 정의합니다.
  • raw / scale / offset — 많은 IIO 값은 레지스터 원시값과 보정 정보가 분리되어 있으므로, 물리량으로 해석하려면 변환 규칙을 함께 읽어야 합니다.
  • trigger + buffer — IIO의 핵심은 값을 "읽는다"가 아니라 "언제 샘플을 잡아 어떤 버퍼 형식으로 밀어 넣는가"입니다.
  • scan layout — 버퍼 바이트열은 채널 순서, 비트폭, 부호, 엔디언, 반복 수, 타임스탬프 유무를 알아야 해석할 수 있습니다.
  • 이벤트와 스트림 분리 — 임계값 초과 같은 이벤트는 연속 샘플 스트림과 다른 경로로 노출되며, 둘을 섞어 생각하면 설계가 흔들립니다.
  • in-kernel consumer — IIO는 사용자 공간뿐 아니라 다른 커널 드라이버가 io-channels와 consumer API로 값을 받아 쓰는 구조도 지원합니다.

단계별 이해

  1. 장치가 어떤 채널을 제공하는지 본다
    in_voltage0_raw, in_accel_x_raw, out_voltage0_raw처럼 파일 이름이 어떤 측정 축과 방향을 뜻하는지 읽습니다.
  2. 값의 해석 규칙을 붙인다
    scale, offset, processed, available 속성을 보고 숫자가 물리량으로 어떻게 바뀌는지 확인합니다.
  3. 버퍼를 켤 때는 scan layout부터 본다
    버퍼의 바이트열 자체보다 _index, _type, _en, 타임스탬프 채널이 먼저입니다.
  4. 트리거 소스를 점검한다
    하드웨어 DRDY인지, hrtimer인지, 외부 sync인지에 따라 지터와 전력 소모, 실제 샘플링 정확도가 달라집니다.
  5. 이벤트와 스트림을 분리해서 설계한다
    임계값 알람은 이벤트 경로, 연속 측정값은 버퍼 경로로 분리해야 ABI와 사용자 공간 로직이 단순해집니다.

IIO가 필요한 이유와 다른 서브시스템과의 경계

IIO는 "센서 값을 커널에 넣는다"는 느슨한 목표가 아니라, 측정 채널의 의미와 샘플 시점을 함께 표준화하려는 서브시스템입니다. ADC, DAC, IMU, 가속도계, 자이로, 자기장 센서, 압력 센서, 조도 센서, 근접 센서, 정밀 계측용 컨버터, 다채널 DAQ처럼 채널 수가 많고 샘플 레이아웃이 중요하며 연속 캡처가 필요한 장치가 주 대상입니다.

실무에서 가장 흔한 오해는 "센서면 전부 IIO" 또는 "숫자를 읽기만 하면 hwmon과 비슷하다"는 생각입니다. 실제로는 하드웨어가 제공하는 데이터의 성격과 사용자 공간이 원하는 소비 방식에 따라 서브시스템 경계가 분명합니다.

서브시스템핵심 목표주요 ABI대표 장치적합한 경우
IIO채널 모델, 트리거, 버퍼, 이벤트/sys/bus/iio/, /dev/iio:deviceXADC, IMU, 고속 센서, DAC샘플 시점과 버퍼 형식이 중요할 때
hwmon느린 상태 모니터링/sys/class/hwmon/온도, 전압, 팬, 전력 모니터서버/보드 상태를 표준 단위로 읽을 때
input사용자 입력 이벤트/dev/input/eventX터치, 버튼, 자이로 기반 제스처UI/입력 이벤트가 목적일 때
power_supply배터리/충전 상태 모델/sys/class/power_supply/배터리, 충전기, USB PD에너지 저장장치 상태와 정책이 중요할 때
IIO의 책임 경계 IIO Core 채널 모델, trigger, buffer, event sysfs 속성 + 문자 디바이스 ABI hwmon 느린 상태 관측 표준 단위의 단일 값 Input 사용자 입력/제스처 좌표, 버튼, gesture 이벤트 Power Supply 배터리/충전 상태 모델 상태, 용량, 충전 정책 ALSA/기타 오디오, 미디어, 전용 ABI 도메인 특화 스트림 같은 하드웨어가 여러 서브시스템에 동시에 걸칠 수 있지만, IIO는 측정 채널과 샘플 타이밍을 표준화하는 역할에 집중한다.
빠른 판단 규칙: 사용자가 "지금 값 하나"를 읽고 싶으면 hwmon일 가능성이 높고, "여러 축을 동기화된 스트림으로 읽고 싶다"면 IIO일 가능성이 높습니다. 같은 센서라도 노출 목적에 따라 hwmon과 IIO를 둘 다 가질 수 있습니다.

IIO를 읽는 기본 관점: 속성 평면, 데이터 평면, 이벤트 평면

IIO는 하나의 인터페이스가 아니라 세 개의 평면이 겹쳐진 구조로 보는 것이 가장 이해하기 쉽습니다.

평면무엇을 다루나대표 인터페이스자주 생기는 오해
속성 평면채널 설명, raw/scale/offset, 샘플링 주기, 캘리브레이션sysfs 속성 파일이 숫자만 읽으면 전체 데이터 경로를 이해한 것처럼 착각
데이터 평면연속 샘플 스트림과 scan layout/dev/iio:deviceX + buffer ABI바이트열만 보면 채널 의미가 자동으로 보인다고 착각
이벤트 평면threshold, rate-of-change, gesture, FIFO 관련 상태events 속성 + 이벤트 큐이벤트를 샘플 스트림의 축약본으로 오해

이 세 평면을 분리해 보면 IIO의 설계 원칙이 보입니다.

핵심 객체 모델: iio_dev, 채널, trigger, buffer, consumer

IIO 드라이버를 읽을 때는 자료구조의 역할을 먼저 고정해 두는 것이 좋습니다. 커널 헤더 기준으로 IIO는 장치 전체를 나타내는 struct iio_dev, 채널 정의를 담는 struct iio_chan_spec, 콜백 집합인 struct iio_info, 샘플 시점을 규정하는 struct iio_trigger, 그리고 연속 샘플 경로를 나타내는 buffer 계층이 맞물려 동작합니다.

객체역할생명주기핵심 포인트
struct iio_dev장치 전체 표현probe 시 생성, remove 시 해제이름, 모드, 채널 배열, info 콜백, private state 연결
struct iio_chan_spec채널 메타데이터보통 정적 상수 배열타입, 인덱스, modifier, scan format, 지원 속성, 이벤트
struct iio_info드라이버 콜백 집합정적 상수read_raw, read_avail, update_scan_mode, 이벤트/트리거 검증
struct iio_trigger샘플 시점 소스장치 또는 별도 trigger 드라이버가 관리IRQ, hrtimer, 외부 sync, validate 함수와 연동
struct iio_buffer연속 샘플 저장 경로kfifo, hw buffer, DMA bufferwatermark, enable, bytes_per_datum, data_available
struct iio_channelin-kernel consumer 핸들다른 드라이버가 획득devm_iio_channel_get(), iio_read_channel_processed() 등 사용
IIO 객체 관계 iio_dev 장치 전체, modes, info, channels, private state iio_chan_spec[] 채널 타입, scan type, event, ext_info iio_info read_raw, read_avail, update_scan_mode iio_trigger IRQ / hrtimer / external sync iio_buffer kfifo / HW FIFO / DMA buffer in-kernel consumer devm_iio_channel_get(), iio_read_channel_processed()

장치 등록의 기본 흐름은 대개 비슷합니다. devm_iio_device_alloc()으로 iio_dev를 할당하고, iio_priv()로 private state를 붙인 다음, 채널 배열과 iio_info를 연결하고, 적절한 버퍼/트리거 helper를 붙인 뒤 devm_iio_device_register()로 마무리합니다.

#include <linux/iio/iio.h>
#include <linux/iio/triggered_buffer.h>

struct acme_state {
    struct regmap *regmap;
    int odr_hz;
};

static int acme_probe(struct spi_device *spi)
{
    struct iio_dev *indio_dev;
    struct acme_state *st;
    int ret;

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

    st = iio_priv(indio_dev);
    st->odr_hz = 200;

    indio_dev->name = "acme-imu";
    indio_dev->info = &acme_info;
    indio_dev->modes = INDIO_DIRECT_MODE | INDIO_BUFFER_TRIGGERED;
    indio_dev->channels = acme_channels;
    indio_dev->num_channels = ARRAY_SIZE(acme_channels);

    ret = devm_iio_triggered_buffer_setup(&spi->dev, indio_dev,
                                            iio_pollfunc_store_time,
                                            acme_trigger_handler, NULL);
    if (ret)
        return ret;

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

indio_dev->modes는 장치가 어떤 사용 모델을 지원하는지를 표현합니다. 최신 커널 헤더에는 INDIO_DIRECT_MODE, INDIO_BUFFER_TRIGGERED, INDIO_BUFFER_SOFTWARE, INDIO_BUFFER_HARDWARE, INDIO_EVENT_TRIGGERED, INDIO_HARDWARE_TRIGGERED가 정의되어 있으며, 이 조합을 보면 드라이버가 원샷 읽기, kfifo 기반 triggered buffer, 순수 하드웨어 버퍼, 이벤트 중심 장치 중 어느 축에 가까운지 짐작할 수 있습니다.

채널 모델과 값 해석 규칙

IIO의 진짜 중심은 struct iio_chan_spec입니다. 이 구조체가 단순히 "채널이 하나 있다"를 넘어서, 이 채널이 무엇을 측정하고 어떤 형식으로 버퍼에 실리며 어떤 속성과 이벤트를 제공하는지를 모두 설명합니다.

#include <linux/iio/iio.h>

static const struct iio_chan_spec acme_channels[] = {
    {
        .type = IIO_ACCEL,
        .modified = 1,
        .channel2 = IIO_MOD_X,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
        .info_mask_shared_by_type =
            BIT(IIO_CHAN_INFO_SCALE) |
            BIT(IIO_CHAN_INFO_SAMP_FREQ),
        .info_mask_shared_by_type_available =
            BIT(IIO_CHAN_INFO_SAMP_FREQ),
        .scan_index = 0,
        .scan_type = {
            .sign = 's',
            .realbits = 16,
            .storagebits = 16,
            .shift = 0,
            .repeat = 1,
            .endianness = IIO_LE,
        },
        .event_spec = acme_accel_events,
        .num_event_specs = ARRAY_SIZE(acme_accel_events),
    },
    IIO_CHAN_SOFT_TIMESTAMP(3),
};

채널 이름은 대개 다음 요소의 조합으로 읽습니다.

예시 파일명읽는 법의미
in_voltage0_raw입력 전압 0번 채널의 원시값ADC 코드, 아직 물리량 아님
in_accel_x_rawX축 가속도 원시값modifier가 축 정보를 제공
out_voltage0_raw출력 전압 0번 원시값DAC 같은 출력 채널
in_temp_offset온도 채널 offsetraw에 더한 뒤 scale 곱하기
in_magn_scale자기장 채널 scale원시값의 물리 단위 변환 계수
in_illuminance_input조도 채널의 처리된 값이미 물리 단위로 환산된 값일 수 있음

헤더를 보면 iio_chan_spec는 단일 숫자 이상을 담고 있습니다.

info_mask_separate, info_mask_shared_by_type, info_mask_shared_by_dir, info_mask_shared_by_all는 sysfs 파일이 채널별인지, 동일 타입 전체 공유인지, 방향 공유인지, 장치 전체 공유인지 결정합니다. 사용자 공간 입장에서는 파일 개수 차이로 보이지만, 드라이버 입장에서는 ABI 안정성과 구현 단순성에 큰 영향을 줍니다.

값 해석 규칙도 채널 모델의 일부입니다. 커널의 in-kernel consumer 헤더는 raw 값을 표준 단위로 쓰려면 대개 (raw + offset) x scale을 적용해야 한다고 설명합니다. 반대로 processed 속성이나 iio_read_channel_processed()를 지원하는 경우, 드라이버 또는 IIO 코어가 이미 단위 변환을 끝낸 값을 돌려줄 수 있습니다.

속성의미드라이버 콜백사용자 공간 주의점
*_raw레지스터 또는 ADC 코드read_raw()단위 해석 불가, scale/offset 필요
*_input 또는 processed처리된 물리량read_raw() 또는 consumer helper장치가 직접 환산했는지 코어가 환산했는지 확인
*_scale원시값 변환 계수read_raw()마이크로 단위, 분수, 로그2 기반 형식을 읽어야 함
*_offset원시값 보정 오프셋read_raw()보통 raw에 먼저 더함
*_sampling_frequencyODR 또는 샘플링 주기read_raw(), write_raw(), read_avail()실제 수신 간격과 항상 일치하지 않을 수 있음
*_oversampling_ratio오버샘플링 비율read_raw(), write_raw()대역폭과 노이즈의 trade-off
*_available허용 값 목록 또는 범위read_avail()하드코딩 대신 이 값을 먼저 보는 것이 안전
label사람이 읽기 쉬운 채널 이름read_label()헤더는 extend_name보다 read_label()을 권장

값 반환 형식도 중요합니다. read_raw()는 단순히 정수를 채우는 것이 아니라, IIO_VAL_INT, IIO_VAL_INT_PLUS_MICRO, IIO_VAL_FRACTIONAL, IIO_VAL_FRACTIONAL_LOG2 같은 형식 코드를 통해 val, val2를 어떻게 해석할지 함께 알려줍니다.

static int acme_read_raw(struct iio_dev *indio_dev,
                         const struct iio_chan_spec *chan,
                         int *val, int *val2, long mask)
{
    struct acme_state *st = iio_priv(indio_dev);
    unsigned int raw;
    int ret;

    switch (mask) {
    case IIO_CHAN_INFO_RAW:
        ret = iio_device_claim_direct_mode(indio_dev);
        if (ret)
            return ret;

        ret = regmap_read(st->regmap, chan->address, &raw);
        iio_device_release_direct_mode(indio_dev);
        if (ret)
            return ret;

        *val = sign_extend32(raw, chan->scan_type.realbits - 1);
        return IIO_VAL_INT;

    case IIO_CHAN_INFO_SCALE:
        *val = 0;
        *val2 = 598;
        return IIO_VAL_INT_PLUS_MICRO;

    case IIO_CHAN_INFO_SAMP_FREQ:
        *val = st->odr_hz;
        return IIO_VAL_INT;
    }

    return -EINVAL;
}
중요: 버퍼가 활성화된 장치에서 원샷 레지스터 읽기를 그대로 수행하면 scan 설정이나 power state와 충돌할 수 있습니다. 이런 장치에서는 iio_device_claim_direct_mode()iio_device_release_direct_mode()가 직접 읽기와 버퍼 모드의 경합을 막는 핵심 장치입니다.

sysfs ABI: 채널 설명과 제어 평면

IIO 코어 문서는 사용자 공간이 장치와 상호작용하는 방법을 크게 두 가지로 설명합니다. 하나는 sysfs 속성 파일을 통한 제어 및 원샷 읽기이고, 다른 하나는 문자 디바이스를 통한 버퍼/이벤트 소비입니다. sysfs는 "설명과 제어", 문자 디바이스는 "연속 데이터"라고 기억하면 대부분 맞습니다.

장치 루트에는 보통 다음 종류의 정보가 있습니다.

실전에서는 먼저 장치 이름과 채널 속성 목록을 확인한 뒤, available 파일을 읽어 어떤 값이 합법적인지 파악하는 습관이 중요합니다. 샘플링 주기, full-scale range, oversampling ratio를 드라이버마다 임의로 추측하면 ABI 위반과 같은 효과를 냅니다.

# IIO 장치 나열
ls /sys/bus/iio/devices/

# 장치 정체성 확인
cat /sys/bus/iio/devices/iio:device0/name

# 채널 값과 메타데이터 확인
cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw
cat /sys/bus/iio/devices/iio:device0/in_accel_scale
cat /sys/bus/iio/devices/iio:device0/in_accel_sampling_frequency
cat /sys/bus/iio/devices/iio:device0/in_accel_sampling_frequency_available

# 사람이 읽기 쉬운 채널 이름과 장착 방향
cat /sys/bus/iio/devices/iio:device0/in_accel_x_label
cat /sys/bus/iio/devices/iio:device0/in_accel_mount_matrix

속성 파일의 단위는 채널마다 다를 수 있습니다. 예를 들어 sampling_frequency는 보통 Hz 계열이지만, scale는 센서 종류에 따라 m/s², degree/s, lux, volt 등 의미가 달라집니다. IIO는 hwmon처럼 모든 속성을 극도로 제한된 단위 집합으로 통일하는 구조가 아니므로, 채널 타입과 드라이버 문맥을 함께 읽어야 합니다.

또한 read_avail()가 구현된 장치는 목록형 또는 범위형 *_available 파일을 제공합니다. 커널 헤더 문서에 따르면 범위형은 min step max의 세 값을, 목록형은 가능한 값을 나열합니다. 사용자 공간 도구는 이를 이용해 유효하지 않은 설정 쓰기를 피할 수 있습니다.

버퍼와 scan layout: IIO에서 가장 중요한 ABI

IIO를 깊게 다룰수록 핵심은 sysfs의 단일 속성이 아니라 buffer ABI라는 점이 분명해집니다. 공식 버퍼 문서는 버퍼 인스턴스 아래에 length, enable, watermark, bytes_per_datum, data_available 같은 속성이 있음을 설명합니다. 여기서 length는 버퍼 깊이, watermark는 사용자 공간을 깨우는 기준, bytes_per_datum은 한 scan의 총 바이트 수, data_available는 현재 읽을 수 있는 양을 나타냅니다.

scan layout은 각 채널별 _index, _type, _en 파일로 설명됩니다. 중요한 점은 예전 문서와 최신 ABI가 경로 표기에서 다를 수 있다는 점입니다.

표기 스타일예시 경로설명
레거시 예시scan_elements/in_accel_x_en, buffer/enable오래된 문서, 드라이버 예제, 배포판 도구에서 자주 보임
다중 버퍼 ABIbuffer0/scan_elements/in_accel_x_en, buffer0/enable커널 5.11 이후 문서에서 강조되는 구조. 버퍼별 구성 가능
현장 규칙: 실제 장비와 배포판에서 두 표기를 모두 볼 수 있습니다. 문서와 스크립트를 읽을 때 "경로가 다르다"보다 "채널 활성화, scan type, watermark, enable의 개념은 같다"를 먼저 잡는 편이 좋습니다.

_type 파일은 버퍼 바이트열을 어떻게 해석해야 하는지 알려주는 핵심 메타데이터입니다. 예를 들어 le:s16/16>>0는 리틀엔디언, 부호 있는 16비트, 저장 16비트, 오른쪽 시프트 0을 의미합니다. 여기에 repeat가 붙으면 같은 형식이 여러 번 반복되는 채널입니다.

scan layout 예시: accel X/Y/Z + timestamp scan_index 0 accel_x, le:s16/16>>0 scan_index 1 accel_y, le:s16/16>>0 scan_index 2 accel_z, le:s16/16>>0 scan_index 3 timestamp, le:s64/64>>0 byte 0-1 X byte 2-3 Y byte 4-5 Z byte 6-7 padding byte 8-15 timestamp padding과 timestamp 위치까지 포함해 해석해야 하므로, 버퍼는 "연속 raw 값 배열"이 아니라 "scan layout이 부착된 구조체 스트림"으로 이해해야 한다.

버퍼 활성화 순서도 중요합니다. 공식 ABI 문서는 enable을 마지막에 쓰라고 설명합니다. 즉, 채널 선택, trigger 선택, length/watermark 설정, 필요한 경우 sampling_frequency 조정이 끝난 뒤에 enable을 켜는 것이 안전합니다.

# 최신 다중 버퍼 ABI 예시
cat /sys/bus/iio/devices/iio:device0/buffer0/scan_elements/in_accel_x_index
cat /sys/bus/iio/devices/iio:device0/buffer0/scan_elements/in_accel_x_type
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/scan_elements/in_accel_x_en
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/scan_elements/in_accel_y_en
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/scan_elements/in_accel_z_en
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/scan_elements/in_timestamp_en
echo 64 > /sys/bus/iio/devices/iio:device0/buffer0/length
echo 16 > /sys/bus/iio/devices/iio:device0/buffer0/watermark
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/enable

# 레거시 문서에서 자주 보이는 표기
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_x_en
echo 64 > /sys/bus/iio/devices/iio:device0/buffer/length
echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable

드라이버 입장에서는 update_scan_mode()가 이 순간의 중심 함수입니다. 어떤 채널이 켜졌는지, scan mask가 어떤지에 따라 하드웨어 레지스터를 다시 프로그래밍해야 하기 때문입니다. 특정 ADC처럼 한 번에 하나의 채널만 선택 가능한 장치라면 iio_validate_scan_mask_onehot() 같은 helper로 제약을 모델링하는 것이 일반적입니다.

Trigger 모델과 triggered buffer 흐름

IIO trigger는 단순 IRQ 번호가 아니라, "샘플 캡처 시점을 표현하는 커널 객체"입니다. 하드웨어 data-ready IRQ, hrtimer 기반 소프트웨어 trigger, 외부 동기 신호, 다른 장치가 제공하는 공용 trigger가 모두 IIO trigger로 모델링될 수 있습니다.

공식 트리거 문서는 trigger 자체가 /sys/bus/iio/devices/triggerY/에 노출되고, 장치는 보통 자신의 trigger/current_trigger를 통해 어떤 trigger를 사용할지 선택한다고 설명합니다. 장치 쪽 validate_trigger()와 trigger 쪽 validate_device()가 맞물려 호환되지 않는 조합을 막습니다.

요소역할관련 API
trigger provider트리거 객체 생성 및 등록devm_iio_trigger_alloc(), devm_iio_trigger_register()
trigger consumer장치에 trigger를 연결current_trigger, validate_trigger()
poll function트리거 발생 시 timestamp 확보와 데이터 수집iio_pollfunc_store_time(), threaded handler
완료 통지trigger use-count와 재활성화 경로 정리iio_trigger_notify_done()
triggered buffer 동작 순서 Trigger source DRDY IRQ / hrtimer / ext sync pollfunc top half iio_pollfunc_store_time() threaded handler 레지스터 읽기, frame 조립 buffer kfifo / DMA userspace read() thread handler에서 반드시 하는 일 1. 활성 scan mask에 맞춰 데이터 읽기 2. 필요하면 timestamp를 frame 끝에 넣기 3. iio_push_to_buffers_with_timestamp() 또는 유사 helper 호출 4. 마지막에 iio_trigger_notify_done() 호출
#include <linux/iio/triggered_buffer.h>
#include <linux/iio/trigger_consumer.h>

static irqreturn_t acme_trigger_handler(int irq, void *p)
{
    struct iio_poll_func *pf = p;
    struct iio_dev *indio_dev = pf->indio_dev;
    struct acme_state *st = iio_priv(indio_dev);
    __le16 frame[4];
    int ret;

    ret = acme_read_xyz_locked(st, frame);
    if (!ret)
        iio_push_to_buffers_with_timestamp(indio_dev, frame, pf->timestamp);

    iio_trigger_notify_done(indio_dev->trig);
    return IRQ_HANDLED;
}

iio_pollfunc_store_time()는 timestamp를 가능한 이른 시점에 잡아 thread handler로 넘기는 helper입니다. scan frame이 자연 정렬을 따르지 않는 packed 형식이라면 iio_push_to_buffers_with_ts_unaligned() 같은 helper가 필요할 수 있습니다.

장치가 외부 trigger를 허용하지 않고 자기 own trigger만 써야 하는 경우, trigger 헤더가 제공하는 iio_trigger_set_immutable()iio_validate_own_trigger() 계열 helper를 활용하면 정책을 분명하게 만들 수 있습니다.

이벤트 ABI: threshold, hysteresis, 상태 변화 알림

IIO 이벤트는 "샘플을 모두 읽어서 조건을 후처리"하는 경로가 아닙니다. 하드웨어나 드라이버가 이미 감지한 의미 있는 상태 변화를 별도로 올려주는 통로입니다. 예를 들면 threshold 상승/하강, rate-of-change, FIFO 상태, gesture, 데이터 준비 완료와 같은 신호가 여기에 들어갑니다.

커널 헤더의 struct iio_event_spec는 이벤트 타입, 방향, 그리고 이벤트 속성의 공유 범위를 정의합니다. 실제 활성화와 값 읽기/쓰기는 read_event_config(), write_event_config(), read_event_value(), write_event_value() 같은 iio_info 콜백으로 이어집니다.

예시 이벤트 파일의미
events/in_accel_x_thresh_rising_enX축 가속도 상승 임계값 이벤트 enable
events/in_accel_x_thresh_rising_value상승 임계값 자체
events/in_accel_x_thresh_rising_hysteresis채터링 방지용 hysteresis 폭
events/in_temp_thresh_falling_en온도 하강 임계값 이벤트 enable

공식 ABI 설명에서 특히 중요한 점은 hysteresis의 적용 방식입니다. 상승 이벤트라면 일반적으로 값이 threshold를 넘을 때 활성화되고, 다시 threshold - hysteresis 아래로 내려가야 해제됩니다. 하강 이벤트는 반대로 threshold + hysteresis 위로 올라가야 해제됩니다. 이 규칙을 이해하지 못하면 "이벤트가 한 번 울린 뒤 왜 바로 다시 안 울리나?" 같은 혼란이 생깁니다.

static const struct iio_event_spec acme_accel_events[] = {
    {
        .type = IIO_EV_TYPE_THRESH,
        .dir = IIO_EV_DIR_RISING,
        .mask_separate =
            BIT(IIO_EV_INFO_VALUE) |
            BIT(IIO_EV_INFO_HYSTERESIS),
    },
    {
        .type = IIO_EV_TYPE_THRESH,
        .dir = IIO_EV_DIR_FALLING,
        .mask_separate =
            BIT(IIO_EV_INFO_VALUE) |
            BIT(IIO_EV_INFO_HYSTERESIS),
    },
};

이벤트가 실제 발생하면 드라이버는 보통 iio_push_event()로 코드와 timestamp를 올립니다. 사용자 공간은 iio_event_monitor 같은 도구나 자체 이벤트 소비 코드로 이를 관찰합니다. 이벤트는 상태 변화 알림이지, 전체 신호를 재구성할 수 있는 샘플 스트림이 아니라는 점을 계속 유지해야 합니다.

사용자 공간 인터페이스: sysfs, 문자 디바이스, libiio, configfs

IIO 코어 문서가 강조하듯 사용자 공간에는 두 가지 기본 상호작용 방식이 있습니다.

  1. sysfs 속성 — 원샷 읽기, 설정 쓰기, scan layout과 이벤트/trigger 정보 확인
  2. 문자 디바이스 — 버퍼 기반 샘플 스트림과 이벤트 소비

실전 도구 생태계에서는 libiio와 함께 iio_info, iio_attr, iio_generic_buffer, iio_event_monitor, lsiio 같은 도구가 자주 쓰입니다. 커널 문서의 IIO tools 페이지도 이러한 유틸리티 집합을 소개합니다.

# 장치와 채널 메타데이터 확인
iio_info -n local

# 특정 속성 조회
iio_attr -q -c local acme-imu in_accel_scale

# 버퍼 스트림 읽기
iio_generic_buffer -n local -c acme-imu -s 128

# 이벤트 관찰
iio_event_monitor -n local acme-imu

/dev/iio:deviceX는 단순 파일처럼 보이지만, 읽기 전에 scan layout과 버퍼 상태를 정확히 구성하지 않으면 얻는 바이트열은 쓸모가 없습니다. 특히 버퍼 크기와 watermark를 적절히 맞추지 않으면 시스템 호출 오버헤드가 커지거나 지연이 늘어납니다.

IIO configfs 지원도 알아둘 만합니다. 커널 문서는 CONFIG_IIO_CONFIGFS를 활성화하면 configfs 아래 /config/iio에 IIO 객체가 생기며, 소프트웨어 trigger를 /config/iio/triggers 아래에서 생성할 수 있다고 설명합니다. hrtimer trigger를 이용하면 하드웨어 DRDY가 없는 환경에서도 실험용 트리거를 만들 수 있습니다.

# configfs mount
mkdir -p /config
mount -t configfs none /config

# IIO 소프트웨어 trigger 확인
ls /config/iio/triggers/

고속 장치에서는 read/write 기반 버퍼 인터페이스 외에 DMABUF 기반 인터페이스도 중요합니다. 공식 DMABUF 문서는 IIO 버퍼에 외부에서 생성한 DMABUF 객체를 붙여 zero-copy에 가까운 전송을 구성할 수 있다고 설명합니다. 이는 IIO와 USB 스택 같은 다른 인터페이스 사이에서 대용량 데이터를 복사 없이 넘기고 싶을 때 특히 유용합니다. 대신 DMA_BUF_SYNC_START, DMA_BUF_SYNC_END와 같은 동기화 비용이 생기므로, "무조건 빠르다"가 아니라 대역폭과 동기화 오버헤드를 저울질해야 하는 기법입니다.

in-kernel consumer API와 io-channels

IIO는 사용자 공간용 프레임워크일 뿐 아니라, 다른 커널 드라이버가 채널을 소비하도록 연결하는 계층이기도 합니다. 예를 들어 배터리 측정용 ADC 채널을 전원 드라이버가 읽거나, 보드 센서의 아날로그 입력을 LED 드라이버나 thermal 보조 드라이버가 읽을 수 있습니다.

커널 헤더 include/linux/iio/consumer.h에는 다음 API들이 정의되어 있습니다.

API용도
devm_iio_channel_get()이름으로 단일 채널 획득
devm_iio_channel_get_all()연결된 채널 전체 획득
devm_fwnode_iio_channel_get_by_name()펌웨어 노드 기준 채널 획득
iio_read_channel_raw()원시값 읽기
iio_read_channel_processed()단위가 적용된 값 읽기
iio_read_channel_scale(), iio_read_channel_offset()변환 메타데이터 개별 읽기
iio_channel_get_all_cb()triggered capture를 콜백으로 수신

Device Tree에서는 보통 provider가 #io-channel-cells를 선언하고, consumer가 io-channelsio-channel-names로 연결합니다. 이후 consumer 드라이버는 이름 기반으로 채널을 요청합니다.

IIO provider ↔ consumer 연결 IIO provider adc@4000 #io-channel-cells = <1> Device Tree binding io-channels = <&adc 3>, <&adc 4> io-channel-names = "battery_voltage", "ntc_temp" consumer driver devm_iio_channel_get() iio_read_channel_processed() consumer는 provider의 채널 번호와 이름을 firmware binding에 의존하므로, provider 쪽 채널 인덱스 정의는 ABI로 취급해야 한다.
/* provider */
adc@4000 {
    compatible = "vendor,sigma-delta-adc";
    reg = <0x4000 0x100>;
    #io-channel-cells = <1>;
    vref-supply = <&vref_1v8>;
};

/* consumer */
battery-monitor {
    compatible = "vendor,battery-monitor";
    io-channels = <&adc 3>, <&adc 4>;
    io-channel-names = "battery_voltage", "ntc_temp";
};
#include <linux/iio/consumer.h>

static int battery_mon_probe(struct platform_device *pdev)
{
    struct iio_channel *vbat;
    int uv;
    int ret;

    vbat = devm_iio_channel_get(&pdev->dev, "battery_voltage");
    if (IS_ERR(vbat))
        return PTR_ERR(vbat);

    ret = iio_read_channel_processed(vbat, &uv);
    if (ret)
        return ret;

    /* uv는 이미 처리된 단위 값일 수 있다. 단위는 provider 문서와 채널 타입을 함께 확인한다. */
    return 0;
}

펌웨어 셀 해석이 단순 채널 번호가 아니라 더 복잡한 경우, 코어 문서에 나온 fwnode_xlate 콜백을 통해 provider가 자신만의 specifier 해석 규칙을 제공할 수 있습니다. 따라서 #io-channel-cells는 단순 숫자 하나일 수도 있지만, 더 많은 셀을 요구하는 ABI로 확장될 수도 있습니다.

드라이버 구현 패턴과 좋은 설계 습관

IIO 드라이버는 센서 레지스터를 읽는 코드만으로 끝나지 않습니다. 좋은 드라이버는 채널 ABI, 트리거 제약, 버퍼 모드, 이벤트, 전원 관리, 디버깅 경로를 함께 설계합니다.

  1. 장치 등록을 먼저 단순화한다
    devm_iio_device_alloc()devm_iio_device_register()를 기본으로 하고, iio_priv()에 state를 보관합니다.
  2. 채널 ABI를 먼저 고정한다
    채널 번호와 파일 이름이 바뀌면 사용자 공간, DT consumer, 테스트 스크립트가 모두 깨질 수 있으므로 초기 설계가 중요합니다.
  3. read_avail()를 적극적으로 구현한다
    지원 ODR, oversampling ratio, full-scale range를 *_available로 노출하면 userspace와 테스트 도구가 안정됩니다.
  4. read_label()을 우선한다
    헤더가 말하듯 extend_name은 sysfs 파일 이름에 영향을 주는 오래된 방식이고, 사람이 읽는 라벨은 read_label()이 더 적절합니다.
  5. update_scan_mode()를 대충 넘기지 않는다
    활성 채널 조합에 따라 레지스터 맵, burst length, FIFO 패킹이 달라지는 장치는 이 함수가 핵심입니다.
  6. direct/buffer 경합을 의식한다
    원샷 읽기 경로는 iio_device_claim_direct_mode()를 검토하고, buffer enabled 상태에서 허용되지 않는 작업을 분리합니다.
  7. 디버그용 register access를 준비한다
    debugfs_reg_access()를 구현하면 bring-up 단계에서 레지스터 확인이 쉬워집니다.
static const struct iio_info acme_info = {
    .read_raw = acme_read_raw,
    .read_avail = acme_read_avail,
    .write_raw = acme_write_raw,
    .read_label = acme_read_label,
    .read_event_config = acme_read_event_config,
    .write_event_config = acme_write_event_config,
    .read_event_value = acme_read_event_value,
    .write_event_value = acme_write_event_value,
    .validate_trigger = iio_validate_own_trigger,
    .update_scan_mode = acme_update_scan_mode,
    .debugfs_reg_access = acme_debugfs_reg_access,
};

write_raw_get_fmt()도 잊기 쉬운 포인트입니다. 쓰기 정밀도를 명확히 정의하지 않으면 사용자 공간은 기본적으로 IIO_VAL_INT_PLUS_MICRO 형식을 기대하게 됩니다. 정밀한 스케일 설정이나 캘리브레이션 값을 쓰는 장치에서는 이 차이가 중요한 ABI 차이로 이어집니다.

고속 수집: kfifo, hardware buffer, DMA engine, DMABUF

IIO는 느린 센서 몇 개만 다루는 프레임워크가 아닙니다. 수십 kSPS부터 수 MSPS 이상까지 올라가는 ADC나 SDR 계열 장치에서는 버퍼 구현 방식이 성능의 핵심이 됩니다.

방식대표 모드/Helper장점주의점
kfifo triggered bufferdevm_iio_triggered_buffer_setup()구현 단순, 범용성 높음IRQ/thread 오버헤드가 커질 수 있음
software kfifodevm_iio_kfifo_buffer_setup()트리거 없이 드라이버가 직접 push 가능드라이버가 생산 시점을 스스로 관리해야 함
hardware/DMA bufferdevm_iio_dmaengine_buffer_setup()고속 장치에 유리, CPU 부담 감소burst 크기, alignment, cache 동기화, watermark 튜닝 필요
DMABUF문서화된 DMABUF 인터페이스zero-copy 성격의 고대역폭 전달동기화 IOCTL 비용과 메모리 생명주기 관리가 필요

코어 문서에는 hwfifo_set_watermark, hwfifo_flush_to_buffer 같은 포인터도 정의되어 있습니다. 이는 장치 내부 FIFO가 별도로 있고, 그 안의 샘플을 IIO 버퍼로 옮겨 담아야 하는 드라이버에서 매우 중요합니다. 즉, "하드웨어 FIFO"와 "IIO 버퍼"는 같은 개념이 아닐 수 있습니다.

고속 IIO 수집 경로 ADC / IMU 고속 샘플 생성 HW FIFO watermark, flush DMA engine burst transfer IIO buffer buffer0 / DMABUF consumer user / kernel 고속 장치에서는 sampling_frequency보다도 burst 길이, FIFO watermark, DMA 완료 주기, CPU cache 동기화가 실제 처리량과 지연을 결정한다. 이 문장은 공식 문서의 직접 인용이 아니라, DMA/FIFO/DMABUF 구조를 종합한 실무적 해석이다.

DMABUF를 쓰더라도 언제나 최선은 아닙니다. 작은 샘플을 간헐적으로 읽는 센서라면 단순 read() 기반 버퍼가 더 낫고, 수 MB/s 이상 고속 장치에서만 DMABUF의 복사 절감 이점이 체감되는 경우가 많습니다.

Device Tree와 펌웨어 연결: provider, consumer, 보드 정보

IIO 장치의 DT 바인딩은 버스 주소만 넣는 수준을 넘습니다. 센서는 공급 전원, 인터럽트, 참조 전압, mount matrix, 채널 번호 ABI, 외부 clock, reset, trigger 연결까지 보드 의존 정보를 많이 가집니다.

센서 provider 노드의 대표적인 요소는 다음과 같습니다.

imu@68 {
    compatible = "vendor,my-imu";
    reg = <0x68>;
    interrupt-parent = <&gpio1>;
    interrupts = <12 1>;
    vdd-supply = <&vdd_3v3>;
    vddio-supply = <&vdd_1v8>;
    mount-matrix = "1", "0", "0",
                   "0", "1", "0",
                   "0", "0", "1";
};

mount-matrix는 특히 IMU 계열에서 중요합니다. 센서 칩의 물리적인 장착 방향이 보드 기준 좌표와 다르면, raw X/Y/Z를 그대로 쓰는 상위 소프트웨어는 전부 틀린 데이터를 보게 됩니다. IIO 헤더는 IIO_MOUNT_MATRIX() helper와 iio_read_mount_matrix()를 제공하여 이 정보를 채널 ext_info로 노출할 수 있게 합니다.

consumer와 연결할 때는 provider가 어떤 채널 번호를 어떤 의미로 해석하는지 문서화해야 합니다. 숫자 하나 바꾸는 것이 사소해 보이지만, io-channels = <&adc 3>를 쓰는 모든 consumer에게는 ABI 변경입니다.

디버깅 절차: 값이 틀린가, 타이밍이 틀린가, ABI 해석이 틀린가

IIO 문제를 디버깅할 때는 "값이 이상하다"를 세 종류로 분해해야 합니다.

  1. 원시값 자체가 틀리다
    레지스터 읽기, 전원, 레퍼런스 전압, range 설정, sign extension이 문제일 가능성이 큽니다.
  2. 값 해석이 틀리다
    scale, offset, label, mount_matrix, _type 해석이 문제일 수 있습니다.
  3. 타이밍이 틀리다
    trigger 선택, watermark, FIFO flush, DMA burst, runtime PM 재개 지연이 문제일 수 있습니다.
# 1. 장치 존재와 이름 확인
for d in /sys/bus/iio/devices/iio:device*; do
    echo "== $d =="
    cat "$d/name"
done

# 2. 핵심 채널 메타데이터 점검
cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw
cat /sys/bus/iio/devices/iio:device0/in_accel_scale
cat /sys/bus/iio/devices/iio:device0/in_accel_sampling_frequency
cat /sys/bus/iio/devices/iio:device0/in_accel_sampling_frequency_available

# 3. 버퍼/scan layout 점검
cat /sys/bus/iio/devices/iio:device0/buffer0/bytes_per_datum
cat /sys/bus/iio/devices/iio:device0/buffer0/data_available
cat /sys/bus/iio/devices/iio:device0/buffer0/scan_elements/in_accel_x_type

# 4. trigger 연결 점검
cat /sys/bus/iio/devices/iio:device0/trigger/current_trigger
ls /sys/bus/iio/devices/trigger*

드라이버가 debugfs_reg_access()를 구현했다면 debugfs 아래에 장치별 direct register access 경로가 나타날 수 있습니다. 다만 debugfs는 안정 ABI가 아니므로, 이는 생산 환경용 인터페이스가 아니라 bring-up과 개발용 관찰점으로 보는 것이 맞습니다.

디버깅 함정: 버퍼가 비어 있는데도 raw 읽기는 정상이라면, 센서 자체보다 trigger 또는 watermark 설정을 먼저 의심하세요. 반대로 raw 값은 이상한데 이벤트만 정상이라면, 이벤트 비교 로직과 데이터 path가 다른 레지스터 또는 스케일을 쓰고 있을 가능성이 큽니다.

흔한 실패 패턴과 원인

증상흔한 원인점검 포인트
버퍼 값이 축이 뒤섞인 것처럼 보임_index_type 무시scan layout, 엔디언, padding 확인
샘플 주기가 불안정함잘못된 trigger, watermark 과대, runtime PM 지연current_trigger, IRQ 상태, FIFO 설정 확인
버퍼를 켠 뒤 raw 읽기가 실패direct mode와 buffer mode 충돌iio_device_claim_direct_mode() 경로 확인
이벤트가 한 번만 울리고 다시 안 울림hysteresis 또는 rearm 조건 오해threshold/hysteresis 수치와 해제 조건 확인
processed 값이 기대 단위와 다름provider의 단위 문맥 오해채널 타입, scale/offset, 드라이버 문서 재확인
consumer 드라이버가 잘못된 채널을 읽음io-channels 인덱스 변경 또는 binding mismatchDT provider ABI와 consumer 이름 매핑 확인
고속 장치에서 샘플 드롭DMA burst/watermark/FIFO flush 불균형HW FIFO, DMA 완료 주기, 버퍼 깊이 재조정

끝까지 따라가는 상태 전이 예제

SPI IMU가 accel X/Y/Z와 timestamp를 triggered buffer로 제공하는 전형적인 예를 따라가 보겠습니다.

  1. probe
    드라이버는 iio_dev를 할당하고 채널 배열, iio_info, triggered buffer helper를 등록합니다.
  2. 사용자 공간 구성
    sampling_frequency를 설정하고, X/Y/Z와 timestamp scan element를 enable하고, current_trigger를 DRDY trigger로 선택합니다.
  3. buffer enable
    update_scan_mode()가 호출되어 하드웨어 burst 형식과 FIFO 패킹을 재설정합니다.
  4. DRDY 발생
    top half가 timestamp를 저장하고, threaded handler가 실제 샘플 frame을 읽어 버퍼에 push합니다.
  5. userspace read
    사용자 공간은 /dev/iio:deviceX에서 scan 단위로 읽고, _type_index로 복원합니다.
  6. threshold event 병행
    같은 장치가 threshold 이벤트도 켜 둔 경우, 별도 이벤트 큐로 임계값 crossing을 보고할 수 있습니다.
상태 전이: IMU triggered buffer probe 완료 장치 등록 scan 구성 채널/trigger 선택 buffer enable update_scan_mode() DRDY 발생 timestamp 확보 buffer push scan frame 저장 read() userspace 병렬 경로: threshold 이벤트가 활성화되어 있으면 같은 샘플 조건을 기준으로 별도 이벤트 큐에 알림이 올라갈 수 있다.

이 흐름을 머리에 넣고 나면 IIO 디버깅도 단순해집니다. 문제가 probe인지, scan 구성인지, trigger인지, frame 조립인지, 사용자 공간 해석인지 단계별로 잘라서 볼 수 있기 때문입니다.