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

验证方法:

  1. 加载模块后执行dmesg | tail,应看到probe成功日志。
  2. 运行录音命令时,dmesg中应依次出现prepare: 16bit 2ch 48000Hz和trigger: START。
  3. 录音结束后出现trigger: STOP,且无错误返回。
  4. 通过cat /proc/asound/card0/pcm0p/sub0/status可观察状态变化:录音期间为RUNNING,停止后为PREPARED。

关键说明

  • 本示例未实现hw_params和pointer回调,实际DMA传输需补充这两个回调才能完整工作;此处聚焦prepare/trigger的状态管理逻辑。
  • snd_pcm_do_prepare()和snd_pcm_trigger_done()是核心层辅助函数,用于推进状态机,驱动中必须调用。
  • DMA控制器的具体寄存器操作因硬件而异,代码中以注释标明配置点,实际移植时替换为对应寄存器读写即可。