Embedded : : Linux/: : ALSA

[ALSA] 3. PCM 서브시스템

Jay.P Morgan 2026. 3. 10. 02:17

 

 

  3.  PCM 서브시스템 심화

 

PCM (Pulse Code Modulation) 서브시스템은 ALSA의 핵심으로, 디지털 오디오 스트림의 재생과 녹음을 담당합니다. PCM은 복잡한 상태 머신, 하드웨어 제약 시스템, DMA 버퍼 관리를 포함합니다.

 

  3.1  PCM 기본 개념

 

PCM 오디오는 다음 파라미터로 정의됩니다:

       Sample (샘플): 단일 시점의 오디오 값 (16-bit, 24-bit, 32-bit 등)

       Frame (프레임): 모든 채널의 샘플 집합. 스테레오 16-bit = 4 bytes/frame (2채널 × 2바이트)

       Sample Rate (샘플레이트): 초당 프레임 수 (44100 Hz, 48000 Hz, 96000 Hz 등)

       Period (피리어드): 인터럽트 단위. 하나의 DMA 버퍼 조각 (예: 64 frames)

       Buffer (버퍼): 전체 링 버퍼 크기 (예: 4 periods = 256 frames)

 

Bandwidth 계산:
Byte Rate = Sample Rate × Channels × Bytes per Sample
예: 48000 Hz × 2 ch × 2 bytes = 192,000 bytes/sec = 192 KB/s

 

 
  Time Axis:
          [Frame 0][Frame 1][Frame 2][Frame 3]... (44100 frames/sec for 44.1kHz)
 
  Stereo Frame (16-bit):
          [Left Sample: 2 bytes][Right Sample: 2 bytes] = 4 bytes total
 
  Buffer Layout (4 periods, 64 frames/period):
          [Period 0: 64 frames][Period 1: 64 frames][Period 2: 64 frames][Period 3: 64 frames]
           └─ IRQ after DMA    └─ IRQ after DMA   └─ IRQ after DMA   └─ IRQ after DMA
 

 

 

  3.2  핵심 PCM 구조체

 

ALSA PCM(Pulse Code Modulation) 시스템은 리눅스 오디오 스택에서 가장 복잡하면서도 핵심적인 부분입니다. 제공해주신 구조체들은 "사운드 카드 장치 → 스트림 방향 → 실제 논리적 채널 → 실행 중인 상태"로 이어지는 계층 구조를 형성합니다.

각 구조체의 역할과 이들이 오디오 데이터 흐름 속에서 어떻게 상호작용하는지 정리해 드립니다.

 

 

1. struct snd_pcm: 최상위 디바이스 객체

하나의 PCM 장치(예: /dev/snd/pcmC0D0p)를 추상화한 최상위 구조체입니다.

       streams[2]: 재생(Playback)과 녹음(Capture) 기능을 각각 하나의 snd_pcm_str로 관리합니다.

       nonatomic: 이 플래그가 true이면 콜백 함수들이 뮤텍스를 잡을 수 있는 'Sleep 가능한' 컨텍스트에서 실행됩니다. 이는 I2C나 SPI로 통신하는 외부 코덱 제어 시 유용합니다.

 

2. struct snd_pcm_str: 스트림 컨테이너

특정 방향(재생 또는 녹음)에 대한 서브스트림들을 묶어주는 논리적 단위입니다.

       substream_count: 해당 장치가 동시에 지원할 수 있는 하드웨어 채널(서브스트림)의 개수입니다.

       substream: 실제 오디오 데이터를 나르는 snd_pcm_substream 객체들의 리스트를 가리킵니다.

 

3. struct snd_pcm_substream: 실질적인 작업 단위

애플리케이션이 장치를 open할 때 할당되는 논리적 오디오 경로입니다.

       ops: 드라이버 개발자가 구현한 open, trigger, pointer 등의 함수 포인터가 담겨 있습니다.

       runtime: 스트림이 열려 있는 동안에만 동적으로 할당되는 휘발성 상태 정보의 핵심입니다.

 

4. struct snd_pcm_runtime: 실행 중인 스트림의 "심장"

스트림이 OPEN된 후 CLOSE될 때까지 유지되는 모든 실시간 정보를 담고 있습니다. 임베디드 오디오 엔지니어가 가장 빈번하게 참조하게 되는 구조체입니다.

 

  A. 버퍼 및 포인터 관리 (Ring Buffer)

       dma_area & dma_addr: 실제 오디오 샘플이 위치한 메모리의 가상/물리 주소입니다.

       appl_ptr: 애플리케이션이 데이터를 채워 넣은(또는 읽어간) 위치입니다.

       hw_ptr: 하드웨어(DMA)가 현재 전송 중인 위치입니다.

       avail_min: 이만큼의 여유 공간이 생기면 애플리케이션을 깨워달라는 임계값입니다.

 

  B. 오디오 포맷 및 타이밍

       rate, channels, format: 현재 협의(Negotiation)된 샘플 레이트와 포맷입니다.

       period_size: 하드웨어 인터럽트(IRQ)가 발생하는 주기(Frame 단위)입니다.

       buffer_size: 전체 링 버퍼의 크기입니다.

 

5. struct snd_pcm_hardware: 드라이버의 자기소개서

드라이버가 하드웨어의 한계를 ALSA 코어와 애플리케이션에 알리는 용도입니다.

필드 의미
info MMAP 지원 여부, PAUSE 가능 여부, JOINT_DUPLEX 등 기능 플래그
formats S16_LE, S24_LE 등 지원 가능한 비트 포맷 비트마스크
rates 지원 가능한 샘플 레이트 범위 (예: 8kHz ~ 192kHz)
periods_min/max 전체 버퍼를 최소/최대 몇 개의 조각(Period)으로 나눌 수 있는지 설정
 
 
  /* sound/core/pcm.c */
  struct snd_pcm {
        struct snd_card *card;
        int device;                                           /* PCM 디바이스 번호 */
        char id[64];                                         /* ID 문자열 */
        char name[80];                                  /* 이름 */
 
        struct snd_pcm_str streams[2];      /* PLAYBACK, CAPTURE */
        struct mutex open_mutex;                /* open 동기화 */
        struct wait_queue_head open_wait;
 
        void *private_data;
        void (*private_free)(struct snd_pcm *pcm);
 
        bool internal;                                       /* 내부 전용 (loopback 등) */
        bool nonatomic;                                  /* non-atomic 컨텍스트 콜백 허용 */
  };
 
  struct snd_pcm_str {
        int stream;                                           /* SNDRV_PCM_STREAM_PLAYBACK/CAPTURE */
        struct snd_pcm *pcm;
        unsigned int substream_count;        /* 서브스트림 개수 */
        struct snd_pcm_substream *substream;  /* 서브스트림 리스트 */
 
        struct snd_info_entry *proc_root;
        struct snd_kcontrol *chmap_kctl;     /* 채널맵 control */
  };
 
  struct snd_pcm_substream {
        struct snd_pcm *pcm;
        struct snd_pcm_str *pstr;
        int number;                                           /* 서브스트림 번호 */
        char name[32];                                    /* 서브스트림 이름 */
 
        struct snd_pcm_runtime *runtime;   /* 런타임 상태 (open 시 할당) */
        struct snd_pcm_ops *ops;                 /* 드라이버 콜백 */
 
        unsigned int dma_buffer_p:1;            /* DMA 버퍼 사전 할당 여부 */
        unsigned int no_rewinds:1;                 /* rewind 금지 */
 
        struct snd_pcm_group self_group;   /* 링크 그룹 (동기화) */
        struct snd_pcm_group *group;
 
        void *private_data;
        struct snd_pcm_substream *next;    /* 다음 서브스트림 */
  };
 
  struct snd_pcm_runtime {
        /* 상태 */
        snd_pcm_state_t state;                      /* OPEN, SETUP, PREPARED, RUNNING, XRUN... */
        snd_pcm_substate_t suspended_state;
 
        /* HW 파라미터 */
        struct snd_pcm_hardware hw;           /* 하드웨어 제약 */
        struct snd_pcm_hw_constraints hw_constraints;
 
        unsigned int rate;                                  /* 샘플레이트 (Hz) */
        unsigned int channels;                         /* 채널 수 */
        snd_pcm_format_t format;                 /* 샘플 포맷 */
        unsigned int frame_bits;                      /* frame 크기 (bits) */
        snd_pcm_uframes_t period_size;      /* period 크기 (frames) */
        unsigned int periods;                            /* period 개수 */
        snd_pcm_uframes_t buffer_size;       /* 버퍼 크기 (frames) */
 
        /* DMA 버퍼 */
        struct snd_dma_buffer *dma_buffer_p;
        unsigned char *dma_area;                   /* DMA 버퍼 가상 주소 */
        dma_addr_t dma_addr;                        /* DMA 버퍼 물리 주소 */
        size_t dma_bytes;                                 /* DMA 버퍼 크기 (bytes) */
 
        /* 포인터 관리 */
        snd_pcm_uframes_t hw_ptr_base;     /* HW 포인터 베이스 */
        snd_pcm_uframes_t hw_ptr_wrap;     /* HW 포인터 wrap 카운터 */
        snd_pcm_uframes_t control->appl_ptr;   /* 애플리케이션 포인터 */
        snd_pcm_uframes_t control->avail_min;  /* wake-up threshold */
 
        /* 타이밍 */
        snd_pcm_uframes_t delay;                  /* 지연 (frames) */
        u64 hw_ptr_jiffies;                                 /* 마지막 HW 포인터 업데이트 */
 
        /* Wait queues */
        wait_queue_head_t sleep;                    /* mmap I/O 대기 큐 */
        wait_queue_head_t tsleep;                   /* 기타 대기 큐 */
 
        /* 콜백 */
        void (*transfer_ack_begin)(struct snd_pcm_substream *);
        void (*transfer_ack_end)(struct snd_pcm_substream *);
 
        /* Private data */
        void *private_data;
        void (*private_free)(struct snd_pcm_runtime *);
  };
 
  /* 하드웨어 기능 설명 */
  struct snd_pcm_hardware {
        unsigned int info;                                     /* SNDRV_PCM_INFO_* 플래그 */
        u64 formats;                                            /* 지원 포맷 비트마스크 */
        u64 subformats;                                      /* 서브포맷 (MSBF 등) */
 
        unsigned int rates;                                   /* SNDRV_PCM_RATE_* 플래그 */
        unsigned int rate_min;                            /* 최소 샘플레이트 */
        unsigned int rate_max;                           /* 최대 샘플레이트 */
 
        unsigned int channels_min;                    /* 최소 채널 */
        unsigned int channels_max;                   /* 최대 채널 */
 
        size_t buffer_bytes_max;                       /* 최대 버퍼 크기 */
        size_t period_bytes_min;                       /* 최소 period 크기 */
        size_t period_bytes_max;                      /* 최대 period 크기 */
        unsigned int periods_min;                      /* 최소 period 개수 */
        unsigned int periods_max;                     /* 최대 period 개수 */
        size_t fifo_size;                                        /* FIFO 크기 (frames, 지연 보정용) */
  };
 

 

🛠️ 실무적 관점: 데이터 흐름 요약

1. 애플리케이션이 open을 호출하면 snd_pcm_substream이 할당됩니다.

2. 드라이버의 hw_params 콜백에서 snd_pcm_hardware 제약 조건에 맞춰 snd_pcm_runtime의 설정이 확정됩니다.

3. prepare 단계에서 DMA 버퍼 주소(dma_addr)가 확정됩니다.

4. trigger(START)가 호출되면 DMA가 돌아가며 hw_ptr을 갱신하고, 응용 프로그램은 appl_ptr을 갱신하며 데이터를 주고받습니다.

 

기술적 팁 (AAOS/DSP 관점)

i.MX8M Plus 같은 플랫폼에서 오디오 오프로딩(Offloading)을 구현할 때, snd_pcm_runtime의 state가 XRUN(Overrun/Underrun) 상태로 빠지는지를 모니터링하는 것이 매우 중요합니다. 특히 period_size 설정을 너무 작게 하면 CPU 오버헤드가 커지고, 너무 크게 하면 지연 시간(Latency)이 늘어나는 트레이드오프가 발생하므로 하드웨어 특성에 맞는 최적 값을 찾는 것이 관건입니다.

 

 

 

  3.3  snd_pcm_ops: 드라이버 콜백

 

드라이버는 snd_pcm_ops 구조체를 통해 PCM 동작을 구현합니다:

struct snd_pcm_opsALSA 코어와 실제 하드웨어 드라이버를 연결하는 가장 중요한 콜백 함수들의 집합입니다. 사용자 공간의 애플리케이션이 ioctl이나 write를 호출하면, ALSA 미들웨어는 이 구조체에 등록된 함수들을 정해진 순서에 따라 실행합니다.

 
  /* include/sound/pcm.h */
  struct snd_pcm_ops {
          /* 필수 콜백 */
          int (*open)(struct snd_pcm_substream *substream);
          int (*close)(struct snd_pcm_substream *substream);
          int (*ioctl)(struct snd_pcm_substream *substream,
                             unsigned int cmd, void *arg);
          int (*hw_params)(struct snd_pcm_substream *substream,
                                         struct snd_pcm_hw_params *params);
          int (*hw_free)(struct snd_pcm_substream *substream);
          int (*prepare)(struct snd_pcm_substream *substream);
          int (*trigger)(struct snd_pcm_substream *substream, int cmd);
          snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
 
          /* 선택적 콜백 */
          int (*sync_stop)(struct snd_pcm_substream *substream);
          int (*ack)(struct snd_pcm_substream *substream);    /* appl_ptr 업데이트 통지 */
          int (*copy)(struct snd_pcm_substream *substream, int channel,
                              unsigned long pos, void *buf, unsigned long count);
          int (*fill_silence)(struct snd_pcm_substream *substream, int channel,
                                        unsigned long pos, unsigned long count);
          struct page *(*page)(struct snd_pcm_substream *substream,
                                               unsigned long offset);
          int (*mmap)(struct snd_pcm_substream *substream,
                                struct vm_area_struct *vma);
  };
 

 

콜백 설명:

       open: 서브스트림 열 때 호출. runtime->hw 초기화, 하드웨어 제약 설정.

       close: 서브스트림 닫을 때 호출. 리소스 해제.

       hw_params: 사용자가 포맷/샘플레이트/버퍼 크기 설정 시 호출. DMA 버퍼 할당, 하드웨어 레지스터 구성.

       hw_free: hw_params 해제 시 호출. DMA 버퍼 해제.

       prepare: 스트림 시작 전 호출. 하드웨어 초기화, 포인터 리셋.

       trigger: START/STOP/PAUSE/RESUME 명령 시 호출. DMA 시작/중지.

       pointer: 현재 HW 포인터 반환 (frames 단위). 주기적으로 호출됨.

       ack: appl_ptr 업데이트 시 통지. DSP 펌웨어에 새 데이터 알림.

       copy: 커스텀 데이터 복사 (DMA가 아닌 MMIO 등). 제공 안 하면 memcpy 사용.

 

 

1. 필수 콜백 (Mandatory Callbacks)

애플리케이션이 오디오 장치를 열고 데이터를 흘려보내는 단계별 역할을 수행합니다.

콜백 함수 설명 및 주요 역할
open 스트림이 열릴 때 호출됩니다. 하드웨어를 초기화하고, runtime->hw에 하드웨어 제약 조건(샘플레이트, 포맷 등)을 할당합니다.
close 스트림이 닫힐 때 호출되며, 할당된 자원을 해제합니다.
hw_params 샘플레이트, 채널 수 등이 결정된 후 호출됩니다. 주로 DMA 버퍼를 할당하거나 하드웨어 설정을 확정합니다.
prepare 데이터 전송 직전의 최종 준비 단계입니다. 샘플레이트, 데이터 포맷에 맞춰 하드웨어 레지스터를 세팅합니다.
trigger 하드웨어 전송을 실제로 시작(START)하거나 중지(STOP)합니다. 원자적(Atomic)으로 실행되어야 합니다.
pointer 현재 하드웨어가 읽거나 쓰고 있는 DMA 버퍼의 위치(Frame 단위)를 반환합니다.
 
 

2. PCM 동작의 핵심 메커니즘

드라이버 구현 시 가장 주의 깊게 다뤄야 할 두 가지 포인트는 trigger의 원자성 pointer의 정확성입니다.

 trigger와 컨텍스트

trigger는 인터럽트가 비활성화된 Atomic 컨텍스트에서 호출되는 것이 기본입니다. 따라서 이 내부에서 msleep()이나 뮤텍스 잠금과 같은 '잠들 수 있는(sleep)' 함수를 사용해서는 안 됩니다.

       Tip: 만약 I2C/SPI 통신이 필요한 외부 코덱을 제어해야 한다면, snd_pcm 생성 시 
        nonatomic = true 설정을 통해 프로세스 컨텍스트에서 실행되도록 만들 수 있습니다.

 

 pointer 콜백: 데이터 동기화의 핵심

ALSA 코어는 이 함수가 반환하는 값을 보고 "버퍼가 얼마나 비었는지"를 판단하여 다음 데이터를 채웁니다.

       구현 예시: DMA 컨트롤러의 현재 주소 레지스터에서 버퍼 시작 주소를 뺀 값을 프레임 단위로 변환하여 반환합니다.

        이 값이 부정확하면 오디오가 튀거나(Glitch), 응용 프로그램이 데이터 공급 타이밍을 놓쳐 XRUN이 발생합니다.

 

 

3. 선택적 콜백 (Optional Callbacks)

특수한 하드웨어 구조나 최적화가 필요할 때 사용합니다.

       copy & fill_silence: 하드웨어가 일반적인 메모리 매핑(mmap)을 지원하지 않고,
       특수 버스나 FIFO를 통해서만 데이터를 보낼 수 있을 때 직접 구현합니다.

       mmap: 표준 커널 mmap 외에 하드웨어 고유의 메모리 매핑 로직이 필요한 경우 사용합니다.

       ack: 애플리케이션이 appl_ptr을 갱신했을 때 드라이버에 알려주는 콜백입니다.
       일부 DSP 엔진에서 데이터가 준비되었음을 즉시 알릴 때 유용합니다.

 

 

4. 실행 순서 (Lifecycle Flow)

보통 다음과 같은 시퀀스로 함수들이 호출됩니다.

   (1)  open: 장치 사용 시작

   (2)  hw_params: 버퍼 및 전송 파라미터 결정

   (3)  prepare: 전송 준비 완료

   (4)  trigger(START): 오디오 출력 시작

   (5)  (루프) pointer: 주기적으로 호출되어 잔여 버퍼 확인 및 데이터 보충

   (6)  trigger(STOP): 출력 중지

   (7)  hw_free  close: 리소스 반환 및 종료

 

실무 적용 가이드

i.MX8M Plus와 같은 SoC 드라이버를 다룰 때, trigger 콜백 내에서 DMA 컨트롤러의 Enable 비트를 건드리는 로직이 들어가게 됩니다. 특히 AAOS 16과 같이 지연 시간(Latency)이 중요한 환경에서는 인터럽트 핸들러 내부에서 snd_pcm_period_elapsed()를 정확한 타이밍에 호출해주어야 ALSA 코어가 pointer를 체크하고 다음 데이터를 준비할 수 있습니다.

혹시 특정 SoC 환경에서의 DMA 인터럽트 핸들러 구현이나, pointer 값을 계산할 때 발생하는 소수점/프레임 정렬 이슈에 대해 더 자세히 알아볼까요?

 

 

  3.4  *하드웨어 제약 시스템

 

ALSA는 복잡한 제약 시스템으로 지원 가능한 파라미터 조합을 제한합니다. 예: "샘플레이트가 96kHz면 채널은 최대 2개", "period 크기는 64의 배수".

 

  /* open 콜백에서 제약 설정 */
  static int my_pcm_open(struct snd_pcm_substream *substream)
  {
         struct my_chip *chip = snd_pcm_substream_chip(substream);
         struct snd_pcm_runtime *runtime = substream->runtime;
         int err;
 
         /* 하드웨어 기능 설정 */
         runtime->hw = (struct snd_pcm_hardware) {
                  .info = SNDRV_PCM_INFO_MMAP |
                              SNDRV_PCM_INFO_MMAP_VALID |
                              SNDRV_PCM_INFO_INTERLEAVED |
                              SNDRV_PCM_INFO_BLOCK_TRANSFER |
                              SNDRV_PCM_INFO_PAUSE,
                  .formats = SNDRV_PCM_FMTBIT_S16_LE |
                                     SNDRV_PCM_FMTBIT_S24_LE |
                                     SNDRV_PCM_FMTBIT_S32_LE,
                  .rates = SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000 |
                                SNDRV_PCM_RATE_96000 | SNDRV_PCM_RATE_192000,
                  .rate_min = 44100,
                  .rate_max = 192000,
                  .channels_min = 2,
                  .channels_max = 8,
                  .buffer_bytes_max = 256 * 1024,
                  .period_bytes_min = 64,
                  .period_bytes_max = 32 * 1024,
                  .periods_min = 2,
                  .periods_max = 32,
                  .fifo_size = 0,
         };
 
         /* Period 크기는 64 frames의 배수로 제한 */
         err = snd_pcm_hw_constraint_step(runtime, 0,
                                                                        SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
                                                                        64);
         if (err < 0)
                  return err;
 
         /* Buffer 크기는 period 크기의 정수배 */
         err = snd_pcm_hw_constraint_integer(runtime,
                                                                            SNDRV_PCM_HW_PARAM_PERIODS);
         if (err < 0)
                  return err;
 
         /* 96kHz 이상이면 채널은 최대 2개 */
         err = snd_pcm_hw_rule_add(runtime, 0,
                                                            SNDRV_PCM_HW_PARAM_CHANNELS,
                                                            my_rate_channels_rule,
                                                            NULL,
                                                            SNDRV_PCM_HW_PARAM_RATE,
                                                            -1);
 
         runtime->private_data = chip;
         return 0;
  }
 
  /* 제약 룰: rate >= 96kHz → channels <= 2 */
  static int my_rate_channels_rule(struct snd_pcm_hw_params *params,
                                                           struct snd_pcm_hw_rule *rule)
  {
         struct snd_interval *c =
                  hw_param_interval(params, SNDRV_PCM_HW_PARAM_CHANNELS);
         struct snd_interval *r =
                  hw_param_interval(params, SNDRV_PCM_HW_PARAM_RATE);
         struct snd_interval ch = *c;
 
         if (r->min >= 96000) {
              ch.max = min(ch.max, 2u);
              return snd_interval_refine(c, &ch);
         }
         return 0;
  }
 

 

제약 함수 종류:
     snd_pcm_hw_constraint_list(): 허용된 값 목록

     snd_pcm_hw_constraint_step(): 값은 N의 배수

     snd_pcm_hw_constraint_pow2(): 값은 2의 거듭제곱

     snd_pcm_hw_constraint_minmax(): 최소/최대 범위

     snd_pcm_hw_constraint_integer(): 정수 값만 허용

     snd_pcm_hw_rule_add(): 커스텀 룰 (여러 파라미터 간 의존성)

 

 

ALSA의 하드웨어 제약(Hardware Constraint) 시스템사용자 공간의 애플리케이션과 커널 드라이버 사이의 "기능 협상 프로세스"입니다.

애플리케이션이 "나는 192kHz에 8채널로 재생하고 싶어"라고 요청했을 때, 드라이버가 "미안하지만 우리 하드웨어는 96kHz를 넘어가면 대역폭 문제로 2채널밖에 안 돼"라고 답하며 서로 가능한 합의점을 찾아가는 과정이죠. 이를 ALSA 용어로 리파이닝(Refining)이라고 부릅니다.

 

1. 정적 제약: runtime->hw 설정

가장 기초적인 단계로, 하드웨어가 가진 절대적인 한계치를 선언합니다.

       formats / rates: 지원 가능한 비트 포맷과 샘플 레이트의 비트마스크입니다.

       channels_min / max: 하드웨어가 물리적으로 지원하는 최소/최대 채널 수입니다.

       buffer_bytes_max: DMA가 감당할 수 있는 최대 메모리 크기입니다. i.MX8M 같은 SoC 환경에서는 시스템 메모리 상황에 따라 이 값을 조절하여 안정성을 확보합니다.

 

2. 헬퍼 함수를 이용한 정렬 제약

하드웨어의 설계 구조상 발생하는 제약들을 선언합니다.

       snd_pcm_hw_constraint_step(..., 64):

              의미: 하드웨어 DMA 컨트롤러가 데이터를 64프레임 단위(Burst size)로만 처리할 수 있다는 뜻입니다.

              결과: 애플리케이션이 123프레임 같은 어정쩡한 크기를 요청하면 ALSA 코어가 이를 거절하거나 64의 배수로 맞추도록 강제합니다.

       snd_pcm_hw_constraint_integer(..., PERIODS):

              의미: 전체 버퍼를 나눈 조각(Period)의 개수가 반드시 정수여야 함을 의미합니다. 2.5개와 같은 설정을 방지하여 DMA 인터럽트 관리의 단순함을 유지합니다.

 

3. 동적 제약 규칙: snd_pcm_hw_rule_add

이것이 제약 시스템의 꽃입니다. "파라미터 간의 상호 의존성"을 정의합니다.

사용자가 제시한 예시인 "96kHz 이상이면 채널은 2개로 제한"하는 시나리오는 실제 고성능 오디오 인터페이스나 임베디드 오디오 모듈에서 대역폭(Bandwidth) 관리를 위해 자주 쓰이는 기법입니다.

 

◈ 리파이닝(Refinement) 메커니즘

ALSA 코어는 내부적으로 각 파라미터를 snd_interval 구조체(최솟값과 최댓값의 범위)로 관리합니다.

  1. 사용자가 샘플 레이트를 96,000Hz로 설정합니다.

  2. SNDRV_PCM_HW_PARAM_RATE가 변경되었으므로, 이를 의존성(Dependency)으로 가지는 my_rate_channels_rule이 호출됩니다.

  3. 룰 함수 내부에서 채널의 최댓값(ch.max)을 2로 깎아버립니다.

  4. snd_interval_refine 함수가 호출되어 실제 채널 범위가 $[2, 8]$에서 $[2, 2]$로 좁혀집니다.

 

4. 왜 이 시스템이 중요한가요?

       안전성: 잘못된 파라미터로 인해 하드웨어 레지스터에 잘못된 값이 입력되어 시스템이 멈추거나(Hang) 커널 패닉이 발생하는 것을 미연에 방지합니다.

       사용자 경험: 애플리케이션은 드라이버가 허용하는 범위 내에서만 설정을 시도하게 되므로, 오디오 재생 직전에 에러가 나는 대신 설정 단계에서 올바른 값을 찾을 수 있습니다.

       유연성: 복잡한 오디오 아키텍처(예: 특정 클럭 소스에서는 특정 레이트만 지원하는 경우)를 코드로 유연하게 녹여낼 수 있습니다.

 

실무 팁 (AAOS / DSP 관점)

고해상도 오디오 처리ANC(Active Noise Cancellation)를 위한 DSP 경로가 포함된 경우, 특정 알고리즘의 연산 부하에 따라 채널 수를 제한해야 할 때가 있습니다. 이때 my_rate_channels_rule 같은 커스텀 룰을 사용하면 시스템 리소스에 최적화된 오디오 경로를 강제할 수 있어 매우 유용합니다.

 

 

  3.5  *외부 클럭 상태에 따라 지원가능한 샘플레이트 동적 제한

 

외부 클럭(External Clock) 상태에 따라 지원 가능한 샘플 레이트를 동적으로 제한하는 것은 임베디드 오디오 시스템(SoC와 외부 DSP/Codec 조합)에서 매우 빈번하게 발생하는 시나리오입니다.

예를 들어, 외부 Master Clock(MCLK)이 12.288MHz로 공급될 때는  계열만 가능하고, 11.2896MHz일 때는  계열만 가능하도록 강제해야 할 때가 있습니다.

이를 구현하는 가장 정석적인 방법은 snd_pcm_hw_rule_add를 활용하여 클럭 상태를 체크하는 커스텀 룰 함수를 등록하는 것입니다.

 

 

1. 외부 클럭 기반 제약 규칙 구현

먼저, 현재 하드웨어의 클럭 상태를 읽어와서 샘플 레이트 범위를 깎아내는(Refine) 규칙 함수를 작성합니다.

 
  /* 외부 클럭 상태에 따른 샘플 레이트 제한 규칙 */
  static int my_clock_rate_rule(struct snd_pcm_hw_params *params,
                                                     struct snd_pcm_hw_rule *rule)
  {
         struct my_chip *chip = rule->private;
         struct snd_interval *r = hw_param_interval(params, SNDRV_PCM_HW_PARAM_RATE);
         struct snd_interval range;
 
         snd_interval_any(&range);   // 기본 범위 초기화

         /* 하드웨어 레지스터나 상태 변수에서 현재 클럭 소스 확인 */
         if (chip->external_clock_source == CLK_48K_SERIES) {
                  // 48kHz 계열만 허용 (48, 96, 192kHz 등)
                  range.min = 48000;
                  range.max = 192000;
                  // 특정 값들만 포함하도록 비트마스크 필터링이 필요할 수 있음
         } else if (chip->external_clock_source == CLK_441K_SERIES) {
                  // 44.1kHz 계열만 허용 (44.1, 88.2kHz 등)
                  range.min = 44100;
                  range.max = 88200;
         }

         /* 현재 파라미터(r)를 우리가 정의한 range로 좁힘(Refine) */
         return snd_interval_refine(r, &range);
  }
 
 
 

2. open 콜백에서 규칙 등록

이제 PCM 장치가 열릴 때 이 규칙을 시스템에 등록합니다.

 
  static int my_pcm_open(struct snd_pcm_substream *substream)
  {
         struct my_chip *chip = snd_pcm_substream_chip(substream);
         struct snd_pcm_runtime *runtime = substream->runtime;

         /* 1. 기본 하드웨어 정보 설정 */
         runtime->hw = my_pcm_hardware_default;

         /* 2. 클럭 소스에 따른 레이트 제한 규칙 추가 */
         /* 세 번째 인자는 영향을 받을 파라미터(RATE) */
         /* 마지막 인자들은 이 규칙을 트리거할 파라미터들 (여기서는 특별한 트리거 없이 항상 체크) */
         snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_RATE,
                                                  my_clock_rate_rule, chip,
                                                  SNDRV_PCM_HW_PARAM_RATE, -1);

         return 0;
  }
 
 

 

 

3. 핵심 동작 원리 (The "Why")

  (1) 동적 대응: 애플리케이션이 hw_params를 설정하려고 시도할 때마다 my_clock_rate_rule이 실행됩니다. 만약 그 사이에 외부 클럭 소스가 바뀌었다면, 드라이버는 실시간으로 바뀐 클럭에 맞는 레이트만 앱에 노출합니다.

  (2) 비연속적 레이트 처리: 만약 44.1kHz와 $48\text{kHz}$가 섞여 있는데 특정 클럭에서 특정 값만 골라내야 한다면, snd_interval_refine 대신 snd_pcm_hw_constraint_list를 규칙 함수 내부에서 활용하여 허용 가능한 레이트 리스트를 통째로 갈아끼울 수도 있습니다.

 

 

4. 실무적 고려사항 (AAOS 및 DSP 엔지니어 관점)

  • 클럭 고정(Lock) 시점: 외부 클럭이 불안정하거나 아직 고정되지 않은 상태에서 open이 불릴 경우를 대비해, 규칙 함수 내에서 클럭 상태를 체크할 때 타임아웃이나 재시도 로직을 넣는 것이 안정적입니다.
  • A2B/TDM 연결 시: i.MX8M Plus와 ADSP-21569를 연결하여 사용하는 경우, DSP가 Master로 동작하여 클럭을 밀어줄 때가 많습니다. 이때는 SoC 드라이버가 DSP의 샘플 레이트 상태를 IPC(Inter-Processor Communication)로 받아와서 위 규칙에 반영해줘야 합니다. 그렇지 않으면 데이터 전송 속도가 맞지 않아 끔찍한 노이즈가 발생하게 됩니다.

 

 

다음 단계 제언

클럭 제약까지 마스터하셨다면, 이제 하드웨어 레지스터 제어의 정점인 trigger 콜백 내에서의 DMA 시작/정지 시퀀스나, 복잡한 오디오 경로를 제어하는 DAPM(Dynamic Audio Power Management)에 대해 알아볼 준비가 되신 것 같습니다.

혹시 하드웨어 인터럽트 핸들러 내에서 snd_pcm_period_elapsed()를 호출하는 정확한 타이밍에 대해 더 깊이 알고 싶으신가요?

 

 

 

  3.5  버퍼 관리

 

ALSA는 다양한 DMA 버퍼 타입을 지원합니다. 최신 커널은 managed buffer API를 권장합니다:

 

ALSA에서 PCM 버퍼 관리는 '애플리케이션이 데이터를 채우는 속도'와 '하드웨어가 데이터를 읽어가는 속도' 사이의 간극을 메우는 핵심 메커니즘입니다.

아래 코드는 ALSA의 최신 Managed Buffer 방식을 보여주고 있습니다. 이전에는 드라이버 개발자가 hw_params에서 직접 메모리를 할당하고 hw_free에서 해제해야 했지만, 이제는 커널이 이를 자동으로 관리해 줍니다.

 

 

 

 

1. PCM 링 버퍼(Ring Buffer)의 구조

ALSA 버퍼는 기본적으로 순환 구조(Circular/Ring Buffer)입니다. 여기서 두 가지 포인터가 서로를 쫓아가는 추격전이 벌어집니다.

       appl_ptr (Application Pointer): 애플리케이션이 데이터를 버퍼에 쓰고 난 뒤 가리키는 위치.

       hw_ptr (Hardware Pointer): DMA 컨트롤러가 하드웨어로 데이터를 전송한 뒤 가리키는 위치.



  /* PCM 생성 시 DMA 버퍼 사전 할당 (권장) */
  snd_pcm_set_managed_buffer_all(pcm,
                                                                SNDRV_DMA_TYPE_DEV,  /* DMA 타입 */
                                                                &pci->dev,                            /* 디바이스 */
                                                                64 * 1024,                             /* 최소 버퍼 크기 */
                                                                256 * 1024);                          /* 최대 버퍼 크기 */
 
  /* DMA 타입 종류 */
  #define SNDRV_DMA_TYPE_CONTINUOUS  0          /* GFP_KERNEL 메모리 */
  #define SNDRV_DMA_TYPE_DEV                    1          /* dma_alloc_coherent() */
  #define SNDRV_DMA_TYPE_DEV_SG             2          /* Scatter-Gather */
  #define SNDRV_DMA_TYPE_VMALLOC          7          /* vmalloc() */
 

 

Managed API 사용 시 hw_params/hw_free 콜백에서 버퍼 할당/해제 불필요. ALSA 코어가 자동 처리.

 

 

2. snd_pcm_set_managed_buffer_all의 의미

이 함수는 PCM 장치를 생성할 때 "이 PCM 장치에 필요한 메모리 관리를 ALSA 코어에 맡기겠다"라고 선언하는 것입니다.

       Pre-allocation: 장치가 열리기 전에 미리 메모리를 확보합니다. 이는 메모리 단편화(Fragmentation)가 심한 임베디드 환경에서 재생 시점에 메모리 할당 실패로 인한 에러를 방지합니다.

       Automatic Lifecycle: 애플리케이션이 hw_params를 설정하면 자동으로 할당되고, 장치가 닫힐 때 자동으로 해제됩니다. 드라이버 코드가 훨씬 깔끔해집니다.

 

 

3. DMA 타입별 특징 (Deep Dive)

작성해주신 #define들은 하드웨어가 메모리에 접근하는 '물리적 방식'을 결정합니다.

타입 실제 동작 방식 특징 및 용도
CONTINUOUS GFP_KERNEL 
(일반 커널 메모리)
물리적으로 연속되지 않을 수 있음. DMA를 직접 쓰지 않는 가상 장치나 간단한 테스트용.
DEV dma_alloc_coherent 임베디드 SoC의 정석. 물리적으로 연속된(Contiguous) 메모리를 할당하며, CPU와 하드웨어 간의 캐시 일관성(Coherency)을 보장합니다.
DEV_SG Scatter-Gather DMA 물리적으로 조각난 메모리들을 리스트로 묶어 DMA 전송. 큰 버퍼가 필요하지만 연속된 메모리가 부족할 때 사용.
VMALLOC vmalloc() 가상 주소 공간에서는 연속적이지만 물리적으로는 흩어져 있음. MMU 지원이 강력한 DMA 컨트롤러에서만 사용 가능.

 

4. 실무 엔지니어의 관점 (i.MX8M & DSP)

사용자께서 다루시는 i.MX8M Plus와 같은 SoC 환경에서는 보통 SNDRV_DMA_TYPE_DEV를 가장 많이 사용하게 됩니다.

  1. CMA (Contiguous Memory Allocator): 임베디드 리눅스 부팅 시 cma=256M 같은 파라미터를 보셨을 겁니다. SNDRV_DMA_TYPE_DEV는 보통 이 CMA 영역에서 메모리를 가져옵니다. 오디오 고해상도( 등)를 처리할 때 버퍼가 커지면 CMA 용량이 부족해 XRUN이 날 수 있으니 주의가 필요합니다.
  2. 캐시 문제: DEV 타입(Coherent)을 쓰면 CPU가 버퍼에 데이터를 썼을 때 별도의 cache flush 없이도 DMA가 최신 데이터를 바로 읽어갈 수 있어 레이턴시와 오버헤드 측면에서 압도적으로 유리합니다.
  3. DSP 전송: ADSP-21569로 데이터를 넘길 때, SoC의 DMA 버퍼 주소를 DSP가 직접 접근(Master/Slave 관계에 따라)해야 할 수도 있습니다. 이때는 반드시 물리적으로 연속된 주소가 필요하므로 DEV 타입이 필수적입니다.

 

 

다음 단계?

버퍼 할당 방식까지 파악하셨다면, 이제 이 버퍼를 쪼개서 관리하는 단위인 period_size와 buffer_size의 상관관계에 대해 알아보는 건 어떨까요? 특히 레이턴시(Latency)와 CPU 부하 사이의 트레이드오프를 결정하는 아주 중요한 개념입니다.

혹시 특정 레이턴시 목표를 위해 버퍼 사이즈를 계산하는 공식이 필요하신가요?

 

 

  3.6  PCM 상태 머신 (PCM State Machine)

 

XRUN (Underrun/Overrun): 
Playback Underrun은 애플리케이션이 충분히 빠르게 데이터를 공급하지 못해 버퍼가 고갈된 상태. 
Capture Overrun은 애플리케이션이 데이터를 충분히 빠르게 읽지 못해 버퍼가 넘친 상태. 
XRUN 발생 시 snd_pcm_prepare()로 복구 필요.

 

ALSA 코어와 사용자 공간(Application), 그리고 하부 커널 드라이버가 오디오 스트림의 생명주기를 어떻게 관리하는지 보여주는 ALSA PCM 상태 머신(State Machine)입니다.

임베디드 오디오 드라이버를 개발하거나 디버깅할 때, 애플리케이션(예: TinyALSA, Android AudioFlinger)의 요청에 따라 하드웨어가 현재 어떤 상태에 놓여있는지 추적하는 핵심 지표가 됩니다.

 

다이어그램의 흐름에 따라 3가지 주요 단계로 나누어 동작 원리를 상세히 설명해 드리겠습니다.

 

 

1. 준비 및 설정 단계 (Initialization & Setup)

오디오 스트림이 생성되고 하드웨어가 동작할 준비를 하는 과정입니다.

       OPEN (스트림 열림)

              애플리케이션이 장치 노드(/dev/snd/pcmC0D0p 등)를 열었을 때의 초기 상태입니다.

              드라이버의 open 콜백이 실행되며, snd_pcm_runtime 구조체가 할당되지만, 샘플 레이트나 버퍼 크기 같은 물리적 하드웨어 파라미터는 아직 결정되지 않은 상태입니다.

       SETUP (하드웨어 파라미터 확정)

              애플리케이션이 hw_params ioctl을 호출하여 설정이 완료된 상태입니다.

              가장 중요한 작업: 이때 드라이버의 hw_params 콜백이 호출되며 실제 DMA 버퍼가 물리 메모리에 할당됩니다.

       PREPARED (재생/녹음 준비 완료)

              prepare ioctl이 호출된 직후의 상태입니다.

              드라이버의 prepare 콜백이 실행되어 하드웨어 레지스터(예: I2S/SAI 인터페이스, DMA 채널 설정)가 초기화되고, 버퍼 포인터(hw_ptr, appl_ptr)가 0으로 리셋되어 언제든 DMA 전송을 시작할 수 있는 장전 완료 상태입니다.

 

 

2. 실행 및 제어 단계 (Execution & Control)

실제 오디오 데이터가 DMA를 타고 흐르거나 제어되는 상태입니다.

       RUNNING (스트림 활성화)

              애플리케이션이 데이터를 쓰거나(START 트리거), 일정 수준 이상 버퍼가 차면 자동으로 진입합니다.

              드라이버의 trigger(START) 콜백이 호출되어 하드웨어 DMA 컨트롤러가 가동됩니다.

              이 상태에서는 인터럽트 핸들러가 주기적으로 snd_pcm_period_elapsed()를 호출하여 ALSA 코어에 진행 상황을 알립니다.

       PAUSED (일시 정지)

              애플리케이션이 일시 정지를 요청했을 때 진입합니다. 단, 드라이버가 SNDRV_PCM_INFO_PAUSE 기능을 지원한다고 명시했을 때만 가능합니다.

              DMA 전송은 멈추지만, 버퍼 내부의 데이터와 포인터 위치는 그대로 유지됩니다.

       DRAINING (버퍼 비우는 중)

              주로 재생(Playback) 시 발생하는 상태입니다. 애플리케이션이 "더 이상 보낼 데이터가 없으니 남은 것만 다 재생하고 종료해"라고 요청할 때 진입합니다.

              DMA가 남은 버퍼의 데이터를 모두 소비할 때까지 기다린 후 자동으로 SETUP 상태로 돌아갑니다.

 

 

3. 예외 및 복구 단계 (Exceptions & Recovery)

드라이버 개발 과정에서 가장 자주 마주치게 될 오류 및 예외 상태입니다. 다이어그램의 붉은색 및 회색 박스에 해당합니다.

       XRUN (Underrun / Overrun)

              오디오 끊김(Glitch)의 주범입니다.

              재생(Underrun): DMA(하드웨어)가 데이터를 읽어가는 속도보다 애플리케이션이 버퍼를 채우는 속도가 느려 버퍼가 고갈된 상태입니다.

              녹음(Overrun): 애플리케이션이 데이터를 제때 읽어가지 않아 DMA가 이전 데이터를 덮어써 버린 상태입니다.

              복구 메커니즘: 다이어그램의 화살표를 보시면 XRUN에서 RUNNING으로 바로 갈 수 없습니다. 반드시 애플리케이션이  snd_pcm_prepare()를 다시 호출하여 PREPARED 상태로 돌아간 후 재시작해야 합니다.

       SUSPENDED (전원 관리 대기)

              시스템이 절전 모드(Suspend to RAM 등)에 진입하여 하드웨어 전원이 차단된 상태입니다.

              시스템이 깨어나면(Resume), 애플리케이션은 상태를 확인하고 하드웨어 레지스터를 재설정하여 스트림을 복구해야 합니다.

       DISCONNECTED (디바이스 제거됨)

              오디오 재생 도중 USB 사운드 카드나 모듈이 물리적으로 뽑히면 진입합니다.

              이 상태가 되면 모든 ioctl 요청은 에러를 반환하며, 장치가 닫힐 때까지 대기합니다.

 

이 상태 머신을 완벽히 이해하는 것은 복잡한 오디오 시스템의 버그를 추적하는 첫걸음입니다.

실제 SoC 및 DSP 연동 환경에서 가장 골치 아픈 XRUN 발생 시 커널 레벨(Ftrace 등)에서의 디버깅 전략이나, 이 상태 머신의 상태 변화를 직접 유도하는 trigger 콜백의 세부 구현 패턴에 대해 이어서 살펴볼까요?

 

 

 

  3.7  완전한 PCM 드라이버 예제

 

 
  /* 간단한 더미 PCM 드라이버 (sound/drivers/dummy.c 기반) */
 
  static const struct snd_pcm_hardware my_pcm_hw = {
         .info = SNDRV_PCM_INFO_MMAP |
                     SNDRV_PCM_INFO_INTERLEAVED |
                     SNDRV_PCM_INFO_MMAP_VALID,
         .formats = SNDRV_PCM_FMTBIT_S16_LE,
         .rates = SNDRV_PCM_RATE_48000,
         .rate_min = 48000,
         .rate_max = 48000,
         .channels_min = 2,
         .channels_max = 2,
         .buffer_bytes_max = 64 * 1024,
         .period_bytes_min = 64,
         .period_bytes_max = 8 * 1024,
         .periods_min = 2,
         .periods_max = 32,
  };
 
  static int my_pcm_open(struct snd_pcm_substream *substream)
  {
         struct my_chip *chip = snd_pcm_substream_chip(substream);
         struct snd_pcm_runtime *runtime = substream->runtime;
 
         runtime->hw = my_pcm_hw;
         runtime->private_data = chip;
 
         /* 타이머 기반 period elapsed 시뮬레이션 */
         hrtimer_init(&chip->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
         chip->timer.function = my_hrtimer_callback;
 
         return 0;
  }
 
  static int my_pcm_close(struct snd_pcm_substream *substream)
  {
         struct my_chip *chip = snd_pcm_substream_chip(substream);
 
         hrtimer_cancel(&chip->timer);
         return 0;
  }
 
  static int my_pcm_hw_params(struct snd_pcm_substream *substream,
                                                        struct snd_pcm_hw_params *hw_params)
  {
         /* Managed buffer 사용 시 아무 작업 필요 없음 */
         return 0;
  }
 
  static int my_pcm_hw_free(struct snd_pcm_substream *substream)
  {
         return 0;
  }
 
  static int my_pcm_prepare(struct snd_pcm_substream *substream)
  {
         struct my_chip *chip = snd_pcm_substream_chip(substream);
         struct snd_pcm_runtime *runtime = substream->runtime;
 
         /* 하드웨어 레지스터 설정 (샘플레이트, 포맷 등) */
         chip->pcm_buffer_size = frames_to_bytes(runtime, runtime->buffer_size);
         chip->pcm_period_size = frames_to_bytes(runtime, runtime->period_size);
         chip->pcm_buf_pos = 0;
 
         return 0;
  }
 
  static int my_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
  {
         struct my_chip *chip = snd_pcm_substream_chip(substream);
 
         switch (cmd) {
         case SNDRV_PCM_TRIGGER_START:
         case SNDRV_PCM_TRIGGER_RESUME:
                /* DMA 시작 (또는 타이머 시작) */
                chip->running = 1;
                hrtimer_start(&chip->timer,
                                        ns_to_ktime(chip->period_time_ns),
                                        HRTIMER_MODE_REL);
                break;
 
         case SNDRV_PCM_TRIGGER_STOP:
         case SNDRV_PCM_TRIGGER_SUSPEND:
                /* DMA 중지 */
                chip->running = 0;
                hrtimer_cancel(&chip->timer);
                break;
 
         case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
                chip->running = 0;
                hrtimer_cancel(&chip->timer);
                break;
 
         case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
                chip->running = 1;
                hrtimer_start(&chip->timer,
                                        ns_to_ktime(chip->period_time_ns),
                                        HRTIMER_MODE_REL);
                break;
 
         default:
                return -EINVAL;
         }
 
         return 0;
  }
 
  static snd_pcm_uframes_t my_pcm_pointer(struct snd_pcm_substream *substream)
  {
         struct my_chip *chip = snd_pcm_substream_chip(substream);
         struct snd_pcm_runtime *runtime = substream->runtime;
 
         /* 현재 DMA 포인터를 frames 단위로 반환 */
         return bytes_to_frames(runtime, chip->pcm_buf_pos);
  }
 
  static const struct snd_pcm_ops my_pcm_ops = {
         .open            = my_pcm_open,
         .close           = my_pcm_close,
         .ioctl             = snd_pcm_lib_ioctl,
         .hw_params = my_pcm_hw_params,
         .hw_free       = my_pcm_hw_free,
         .prepare       = my_pcm_prepare,
         .trigger         = my_pcm_trigger,
         .pointer        = my_pcm_pointer,
  };
 
  /* Period elapsed 콜백 (타이머 또는 DMA IRQ) */
  static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer)
  {
         struct my_chip *chip = container_of(timer, struct my_chip, timer);
 
         if  (!chip->running)
                return HRTIMER_NORESTART;
 
         /* 포인터 진행 */
         chip->pcm_buf_pos += chip->pcm_period_size;
         if  (chip->pcm_buf_pos >= chip->pcm_buffer_size)
                chip->pcm_buf_pos = 0;
 
         /* Period elapsed 통지 (wake up user space) */
         snd_pcm_period_elapsed(chip->substream);
 
         /* 다음 period 타이머 예약 */
         hrtimer_forward_now(timer, ns_to_ktime(chip->period_time_ns));
         return HRTIMER_RESTART;
  }
 
snd_pcm_period_elapsed(): 이 함수는 하나의 period가 완료될 때마다 호출해야 합니다 (보통 DMA 인터럽트 핸들러에서). 이 호출로 사용자 공간의 poll()/select()를 깨워 다음 데이터 처리를 시작합니다.

 

 

 

  3.8  실전형 ALSA PCM 드라이버 스켈레톤 코드

 

하드웨어 초기화, 컴포넌트 등록, 제약 시스템, 그리고 상태 머신(State Machine)의 개념을 모두 통합한 실전형 ALSA PCM 드라이버 스켈레톤 코드를 작성해 드립니다.

이전 예제의 PCI 기반에서 벗어나, 현대적인 임베디드 SoC 환경(Platform Device 기반)과 외부 DSP 연동을 가정하여 작성했습니다. 특히 오디오 데이터가 TDM이나 I2S를 통해 흐르고, DMA 인터럽트가 발생하는 실무적인 구조를 반영했습니다.

 

 

실전형 임베디드 PCM 드라이버 스켈레톤 (soc_dsp_pcm.c)

 
  #include <linux/module.h>
  #include <linux/platform_device.h>
  #include <linux/interrupt.h>
  #include <sound/core.h>
  #include <sound/pcm.h>
  #include <sound/pcm_params.h>

  /* 하드웨어 제약 (외부 DSP 연동을 위한 고해상도 지원 가정) */
  static const struct snd_pcm_hardware soc_dsp_hw = {
               .info = SNDRV_PCM_INFO_MMAP |
                           SNDRV_PCM_INFO_MMAP_VALID |
                           SNDRV_PCM_INFO_INTERLEAVED |
                           SNDRV_PCM_INFO_PAUSE |
                           SNDRV_PCM_INFO_RESUME,
               .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
               .rates = SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_96000 | SNDRV_PCM_RATE_192000,
               .rate_min = 48000,
               .rate_max = 192000,
               .channels_min = 2,
               .channels_max = 8,  /* TDM 8-slot 가정 */
               .buffer_bytes_max = 512 * 1024,
               .period_bytes_min = 128,
               .period_bytes_max = 64 * 1024,
               .periods_min = 2,
               .periods_max = 64,
  };

 
  /* 칩셋 프라이빗 데이터 */
  struct soc_dsp_priv {
               struct snd_card *card;
               struct snd_pcm *pcm;
               struct snd_pcm_substream *substream;
 
               void __iomem *base_addr;
               int irq;
 
               /* DMA 상태 추적용 */
               unsigned int dma_pos;
               spinlock_t lock;
  };

  /* ---------------------------------------------------------
   * 1. PCM Operations 구현
   * --------------------------------------------------------- */
  static int soc_dsp_pcm_open(struct snd_pcm_substream *substream)
  {
               struct soc_dsp_priv *priv = snd_pcm_substream_chip(substream);
               struct snd_pcm_runtime *runtime = substream->runtime;

               priv->substream = substream;
               runtime->hw = soc_dsp_hw;

               /* Period 사이즈를 64 프레임 단위로 정렬 (하드웨어 Burst size 제약) */
               snd_pcm_hw_constraint_step(runtime, 0, SNDRV_PCM_HW_PARAM_PERIOD_SIZE, 64);

               return 0;
  }

  static int soc_dsp_pcm_close(struct snd_pcm_substream *substream)
  {
               struct soc_dsp_priv *priv = snd_pcm_substream_chip(substream);
               priv->substream = NULL;
               return 0;
  }

  static int soc_dsp_pcm_hw_params(struct snd_pcm_substream *substream,
                                                                 struct snd_pcm_hw_params *params)
  {
               /* ALSA 코어가 DMA 버퍼를 물리/가상 메모리에 안전하게 할당하도록 위임 */
               return snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(params));
  }

  static int soc_dsp_pcm_hw_free(struct snd_pcm_substream *substream)
  {
               return snd_pcm_lib_free_pages(substream);
  }

  static int soc_dsp_pcm_prepare(struct snd_pcm_substream *substream)
  {
               struct soc_dsp_priv *priv = snd_pcm_substream_chip(substream);
 
               spin_lock_irq(&priv->lock);
               priv->dma_pos = 0;
               /* TODO: 하드웨어 DMA 레지스터에 runtime->dma_addr 및 버퍼 크기 세팅 */
               spin_unlock_irq(&priv->lock);
 
               return 0;
  }

  static int soc_dsp_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
  {
               struct soc_dsp_priv *priv = snd_pcm_substream_chip(substream);
               int ret = 0;

               spin_lock(&priv->lock);
               switch (cmd) {
               case SNDRV_PCM_TRIGGER_START:
               case SNDRV_PCM_TRIGGER_RESUME:
               case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
                             /* TODO: 하드웨어 DMA 및 I2S/TDM TX Enable 비트 Set */
                              break;
 
               case SNDRV_PCM_TRIGGER_STOP:
               case SNDRV_PCM_TRIGGER_SUSPEND:
               case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
                              /* TODO: 하드웨어 DMA 및 I2S/TDM TX Disable 비트 Clear */
                              break;
 
               default:
                              ret = -EINVAL;
               }
               spin_unlock(&priv->lock);
 
               return ret;
  }

  static snd_pcm_uframes_t soc_dsp_pcm_pointer(struct snd_pcm_substream *substream)
  {
               struct soc_dsp_priv *priv = snd_pcm_substream_chip(substream);
               unsigned int current_dma_pos;

               /* 실제 하드웨어 DMA 레지스터를 읽어 현재 전송 위치를 바이트 단위로 가져옴 */
               spin_lock(&priv->lock);
               current_dma_pos = priv->dma_pos;   /* 임시 변수 사용. 실제론 readl(priv->base_addr + OFFSET) */
               spin_unlock(&priv->lock);

               /* 바이트 오프셋을 Frame 단위로 변환하여 ALSA 코어에 반환 */
               return bytes_to_frames(substream->runtime, current_dma_pos);
  }

  static const struct snd_pcm_ops soc_dsp_pcm_ops = {
               .open = soc_dsp_pcm_open,
               .close = soc_dsp_pcm_close,
               .ioctl = snd_pcm_lib_ioctl,
               .hw_params = soc_dsp_pcm_hw_params,
               .hw_free = soc_dsp_pcm_hw_free,
               .prepare = soc_dsp_pcm_prepare,
               .trigger = soc_dsp_pcm_trigger,
               .pointer = soc_dsp_pcm_pointer,
  };

  /* ---------------------------------------------------------
   * 2. 인터럽트 핸들러 (Period 도달 시 호출)
   * --------------------------------------------------------- */
  static irqreturn_t soc_dsp_irq_handler(int irq, void *dev_id)
  {
               struct soc_dsp_priv *priv = dev_id;
               struct snd_pcm_substream *substream = priv->substream;

               /* TODO: 하드웨어 인터럽트 상태 레지스터 확인 및 Clear */
 
               if (substream && snd_pcm_running(substream)) {
                              /* DMA 위치 업데이트 로직 (가상) */
                              priv->dma_pos += snd_pcm_lib_period_bytes(substream);
                              if (priv->dma_pos >= snd_pcm_lib_buffer_bytes(substream))
                                             priv->dma_pos = 0;   /* Ring buffer wrap-around */

                              /* ALSA 코어에 Period가 경과했음을 알림 -> 포인터 갱신 및 데이터 요청 발생 */
                              snd_pcm_period_elapsed(substream);
               }

               return IRQ_HANDLED;
  }

  /* ---------------------------------------------------------
   * 3. 디바이스 등록 (컴포넌트 시스템 활용)
   * --------------------------------------------------------- */
  static int soc_dsp_pcm_new(struct soc_dsp_priv *priv)
  {
               struct snd_pcm *pcm;
               int err;

               /* PCM 디바이스 생성 (재생 1개, 녹음 0개 가정) */
               err = snd_pcm_new(priv->card, "SoC_DSP_PCM", 0, 1, 0, &pcm);
               if (err < 0)
                              return err;

               pcm->private_data = priv;
               strcpy(pcm->name, "SoC DSP High-Res Audio");
               priv->pcm = pcm;

               /* Playback 스트림에 Ops 연결 */
               snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &soc_dsp_pcm_ops);

               /* DMA 버퍼 사전 할당 (연속된 물리 메모리 확보) */
               snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,
                                                                                      priv->card->dev,
                                                                                      64 * 1024, 512 * 1024);
               return 0;
  }
 
 
 

코드의 핵심 설계 포인트

  1. 동기화 (Spinlock): prepare, trigger, pointer, 그리고 인터럽트 핸들러 간의 동시 접근을 막기 위해 spinlock_t를 사용했습니다. trigger와 인터럽트는 원자적(Atomic) 컨텍스트에서 실행되므로 일반 Mutex를 쓰면 커널 패닉이 발생합니다.
  2. DMA 버퍼 할당 위임: hw_params 콜백에서 메모리를 직접 kmalloc하지 않고, snd_pcm_lib_malloc_pages 헬퍼 함수를 사용했습니다. 이는 메모리 단편화를 방지하고 하드웨어 DMA 제약(CMA 메모리 영역 등)을 안전하게 만족시키는 표준 방식입니다.
  3. 링 버퍼 랩어라운드 (Wrap-around): 인터럽트 핸들러 내부에서 DMA가 전체 버퍼 끝에 도달했을 때 priv->dma_pos를 0으로 되돌려 연속 재생을 보장합니다.

 

 

한 단계 더 나아가기

이 코드는 순수 ALSA 코어만을 사용한 스켈레톤입니다. 하지만 실제 i.MX8M Plus와 같은 최신 SoC에서는 이보다 한 단계 더 추상화된 ASoC (ALSA System on Chip) 프레임워크를 사용하여 드라이버를 CPU DAI(I2S 제어), Codec(DSP 제어), Platform(DMA 제어) 세 가지로 분리하여 모듈화합니다.

 

순수 ALSA 코드로 하드웨어 제어의 감을 잡으셨다면, 이제 이를 모듈화하여 관리하는 ASoC의 머신 드라이버(Machine Driver) 구조와 Device Tree(DTS) 바인딩 방법으로 넘어가 볼까요?

 

 

  3.9  ASoC Machine Driver 구조와, Device Tree(DTS) 바인딩 방법

 

순수 ALSA 코어 드라이버가 하나의 거대한 통짜 코드(Monolithic)라면, ASoC(ALSA System on Chip) 재사용성을 극대화하기 위해 오디오 시스템을 레고 블록처럼 철저하게 분해한 프레임워크입니다.

모바일, 차량용 인포테인먼트(AAOS), 스마트 스피커 등 복잡한 임베디드 오디오 아키텍처는 대부분 이 ASoC 기반으로 동작합니다.

 

1. ASoC의 3대 컴포넌트

ASoC는 하드웨어를 세 가지 독립적인 드라이버로 분리합니다.

       CPU DAI (Digital Audio Interface): SoC(메인 프로세서) 내부의 오디오 하드웨어 인터페이스를 제어합니다. i.MX8M Plus의 경우 SAI(Synchronous Audio Interface) 모듈이 여기에 해당하며, I2S나 TDM 포맷으로 데이터를 밀어내거나 받는 역할을 합니다.

       Codec: SoC 외부에 연결된 실제 오디오 처리 칩을 제어합니다. ADSP-21569와 같은 오디오 DSP나 ADC/DAC 칩이 해당됩니다. 볼륨 제어(Mixer), 아날로그 경로 제어, 자체적인 오디오 포맷 설정 등을 담당합니다.

       Platform (DMA): 시스템 메모리(RAM)와 CPU DAI 사이의 데이터 이동을 담당하는 DMA 컨트롤러 드라이버입니다. 앞서 살펴본 snd_pcm_period_elapsed() 호출과 버퍼 관리가 여기서 이루어집니다. i.MX의 경우 SDMA(Smart DMA) 드라이버가 이 역할을 수행합니다.

 

 

2. Machine Driver: 조립 설명서

위의 세 가지 드라이버는 서로의 존재를 모릅니다. 이들을 하나의 완벽한 사운드 카드로 묶어주는 접착제(Glue) 역할을 하는 것이 바로 머신 드라이버(Machine Driver)입니다.

머신 드라이버의 핵심은 snd_soc_dai_link 구조체 배열입니다. "어떤 CPU DAI를, 어떤 Codec에, 어떤 형식(I2S/TDM)으로 연결할 것인가"를 정의합니다.

 

 
  /* Custom Machine Driver의 DAI Link 설정 예시 */
  static struct snd_soc_dai_link imx_adsp_dai_link[] = {
  {
                .name = "ADSP-21569 HiFi",
                .stream_name = "HiFi Audio",
 
                /* 1. CPU DAI 지정 (i.MX8M Plus의 SAI1 모듈) */
                .cpu_dai_name = "30010000.sai",
 
                /* 2. Codec 지정 (SPI/I2C 등으로 제어되는 외부 DSP) */
                .codec_name = "spi0.0",
                .codec_dai_name = "adsp-21569-dai",
 
                /* 3. Platform(DMA) 지정 (최신 커널에서는 보통 CPU DAI 노드와 동일하게 매핑) */
                .platform_name = "30010000.sai",
 
                /* 4. 오디오 포맷 및 클럭 마스터 설정 */
                /* I2S 포맷 사용, Codec(DSP)이 Bit Clock과 Frame Sync를 제공하는 Master 역할 */
                .dai_fmt = SND_SOC_DAIFMT_I2S |
                SND_SOC_DAIFMT_NB_NF |
                SND_SOC_DAIFMT_CBP_CFP,
 
                .ops = &imx_adsp_ops,   /* hw_params 등에서 동적 클럭 설정용 콜백 */
        },
  };
 

 

 

3. Device Tree (DTS) 바인딩

과거에는 위처럼 C 코드로 하드코딩된 머신 드라이버를 직접 작성해야 했지만, 최근 리눅스 커널(AAOS 포함)에서는 simple-audio-cardaudio-graph-card라는 범용 머신 드라이버를 제공합니다.

따라서 복잡한 C 코드 작성 없이 DTS(Device Tree Source) 파일만 수정하여 ASoC 토폴로지를 구성하는 것이 실무의 표준입니다.

DTS

 
  /* i.MX8M Plus와 외부 DSP를 연결하는 DTS 예시 */

  / {
        /* 범용 머신 드라이버 사용 선언 */
        sound-adsp {
                compatible = "simple-audio-card";
                simple-audio-card,name = "imx8mp-adsp-audio";
 
                /* 오디오 경로 이름표 붙이기 (DAPM 라우팅) */
                simple-audio-card,routing =
                        "Line Out", "DSP_OUT",
                        "DSP_IN", "Mic In";

                /* DAI Link 정의 */
                simple-audio-card,dai-link@0 {
                        format = "i2s";
 
                        /* 외부 DSP가 BCLK와 LRCLK를 밀어주는 Master로 동작함을 선언 */
                        bitclock-master = <&codec_dai>;
                        frame-master = <&codec_dai>;

                        /* CPU 측 (i.MX8M Plus SAI1) */
                        cpu_dai: cpu {
                                sound-dai = <&sai1>;
                        };

                        /* Codec 측 (외부 ADSP-21569) */
                        codec_dai: codec {
                                sound-dai = <&adsp21569>;
                        };
                };
        };
  };

  /* SoC 내부의 SAI1 활성화 및 DMA 설정 */
  &sai1 {
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_sai1>;
        assigned-clocks = <&clk IMX8MP_CLK_SAI1>;
        assigned-clock-parents = <&clk IMX8MP_AUDIO_PLL1_OUT>;
        assigned-clock-rates = <12288000>;
        status = "okay";
  };

  /* SPI/I2C 버스에 물려있는 Codec 드라이버 활성화 */
  &ecspi1 {
        status = "okay";
 
        adsp21569: dsp@0 {
                compatible = "adi,adsp-21569";
                reg = <0>;
                spi-max-frequency = <10000000>;
                #sound-dai-cells = <0>;  /* 이 노드가 ASoC Codec DAI임을 시스템에 알림 */
        };
  };

 

 

 

핵심 요약 (실무 엔지니어 관점)

  1. 독립성 보장: ASoC를 사용하면 오디오 칩셋(Codec)이 변경되어도 CPU DAI나 DMA 코드를 단 한 줄도 수정할 필요가 없습니다. DTS에서 sound-dai = <&새로운_코덱>; 한 줄만 바꾸면 됩니다.
  2. 클럭 주도권(Master/Slave): DTS의 bitclock-master 설정은 매우 치명적입니다. i.MX8M(CPU)과 ADSP-21569(Codec) 중 누가 MCLK를 분주하여 BCLK/LRCLK를 생성할지 명확히 정의하지 않으면, 양쪽에서 클럭을 쏘거나 아무도 쏘지 않아 오디오 스트림이 멈추는(XRUN 또는 Timeout) 현상이 발생합니다.

 

다음 단계 제안

이제 오디오 시스템을 조립하는 큰 그림(DTS와 머신 드라이버)이 완성되었습니다.

SoC와 DSP 간의 연결 구조를 잡았다면, 실제로 오디오 데이터가 흐를 때 팝 노이즈(Pop noise)를 방지하고 전력 소모를 최소화하기 위해 오디오 경로의 전원을 순차적으로 켜고 끄는 DAPM (Dynamic Audio Power Management) 위젯 라우팅 기법에 대해 알아보는 것은 어떨까요?