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는 단순 파일 몇 개를 읽는 수준이 아니라, 채널 정의, 메타데이터, 트리거, 버퍼(Buffer) ABI, 샘플 타이밍까지 함께 설계해야 이해가 됩니다.
일상 비유: 멀티미터가 순간값 하나를 보는 도구라면 IIO는 다채널 데이터 로거 + 오실로스코프 + 트리거 장치에 가깝습니다. 어떤 축을 어떤 순서로, 어떤 시점에, 어떤 형식으로 잡았는지까지 함께 저장해야 나중에 데이터가 의미를 갖습니다.

핵심 요약

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

#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원시값 보정 오프셋(Offset)read_raw()보통 raw에 먼저 더함
*_sampling_frequencyODR 또는 샘플링 주기read_raw(), write_raw(), read_avail()실제 수신 간격과 항상 일치하지 않을 수 있음
*_oversampling_ratio오버샘플링 비율read_raw(), write_raw()대역폭(Bandwidth)과 노이즈의 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 페이지(Page)도 이러한 유틸리티 집합을 소개합니다.

# 장치와 채널 메타데이터 확인
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를 적절히 맞추지 않으면 시스템 호출(System Call) 오버헤드(Overhead)가 커지거나 지연(Latency)이 늘어납니다.

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()펌웨어(Firmware) 노드 기준 채널 획득
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, 트리거 제약, 버퍼 모드, 이벤트, 전원 관리(Power Management), 디버깅(Debugging) 경로를 함께 설계합니다.

  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 바인딩은 버스(Bus) 주소만 넣는 수준을 넘습니다. 센서는 공급 전원, 인터럽트(Interrupt), 참조 전압, 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 이름 매핑(Mapping) 확인
고속 장치에서 샘플 드롭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 조립인지, 사용자 공간 해석인지 단계별로 잘라서 볼 수 있기 때문입니다.

iio_info 콜백 구조

struct iio_info는 IIO 디바이스의 sysfs 읽기/쓰기 동작을 정의하는 핵심 콜백 테이블입니다. 드라이버 작성 시 반드시 구현해야 합니다:

/* include/linux/iio/iio.h */
struct iio_info {
    /* 채널 값 읽기: sysfs에서 in_voltage0_raw 읽을 때 호출 */
    int (*read_raw)(struct iio_dev *indio_dev,
                    struct iio_chan_spec const *chan,
                    int *val, int *val2, long mask);

    /* 채널 값 쓰기: sysfs에서 out_voltage0_raw 쓸 때 호출 */
    int (*write_raw)(struct iio_dev *indio_dev,
                     struct iio_chan_spec const *chan,
                     int val, int val2, long mask);

    /* 사용 가능한 값 목록 제공: _available sysfs 파일 */
    int (*read_avail)(struct iio_dev *indio_dev,
                      struct iio_chan_spec const *chan,
                      const int **vals, int *type,
                      int *length, long mask);
};

/* mask 값에 따른 동작 분기 */
switch (mask) {
case IIO_CHAN_INFO_RAW:       /* ADC 원시 값 (예: 0~4095) */
    *val = read_adc_register(chan->channel);
    return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:     /* 스케일: 실제값 = raw × scale */
    *val = 0;
    *val2 = 805664;           /* 3.3V / 4096 = 0.000805664V */
    return IIO_VAL_INT_PLUS_NANO;
case IIO_CHAN_INFO_SAMP_FREQ: /* 샘플링 주파수 (Hz) */
    *val = 100;
    return IIO_VAL_INT;
}
반환값 의미 sysfs 출력 형식 예시
IIO_VAL_INT 정수 val 2048
IIO_VAL_INT_PLUS_MICRO 정수 + 마이크로 val.val2 (6자리) 1.500000 (1V 500mV)
IIO_VAL_INT_PLUS_NANO 정수 + 나노 val.val2 (9자리) 0.000805664
IIO_VAL_FRACTIONAL 분수 val/val2 3300/4096
IIO_VAL_FRACTIONAL_LOG2 분수 log2 val/2^val2 3300/2^12
💡

실제값 계산: 사용자 공간에서 실제 물리량은 raw × scale + offset으로 계산합니다. 예를 들어 12-bit ADC(0~4095)에 3.3V 레퍼런스면: scale = 3.3/4096 = 0.000805664, 실제 전압 = raw × 0.000805664V입니다. read_avail()로 지원하는 스케일/샘플링 레이트 목록을 제공하면 사용자가 *_available sysfs 파일로 확인할 수 있습니다.

DMA와 IIO 통합

고속 샘플 수집(>100kSPS)에서는 CPU 인터럽트 기반 전송이 병목(Bottleneck)이 됩니다. DMAEngine consumer API를 사용하면 ADC 하드웨어 FIFO에서 직접 메모리로 데이터를 전송하여 CPU 부하를 최소화할 수 있습니다:

/* IIO DMA buffer 설정 예제 (drivers/iio/buffer/industrialio-buffer-dmaengine.c) */

/* probe()에서 DMA 채널 요청 */
struct dma_chan *chan = dma_request_chan(dev, "rx");

/* IIO DMA buffer 등록 */
ret = devm_iio_dmaengine_buffer_setup(dev, indio_dev, "rx");

/* Device Tree 바인딩:
 * adc@48000000 {
 *     compatible = "ti,adc108s102";
 *     dmas = <&edma 0 24>;       // DMA 채널 지정
 *     dma-names = "rx";
 *     #io-channel-cells = <1>;
 * };
 */

/* DMA buffer vs kfifo buffer 비교:
 * kfifo:   CPU가 IRQ마다 데이터 복사 → 저속(~10kSPS)에 적합
 * DMA:     하드웨어가 직접 메모리에 기록 → 고속(>100kSPS)에 필수
 * DMABUF:  유저 공간과 zero-copy 공유 (커널 6.4+)
 */
ℹ️

IIO DMABUF API (커널 6.4+): 새로운 IIO_BUFFER_DMABUF 인터페이스는 유저 공간 애플리케이션이 DMA 버퍼를 직접 매핑하여 zero-copy로 데이터에 접근할 수 있게 합니다. GPU 처리 파이프라인(Pipeline)이나 FPGA와 연동할 때 특히 유용합니다. 자세한 내용은 DMA Engine 페이지를 참고하세요.

커널 소스 분석: iio_device_register() 호출 체인

devm_iio_device_register()는 대부분의 IIO 드라이버가 probe 마지막에 호출하는 함수입니다. 이 함수는 내부적으로 iio_device_register()를 호출하고, 여기서 sysfs 속성 등록, 버퍼 마스크 할당, 문자 디바이스(Character Device) 등록이 순차적으로 이루어집니다. 커널 소스(drivers/iio/industrialio-core.c)를 따라가면 다음과 같은 체인이 보입니다.

/* drivers/iio/industrialio-core.c — 등록 호출 체인 (간략화) */

int __iio_device_register(struct iio_dev *indio_dev, struct module *this_mod)
{
    struct iio_dev_opaque *iio_dev_opaque = to_iio_dev_opaque(indio_dev);
    int ret;

    /* 1단계: sysfs 속성 그룹 등록 — 채널별 파일 생성 */
    ret = iio_device_register_sysfs(indio_dev);
    if (ret)
        return ret;

    /* 2단계: 버퍼 sysfs + scan mask 할당 */
    ret = iio_buffer_alloc_sysfs_and_mask(indio_dev);
    if (ret)
        goto error_unreg_sysfs;

    /* 3단계: cdev + device 등록 → /dev/iio:deviceX 생성 */
    ret = cdev_device_add(&iio_dev_opaque->chrdev, &indio_dev->dev);
    if (ret)
        goto error_free_sysfs;

    return 0;

error_free_sysfs:
    iio_buffer_free_sysfs_and_mask(indio_dev);
error_unreg_sysfs:
    iio_device_unregister_sysfs(indio_dev);
    return ret;
}
코드 설명
  • iio_device_register_sysfs()iio_chan_spec 배열의 info_mask_* 비트마스크를 순회하며 채널별 sysfs 속성 파일(in_voltage0_raw, in_accel_scale 등)을 동적으로 생성합니다. ext_info가 있으면 벤더 확장 속성도 이 단계에서 등록됩니다.
  • iio_buffer_alloc_sysfs_and_mask() — 버퍼의 scan_elements/ 디렉터리를 생성하고, 각 채널의 _en, _index, _type 파일을 만듭니다. available_scan_masks가 설정되어 있으면 하드웨어가 지원하는 유효한 채널 조합만 허용하도록 마스크를 검증합니다.
  • cdev_device_add() — 문자 디바이스와 struct device를 동시에 등록합니다. 이 시점에서 /dev/iio:deviceX가 생기고, 사용자 공간은 open()/read()/ioctl()로 버퍼 데이터와 이벤트를 소비할 수 있게 됩니다.
  • to_iio_dev_opaque()iio_dev의 불투명(opaque) 컨테이너를 얻는 매크로입니다. 커널 5.13 이후 IIO 내부 필드(chrdev, event_interface, 잠금 등)가 iio_dev_opaque로 분리되어 드라이버가 직접 접근할 수 없게 캡슐화되었습니다.
  • 오류 경로 — 각 단계가 실패하면 이전 단계의 자원을 역순으로 해제합니다. devm_ 접두사를 쓰면 devres가 이 정리를 자동으로 처리합니다.
iio_device_register() 호출 체인 devm_iio_device_register() __iio_device_register() iio_device_register_sysfs() 채널 속성 파일 생성 1단계 iio_buffer_alloc_sysfs_and_mask() scan_elements/ + 마스크 할당 2단계 cdev_device_add() /dev/iio:deviceX 생성 3단계 실패 시 역순 정리 cdev_device_del() ← iio_buffer_free_sysfs_and_mask() ← iio_device_unregister_sysfs()

커널 소스 분석: struct iio_dev 필드별 해설

struct iio_dev는 IIO 서브시스템에서 하나의 물리 장치를 표현하는 최상위 구조체입니다. 커널 헤더(include/linux/iio/iio.h)에서 드라이버가 직접 접근하는 필드와, iio_dev_opaque에 숨겨진 내부 필드가 분리되어 있습니다.

/* include/linux/iio/iio.h — struct iio_dev 주요 필드 (간략화) */
struct iio_dev {
    int                          modes;          /* INDIO_DIRECT_MODE | INDIO_BUFFER_TRIGGERED 등 */
    int                          currentmode;    /* 현재 활성 모드 (코어가 관리) */
    struct device                dev;            /* 내장 device — sysfs, devres 연결 */

    struct iio_buffer            *buffer;        /* 주 버퍼 (레거시 호환) */
    int                          scan_bytes;     /* 활성 채널 기준 한 scan의 바이트 수 */

    const struct iio_chan_spec   *channels;      /* 채널 정의 배열 포인터 */
    int                          num_channels;   /* 채널 배열 크기 */

    const struct iio_info        *info;          /* 콜백 테이블 (read_raw 등) */
    const unsigned long          *available_scan_masks; /* HW가 지원하는 채널 조합 */
    unsigned                     masklength;     /* scan mask 비트 길이 */

    struct iio_trigger           *trig;          /* 현재 연결된 trigger */
    const char                   *name;          /* 장치 이름 (sysfs name 파일) */
    const char                   *label;         /* 보드별 라벨 (DT label 프로퍼티) */

    void                         *priv;          /* iio_priv()로 접근하는 드라이버 state */
    const struct iio_mount_matrix *(*read_mount_matrix)(...);
};
코드 설명
  • modes — 드라이버가 probe 시 설정하는 비트 플래그입니다. INDIO_DIRECT_MODE는 sysfs를 통한 원샷 읽기, INDIO_BUFFER_TRIGGERED는 trigger 기반 연속 수집, INDIO_BUFFER_SOFTWARE는 드라이버가 직접 push하는 방식을 의미합니다. 여러 모드를 OR로 조합할 수 있습니다.
  • currentmode — IIO 코어가 버퍼 enable/disable 시 자동으로 갱신합니다. 드라이버가 직접 변경하면 안 됩니다. iio_device_claim_direct_mode()는 이 값을 검사해서 버퍼 활성 상태에서 원샷 읽기를 차단합니다.
  • dev — 임베디드 struct device로, sysfs 디렉터리, devres 관리, 전원 관리(PM), kobject 참조 카운팅의 기반입니다. devm_iio_device_alloc()이 이 구조체를 초기화합니다.
  • buffer / scan_bytesbuffer는 레거시 단일 버퍼 호환 포인터이고, 최신 커널은 다중 버퍼를 리스트로 관리합니다. scan_bytes는 활성 scan mask에 따라 코어가 계산하는 한 샘플 프레임의 총 바이트 수입니다.
  • channels / num_channels — 보통 정적 const 배열을 가리킵니다. 런타임에 채널 구성이 바뀌는 특수 장치는 probe 시 동적으로 배열을 할당합니다.
  • available_scan_masks — 하드웨어가 특정 채널 조합만 동시에 읽을 수 있는 경우(예: burst 모드가 X/Y/Z 동시만 지원), 유효한 조합을 0으로 끝나는 비트마스크 배열로 제공합니다. 코어는 사용자가 요청한 mask의 상위 집합(superset)인 첫 번째 유효 mask를 자동 선택합니다.
  • trig — 현재 연결된 trigger 포인터입니다. 사용자 공간이 current_trigger에 이름을 쓰면 코어가 이 포인터를 갱신합니다.
  • privdevm_iio_device_alloc()에 전달한 extra 크기만큼의 메모리가 iio_dev 뒤에 연속 할당되고, iio_priv() 매크로로 이 영역의 포인터를 얻습니다.

커널 소스 분석: struct iio_chan_spec 필드별 해설

struct iio_chan_spec는 IIO 채널 하나의 메타데이터 전체를 담는 구조체입니다. 드라이버는 보통 정적 배열로 이것을 정의하며, IIO 코어는 이 정보를 바탕으로 sysfs 파일명, scan layout, 이벤트 인터페이스를 자동 생성합니다.

/* include/linux/iio/iio.h — struct iio_chan_spec 주요 필드 (간략화) */
struct iio_chan_spec {
    enum iio_chan_type  type;       /* IIO_VOLTAGE, IIO_ACCEL, IIO_TEMP 등 */
    int                channel;    /* 채널 번호 (indexed=1일 때 사용) */
    int                channel2;   /* modifier: IIO_MOD_X, IIO_MOD_Y 또는 차동 pair */
    unsigned long      address;    /* 드라이버 전용: 레지스터 주소 등 */

    int                scan_index; /* 버퍼 내 순서 (-1이면 버퍼 불가) */
    struct {
        char           sign;       /* 's' 부호 있음, 'u' 부호 없음 */
        u8             realbits;   /* 유효 데이터 비트 수 */
        u8             storagebits;/* 저장 공간 비트 수 (8의 배수) */
        u8             shift;      /* storagebits 내 오른쪽 shift 양 */
        u8             repeat;     /* 반복 횟수 (oversampled 등) */
        enum iio_endian endianness; /* IIO_LE / IIO_BE / IIO_CPU */
    }                  scan_type;

    long  info_mask_separate;          /* 채널별 고유 속성 (BIT(IIO_CHAN_INFO_RAW) 등) */
    long  info_mask_shared_by_type;    /* 같은 타입 채널이 공유하는 속성 */
    long  info_mask_shared_by_dir;     /* 같은 방향 채널이 공유하는 속성 */
    long  info_mask_shared_by_all;     /* 전체 장치가 공유하는 속성 */

    const struct iio_event_spec *event_spec;
    unsigned int                  num_event_specs;

    unsigned  modified:1;   /* channel2를 modifier로 사용 */
    unsigned  indexed:1;    /* 파일명에 채널 번호 포함 */
    unsigned  output:1;     /* 출력 채널 (DAC 등) */
    unsigned  differential:1;/* 차동 채널 */
};
코드 설명
  • type — 채널이 측정하는 물리량의 종류입니다. 커널은 IIO_VOLTAGE, IIO_CURRENT, IIO_ACCEL, IIO_ANGL_VEL(각속도), IIO_MAGN(자기장), IIO_TEMP, IIO_LIGHT, IIO_PRESSURE 등 수십 가지 타입을 정의합니다. 이 값이 sysfs 파일명의 voltage, accel, temp 부분을 결정합니다.
  • channel / channel2indexed=1이면 channel이 파일명의 숫자(in_voltage0_raw의 0)가 됩니다. modified=1이면 channel2가 축(IIO_MOD_X_x)이나 컬러(IIO_MOD_RED_red) modifier가 됩니다. 차동 채널에서는 channelchannel2가 양극/음극 쌍을 나타냅니다.
  • address — IIO 코어가 사용하지 않고 드라이버가 자유롭게 쓰는 필드입니다. 대부분의 드라이버는 해당 채널의 하드웨어 레지스터 주소를 저장하여 read_raw()에서 chan->address로 참조합니다.
  • scan_index — 버퍼 내에서 이 채널 데이터가 몇 번째로 배치되는지를 나타냅니다. -1로 설정하면 이 채널은 버퍼에 들어가지 않습니다(sysfs 전용). 타임스탬프 채널은 보통 가장 큰 인덱스를 갖습니다.
  • scan_type — 버퍼에 저장될 때의 이진(Binary) 형식을 완전히 기술합니다. 예를 들어 12비트 ADC가 16비트 공간에 LSB 정렬로 들어가면 realbits=12, storagebits=16, shift=0입니다. 사용자 공간은 _type sysfs 파일에서 이 정보를 le:s16/16>>0 형태로 읽습니다.
  • info_mask_* — 네 가지 공유 수준으로 속성을 분류합니다. separate는 채널마다 독립 파일, shared_by_type은 같은 타입의 모든 채널이 하나의 파일 공유, shared_by_dir은 같은 입출력 방향 공유, shared_by_all은 장치 전체 공유입니다. 예: 가속도 X/Y/Z가 같은 ODR을 쓰면 IIO_CHAN_INFO_SAMP_FREQshared_by_type에 넣습니다.
  • event_spec — threshold, rate-of-change 같은 이벤트를 정의하는 iio_event_spec 배열입니다. 각 항목에 이벤트 타입, 방향(상승/하강), 그리고 어떤 설정값(threshold, period, hysteresis)을 읽기/쓰기할 수 있는지가 명시됩니다.

커널 소스 분석: struct iio_info 주요 콜백

struct iio_info는 IIO 코어가 sysfs 읽기/쓰기, 이벤트 설정, 트리거 검증, 디버그 접근 시 호출하는 콜백 함수 포인터 테이블입니다. 드라이버는 이 구조체를 정적 상수로 정의하고 indio_dev->info에 연결합니다.

/* include/linux/iio/iio.h — struct iio_info 주요 콜백 (간략화) */
struct iio_info {
    /* ── 채널 값 읽기/쓰기 ── */
    int (*read_raw)(struct iio_dev *indio_dev,
                    struct iio_chan_spec const *chan,
                    int *val, int *val2, long mask);

    int (*write_raw)(struct iio_dev *indio_dev,
                     struct iio_chan_spec const *chan,
                     int val, int val2, long mask);

    int (*read_avail)(struct iio_dev *indio_dev,
                      struct iio_chan_spec const *chan,
                      const int **vals, int *type,
                      int *length, long mask);

    int (*write_raw_get_fmt)(struct iio_dev *indio_dev,
                            struct iio_chan_spec const *chan,
                            long mask);

    /* ── 이벤트 ── */
    int (*read_event_config)(struct iio_dev *indio_dev,
                            const struct iio_chan_spec *chan,
                            enum iio_event_type type,
                            enum iio_event_direction dir);

    int (*write_event_config)(struct iio_dev *indio_dev,
                             const struct iio_chan_spec *chan,
                             enum iio_event_type type,
                             enum iio_event_direction dir,
                             int state);

    int (*read_event_value)(struct iio_dev *indio_dev,
                           const struct iio_chan_spec *chan,
                           enum iio_event_type type,
                           enum iio_event_direction dir,
                           enum iio_event_info info,
                           int *val, int *val2);

    int (*write_event_value)(struct iio_dev *indio_dev,
                            const struct iio_chan_spec *chan,
                            enum iio_event_type type,
                            enum iio_event_direction dir,
                            enum iio_event_info info,
                            int val, int val2);

    /* ── 트리거/스캔/디버그 ── */
    int (*validate_trigger)(struct iio_dev *indio_dev,
                           struct iio_trigger *trig);

    int (*update_scan_mode)(struct iio_dev *indio_dev,
                           const unsigned long *scan_mask);

    int (*debugfs_reg_access)(struct iio_dev *indio_dev,
                             unsigned reg, unsigned writeval,
                             unsigned *readval);

    int (*read_label)(struct iio_dev *indio_dev,
                     struct iio_chan_spec const *chan,
                     char *label);
};
코드 설명
  • read_raw() — IIO에서 가장 핵심적인 콜백입니다. mask 파라미터가 IIO_CHAN_INFO_RAW이면 원시값, IIO_CHAN_INFO_SCALE이면 스케일, IIO_CHAN_INFO_SAMP_FREQ이면 샘플링 주파수를 반환합니다. 반환값은 IIO_VAL_INT, IIO_VAL_INT_PLUS_MICRO 등으로 val/val2의 해석 방법을 알려줍니다.
  • write_raw() — 사용자 공간이 sysfs에 값을 쓸 때 호출됩니다. ODR 변경, 풀스케일 레인지 설정, 오버샘플링 비율 조정 같은 장치 설정에 사용합니다. write_raw_get_fmt()가 함께 구현되어야 커널이 사용자 공간에서 온 문자열을 올바른 정밀도로 파싱합니다.
  • read_avail()*_available sysfs 파일을 지원합니다. IIO_AVAIL_LIST는 허용 값 목록, IIO_AVAIL_RANGE는 최솟값/스텝/최댓값을 의미합니다. 사용자 공간 도구는 이것을 먼저 읽어 유효한 설정값을 파악합니다.
  • read_event_config() / write_event_config() — 특정 채널의 특정 이벤트(예: 가속도 X축 threshold 상승)가 현재 켜져 있는지 확인하거나 켜고 끕니다. 하드웨어 인터럽트 마스크 레지스터와 직접 연동됩니다.
  • read_event_value() / write_event_value() — 이벤트의 threshold 값, period, hysteresis 같은 파라미터를 읽거나 설정합니다. info 파라미터가 IIO_EV_INFO_VALUE(임계값)인지 IIO_EV_INFO_HYSTERESIS(히스테리시스)인지에 따라 동작이 분기됩니다.
  • validate_trigger() — 사용자 공간이 current_trigger를 변경할 때 호출됩니다. iio_validate_own_trigger()를 넣으면 장치 자체 trigger만 허용하고, NULL이면 모든 trigger를 허용합니다.
  • update_scan_mode() — 버퍼 활성화 시 최종 scan mask가 결정된 후 호출됩니다. 드라이버는 여기서 하드웨어의 burst 읽기 모드, FIFO 패킹, DMA 전송 크기를 재설정합니다.
  • debugfs_reg_access()readvalNULL이면 쓰기, 아니면 읽기입니다. bring-up 단계에서 레지스터를 직접 확인할 때 유용합니다.

커널 소스 분석: iio_read_channel_raw() 데이터 경로

사용자 공간이 sysfs에서 in_voltage0_raw를 읽거나, 커널 내 다른 드라이버가 iio_read_channel_raw()를 호출할 때, 데이터는 하드웨어에서 최종 소비자까지 다음 경로를 따라 흐릅니다.

/* 경로 1: 사용자 공간 sysfs 읽기 (industrialio-core.c 간략화) */

/* cat /sys/bus/iio/.../in_voltage0_raw 시 호출되는 show 함수 */
static ssize_t iio_read_channel_info(struct device *dev, ...)
{
    struct iio_dev *indio_dev = dev_to_iio_dev(dev);
    struct iio_chan_spec *chan = ...;
    int val, val2;

    /* 드라이버의 read_raw() 콜백 호출 */
    ret = indio_dev->info->read_raw(indio_dev, chan, &val, &val2, mask);

    /* 반환 형식에 따라 문자열 변환 */
    return iio_format_value(buf, ret, 2, vals);
}

/* 경로 2: in-kernel consumer (industrialio-core.c 간략화) */
int iio_read_channel_raw(struct iio_channel *chan, int *val)
{
    struct iio_dev *indio_dev = chan->indio_dev;
    int val2;

    /* 동일한 read_raw() 콜백을 IIO_CHAN_INFO_RAW로 호출 */
    ret = indio_dev->info->read_raw(indio_dev, chan->channel,
                                     val, &val2, IIO_CHAN_INFO_RAW);
    return ret;
}

/* iio_read_channel_processed()는 추가로 scale/offset 변환을 적용 */
int iio_read_channel_processed(struct iio_channel *chan, int *val)
{
    /* 먼저 read_raw(PROCESSED)를 시도 */
    /* 실패하면 read_raw(RAW) + scale 변환을 코어가 수행 */
    ret = iio_channel_read(chan, val, NULL, IIO_CHAN_INFO_PROCESSED);
    if (ret == -EINVAL) {
        ret = iio_channel_read(chan, val, NULL, IIO_CHAN_INFO_RAW);
        if (!ret)
            ret = iio_convert_raw_to_processed(chan, *val, val, 1);
    }
    return ret;
}
코드 설명
  • sysfs 경로 — 사용자 공간이 cat으로 sysfs 파일을 읽으면, 커널의 sysfs show 핸들러가 iio_read_channel_info()를 호출합니다. 이 함수는 해당 채널의 iio_chan_spec와 속성 종류(mask)를 결정한 뒤, 드라이버의 read_raw() 콜백을 호출합니다. 반환된 val/val2와 형식 코드를 iio_format_value()가 사람이 읽을 수 있는 문자열로 변환합니다.
  • in-kernel consumer 경로 — 다른 커널 드라이버(예: power_supply, hwmon bridge)가 iio_read_channel_raw()를 호출하면, 동일한 read_raw() 콜백이 IIO_CHAN_INFO_RAW mask로 호출됩니다. consumer는 DT의 io-channels 바인딩으로 provider 채널을 참조하며, devm_iio_channel_get()으로 핸들을 얻습니다.
  • iio_read_channel_processed() — 먼저 드라이버가 직접 환산된 값을 제공하는지(IIO_CHAN_INFO_PROCESSED) 확인합니다. 드라이버가 이를 지원하지 않으면 raw 값을 읽은 뒤 코어가 scaleoffset을 조합하여 (raw + offset) * scale을 계산합니다. 이 fallback 경로 덕분에 consumer 드라이버는 provider가 어떤 방식을 사용하든 동일한 API로 물리량을 얻을 수 있습니다.
  • iio_convert_raw_to_processed()read_raw()에서 얻은 scale의 형식 코드(IIO_VAL_INT_PLUS_MICRO 등)에 따라 정수 연산으로 변환을 수행합니다. 마지막 인자 scale은 추가 배율(보통 1)입니다.
데이터 읽기 경로: HW → read_raw() → 소비자 HW 레지스터 ADC/센서 raw data read_raw() 콜백 val, val2, format code sysfs show iio_format_value() iio_read_channel_raw() in-kernel consumer 사용자 공간 cat in_voltage0_raw 커널 드라이버 power_supply 등 iio_read_channel_processed() fallback 경로 PROCESSED 실패 → RAW 읽기 + iio_convert_raw_to_processed() 물리량 = (raw + offset) × scale — 코어가 자동 변환하거나 드라이버가 직접 환산

커널 소스 분석: iio_triggered_buffer_setup() 내부

devm_iio_triggered_buffer_setup()은 IIO 드라이버에서 가장 많이 쓰이는 helper 중 하나입니다. 이 한 줄의 호출이 내부적으로 kfifo 버퍼 할당, pollfunc 등록, trigger consumer 연결을 모두 처리합니다. 커널 소스(drivers/iio/buffer/industrialio-triggered-buffer.c)를 따라가면 다음 단계로 분해됩니다.

/* drivers/iio/buffer/industrialio-triggered-buffer.c (간략화) */

int iio_triggered_buffer_setup_ext(struct iio_dev *indio_dev,
    irqreturn_t (*h)(int, void *),      /* top half: 보통 iio_pollfunc_store_time */
    irqreturn_t (*thread)(int, void *), /* bottom half: 드라이버 handler */
    enum iio_buffer_direction direction,
    const struct iio_buffer_setup_ops *setup_ops,
    const struct iio_dev_attr *ext_info)
{
    struct iio_buffer *buffer;

    /* 1단계: kfifo 기반 소프트웨어 버퍼 할당 */
    buffer = iio_kfifo_allocate();
    if (!buffer)
        return -ENOMEM;

    /* 2단계: pollfunc 할당 — top half + threaded handler 쌍 */
    indio_dev->pollfunc = iio_alloc_pollfunc(h, thread,
                                              IRQF_ONESHOT,
                                              indio_dev, "%s_consumer%d",
                                              ...);
    if (!indio_dev->pollfunc)
        goto error_kfifo_free;

    /* 3단계: setup_ops 연결 (preenable, postenable, predisable 등) */
    buffer->setup_ops = setup_ops;
    buffer->direction = direction;

    /* 4단계: 버퍼를 iio_dev에 연결 */
    ret = iio_device_attach_buffer(indio_dev, buffer);
    if (ret)
        goto error_pollfunc_free;

    return 0;

error_pollfunc_free:
    iio_dealloc_pollfunc(indio_dev->pollfunc);
error_kfifo_free:
    iio_kfifo_free(buffer);
    return ret;
}
코드 설명
  • iio_kfifo_allocate() — 커널의 kfifo(커널 FIFO) 자료구조를 기반으로 하는 소프트웨어 버퍼를 할당합니다. kfifo는 lock-free 단일 생산자/소비자 큐를 제공하여, trigger handler(생산자)와 사용자 공간 read()(소비자)가 최소한의 동기화로 동작합니다.
  • iio_alloc_pollfunc() — trigger가 발생했을 때 호출될 함수 쌍을 등록합니다. h(top half)는 하드 IRQ 컨텍스트에서 실행되어 타임스탬프를 빠르게 잡고, thread(bottom half)는 threaded IRQ에서 실행되어 실제 센서 레지스터를 읽습니다. IRQF_ONESHOT은 threaded handler가 끝날 때까지 같은 IRQ를 재활성화하지 않도록 합니다.
  • setup_opspreenable, postenable, predisable, postdisable 콜백으로, 버퍼가 켜지거나 꺼질 때 드라이버가 하드웨어를 재설정할 기회를 줍니다. 대부분의 드라이버는 NULL을 전달하여 기본 동작을 사용합니다.
  • iio_device_attach_buffer() — 할당된 버퍼를 iio_dev의 버퍼 리스트에 연결합니다. 최신 커널은 다중 버퍼를 지원하므로, 같은 장치에 kfifo와 HW FIFO를 동시에 붙일 수도 있습니다.
  • 전체 동작 흐름 — trigger 발생 → iio_pollfunc_store_time()에서 ktime_get_ns()로 timestamp 확보 → threaded handler에서 센서 데이터 읽기 → iio_push_to_buffers_with_timestamp()로 kfifo에 push → 사용자 공간 read()가 kfifo에서 pop.
💡

devm_ 접두사의 의미: devm_iio_triggered_buffer_setup()을 사용하면 위 모든 자원이 devres에 등록되어, 드라이버의 remove()나 probe 실패 시 자동으로 역순 해제됩니다. 수동으로 iio_triggered_buffer_cleanup()을 호출할 필요가 없으므로, 거의 모든 최신 드라이버가 devm_ 버전을 사용합니다.

커널 소스 분석: trigger handler 해부

triggered buffer의 threaded handler는 IIO 드라이버에서 실제 데이터가 하드웨어에서 버퍼로 전달되는 핵심 지점입니다. 실전 드라이버의 전형적인 패턴을 분석합니다.

/* 실전 패턴: 3축 가속도계 + 타임스탬프 trigger handler */

struct acme_state {
    struct regmap *regmap;
    /* 자연 정렬 보장을 위한 버퍼: s16[3] + padding + s64 timestamp */
    struct {
        __le16 channels[3];  /* accel X, Y, Z — 각 16비트 */
        s64 timestamp;        /* 8바이트 정렬 필요 */
    } __aligned(8) scan;       /* 컴파일러가 자연 정렬 보장 */
};

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

    /* 1. 활성 채널 데이터를 burst 읽기 */
    ret = regmap_bulk_read(st->regmap, ACME_REG_ACCEL_X_L,
                           st->scan.channels,
                           sizeof(st->scan.channels));
    if (ret)
        goto done;

    /* 2. 타임스탬프와 함께 kfifo에 push */
    iio_push_to_buffers_with_timestamp(indio_dev,
                                        &st->scan,
                                        pf->timestamp);

done:
    /* 3. trigger use-count 감소 → 다음 trigger 재활성화 */
    iio_trigger_notify_done(indio_dev->trig);
    return IRQ_HANDLED;
}
코드 설명
  • scan 버퍼 정렬iio_push_to_buffers_with_timestamp()는 타임스탬프(s64)가 8바이트 경계에 정렬되어야 합니다. __aligned(8)과 구조체 패딩이 이를 보장합니다. 패딩을 빼먹으면 ARM 같은 플랫폼에서 unaligned access fault가 발생하거나, 데이터가 잘못된 위치에 들어갑니다.
  • regmap_bulk_read() — I2C/SPI 버스를 통해 연속 레지스터를 한 번의 트랜잭션으로 읽습니다. 채널별로 regmap_read()를 따로 호출하면 축 간 시간차가 생겨 고속 센서에서 데이터 왜곡이 발생할 수 있습니다.
  • iio_push_to_buffers_with_timestamp() — scan 데이터와 타임스탬프를 kfifo에 원자적으로 삽입합니다. 내부적으로 iio_push_to_buffers()를 호출하며, 타임스탬프 채널이 활성화되어 있으면 scan 프레임 끝에 s64 타임스탬프를 붙입니다.
  • iio_trigger_notify_done() — 반드시 마지막에 호출해야 합니다. trigger의 사용 카운트를 감소시키고, 모든 consumer가 완료되면 IRQ를 재활성화합니다. 이 호출을 빠뜨리면 trigger가 영구적으로 멈추고 버퍼에 데이터가 더 이상 들어오지 않습니다.
  • 오류 처리 — I2C/SPI 통신 실패 시에도 iio_trigger_notify_done()은 반드시 호출해야 합니다. 따라서 goto done 패턴이 거의 모든 trigger handler에서 사용됩니다.
  • scan mask 반영 — 위 예제는 항상 3축 전체를 읽는 단순한 경우입니다. 채널별 개별 활성화가 필요한 장치에서는 iio_for_each_active_channel()이나 for_each_set_bit()으로 활성 채널만 선택적으로 읽어야 합니다.

채널 타입 레퍼런스: IIO가 지원하는 모든 측정 종류

IIO 코어는 수십 가지 채널 타입을 정의하며, 각 타입은 sysfs 파일명의 접두사와 표준 단위를 결정합니다. 실무에서 자주 만나는 타입을 분류별로 정리합니다.

분류채널 타입sysfs 접두사표준 단위대표 장치
전기IIO_VOLTAGEin_voltagemVADC, 전압 모니터
IIO_CURRENTin_currentmA전류 센서, INA219
IIO_RESISTANCEin_resistanceohm저항 측정, RTD
관성IIO_ACCELin_accelm/s^2가속도계, IMU
IIO_ANGL_VELin_anglvelrad/s자이로스코프
IIO_MAGNin_magnGauss지자기 센서
IIO_INCLIin_inclidegree경사계
환경IIO_TEMPin_tempmillidegree C온도 센서
IIO_PRESSUREin_pressurekPa기압계, BMP280
IIO_HUMIDITYRELATIVEin_humidityrelativemilli %습도 센서, SHT3x
광학IIO_LIGHTin_illuminancelux조도 센서
IIO_PROXIMITYin_proximity(장치별)근접 센서
IIO_INTENSITYin_intensity(장치별)RGB, IR 센서
위치IIO_ROTin_rotdegree회전각 엔코더
IIO_STEPSin_stepssteps만보계 센서
출력IIO_VOLTAGE (output)out_voltagemVDAC
단위 주의: IIO의 표준 단위는 SI 기반이지만, 실제 스케일 값은 드라이버마다 다릅니다. 예를 들어 가속도의 scale이 0.000598이면 단위는 m/s^2이고, 자이로의 scale이 0.001065이면 단위는 rad/s입니다. 반드시 채널 타입과 함께 scale을 해석해야 합니다.

완전한 ADC 드라이버 예제

SPI 기반 12비트 ADC를 IIO 드라이버로 구현하는 완전한 예제입니다. 4채널 single-ended 입력, triggered buffer, sysfs 원샷 읽기를 모두 지원합니다.

#include <linux/spi/spi.h>
#include <linux/module.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/trigger_consumer.h>
#include <linux/iio/triggered_buffer.h>
#include <linux/regulator/consumer.h>

#define EXAMPLE_ADC_CHANNELS  4
#define EXAMPLE_ADC_BITS      12

struct example_adc {
    struct spi_device *spi;
    struct regulator *vref;
    struct mutex lock;
    /* 자연 정렬 scan 버퍼: u16[4] + s64 timestamp */
    struct {
        __be16 channels[EXAMPLE_ADC_CHANNELS];
        s64 timestamp;
    } __aligned(8) scan;
};

/* SPI를 통해 특정 채널의 ADC 값을 읽는 함수 */
static int example_adc_read_channel(
    struct example_adc *adc, int ch, int *val)
{
    u8 tx[3] = { 0x06 | (ch >> 2), (ch & 3) << 6, 0 };
    u8 rx[3] = {};
    struct spi_transfer xfer = {
        .tx_buf = tx, .rx_buf = rx, .len = 3,
    };
    int ret;

    ret = spi_sync_transfer(adc->spi, &xfer, 1);
    if (ret)
        return ret;

    *val = ((rx[1] & 0x0f) << 8) | rx[2];
    return 0;
}

static int example_adc_read_raw(
    struct iio_dev *indio_dev,
    const struct iio_chan_spec *chan,
    int *val, int *val2, long mask)
{
    struct example_adc *adc = iio_priv(indio_dev);
    int ret, uv;

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

        mutex_lock(&adc->lock);
        ret = example_adc_read_channel(adc,
                                        chan->channel, val);
        mutex_unlock(&adc->lock);
        iio_device_release_direct_mode(indio_dev);

        return ret ? ret : IIO_VAL_INT;

    case IIO_CHAN_INFO_SCALE:
        /* scale = Vref / 2^12 (마이크로볼트 단위) */
        uv = regulator_get_voltage(adc->vref);
        if (uv < 0)
            return uv;
        *val = uv / 1000;
        *val2 = EXAMPLE_ADC_BITS;
        return IIO_VAL_FRACTIONAL_LOG2;
    }

    return -EINVAL;
}

static const struct iio_info example_adc_info = {
    .read_raw = example_adc_read_raw,
};

#define EXAMPLE_ADC_CHANNEL(idx)                    \
{                                                   \
    .type = IIO_VOLTAGE,                           \
    .indexed = 1,                                  \
    .channel = (idx),                               \
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),  \
    .info_mask_shared_by_type =                     \
        BIT(IIO_CHAN_INFO_SCALE),                   \
    .scan_index = (idx),                            \
    .scan_type = {                                  \
        .sign = 'u',                              \
        .realbits = 12,                             \
        .storagebits = 16,                          \
        .endianness = IIO_BE,                      \
    },                                              \
}

static const struct iio_chan_spec example_adc_channels[] = {
    EXAMPLE_ADC_CHANNEL(0),
    EXAMPLE_ADC_CHANNEL(1),
    EXAMPLE_ADC_CHANNEL(2),
    EXAMPLE_ADC_CHANNEL(3),
    IIO_CHAN_SOFT_TIMESTAMP(EXAMPLE_ADC_CHANNELS),
};

static irqreturn_t example_adc_trigger_handler(
    int irq, void *p)
{
    struct iio_poll_func *pf = p;
    struct iio_dev *indio_dev = pf->indio_dev;
    struct example_adc *adc = iio_priv(indio_dev);
    int val, bit, j = 0;

    mutex_lock(&adc->lock);

    /* 활성 채널만 읽어서 scan 버퍼에 채움 */
    iio_for_each_active_channel(indio_dev, bit) {
        if (example_adc_read_channel(adc, bit, &val))
            goto done;
        adc->scan.channels[j++] = cpu_to_be16(val);
    }

    iio_push_to_buffers_with_timestamp(indio_dev,
                                        &adc->scan,
                                        pf->timestamp);
done:
    mutex_unlock(&adc->lock);
    iio_trigger_notify_done(indio_dev->trig);
    return IRQ_HANDLED;
}

static int example_adc_probe(struct spi_device *spi)
{
    struct iio_dev *indio_dev;
    struct example_adc *adc;
    int ret;

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

    adc = iio_priv(indio_dev);
    adc->spi = spi;
    mutex_init(&adc->lock);

    adc->vref = devm_regulator_get(&spi->dev, "vref");
    if (IS_ERR(adc->vref))
        return PTR_ERR(adc->vref);

    ret = regulator_enable(adc->vref);
    if (ret)
        return ret;

    indio_dev->name = "example-adc";
    indio_dev->info = &example_adc_info;
    indio_dev->modes = INDIO_DIRECT_MODE;
    indio_dev->channels = example_adc_channels;
    indio_dev->num_channels =
        ARRAY_SIZE(example_adc_channels);

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

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

static const struct of_device_id example_adc_of_match[] = {
    { .compatible = "example,adc12" },
    { },
};
MODULE_DEVICE_TABLE(of, example_adc_of_match);

static struct spi_driver example_adc_driver = {
    .driver = {
        .name = "example-adc",
        .of_match_table = example_adc_of_match,
    },
    .probe = example_adc_probe,
};
module_spi_driver(example_adc_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example 12-bit SPI ADC IIO Driver");
ADC IIO 드라이버의 두 가지 읽기 경로 SPI ADC HW 12-bit, 4채널 경로 1: Direct mode (sysfs) 경로 2: Triggered buffer Direct mode (원샷 읽기) 1. iio_device_claim_direct_mode() 2. SPI 트랜잭션으로 채널 읽기 3. iio_device_release_direct_mode() 4. IIO_VAL_INT 또는 IIO_VAL_FRACTIONAL_LOG2 반환 Triggered buffer (연속 읽기) 1. Trigger 발생 (DRDY/hrtimer) 2. pollfunc: timestamp 확보 3. handler: 활성 채널 burst 읽기 4. iio_push_to_buffers_with_timestamp() 결과: sysfs cat in_voltage0_raw 결과: /dev/iio:deviceX read() 두 경로 모두 같은 SPI read 함수를 공유하지만, 동기화와 결과 전달 방식이 다릅니다.

가속도계/자이로 드라이버 패턴: 6축 IMU

6축 IMU(관성 측정 장치, Inertial Measurement Unit)는 가속도 3축과 각속도 3축을 동시에 제공하는 장치로, IIO 드라이버에서 가장 복잡한 패턴 중 하나입니다. 채널 modifier, 공유 속성, triggered buffer, 이벤트를 모두 활용합니다.

IMU 채널 정의 패턴

/* 6축 IMU 채널 정의: accel X/Y/Z + gyro X/Y/Z + timestamp */

#define IMU_ACCEL_CHAN(axis, idx)                     \
{                                                       \
    .type = IIO_ACCEL,                                \
    .modified = 1,                                    \
    .channel2 = IIO_MOD_##axis,                       \
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |    \
                          BIT(IIO_CHAN_INFO_CALIBBIAS),\
    .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_SCALE) |                      \
        BIT(IIO_CHAN_INFO_SAMP_FREQ),                  \
    .scan_index = (idx),                                \
    .scan_type = {                                      \
        .sign = 's',                                  \
        .realbits = 16,                                 \
        .storagebits = 16,                              \
        .endianness = IIO_LE,                          \
    },                                                  \
    .event_spec = imu_accel_events,                     \
    .num_event_specs = ARRAY_SIZE(imu_accel_events),    \
}

#define IMU_GYRO_CHAN(axis, idx)                      \
{                                                       \
    .type = IIO_ANGL_VEL,                             \
    .modified = 1,                                    \
    .channel2 = IIO_MOD_##axis,                       \
    .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_SCALE) |                      \
        BIT(IIO_CHAN_INFO_SAMP_FREQ),                  \
    .scan_index = (idx),                                \
    .scan_type = {                                      \
        .sign = 's',                                  \
        .realbits = 16,                                 \
        .storagebits = 16,                              \
        .endianness = IIO_LE,                          \
    },                                                  \
}

static const struct iio_chan_spec imu_channels[] = {
    IMU_ACCEL_CHAN(X, 0),
    IMU_ACCEL_CHAN(Y, 1),
    IMU_ACCEL_CHAN(Z, 2),
    IMU_GYRO_CHAN(X, 3),
    IMU_GYRO_CHAN(Y, 4),
    IMU_GYRO_CHAN(Z, 5),
    IIO_CHAN_SOFT_TIMESTAMP(6),
};

read_avail() 구현: 허용 값 노출

/* ODR과 full-scale range의 허용 값 목록 */

static const int imu_accel_scale_avail[] = {
    0, 598,     /* +-2g: 0.000598 m/s^2 per LSB */
    0, 1196,    /* +-4g */
    0, 2392,    /* +-8g */
    0, 4785,    /* +-16g */
};

static const int imu_odr_avail[] = {
    12, 26, 52, 104, 208, 416, 833,
};

static int imu_read_avail(struct iio_dev *indio_dev,
    const struct iio_chan_spec *chan,
    const int **vals, int *type,
    int *length, long mask)
{
    switch (mask) {
    case IIO_CHAN_INFO_SCALE:
        if (chan->type == IIO_ACCEL) {
            *vals = imu_accel_scale_avail;
            *type = IIO_VAL_INT_PLUS_MICRO;
            *length = ARRAY_SIZE(imu_accel_scale_avail);
            return IIO_AVAIL_LIST;
        }
        break;

    case IIO_CHAN_INFO_SAMP_FREQ:
        *vals = imu_odr_avail;
        *type = IIO_VAL_INT;
        *length = ARRAY_SIZE(imu_odr_avail);
        return IIO_AVAIL_LIST;
    }

    return -EINVAL;
}

mount_matrix: 보드 장착 방향 보정

/* DT에서 mount-matrix를 읽어 ext_info로 노출 */

static const struct iio_mount_matrix *
imu_get_mount_matrix(const struct iio_dev *indio_dev,
                     const struct iio_chan_spec *chan)
{
    struct imu_state *st = iio_priv(indio_dev);
    return &st->orientation;
}

static const struct iio_chan_spec_ext_info imu_ext_info[] = {
    IIO_MOUNT_MATRIX(IIO_SHARED_BY_DIR, imu_get_mount_matrix),
    { },
};

/* probe에서 DT mount-matrix 파싱 */
iio_read_mount_matrix(&spi->dev, &st->orientation);

/* 사용자 공간에서 확인:
 * cat /sys/bus/iio/devices/iio:device0/in_accel_mount_matrix
 * 1, 0, 0; 0, 1, 0; 0, 0, 1
 */
IMU 드라이버 핵심 주의사항: 가속도(m/s^2)와 각속도(rad/s)는 서로 다른 물리량이므로, scale이 shared_by_type이어야 합니다. 실수로 shared_by_all로 만들면 가속도 scale과 자이로 scale이 같은 파일을 공유하게 되어 ABI가 깨집니다.

IIO 소비자: 다른 서브시스템과의 연동

IIO의 in-kernel consumer API는 다양한 서브시스템에서 활용됩니다. 대표적인 연동 패턴을 정리합니다.

주요 소비자 패턴

소비자 서브시스템IIO 채널 용도API 사용 패턴
power_supply배터리 전압, 온도, 전류iio_read_channel_processed()
hwmonADC 기반 온도/전압 모니터iio_read_channel_raw() + scale 변환
thermalNTC 서미스터(Thermistor) 온도iio_read_channel_processed()
input아날로그 조이스틱, 터치iio_channel_get_all_cb()
LED주변광(Ambient Light) 센서iio_read_channel_raw()
사용자 드라이버커스텀 아날로그 입력devm_iio_channel_get()

power_supply 연동 예제

#include <linux/iio/consumer.h>
#include <linux/power_supply.h>

struct battery_gauge {
    struct iio_channel *voltage_chan;
    struct iio_channel *current_chan;
    struct iio_channel *temp_chan;
    struct power_supply *psy;
};

static int battery_get_voltage(struct battery_gauge *bg,
                               int *uv)
{
    int ret;

    /* IIO ADC에서 배터리 전압 읽기 (마이크로볼트) */
    ret = iio_read_channel_processed(bg->voltage_chan, uv);
    if (ret)
        return ret;

    /* voltage divider 보정 (1:2 분압) */
    *uv *= 2;

    return 0;
}

static int battery_get_temp(struct battery_gauge *bg,
                             int *temp)
{
    int raw, ret;

    /* NTC 서미스터 ADC 값 읽기 */
    ret = iio_read_channel_raw(bg->temp_chan, &raw);
    if (ret)
        return ret;

    /* ADC 코드를 온도로 변환 (보드별 NTC 특성 테이블) */
    *temp = ntc_adc_to_temp(raw);

    return 0;
}

static int battery_gauge_probe(struct platform_device *pdev)
{
    struct battery_gauge *bg;

    bg = devm_kzalloc(&pdev->dev, sizeof(*bg), GFP_KERNEL);
    if (!bg)
        return -ENOMEM;

    /* DT io-channel-names로 채널 획득 */
    bg->voltage_chan = devm_iio_channel_get(&pdev->dev,
                                            "battery_voltage");
    if (IS_ERR(bg->voltage_chan))
        return PTR_ERR(bg->voltage_chan);

    bg->temp_chan = devm_iio_channel_get(&pdev->dev,
                                         "battery_temp");
    if (IS_ERR(bg->temp_chan))
        return PTR_ERR(bg->temp_chan);

    /* power_supply 등록 ... */
    return 0;
}
/* Device Tree에서의 IIO consumer 연결 */
adc@48000000 {
    compatible = "ti,am3359-adc";
    #io-channel-cells = <1>;
};

battery-gauge {
    compatible = "vendor,battery-gauge";
    io-channels = <&adc 5>, <&adc 6>;
    io-channel-names = "battery_voltage", "battery_temp";
};
IIO consumer 연동: ADC가 여러 서브시스템에 값을 공급 IIO ADC Provider #io-channel-cells = <1>, 8채널 power_supply 배터리 전압, 온도 hwmon bridge 보드 전압 모니터 thermal zone NTC 서미스터 온도 사용자 공간 sysfs / chardev 하나의 ADC provider가 여러 consumer에 채널을 공급합니다. consumer마다 다른 채널을 쓰므로, DT의 io-channels / io-channel-names 바인딩이 매핑의 핵심입니다.

트리거 종류별 상세: hrtimer, sysfs, interrupt, data-ready

IIO trigger는 "샘플 캡처 시점을 결정하는 커널 객체"입니다. 실무에서 자주 쓰이는 trigger 종류를 비교합니다.

트리거 종류소스등록 방법적합한 경우주의점
hrtimer고해상도 타이머configfs에서 생성HW DRDY가 없는 장치jitter가 있을 수 있음
sysfssysfs 쓰기 이벤트configfs에서 생성수동 트리거, 테스트높은 속도에 부적합
interrupt하드웨어 DRDY IRQ드라이버가 등록센서 내장 DRDYIRQ 배선 필수
외부 sync외부 클럭/sync 신호드라이버 또는 별도 trigger 드라이버다채널 동기 캡처위상 정렬 필요
다른 IIO 장치다른 IIO triggercurrent_trigger에서 선택장치 간 동기validate_trigger로 호환성 확인

hrtimer 트리거 생성 (configfs)

# hrtimer trigger 생성
mkdir /config/iio/triggers/hrtimer/my_trigger

# trigger가 등록되었는지 확인
ls /sys/bus/iio/devices/trigger*
cat /sys/bus/iio/devices/trigger0/name

# 장치에 trigger 연결
echo "my_trigger" > /sys/bus/iio/devices/iio:device0/trigger/current_trigger

# sampling frequency 설정 (trigger가 아닌 장치 속성)
echo 100 > /sys/bus/iio/devices/trigger0/sampling_frequency

# 채널 활성화 + 버퍼 시작
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/scan_elements/in_voltage0_en
echo 64 > /sys/bus/iio/devices/iio:device0/buffer0/length
echo 1 > /sys/bus/iio/devices/iio:device0/buffer0/enable

자체 trigger 드라이버 등록

/* 장치가 자체 DRDY IRQ를 trigger로 등록하는 패턴 */

static int sensor_setup_trigger(struct iio_dev *indio_dev)
{
    struct sensor_state *st = iio_priv(indio_dev);
    struct iio_trigger *trig;
    int ret;

    trig = devm_iio_trigger_alloc(&st->spi->dev,
                                   "%s-drdy%d",
                                   indio_dev->name,
                                   iio_device_id(indio_dev));
    if (!trig)
        return -ENOMEM;

    /* DRDY IRQ를 trigger에 연결 */
    ret = devm_request_irq(&st->spi->dev,
                           st->irq,
                           iio_trigger_generic_data_rdy_poll,
                           IRQF_ONESHOT,
                           "sensor-drdy",
                           trig);
    if (ret)
        return ret;

    ret = devm_iio_trigger_register(&st->spi->dev, trig);
    if (ret)
        return ret;

    /* 자체 trigger만 허용하도록 설정 */
    iio_trigger_set_immutable(indio_dev, trig);
    indio_dev->trig = iio_trigger_get(trig);

    return 0;
}

이벤트 모니터링 실전: threshold 설정부터 userspace 수신까지

IIO 이벤트의 전체 흐름을 끝에서 끝까지 따라가 봅니다.

이벤트 흐름 전체

/* 드라이버: 이벤트 콜백 구현 */

static int sensor_write_event_config(
    struct iio_dev *indio_dev,
    const struct iio_chan_spec *chan,
    enum iio_event_type type,
    enum iio_event_direction dir,
    int state)
{
    struct sensor_state *st = iio_priv(indio_dev);
    u8 reg_val;

    /* 하드웨어 인터럽트 마스크 설정/해제 */
    regmap_read(st->regmap, SENSOR_INT_MASK, &reg_val);

    if (state)
        reg_val |= BIT(chan->scan_index);
    else
        reg_val &= ~BIT(chan->scan_index);

    return regmap_write(st->regmap, SENSOR_INT_MASK, reg_val);
}

static int sensor_write_event_value(
    struct iio_dev *indio_dev,
    const struct iio_chan_spec *chan,
    enum iio_event_type type,
    enum iio_event_direction dir,
    enum iio_event_info info,
    int val, int val2)
{
    struct sensor_state *st = iio_priv(indio_dev);

    switch (info) {
    case IIO_EV_INFO_VALUE:
        /* threshold 값을 하드웨어 레지스터에 쓰기 */
        return regmap_write(st->regmap,
                            dir == IIO_EV_DIR_RISING ?
                            SENSOR_THRESH_HIGH : SENSOR_THRESH_LOW,
                            val);
    case IIO_EV_INFO_HYSTERESIS:
        return regmap_write(st->regmap,
                            SENSOR_HYST, val);
    }
    return -EINVAL;
}

/* 이벤트 발생 시 IRQ handler에서 push */
static irqreturn_t sensor_event_handler(int irq, void *p)
{
    struct iio_dev *indio_dev = p;
    struct sensor_state *st = iio_priv(indio_dev);
    u8 status;

    regmap_read(st->regmap, SENSOR_INT_STATUS, &status);

    if (status & SENSOR_THRESH_HIGH_BIT)
        iio_push_event(indio_dev,
            IIO_UNMOD_EVENT_CODE(IIO_VOLTAGE, 0,
                                IIO_EV_TYPE_THRESH,
                                IIO_EV_DIR_RISING),
            iio_get_time_ns(indio_dev));

    /* status 클리어 */
    regmap_write(st->regmap, SENSOR_INT_STATUS, status);

    return IRQ_HANDLED;
}

사용자 공간에서 이벤트 수신

# 1. threshold 설정
echo 3000 > /sys/bus/iio/devices/iio:device0/events/in_voltage0_thresh_rising_value
echo 100 > /sys/bus/iio/devices/iio:device0/events/in_voltage0_thresh_rising_hysteresis

# 2. 이벤트 활성화
echo 1 > /sys/bus/iio/devices/iio:device0/events/in_voltage0_thresh_rising_en

# 3. 이벤트 모니터링
iio_event_monitor /dev/iio:device0

# 출력 예시:
# Event: time: 1234567890, type: voltage, channel: 0,
#        evtype: thresh, direction: rising

IIO API 레퍼런스 요약

Core API

API용도헤더
devm_iio_device_alloc()iio_dev + private state 할당iio/iio.h
devm_iio_device_register()IIO 장치 등록 (sysfs + chardev)iio/iio.h
iio_priv()드라이버 private state 포인터 획득iio/iio.h
iio_device_claim_direct_mode()direct mode 경합 방지iio/iio.h
iio_push_to_buffers_with_timestamp()scan frame + timestamp를 버퍼에 pushiio/buffer.h

Trigger API

API용도
devm_iio_trigger_alloc()trigger 객체 할당
devm_iio_trigger_register()trigger 등록
iio_trigger_set_immutable()자체 trigger만 허용 설정
iio_trigger_notify_done()handler 완료 통지 (필수)
iio_validate_own_trigger()자체 trigger만 허용하는 validate 함수

Consumer API

API용도반환값
devm_iio_channel_get()이름으로 채널 획득struct iio_channel *
devm_iio_channel_get_all()모든 채널 획득struct iio_channel *
iio_read_channel_raw()원시값 읽기int
iio_read_channel_processed()처리된 물리량 읽기int
iio_read_channel_scale()스케일 값 읽기int
iio_channel_get_all_cb()콜백 기반 연속 수신struct iio_cb_buffer *