Rockchip RK3399 - ALSA 声卡之PCM设备
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4开发板
eMMC :16GB
LPDDR3 :4GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot :2023.04
linux :6.3
----------------------------------------------------------------------------------------------------------------------------
我们在Rockchip RK3399 - ALC5651 & I2S基础中实际上已经介绍过PCM,它是一种音频编码格式,更确切的说是一种将声音从模拟信号转换成数字信号的技术。
音频驱动的两大核心任务就是:
- playback :如何把用户空间的应用程序发过来的PCM数据,转化为人耳可以辨别的模拟音频;
- capture :把mic拾取到得模拟信号,经过采样、量化,转换为PCM数据送回给用户空间的应用程序;
ALSA CORE已经实现了PCM中间层,我们编写的驱动都是调用PCM相关的API,基本上只需要实现底层的需要访问硬件的函数即可;
- include/sound/pcm.h:提供访问 PCM 中间层代码的API;
- include/sound/pcm_params.h:提供访问一些与hw_param 相关的函数;
一、核心数据结构
1.1 ALSA中的数据结构
1.1.1 struct pcm_new
pcm使用struct snd_pcm数据结构来描述,一个pcm实例由一个playback stream和一个capture stream组成,这两个stream又分别有一个或多个substreams组成。
在嵌入式系统中,通常不会像图中这么复杂,大多数情况下是一个声卡,一个pcm实例,pcm下面有一个playback stream和capture stream,playback和capture下面各自有一个substream。
snd_pcm数据结构定义在include/sound/pcm.h;
struct snd_pcm { struct snd_card *card; struct list_head list; int device; /* device number */ unsigned int info_flags; unsigned short dev_class; unsigned short dev_subclass; char id[64]; char name[80]; struct snd_pcm_str streams[2]; struct mutex open_mutex; wait_queue_head_t open_wait; void *private_data; void (*private_free) (struct snd_pcm *pcm); bool internal; /* pcm is for internal use only */ bool nonatomic; /* whole PCM operations are in non-atomic context */ bool no_device_suspend; /* don't invoke device PM suspend */ #if IS_ENABLED(CONFIG_SND_PCM_OSS) struct snd_pcm_oss oss; #endif };
这里重要的变量如下:
- card:声卡设备;
- device:pcm设备编号;
- streams:数组长度为2,元素0指向playback stream设备,元素1指向capture stream设备;
- private_data:在很多数据结构里面都可以看到,一般用于指向私有数据;
1.1.2 struct snd_pcm_str
struct snd_pcm_str用于表示pcm stream,定义在include/sound/pcm.h;
struct snd_pcm_str { int stream; /* stream (direction) */ struct snd_pcm *pcm; /* -- substreams -- */ unsigned int substream_count; unsigned int substream_opened; struct snd_pcm_substream *substream; #if IS_ENABLED(CONFIG_SND_PCM_OSS) /* -- OSS things -- */ struct snd_pcm_oss_stream oss; #endif #ifdef CONFIG_SND_VERBOSE_PROCFS struct snd_info_entry *proc_root; #ifdef CONFIG_SND_PCM_XRUN_DEBUG unsigned int xrun_debug; /* 0 = disabled, 1 = verbose, 2 = stacktrace */ #endif #endif struct snd_kcontrol *chmap_kctl; /* channel-mapping controls */ struct device dev; };
snd_pcm_str的主要作用是指向snd_pcm_substream,而snd_pcm_substream可以有多个,这也是snd_pcm_str存在的原因,否则snd_pcm直接指向snd_pcm_substream就可以了。
其中:
- stream:类类型,比如SNDRV_PCM_STREAM_PLAYBACK表示播放,SNDRV_PCM_STREAM_CAPTURE表示捕获;
- pcm:指向pcm设备;
- substream_count:substream的个数;
- substream_opened:substream打开标志;
- substream:用于保存所有的substream,struct snd_pcm_substream实是一个链表节点类型的数据结构;
- dev:设备驱动模型中的device,可以将snd_pcm_str看做其子类;
1.1.3 struct snd_pcm_substream
struct snd_pcm_substream用于表示pcm substream,定义在include/sound/pcm.h;
struct snd_pcm_substream { struct snd_pcm *pcm; struct snd_pcm_str *pstr; void *private_data; /* copied from pcm->private_data */ int number; char name[32]; /* substream name */ int stream; /* stream (direction) */ struct pm_qos_request latency_pm_qos_req; /* pm_qos request */ size_t buffer_bytes_max; /* limit ring buffer size */ struct snd_dma_buffer dma_buffer; size_t dma_max; /* -- hardware operations -- */ const struct snd_pcm_ops *ops; /* -- runtime information -- */ struct snd_pcm_runtime *runtime; /* -- timer section -- */ struct snd_timer *timer; /* timer */ unsigned timer_running: 1; /* time is running */ long wait_time; /* time in ms for R/W to wait for avail */ /* -- next substream -- */ struct snd_pcm_substream *next; /* -- linked substreams -- */ struct list_head link_list; /* linked list member */ struct snd_pcm_group self_group; /* fake group for non linked substream (with substream lock inside) */ struct snd_pcm_group *group; /* pointer to current group */ /* -- assigned files -- */ int ref_count; atomic_t mmap_count; unsigned int f_flags; void (*pcm_release)(struct snd_pcm_substream *); struct pid *pid; #if IS_ENABLED(CONFIG_SND_PCM_OSS) /* -- OSS things -- */ struct snd_pcm_oss_substream oss; #endif #ifdef CONFIG_SND_VERBOSE_PROCFS struct snd_info_entry *proc_root; #endif /* CONFIG_SND_VERBOSE_PROCFS */ /* misc flags */ unsigned int hw_opened: 1; };
snd_pcm_substream的内容有些多,此处只需要重要的进行介绍;
- ops:pcm操作集,这部分具体的操作需要实现者的参与,留给实现者的函数指针集。这个和文件操作的设计策略是一致的;
- runtime:pcm运行时实例,读写数据的时候由它来控制;
- next:指向下一个pcm substream,用于将多个snd_pcm_substream对象链接起来;
- pstrt:指向所属的pcm stream;
- group:在用户空间可以通过SNDRV_PCM_IOCTL_LINK将多个substream链接起来。然后就可以对这些对象进行统一的操作。
1.1.4 snd_pcm_hw_params
struct snd_pcm_hw_params定义在include/uapi/sound/asound.h,是用于配置音频硬件参数的结构体,比如通道数、采样率、数据格式等;
struct snd_pcm_hw_params { unsigned int flags; struct snd_mask masks[SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]; struct snd_mask mres[5]; /* reserved masks */ struct snd_interval intervals[SNDRV_PCM_HW_PARAM_LAST_INTERVAL - // 宏的值为19 SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]; // 宏的值为8 struct snd_interval ires[9]; /* reserved intervals */ unsigned int rmask; /* W: requested masks */ unsigned int cmask; /* R: changed masks */ unsigned int info; /* R: Info flags for returned setup */ unsigned int msbits; /* R: used most significant bits */ unsigned int rate_num; /* R: rate numerator */ unsigned int rate_den; /* R: rate denominator */ snd_pcm_uframes_t fifo_size; /* R: chip FIFO size in frames */ unsigned char reserved[64]; /* reserved for future */ };
该结构体定义了一组成员变量,用于存储和管理音频硬件参数的信息。下面是各成员变量的含义:
- flags:标志位,用于指示参数的特性和状态。
- masks:用于描述硬件参数的位掩码。在数组中的每个元素都代表一个具体的参数;
- mres:保留的位掩码;
- intervals:这是一个数组,数组长度为12,用于描述硬件参数的取值范围;每一个元素依次表示的意义为:
- SNDRV_PCM_HW_PARAM_SAMPLE_BITS:Bits per sample ,每个采样点的位数,即位宽度;
- SNDRV_PCM_HW_PARAM_FRAME_BITS:Bits per frame,每帧所占用的位数,假设有一个音频流,采样率为 44100 Hz,声道数为 2(立体声),每个采样点的比特数为 16bit。在这种情况下,每个采样点占用 16 个比特,每帧就包含了双声道的采样点数据。因此,每帧的比特数可以计算为:16 bits/sample × 2 channels = 32 bits/frame;
- SNDRV_PCM_HW_PARAM_CHANNELS:Channels,通道数;
- SNDRV_PCM_HW_PARAM_RATE:Approx rate,近似采样率;
- SNDRV_PCM_HW_PARAM_PERIOD_TIME:Approx distance between interrupts in us;
- SNDRV_PCM_HW_PARAM_PERIOD_SIZE:Approx frames between interrupts;
- SNDRV_PCM_HW_PARAM_PERIOD_BYTES:Approx bytes between interrupts;
- SNDRV_PCM_HW_PARAM_PERIODS:Approx interrupts per buffer;
- SNDRV_PCM_HW_PARAM_BUFFER_TIME:Approx duration of buffer in us;
- SNDRV_PCM_HW_PARAM_BUFFER_SIZE:Size of buffer in frames;
- SNDRV_PCM_HW_PARAM_BUFFER_BYTES:Size of buffer in bytes
- SNDRV_PCM_HW_PARAM_TICK_TIME:Approx tick duration in us;
- ires:保留的取值范围;
- rmask:请求的位掩码;
- cmask:变化的位掩码;
- info:返回的设置信息标志,用于指示配置的相关信息;
- msbits:使用的最高有效位数;
- rate_num:采样率的分子部分(分子/分母构成实际的采样率);
- rate_den:采样率的分母部分;
- fifo_size:芯片 FIFO(First In First Out)缓冲区的大小;
- reserved:保留字段,用于未来的扩展;
其中struct snd_interval数据结构定义如下:
struct snd_interval { unsigned int min, max; unsigned int openmin:1, openmax:1, integer:1, empty:1; };
1.2 ASoC中的数据结构
1.2.1 struct snd_soc_pcm_stream
struct snd_soc_pcm_stream是ASoC中定义的pcm stream,定义在include/sound/soc.h,用于描述SoC pcm stream的信息;
/* SoC PCM stream information */ struct snd_soc_pcm_stream { const char *stream_name; u64 formats; /* SNDRV_PCM_FMTBIT_* */ unsigned int rates; /* SNDRV_PCM_RATE_* */ unsigned int rate_min; /* min rate */ unsigned int rate_max; /* max rate */ unsigned int channels_min; /* min channels */ unsigned int channels_max; /* max channels */ unsigned int sig_bits; /* number of bits of content */ };
其中:
- stream_name:stream名称;
- formats:位深度,也就是数据传输的位宽;
- rate:音频采样率;
- rate_min:最低采样率;
- rate_max:最高采样率;
- channels_min:最少通道数;
- channels_max:最大通道数;
- sig_bits:采样位宽;
位深度种类很多,比如:

#define SNDRV_PCM_FMTBIT_S8 _SNDRV_PCM_FMTBIT(S8) // 8位整数采样格式 #define SNDRV_PCM_FMTBIT_U8 _SNDRV_PCM_FMTBIT(U8) #define SNDRV_PCM_FMTBIT_S16_LE _SNDRV_PCM_FMTBIT(S16_LE) // 16为整数采样格式(小端) #define SNDRV_PCM_FMTBIT_S16_BE _SNDRV_PCM_FMTBIT(S16_BE) #define SNDRV_PCM_FMTBIT_U16_LE _SNDRV_PCM_FMTBIT(U16_LE) #define SNDRV_PCM_FMTBIT_U16_BE _SNDRV_PCM_FMTBIT(U16_BE) #define SNDRV_PCM_FMTBIT_S24_LE _SNDRV_PCM_FMTBIT(S24_LE) #define SNDRV_PCM_FMTBIT_S24_BE _SNDRV_PCM_FMTBIT(S24_BE) #define SNDRV_PCM_FMTBIT_U24_LE _SNDRV_PCM_FMTBIT(U24_LE) #define SNDRV_PCM_FMTBIT_U24_BE _SNDRV_PCM_FMTBIT(U24_BE) #define SNDRV_PCM_FMTBIT_S32_LE _SNDRV_PCM_FMTBIT(S32_LE) #define SNDRV_PCM_FMTBIT_S32_BE _SNDRV_PCM_FMTBIT(S32_BE) #define SNDRV_PCM_FMTBIT_U32_LE _SNDRV_PCM_FMTBIT(U32_LE) #define SNDRV_PCM_FMTBIT_U32_BE _SNDRV_PCM_FMTBIT(U32_BE) #define SNDRV_PCM_FMTBIT_FLOAT_LE _SNDRV_PCM_FMTBIT(FLOAT_LE) #define SNDRV_PCM_FMTBIT_FLOAT_BE _SNDRV_PCM_FMTBIT(FLOAT_BE)
音频采样率种类很多,比如:

/* If you change this don't forget to change rates[] table in pcm_native.c */ #define SNDRV_PCM_RATE_5512 (1<<0) /* 5512Hz */ #define SNDRV_PCM_RATE_8000 (1<<1) /* 8000Hz */ #define SNDRV_PCM_RATE_11025 (1<<2) /* 11025Hz */ #define SNDRV_PCM_RATE_16000 (1<<3) /* 16000Hz */ #define SNDRV_PCM_RATE_22050 (1<<4) /* 22050Hz */ #define SNDRV_PCM_RATE_32000 (1<<5) /* 32000Hz */ #define SNDRV_PCM_RATE_44100 (1<<6) /* 44100Hz */ #define SNDRV_PCM_RATE_48000 (1<<7) /* 48000Hz */ #define SNDRV_PCM_RATE_64000 (1<<8) /* 64000Hz */ #define SNDRV_PCM_RATE_88200 (1<<9) /* 88200Hz */ #define SNDRV_PCM_RATE_96000 (1<<10) /* 96000Hz */ #define SNDRV_PCM_RATE_176400 (1<<11) /* 176400Hz */ #define SNDRV_PCM_RATE_192000 (1<<12) /* 192000Hz */ #define SNDRV_PCM_RATE_CONTINUOUS (1<<30) /* continuous range */ #define SNDRV_PCM_RATE_KNOT (1<<31) /* supports more non-continuos rates */ #define SNDRV_PCM_RATE_8000_44100 (SNDRV_PCM_RATE_8000|SNDRV_PCM_RATE_11025|\ SNDRV_PCM_RATE_16000|SNDRV_PCM_RATE_22050|\ SNDRV_PCM_RATE_32000|SNDRV_PCM_RATE_44100) #define SNDRV_PCM_RATE_8000_48000 (SNDRV_PCM_RATE_8000_44100|SNDRV_PCM_RATE_48000) #define SNDRV_PCM_RATE_8000_96000 (SNDRV_PCM_RATE_8000_48000|SNDRV_PCM_RATE_64000|\ SNDRV_PCM_RATE_88200|SNDRV_PCM_RATE_96000) #define SNDRV_PCM_RATE_8000_192000 (SNDRV_PCM_RATE_8000_96000|SNDRV_PCM_RATE_176400|\ SNDRV_PCM_RATE_192000)
1.2.2 struct snd_soc_pcm_runtime
struct snd_soc_pcm_runtime是ASoC中定义的pcm runtime,定义在include/sound/soc.h,这个数据结构非常用重要;在注册ASoC声卡时会调用soc_new_pcm_runtime为每一个音频数据链路(struct snd_soc_dai_link)分配一个snd_soc_pcm_runtime ,snd_soc_pcm_runtime 是ASoC的桥梁,保存音频数据链路的dai、以及dai所属的component等信息;
/* SoC machine DAI configuration, glues a codec and cpu DAI together */ struct snd_soc_pcm_runtime { struct device *dev; struct snd_soc_card *card; struct snd_soc_dai_link *dai_link; struct snd_pcm_ops ops; unsigned int params_select; /* currently selected param for dai link */ /* Dynamic PCM BE runtime data */ struct snd_soc_dpcm_runtime dpcm[SNDRV_PCM_STREAM_LAST + 1]; struct snd_soc_dapm_widget *c2c_widget[SNDRV_PCM_STREAM_LAST + 1]; long pmdown_time; /* runtime devices */ struct snd_pcm *pcm; struct snd_compr *compr; /* * dais = cpu_dai + codec_dai * see * soc_new_pcm_runtime() * asoc_rtd_to_cpu() * asoc_rtd_to_codec() */ struct snd_soc_dai **dais; struct delayed_work delayed_work; void (*close_delayed_work_func)(struct snd_soc_pcm_runtime *rtd); #ifdef CONFIG_DEBUG_FS struct dentry *debugfs_dpcm_root; #endif unsigned int num; /* 0-based and monotonic increasing */ struct list_head list; /* rtd list of the soc card */ /* function mark */ struct snd_pcm_substream *mark_startup; struct snd_pcm_substream *mark_hw_params; struct snd_pcm_substream *mark_trigger; struct snd_compr_stream *mark_compr_startup; /* bit field */ unsigned int pop_wait:1; unsigned int fe_compr:1; /* for Dynamic PCM */ int num_components; struct snd_soc_component *components[]; /* CPU/Codec/Platform */ };
其中:
- dev:设备驱动模型中的device,可以将snd_soc_pcm_runtime看做其子类;
- dev->parent一般会被设置为ASoC声卡card->dev,以后面我们将要介绍的machine驱动(simple-card)为例,card->dev为rt5651-sound设备;
- dev->release一般会被设置为soc_release_rtd_dev;
- dev设备名称会为设置为snd_soc_dai_link的名称,比如ALC5651音频驱动,这个名称为"rockchip-i2s-rt5651";
- dev设备驱动数据会被设置为当前snd_soc_pcm_runtime ;
- card:指向ASoC声卡的指针;
- dai_link:指向音频数据链路的指针;
- dpcm:Dynamic PCM BE runtime data;
- dais:指向一个指针数组,指针数组中每个元素指向一个dai,用于保存当前音频数据链路上的dai,包括cpu dai、codec dai;
- delayed_word:延迟工作;
- num:为ASoC声卡设备的每一个pcm runtime会分配一个编号,从0开始;
- pmdown_time:下电超时时间;
- list:链表节点,用于构建双向链表,将当前pcm runtime添加到ASoC声卡的rtd_list链表中;
- components:指向一个数组,用于存储当前音频数据链路上的代表platform以及codec设备的component;
- num_components:components数组的长度;
如果你去比较snd_soc_dai_link和soc_new_pcm_runtime数据结构,你就可以发现这个数据结构实际上都是用来描述音频数据链路的,只不过一个描述的是静态数据,另一个描述是动态数据。
1.3 关系图
下面一张图列出了PCM中间层几个重要的结构,可以让我们从uml的角度看一看这列结构的关系,理清他们之间的关系,对我们理解PCM中间层的实现方式。
其中我们比较关注的点是:
- pcm是挂在snd_card成员devices链表下面的一个snd_device;
- snd_pcm中的字段:streams[2],该数组中的两个元素指向两个snd_pcm_str结构,分别代表playback stream和capture stream;
- snd_pcm_str中的substream字段,指向snd_pcm_substream结构;
- snd_pcm_substream是pcm中间层的核心,绝大部分任务都是在substream中处理,尤其是它的ops(snd_pcm_ops)字段,许多user空间的应用程序通过alsa-lib对驱动程序的请求都是由该结构中的函数处理。它的runtime字段则指向snd_pcm_runtime结构,snd_pcm_runtime记录这substream的一些重要的软件和硬件运行环境和参数;
二、核心API
2.1 ALSA创建PCM设备(snd_pcm_new)
在ALSA中,PCM设备的创建可以通过snd_pcm_new函数来完成,函数接收6个参数:
- card:ALSA声卡设备;
- id:pcm设备唯一标识;
- device:设备号,表示目前创建的是该声卡下的第几个pcm,第一个pcm设备从0开始;
- playback_count: 表示该pcm将会有几个playback substream;
- capture_count:表示该pcm将会有几个capture substream;
- rpcm:保存创建的pcm设备;
函数定义在sound/core/pcm.c,
/** * snd_pcm_new - create a new PCM instance * @card: the card instance * @id: the id string * @device: the device index (zero based) * @playback_count: the number of substreams for playback * @capture_count: the number of substreams for capture * @rpcm: the pointer to store the new pcm instance * * Creates a new PCM instance. * * The pcm operators have to be set afterwards to the new instance * via snd_pcm_set_ops(). * * Return: Zero if successful, or a negative error code on failure. */ int snd_pcm_new(struct snd_card *card, const char *id, int device, int playback_count, int capture_count, struct snd_pcm **rpcm) { return _snd_pcm_new(card, id, device, playback_count, capture_count, false, rpcm); }
这段代码实际上就是调用_snd_pcm_new创建一个pcm_new实例,并根据playback_count、capture_count等参数初始化playback stream和capture stream,card、device等成员;
static int _snd_pcm_new(struct snd_card *card, const char *id, int device, int playback_count, int capture_count, bool internal, struct snd_pcm **rpcm) { struct snd_pcm *pcm; int err; static const struct snd_device_ops ops = { // 声卡pcm设备操作集 .dev_free = snd_pcm_dev_free, .dev_register = snd_pcm_dev_register, .dev_disconnect = snd_pcm_dev_disconnect, }; static const struct snd_device_ops internal_ops = { .dev_free = snd_pcm_dev_free, }; if (snd_BUG_ON(!card)) return -ENXIO; if (rpcm) *rpcm = NULL; pcm = kzalloc(sizeof(*pcm), GFP_KERNEL); // 动态申请内存,申请一个snd_pcm数据结构 if (!pcm) return -ENOMEM; pcm->card = card; // 设置声卡设备 pcm->device = device; // 设置pcm设备编号 pcm->internal = internal; // 设置内部标志位 mutex_init(&pcm->open_mutex); // 初始化互斥锁 init_waitqueue_head(&pcm->open_wait); // 初始化等待队列头 INIT_LIST_HEAD(&pcm->list); // 初始化链表节点 if (id) strscpy(pcm->id, id, sizeof(pcm->id)); // 设置id err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count); // 创建playback stream if (err < 0) goto free_pcm; err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count); // 创建capture stream if (err < 0) goto free_pcm; err = snd_device_new(card, SNDRV_DEV_PCM, pcm, // device_data设置为pcm internal ? &internal_ops : &ops); // 创建一个新的snd_device实例,并将添加到声卡设备的devices链表中 if (err < 0) goto free_pcm; if (rpcm) *rpcm = pcm; // 写回 return 0; free_pcm: snd_pcm_free(pcm); return err; }
2.1.1 snd_pcm_new_stream
这里我们需要关注一下pcm stream的创建函数snd_pcm_new_stream,第一个参数为pcm实例,第二个参数为pcm数据流方向,即playback还是capture,第三个参数为substream数量;
/** * snd_pcm_new_stream - create a new PCM stream * @pcm: the pcm instance * @stream: the stream direction, SNDRV_PCM_STREAM_XXX * @substream_count: the number of substreams * * Creates a new stream for the pcm. * The corresponding stream on the pcm must have been empty before * calling this, i.e. zero must be given to the argument of * snd_pcm_new(). * * Return: Zero if successful, or a negative error code on failure. */ int snd_pcm_new_stream(struct snd_pcm *pcm, int stream, int substream_count) // 假设pcm->internal=0 { int idx, err; struct snd_pcm_str *pstr = &pcm->streams[stream]; struct snd_pcm_substream *substream, *prev; #if IS_ENABLED(CONFIG_SND_PCM_OSS) mutex_init(&pstr->oss.setup_mutex); #endif pstr->stream = stream; // 设置pcm数据流方向,即playback还是capture pstr->pcm = pcm; // 设置pcm实例 pstr->substream_count = substream_count; // substream数量 if (!substream_count) return 0; snd_device_initialize(&pstr->dev, pcm->card); // 初始化pstr->dev成员 pstr->dev.groups = pcm_dev_attr_groups; pstr->dev.type = &pcm_dev_type; dev_set_name(&pstr->dev, "pcmC%iD%i%c", pcm->card->number, pcm->device, stream == SNDRV_PCM_STREAM_PLAYBACK ? 'p' : 'c'); // 设置设备名称为pcmC0Dxp或pcmC0Dxc if (!pcm->internal) { err = snd_pcm_stream_proc_init(pstr); // proc文件系统相关 if (err < 0) { pcm_err(pcm, "Error in snd_pcm_stream_proc_init\n"); return err; } } prev = NULL; for (idx = 0, prev = NULL; idx < substream_count; idx++) { // 创建若干个snd_pcm_substream实例,填充 substream 字段,
// 如 streams,number,stream等,并将所有的 substream 填充到 pcm->snd_pcm_substream->substream 中。 substream = kzalloc(sizeof(*substream), GFP_KERNEL); if (!substream) return -ENOMEM; substream->pcm = pcm; substream->pstr = pstr; substream->number = idx; substream->stream = stream; sprintf(substream->name, "subdevice #%i", idx); substream->buffer_bytes_max = UINT_MAX; if (prev == NULL) pstr->substream = substream; else prev->next = substream; if (!pcm->internal) { err = snd_pcm_substream_proc_init(substream); if (err < 0) { pcm_err(pcm, "Error in snd_pcm_stream_proc_init\n"); if (prev == NULL) pstr->substream = NULL; else prev->next = NULL; kfree(substream); return err; } } substream->group = &substream->self_group; snd_pcm_group_init(&substream->self_group); list_add_tail(&substream->link_list, &substream->self_group.substreams); atomic_set(&substream->mmap_count, 0); prev = substream; } return 0; }
2.1.2 snd_pcm_stream_proc_init
snd_pcm_stream_proc_init函数实现如下:
static int snd_pcm_stream_proc_init(struct snd_pcm_str *pstr) { struct snd_pcm *pcm = pstr->pcm; struct snd_info_entry *entry; char name[16]; sprintf(name, "pcm%i%c", pcm->device, pstr->stream == SNDRV_PCM_STREAM_PLAYBACK ? 'p' : 'c'); entry = snd_info_create_card_entry(pcm->card, name, pcm->card->proc_root); // 创建pcmxp/c目录 if (!entry) return -ENOMEM; entry->mode = S_IFDIR | 0555; pstr->proc_root = entry; entry = snd_info_create_card_entry(pcm->card, "info", pstr->proc_root); // 在 pcmxp|c下创建info文件 if (entry) snd_info_set_text_ops(entry, pstr, snd_pcm_stream_proc_info_read); #ifdef CONFIG_SND_PCM_XRUN_DEBUG entry = snd_info_create_card_entry(pcm->card, "xrun_debug", pstr->proc_root); if (entry) { snd_info_set_text_ops(entry, pstr, snd_pcm_xrun_debug_read); entry->c.text.write = snd_pcm_xrun_debug_write; entry->mode |= 0200; } #endif return 0; }
当我们查看/proc/asound/card0/pcm0p/info文件的内容:
root@rk3399:/# cat /proc/asound/card0/pcm0p/info card: 0 device: 0 subdevice: 0 stream: PLAYBACK id: ff880000.i2s-rt5651-aif1 rt5651-aif1-0 name: ff880000.i2s-rt5651-aif1 rt5651-aif1-0 subname: subdevice #0 class: 0 subclass: 0 subdevices_count: 1 subdevices_avail: 1
实际上就是调用的snd_pcm_stream_proc_info_read函数;
static void snd_pcm_proc_info_read(struct snd_pcm_substream *substream, struct snd_info_buffer *buffer) { struct snd_pcm_info *info; int err; if (! substream) return; info = kmalloc(sizeof(*info), GFP_KERNEL); if (!info) return; err = snd_pcm_info(substream, info); if (err < 0) { snd_iprintf(buffer, "error %d\n", err); kfree(info); return; } snd_iprintf(buffer, "card: %d\n", info->card); snd_iprintf(buffer, "device: %d\n", info->device); snd_iprintf(buffer, "subdevice: %d\n", info->subdevice); snd_iprintf(buffer, "stream: %s\n", snd_pcm_stream_name(info->stream)); snd_iprintf(buffer, "id: %s\n", info->id); snd_iprintf(buffer, "name: %s\n", info->name); snd_iprintf(buffer, "subname: %s\n", info->subname); snd_iprintf(buffer, "class: %d\n", info->dev_class); snd_iprintf(buffer, "subclass: %d\n", info->dev_subclass); snd_iprintf(buffer, "subdevices_count: %d\n", info->subdevices_count); snd_iprintf(buffer, "subdevices_avail: %d\n", info->subdevices_avail); kfree(info); } static void snd_pcm_stream_proc_info_read(struct snd_info_entry *entry, struct snd_info_buffer *buffer) { snd_pcm_proc_info_read(((struct snd_pcm_str *)entry->private_data)->substream, buffer); }
2.1.3 snd_pcm_dev_register
在注册声卡设备card时会遍历声卡设备的逻辑设备链表devices,并调用声卡逻辑设备操作集中的dev_register函数,对于pcm设备也就是snd_pcm_dev_register函数,该函数调用snd_register_device依次注册pcm播放设备、pcm录音设备;
static int snd_pcm_dev_register(struct snd_device *device) { int cidx, err; struct snd_pcm_substream *substream; struct snd_pcm *pcm; if (snd_BUG_ON(!device || !device->device_data)) return -ENXIO; pcm = device->device_data; // 获取snd_pcm实例 mutex_lock(®ister_mutex); err = snd_pcm_add(pcm); // 将pcm添加到全局链表snd_pcm_devices if (err) goto unlock; for (cidx = 0; cidx < 2; cidx++) { int devtype = -1; // 设备类型 if (pcm->streams[cidx].substream == NULL) continue; switch (cidx) { case SNDRV_PCM_STREAM_PLAYBACK: //0 devtype = SNDRV_DEVICE_TYPE_PCM_PLAYBACK; break; case SNDRV_PCM_STREAM_CAPTURE: //1 devtype = SNDRV_DEVICE_TYPE_PCM_CAPTURE; break; } /* register pcm */ err = snd_register_device(devtype, pcm->card, pcm->device, // 设备类型 声卡设备 设备号 &snd_pcm_f_ops[cidx], pcm, &pcm->streams[cidx].dev); if (err < 0) { list_del_init(&pcm->list); goto unlock; } for (substream = pcm->streams[cidx].substream; substream; substream = substream->next) snd_pcm_timer_init(substream); } pcm_call_notify(pcm, n_register); unlock: mutex_unlock(®ister_mutex); return err; }
2.1.4 时序图表示
通过分析,我们将声卡设备创建、以及pcm设备创建的完整流程通过时序图表示如下:
(1) snd_card_create :pcm是声卡下的一个设备(部件),所以第一步是要创建一个声卡;
(2) snd_pcm_new:调用该API创建一个pcm,在该API中会做以下事情;
- 建立playback stream,相应的substream也同时建立;
- 建立capture stream,相应的substream也同时建立;
- 调用snd_device_new把该pcm挂到声卡中,参数ops中的dev_register字段指向了函数snd_pcm_dev_register,这个回调函数会在声卡的注册阶段被调用;
(3) snd_pcm_set_ops:设置操作该pcm的控制/操作接口函数,参数中的snd_pcm_ops结构中的函数通常就是我们驱动要实现的函数;
(4) snd_card_register:注册声卡,在这个阶段会遍历声卡下的所有逻辑设备,并且调用各设备的注册回调函数,对于pcm,就是第二步提到的snd_pcm_dev_register函数,该回调函数建立了和用户空间应用程序(比如alsa-lib)通信所用的设备文件节点:/dev/snd/pcmCxxDxxp和/dev/snd/pcmCxxDxxc;
2.2 ASoC创建PCM设备
在ASoC中,PCM设备的创建可以通过soc_create_pcm函数来完成,函数接收5个参数:
- pcm:存放通过snd_pcm_new创建的pcm设备;
- rtd:pcm runtime;
- num:设备号,表示目前创建的是该声卡下的第几个pcm,第一个pcm设备从0开始;
- playback: 表示该pcm将会有几个playback substream;
- capture:表示该pcm将会有几个capture substream;
函数定义在sound/soc/soc-pcm.c,实际上该函数就是调用snd_pcm_new来完成pcm设备的创建;
static int soc_create_pcm(struct snd_pcm **pcm, struct snd_soc_pcm_runtime *rtd, int playback, int capture, int num) { char new_name[64]; int ret; /* create the PCM */ if (rtd->dai_link->params) { snprintf(new_name, sizeof(new_name), "codec2codec(%s)", rtd->dai_link->stream_name); ret = snd_pcm_new_internal(rtd->card->snd_card, new_name, num, playback, capture, pcm); } else if (rtd->dai_link->no_pcm) { snprintf(new_name, sizeof(new_name), "(%s)", rtd->dai_link->stream_name); ret = snd_pcm_new_internal(rtd->card->snd_card, new_name, num, playback, capture, pcm); } else { // 进入这个分支 if (rtd->dai_link->dynamic) snprintf(new_name, sizeof(new_name), "%s (*)", rtd->dai_link->stream_name); else snprintf(new_name, sizeof(new_name), "%s %s-%d", // 比如ff880000.i2s-rt5651-aif1 rt5651-aif1-0 rtd->dai_link->stream_name, // ff880000.i2s-rt5651-aif1 soc_codec_dai_name(rtd), num); // rt5651-aif1 0 ret = snd_pcm_new(rtd->card->snd_card, new_name, num, playback, // 创建pcm设备 比new_name=“ff880000.i2s-rt5651-aif1 rt5651-aif1-0”,num=0 capture, pcm); } if (ret < 0) { dev_err(rtd->card->dev, "ASoC: can't create pcm %s for dailink %s: %d\n", new_name, rtd->dai_link->name, ret); return ret; } dev_dbg(rtd->card->dev, "ASoC: registered pcm #%d %s\n",num, new_name); return 0; }
三、PCM设备文件
当我们移植外音频驱动之后,我们是可以在/dev/snd目录下看到pcm设备文件的,比如在NanoPC-T4开发板移植了ALC5651声卡驱动之后;
root@rk3399:~# ll /dev/snd drwxr-xr-x 2 root root 60 Aug 24 21:31 by-path/ crw-rw---- 1 root audio 116, 4 Aug 24 21:31 controlC0 crw-rw---- 1 root audio 116, 3 Aug 24 21:31 pcmC0D0c crw-rw---- 1 root audio 116, 2 Aug 24 21:31 pcmC0D0p crw-rw---- 1 root audio 116, 1 Aug 24 21:31 seq crw-rw---- 1 root audio 116, 33 Aug 24 21:31 timer
可以看到这些字符设备的主设备号都是116,其中:
-
controlC0:用于声卡的控制,例如通道选择,混音,麦克控制,音量加减,开关等;
-
pcmC0D0c:用于录音的pcm设备;
-
pcmC0D0p:用于播放的pcm设备;
-
-
timer:定时器;
C0D0代表的是声卡0中的设备0,pcmC0D0c最后一个c代表capture,pcmC0D0p最后一个p代表playback。
3.1 应用层打开PCM设备
当我们在应用成打开一个PCM设备,比如dev/snd/pcmC0D0p。对于字符设备,当我们进行open操作时,实际上会执行字符设备的文件操作集的open函数,也就是snd_pcm_f_ops[0]中的open函数,即snd_pcm_playback_open;
/* * Register section */ const struct file_operations snd_pcm_f_ops[2] = { { .owner = THIS_MODULE, .write = snd_pcm_write, .write_iter = snd_pcm_writev, .open = snd_pcm_playback_open, .release = snd_pcm_release, .llseek = no_llseek, .poll = snd_pcm_poll, .unlocked_ioctl = snd_pcm_ioctl, .compat_ioctl = snd_pcm_ioctl_compat, .mmap = snd_pcm_mmap, .fasync = snd_pcm_fasync, .get_unmapped_area = snd_pcm_get_unmapped_area, }, { .owner = THIS_MODULE, .read = snd_pcm_read, .read_iter = snd_pcm_readv, .open = snd_pcm_capture_open, .release = snd_pcm_release, .llseek = no_llseek, .poll = snd_pcm_poll, .unlocked_ioctl = snd_pcm_ioctl, .compat_ioctl = snd_pcm_ioctl_compat, .mmap = snd_pcm_mmap, .fasync = snd_pcm_fasync, .get_unmapped_area = snd_pcm_get_unmapped_area, } };
下面的时序图展示了应用程序如何最终调用到snd_pcm_f_ops中的回调函数:
下面我绘制了一张流程图更加形象的展示打开PCM设备的过程:
3.1.1 snd_pcm_playback_open
当打开音频文件时,会调用snd_pcm_playback_open函数,定义在sound/core/pcm_native.c;
static int snd_pcm_playback_open(struct inode *inode, struct file *file) { struct snd_pcm *pcm; int err = nonseekable_open(inode, file); if (err < 0) return err; pcm = snd_lookup_minor_data(iminor(inode), // 获取pcm实例 SNDRV_DEVICE_TYPE_PCM_PLAYBACK); err = snd_pcm_open(file, pcm, SNDRV_PCM_STREAM_PLAYBACK); // 重点 if (pcm) snd_card_unref(pcm->card); return err; }
3.1.2 snd_pcm_open
static int snd_pcm_open(struct file *file, struct snd_pcm *pcm, int stream) { int err; wait_queue_entry_t wait; if (pcm == NULL) { err = -ENODEV; goto __error1; } err = snd_card_file_add(pcm->card, file); if (err < 0) goto __error1; if (!try_module_get(pcm->card->module)) { err = -EFAULT; goto __error2; } init_waitqueue_entry(&wait, current); // 初始化等待队列元素,并设置private为当前进程 add_wait_queue(&pcm->open_wait, &wait); // 向等待队列open_wait添加元素wait mutex_lock(&pcm->open_mutex); while (1) { err = snd_pcm_open_file(file, pcm, stream); if (err >= 0) break; if (err == -EAGAIN) { if (file->f_flags & O_NONBLOCK) { // 非堵塞 err = -EBUSY; break; } } else break; set_current_state(TASK_INTERRUPTIBLE); // 设置设置当前进程状态为TASK_INTERRUPTIBLE,在该状态下,进程如果休眠的话可以被信号和wake_up唤醒 mutex_unlock(&pcm->open_mutex); schedule(); // // 让出CPU,进程进入睡眠,那什么时候会唤醒该进程呢,一般会在xxxxx中调用wake_up_interruptible(&pcm->open_wait)唤醒该进程 mutex_lock(&pcm->open_mutex); if (pcm->card->shutdown) { err = -ENODEV; break; } if (signal_pending(current)) { // 如果有信号处理,跳出处理信号,然后便会进入对应的signal处理函数 err = -ERESTARTSYS; break; } } remove_wait_queue(&pcm->open_wait, &wait); // 从等待队列移除元素wait mutex_unlock(&pcm->open_mutex); if (err < 0) goto __error; return err; __error: module_put(pcm->card->module); __error2: snd_card_file_remove(pcm->card, file); __error1: return err; }
3.1.3 snd_pcm_open_file
snd_pcm_open_file函数实现如下:
static int snd_pcm_open_file(struct file *file, struct snd_pcm *pcm, int stream) { struct snd_pcm_file *pcm_file; struct snd_pcm_substream *substream; int err; err = snd_pcm_open_substream(pcm, stream, file, &substream); // 重点 if (err < 0) return err; pcm_file = kzalloc(sizeof(*pcm_file), GFP_KERNEL); if (pcm_file == NULL) { snd_pcm_release_substream(substream); return -ENOMEM; } pcm_file->substream = substream; if (substream->ref_count == 1) substream->pcm_release = pcm_release_private; file->private_data = pcm_file; return 0; }
3.1.4 snd_pcm_open_substream
snd_pcm_open_substream函数实现如下:
int snd_pcm_open_substream(struct snd_pcm *pcm, int stream, struct file *file, struct snd_pcm_substream **rsubstream) { struct snd_pcm_substream *substream; int err; err = snd_pcm_attach_substream(pcm, stream, file, &substream); // 获取一个空闲的pcm substream(substream = pcm->streams[stream]->substream[n]) if (err < 0) return err; if (substream->ref_count > 1) { *rsubstream = substream; return 0; } err = snd_pcm_hw_constraints_init(substream); if (err < 0) { pcm_dbg(pcm, "snd_pcm_hw_constraints_init failed\n"); goto error; } err = substream->ops->open(substream); // 重点 if (err < 0) goto error; substream->hw_opened = 1; err = snd_pcm_hw_constraints_complete(substream); if (err < 0) { pcm_dbg(pcm, "snd_pcm_hw_constraints_complete failed\n"); goto error; } /* automatically set EXPLICIT_SYNC flag in the managed mode whenever * the DMA buffer requires it */ if (substream->managed_buffer_alloc && substream->dma_buffer.dev.need_sync) substream->runtime->hw.info |= SNDRV_PCM_INFO_EXPLICIT_SYNC; *rsubstream = substream; return 0; error: snd_pcm_release_substream(substream); return err; }
pcm subustream的ops(即PCM操作集)在ASoC声卡注册的时候被初始为rtd->ops,其中rtd为pcm runtime;
rtd->ops.open = soc_pcm_open; rtd->ops.hw_params = soc_pcm_hw_params; rtd->ops.prepare = soc_pcm_prepare; rtd->ops.trigger = soc_pcm_trigger; rtd->ops.hw_free = soc_pcm_hw_free; rtd->ops.close = soc_pcm_close; rtd->ops.pointer = soc_pcm_pointer;
这些函数都定义在sound/soc/soc-pcm.c。
3.1.5 soc_pcm_open
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了