4. 车载音频PCM录音通路驱动实现
任务目标
在ALSA驱动中,prepare回调用于在PCM流启动前完成硬件参数准备(如设置格式、通道数、周期大小),trigger回调则负责响应START/STOP等命令,执行DMA传输的启动与停止。本案例实现这两个回调,并管理PCM流在PREPARED与RUNNING状态间的合法转换。
核心实现
#include <linux/module.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/soc.h>
#define DRV_NAME "demo-pcm"
struct demo_runtime {
struct snd_pcm_substream *substream;
dma_addr_t dma_addr;
size_t buffer_bytes;
int format_width; /* 16/24/32 */
unsigned int channels;
unsigned int rate;
};
static int demo_prepare(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *rt = substream->runtime;
struct demo_runtime *pr = snd_pcm_substream_chip(substream);
struct snd_pcm_hw_params *params = NULL;
/* 从runtime获取当前硬件参数 */
pr->format_width = snd_pcm_format_width(rt->format);
pr->channels = rt->channels;
pr->rate = rt->rate;
pr->buffer_bytes = frames_to_bytes(rt, rt->buffer_size);
pr->dma_addr = rt->dma_addr;
/* 实际驱动中在此配置DMA控制器:
* 1. 设置源/目的地址为pr->dma_addr
* 2. 按format_width配置FIFO位宽
* 3. 按channels配置通道映射
* 4. 按rate配置采样时钟分频
*/
dev_info(rt->dev, "prepare: %dbit %dch %dHz buf=%zu\n",
pr->format_width, pr->channels, pr->rate, pr->buffer_bytes);
/* 状态由SETUP转为PREPARED */
snd_pcm_do_prepare(substream);
return 0;
}
static int demo_trigger(struct snd_pcm_substream *substream, int cmd)
{
struct demo_runtime *pr = snd_pcm_substream_chip(substream);
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
/* 启动DMA传输 */
pr->substream = substream;
/* 写DMA控制寄存器使能传输 */
dev_info(substream->pcm->dev, "trigger: START\n");
/* 状态由PREPARED转为RUNNING */
snd_pcm_trigger_done(substream, substream);
break;
case SNDRV_PCM_TRIGGER_STOP:
case SNDRV_PCM_TRIGGER_SUSPEND:
/* 停止DMA并复位指针 */
snd_pcm_period_elapsed(substream); /* 实际驱动中在DMA中断里调用 */
dev_info(substream->pcm->dev, "trigger: STOP\n");
/* 状态由RUNNING转回PREPARED */
snd_pcm_trigger_done(substream, substream);
break;
default:
return -EINVAL;
}
return 0;
}
static int demo_pcm_open(struct snd_pcm_substream *substream)
{
struct demo_runtime *pr = kzalloc(sizeof(*pr), GFP_KERNEL);
if (!pr)
return -ENOMEM;
snd_pcm_substream_chip(substream) = pr;
return 0;
}
static int demo_pcm_close(struct snd_pcm_substream *substream)
{
kfree(snd_pcm_substream_chip(substream));
return 0;
}
static const struct snd_pcm_ops demo_pcm_ops = {
.open = demo_pcm_open,
.close = demo_pcm_close,
.prepare = demo_prepare,
.trigger = demo_trigger,
};
static int demo_pcm_new(struct snd_soc_pcm_runtime *rtd)
{
struct snd_pcm *pcm = rtd->pcm;
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &demo_pcm_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &demo_pcm_ops);
return 0;
}
static struct snd_soc_component_driver demo_component = {
.name = DRV_NAME,
.pcm_new = demo_pcm_new,
};
static int demo_platform_probe(struct platform_device *pdev)
{
return devm_snd_soc_register_component(&pdev->dev, &demo_component,
NULL, 0);
}
static struct platform_driver demo_pcm_driver = {
.driver = {
.name = DRV_NAME,
},
.probe = demo_platform_probe,
};
module_platform_driver(demo_pcm_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("ALSA PCM prepare/trigger demo");
状态转换逻辑
ALSA核心层维护PCM流状态机,驱动回调在其中起关键作用:
- prepare:应用调用
snd_pcm_prepare()后触发,驱动完成硬件配置,状态从SNDRV_PCM_STATE_SETUP变为PREPARED。此时DMA尚未启动,但硬件已就绪。 - trigger(START):应用调用
snd_pcm_start()后触发,驱动启动DMA传输,状态从PREPARED变为RUNNING。 - trigger(STOP):应用调用
snd_pcm_stop()后触发,驱动停止DMA,状态从RUNNING回到PREPARED(非XRUN路径)。
非法状态转换(如未prepare直接start)会被ALSA核心层拦截,不会进入驱动回调。
编译与验证
# 编译为内核模块
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# 加载模块
sudo insmod demo-pcm.ko
# 使用arecord/aplay触发prepare和trigger
# 先创建虚拟声卡(需在machine驱动中绑定本component)
# 然后执行:
arecord -D hw:0,0 -f S16_LE -r 48000 -c 2 -d 1 /dev/null
验证方法:
- 加载模块后执行
dmesg | tail,应看到probe成功日志。 - 运行录音命令时,dmesg中应依次出现
prepare: 16bit 2ch 48000Hz和trigger: START。 - 录音结束后出现
trigger: STOP,且无错误返回。 - 通过
cat /proc/asound/card0/pcm0p/sub0/status可观察状态变化:录音期间为RUNNING,停止后为PREPARED。
关键说明
- 本示例未实现
hw_params和pointer回调,实际DMA传输需补充这两个回调才能完整工作;此处聚焦prepare/trigger的状态管理逻辑。 snd_pcm_do_prepare()和snd_pcm_trigger_done()是核心层辅助函数,用于推进状态机,驱动中必须调用。- DMA控制器的具体寄存器操作因硬件而异,代码中以注释标明配置点,实际移植时替换为对应寄存器读写即可。