【车载HIFI播放器开发】Linux音频驱动 PCM设备

软件开发大郭
0 评论
/
23 阅读
/
19950 字
01 2022-06

概述

1. 什么是pcm?

pcm(Pulse-code modulation)脉冲编码调制,是将模拟信号转化为数字信号的一种方法。声音的转化的过程为,先对连续的模拟信号按照固定频率周期性采样,将采样到的数据按照一定的精度进行量化,量化后的信号和采样后的信号差值叫做量化误差,将量化后的数据进行最后的编码存储,最终模拟信号变化为数字信号。

2. pcm的两个重要属性

a. 采样率: 单位时间内采样的次数,采样频率越高越高,

b. 采样位数: 一个采样信号的位数,也是对采样精度的变现。

对于人类而言,能接受声音的频率范围是20Hz-20KHz, 所以采样的频率44.1KHz 以及16bit的采样位数就可以有很好的保真能力(CD格式的采样率和采样位数)。

Linux PCM

图1-1 声音的录音和播放过程

数据结构

在ALSA架构下,pcm也被称为设备,所谓的逻辑设备。在linux系统中使用snd_pcm结构表示一个pcm设备。

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);
    struct device *dev; /* actual hw device this belongs to */
    bool internal; /* pcm is for internal use only */
    bool nonatomic; /* whole PCM operations are in non-atomic context */
#if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
    struct snd_pcm_oss oss;
#endif
};

.card: 此pcm设备所属的card。

.list: 用于将pcm设备链接起来,最终所有的pcm设备会放入snd_pcm_devices链表中。

.device: 该pcm的索引号。

.id: 该pcm的标识。

.streams: 指向pcm的capture和playback stream,通常0代表playback,1代表capture。

通常一个pcm下会有两个stream, 分别为capture stream和playback stream,在每个stream下又会存在多个substream。

linux系统中使用snd_pcm_str定义stream, 使用snd_pcm_substream定义substream。

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;
};

.stream: 当前stream的方向,capture or playback。

.pcm: 所属的pcm。

.substream_count: 该stream下substream的个数。

.substream_opened: 该stream下open的substream个数。

.substream: 该stream下的substream.

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 */
    /* -- 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 -- */
    void *file;
    int ref_count;
    atomic_t mmap_count;
    unsigned int f_flags;
    void (*pcm_release)(struct snd_pcm_substream *);
    struct pid *pid;
    /* misc flags */
    unsigned int hw_opened: 1;
};

.pcm: 所属的pcm。

.pstr: 所属的stream。

.id: 代表的该stream下第几个substream,也就是序号。

.stream: 该substream的方向流,是palyback or capture。

.name: 该substrem的名字。

.ops: 硬件操作函数集合。

.runtime: 运行时的pcm的一些信息。

.next: 用于链接下一个sub stream。

下图是对这几个结构体之间的简单表述。

Linux PCM

pcm设备的创建

创建流程

  1. 创建实例
    创建一个pcm设备的实例,使用snd_pcm_new函数。
  2. 设置参数
    另一个用于设置pcm操作函数接口的api:

    void snd_pcm_set_ops(struct snd_pcm *pcm, int direction, struct snd_pcm_ops *ops);

    新建一个pcm可以用下面一张新建pcm的调用的序列图进行描述:
    Linux PCM 图3.1 新建pcm的序列图

▲图3.1 新建pcm的序列图

snd_card_create pcm是声卡下的一个设备(部件),所以第一步是要创建一个声卡

snd_pcm_new 调用该api创建一个pcm,才该api中会做以下事情

如果有,建立playback stream,相应的substream也同时建立

如果有,建立capture stream,相应的substream也同时建立

调用snd_device_new()把该pcm挂到声卡中,参数ops中的dev_register字段指向了函数snd_pcm_dev_register,这个回调函数会在声卡的注册阶段被调用。

snd_pcm_set_ops 设置操作该pcm的控制/操作接口函数,参数中的snd_pcm_ops结构中的函数通常就是我们驱动要实现的函数

snd_card_register 注册声卡,在这个阶段会遍历声卡下的所有逻辑设备,并且调用各设备的注册回调函数,对于pcm,就是第二步提到的snd_pcm_dev_register函数,该回调函数建立了和用户空间应用程序(alsa-lib)通信所用的设备文件节点:/dev/snd/pcmCxxDxxp和/dev/snd/pcmCxxDxxc

使用snd_pcm_new函数

详细说说怎么使用snd_pcm_new函数

/**
 * 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);
}

此函数会传入六个参数,其中该函数的注释写的很清楚,不做过多解释。

函数最终会返回rpcm参数。

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 struct snd_device_ops ops = {
        .dev_free = snd_pcm_dev_free,
        .dev_register =    snd_pcm_dev_register,
        .dev_disconnect = snd_pcm_dev_disconnect,
    };

    if (snd_BUG_ON(!card))
        return -ENXIO;
    if (rpcm)
        *rpcm = NULL;
    pcm = kzalloc(sizeof(*pcm), GFP_KERNEL);
    if (pcm == NULL) {
        dev_err(card->dev, "Cannot allocate PCM\n");
        return -ENOMEM;
    }
    pcm->card = card;
    pcm->device = device;
    pcm->internal = internal;
    if (id)
        strlcpy(pcm->id, id, sizeof(pcm->id));
    if ((err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count)) < 0) {
        snd_pcm_free(pcm);
        return err;
    }
    if ((err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count)) < 0) {
        snd_pcm_free(pcm);
        return err;
    }
    mutex_init(&pcm->open_mutex);
    init_waitqueue_head(&pcm->open_wait);
    if ((err = snd_device_new(card, SNDRV_DEV_PCM, pcm, &ops)) < 0) {
        snd_pcm_free(pcm);
        return err;
    }
    if (rpcm)
        *rpcm = pcm;
    return 0;
}
  1. 分配一个snd_pcm结构体。
  2. 根据传递进来的参数设置card, device, internal, id。
  3. 分别创建palyback & capture stream。
  4. 调用snd_device_new接口创建pcm设备。

    int snd_pcm_new_stream(struct snd_pcm *pcm, int stream, int substream_count)
    {
     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;
     pstr->pcm = pcm;
     pstr->substream_count = substream_count;
     if (substream_count > 0 && !pcm->internal) {
         err = snd_pcm_stream_proc_init(pstr);
         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++) {
         substream = kzalloc(sizeof(*substream), GFP_KERNEL);
         if (substream == NULL) {
             pcm_err(pcm, "Cannot allocate PCM substream\n");
             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;
         spin_lock_init(&substream->self_group.lock);
         mutex_init(&substream->self_group.mutex);
         INIT_LIST_HEAD(&substream->self_group.substreams);
         list_add_tail(&substream->link_list, &substream->self_group.substreams);
         atomic_set(&substream->mmap_count, 0);
         prev = substream;
     }
     return 0;
    }            

调用snd_pcm_new_stream创建一个stream

  1. 根据传递进来的参数,设置pcm的stream, pcm, substream_count的值。
  2. 在proc下创建pcm相关目录信息。会调用snd_pcm_stream_proc_init函数,根据stream的类型创建pcm0p/pcm0c文件夹,然后会在此文件夹下创建info文件。info文件的类型会通过snd_pcm_stream_proc_info_read函数获得。代表就不贴出来了。:(

    root@test:/proc/asound/card0/pcm0c$ cat info 
    card: 0
    device: 0
    subdevice: 0
    stream: CAPTURE
    id: ALC662 rev1 Analog
    name: ALC662 rev1 Analog
    subname: subdevice #0
    class: 0
    subclass: 0
    subdevices_count: 1
    subdevices_avail: 1
  3. 会根据substrem_count的个数,进行for循环操作。
  4. 分配一个substream结构,设置必要的参数,如: pcm, pstr, number, stream, name等。
  5. 调用snd_pcm_substream_proc_init函数,创建sub0目录,然后在此目录下创建info, hw_params, sw_params,status等文件。
  6. 将所有的substream会通过linklist链表保存,同时如果有多个substream会通过next指针相连。

至此,pcm设备就全部创建完成,创建完成后会形成如下的逻辑试图。

Linux PCM

大体上就是一棵树,根节点是card0, 然后子节点是pcm设备,pcm设备分为capture & playback stream, 然后在stream下又分为substrem。

PCM硬件操作函数集设置

实例化一个pcm设备之后,还需要通过snd_pcm_set_ops函数设置该硬件的操作集合。

该函数会根据当前stream的方向/类型,设置该硬件对应的snd_pcm_ops操作集合。

void snd_pcm_set_ops(struct snd_pcm *pcm, int direction,
             const struct snd_pcm_ops *ops)
{
    struct snd_pcm_str *stream = &pcm->streams[direction];
    struct snd_pcm_substream *substream;
    
    for (substream = stream->substream; substream != NULL; substream = substream->next)
        substream->ops = ops;
}

整个流程梳理

Linux PCM

PCM设备节点创建

当调用snd_card_register的时候,就会依次调用card列表下每个设备的dev_register回调函数,对pcm设备来说就是在_snd_pcm_new函数中的

    static struct snd_device_ops ops = {
        .dev_free = snd_pcm_dev_free,
        .dev_register =    snd_pcm_dev_register,
        .dev_disconnect = snd_pcm_dev_disconnect,
    };

此时会调用到snd_pcm_dev_register回调处理函数。

static int snd_pcm_dev_register(struct snd_device *device)
{
    int cidx, err;
    struct snd_pcm_substream *substream;
    struct snd_pcm_notify *notify;
    char str[16];
    struct snd_pcm *pcm;
    struct device *dev;

    if (snd_BUG_ON(!device || !device->device_data))
        return -ENXIO;
    pcm = device->device_data;
    mutex_lock(&register_mutex);
    err = snd_pcm_add(pcm);
    if (err) {
        mutex_unlock(&register_mutex);
        return err;
    }
    for (cidx = 0; cidx < 2; cidx++) {
        int devtype = -1;
        if (pcm->streams[cidx].substream == NULL || pcm->internal)
            continue;
        switch (cidx) {
        case SNDRV_PCM_STREAM_PLAYBACK:
            sprintf(str, "pcmC%iD%ip", pcm->card->number, pcm->device);
            devtype = SNDRV_DEVICE_TYPE_PCM_PLAYBACK;
            break;
        case SNDRV_PCM_STREAM_CAPTURE:
            sprintf(str, "pcmC%iD%ic", pcm->card->number, pcm->device);
            devtype = SNDRV_DEVICE_TYPE_PCM_CAPTURE;
            break;
        }
        /* device pointer to use, pcm->dev takes precedence if
         * it is assigned, otherwise fall back to card's device
         * if possible */
        dev = pcm->dev;
        if (!dev)
            dev = snd_card_get_device_link(pcm->card);
        /* register pcm */
        err = snd_register_device_for_dev(devtype, pcm->card,
                          pcm->device,
                          &snd_pcm_f_ops[cidx],
                          pcm, str, dev);
        if (err < 0) {
            list_del(&pcm->list);
            mutex_unlock(&register_mutex);
            return err;
        }

        dev = snd_get_device(devtype, pcm->card, pcm->device);
        if (dev) {
            err = sysfs_create_groups(&dev->kobj,
                          pcm_dev_attr_groups);
            if (err < 0)
                dev_warn(dev,
                     "pcm %d:%d: cannot create sysfs groups\n",
                     pcm->card->number, pcm->device);
            put_device(dev);
        }

        for (substream = pcm->streams[cidx].substream; substream; substream = substream->next)
            snd_pcm_timer_init(substream);
    }

    list_for_each_entry(notify, &snd_pcm_notify_list, list)
        notify->n_register(pcm);

    mutex_unlock(&register_mutex);
    return 0;
}
  1. 合法性判断,对pcm设备来说,snd_device->device_data存放的是当前的pcm指针。
  2. 会调用snd_pcm_add此函数,判断此pcm设备是存在snd_pcm_devices链表中存在,存在就返回错误,不存在就添加。
  3. 设置当前pcm设备name, 以及具体的pcm设备类型,PCM_CAPTURE or PCM_PLAYBACK。
  4. 调用snd_register_device_for_dev添加pcm设备到系统中。
  5. 调用snd_get_device此函数返回当前注册的pcm设备,然后设置该pcm的属性。
  6. 调用snd_pcm_timer_init函数,进行pcm定时器的初始化。

    struct snd_minor {
     int type;            /* SNDRV_DEVICE_TYPE_XXX */
     int card;            /* card number */
     int device;            /* device number */
     const struct file_operations *f_ops;    /* file operations */
     void *private_data;        /* private data for f_ops->open */
     struct device *dev;        /* device for sysfs */
     struct snd_card *card_ptr;    /* assigned card instance */
    };

在继续分析snd_register_device_for_dev函数之前需要先介绍一个结构体。struct snd_minor。

.type: 设备类型,比如是pcm, control, timer等设备。

.card_number: 所属的card。

.device: 当前设备类型下的设备编号。

.f_ops: 具体设备的文件操作集合。

.private_data: open函数的私有数据。

.card_ptr: 所属的card。

此结构体是用来保存当前设备的上下文信息,该card下所有逻辑设备都存在此结构。

int snd_register_device_for_dev(int type, struct snd_card *card, int dev,
                const struct file_operations *f_ops,
                void *private_data,
                const char *name, struct device *device)
{
    int minor;
    struct snd_minor *preg;

    if (snd_BUG_ON(!name))
        return -EINVAL;
    preg = kmalloc(sizeof *preg, GFP_KERNEL);
    if (preg == NULL)
        return -ENOMEM;
    preg->type = type;
    preg->card = card ? card->number : -1;
    preg->device = dev;
    preg->f_ops = f_ops;
    preg->private_data = private_data;
    preg->card_ptr = card;
    mutex_lock(&sound_mutex);
#ifdef CONFIG_SND_DYNAMIC_MINORS
    minor = snd_find_free_minor(type);
#else
    minor = snd_kernel_minor(type, card, dev);
    if (minor >= 0 && snd_minors[minor])
        minor = -EBUSY;
#endif
    if (minor < 0) {
        mutex_unlock(&sound_mutex);
        kfree(preg);
        return minor;
    }
    snd_minors[minor] = preg;
    preg->dev = device_create(sound_class, device, MKDEV(major, minor),
                  private_data, "%s", name);
    if (IS_ERR(preg->dev)) {
        snd_minors[minor] = NULL;
        mutex_unlock(&sound_mutex);
        minor = PTR_ERR(preg->dev);
        kfree(preg);
        return minor;
    }

    mutex_unlock(&sound_mutex);
    return 0;
}
  1. 首先上来就分配一个snd_minor结构体。
  2. 根据传递进来的参数,各种参数。对于pcm设备来说,当前的private_data就是pcm。此处需要重点介绍file_operations结构。此函数最终会在应用程序调用open的时候走到此处

    const struct file_operations snd_pcm_f_ops[2] = {
     {
         .owner =        THIS_MODULE,
         .write =        snd_pcm_write,
         .aio_write =        snd_pcm_aio_write,
         .open =            snd_pcm_playback_open,
         .release =        snd_pcm_release,
         .llseek =        no_llseek,
         .poll =            snd_pcm_playback_poll,
         .unlocked_ioctl =    snd_pcm_playback_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,
         .aio_read =        snd_pcm_aio_read,
         .open =            snd_pcm_capture_open,
         .release =        snd_pcm_release,
         .llseek =        no_llseek,
         .poll =            snd_pcm_capture_poll,
         .unlocked_ioctl =    snd_pcm_capture_ioctl,
         .compat_ioctl =     snd_pcm_ioctl_compat,
         .mmap =            snd_pcm_mmap,
         .fasync =        snd_pcm_fasync,
         .get_unmapped_area =    snd_pcm_get_unmapped_area,
     }
    };
  3. 调用snd_kernel_minor函数获得设备的此设备号。该此设备号已经存在则返回BUSY,小于返回错误。
  4. 用次设备号为下标,将当前申请的snd_minor放入到全局的snd_minors结构体数组中。

    static struct snd_minor *snd_minors[SNDRV_OS_MINORS];
  5. 调用device_create函数创建该pcm的设备节点。
  6. 为什么创建出的设备节点全在/dev/snd下呢? 此问题源自sound_class创建的时候,设置的devnode参数。

    static char *sound_devnode(struct device *dev, umode_t *mode)
    {
     if (MAJOR(dev->devt) == SOUND_MAJOR)
         return NULL;
     return kasprintf(GFP_KERNEL, "snd/%s", dev_name(dev));
    }
    
    static int __init init_soundcore(void)
    {
     int rc;
    
     rc = init_oss_soundcore();
     if (rc)
         return rc;
    
     sound_class = class_create(THIS_MODULE, "sound");
     if (IS_ERR(sound_class)) {
         cleanup_oss_soundcore();
         return PTR_ERR(sound_class);
     }
    
     sound_class->devnode = sound_devnode;
    
     return 0;
    }

当调用device_create的时候,最终会调用到device_add->devtmpfs_create_node->device_get_devnode中

    /* the class may provide a specific name */
    if (dev->class && dev->class->devnode)
        *tmp = dev->class->devnode(dev, mode);

最终出现的设备节点会出现在/dev/snd下。

应用到驱动的过程

当应用程序在通过open系统调用打开/dev/pcmC0D0c的过程

  1. 先会调用到在alsa_sound_init中注册的字符设备"alsa"的file_operations中的open函数中。

    static const struct file_operations snd_fops =
    {
     .owner =    THIS_MODULE,
     .open =        snd_open,
     .llseek =    noop_llseek,
    };
  2. 此处会根据次设备号在snd_minors中获得注册的pcm的snd_minor结构,然后调用open回调

     if (file->f_op->open)
         err = file->f_op->open(inode, file);
  3. 此处的open回调就是snd_pcm_f_ops中的open。
  4. 当应用程序执行ioctl的时候,就直接调用file文件中的file_operaions中的ioctl即可,因为在此处已经将snd_minor中的file_operation替换到file中。

    #define replace_fops(f, fops) \
     do {    \
         struct file *__file = (f); \
         fops_put(__file->f_op); \
         BUG_ON(!(__file->f_op = (fops))); \
     } while(0)
  5. 比如当前调用的是playback中的open,会调用snd_pcm_playback_open函数,此函数会设置pcm的runtime信息,最终会调用硬件相关的open函数中。
    if ((err = substream->ops->open(substream)) < 0)

至此,整个pcm设备创建,调用,以及应用到驱动整个流程分析完毕。:)

    暂无数据