PCM data flow - 4 - ASoC platform driver

本文深入探讨ALSA音频系统中的Platform驱动与PCM DMA驱动,详细分析了它们的主要职责、实现方式及其相互之间的交互过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述中提到音频Platform驱动主要作用是音频数据的传输,这里又细分为两步:

·          把音频数据从dma buffer通过dma操作搬运到cpu_dai FIFO,这部分驱动用snd_soc_platform_driver描述,后面分析用pcm_dma指代它。

·          把音频数据从cpu_dai FIFO通过数字音频接口(I2S/PCM/AC97)传送到codec_dai,这部分驱动用snd_soc_dai_driver描述,后面分析用cpu_dai指代它。

那么dma buffer中的音频数据从哪而来?保留这个问题,在后面章节pcm native分析。


我们浏览下platform_drv中的几个重要的结构体,其中浅蓝色部分是cpu_dai相关的,浅绿色部分是pcm_dma相关的。而snd_soc_dai是cpu_dai注册时所创建的dai实例,snd_soc_platform是pcm_dma注册时所创建的platform实例,这些实例方便asoc-core管理。从snd_soc_dai中,我们看到一个dai必须绑定它链结的codec和platform。



cpu dai


从上图我们可知,实现一个cpu_dai驱动主要工作有:

·          实现dai操作函数,见snd_soc_dai_ops字段定义,用于配置控制dai,如系统时钟设置set_sysclk()、格式设置set_fmt()、硬件参数设置hw_params()、触发dai启动或停止传输trigger()等。

·          实现dai_drv的probe函数(dai初始化工作)、remove函数(dai卸载工作)、suspend/resume函数(dai电源管理配置)

·          初始化dai_drv实例snd_soc_dai_driver,包括playback和capture的能力描述信息、dai操作函数集指针、probe/remove回调、电源管理相关的suspend/resume回调。

·          通过snd_soc_register_dai()把初始化完成的snd_soc_dai_driver注册到soc-core:首先创建并初始化一个snd_soc_dai实例,然后把该snd_soc_dai实例插入到dai_list链表,Machine驱动初始化时会遍历该链表,以找到dai_link声明的cpu_dai并绑定。

/**
 * snd_soc_register_dai - Register a DAI with the ASoC core
 *
 * @dai: DAI to register
 */
int snd_soc_register_dai(struct device *dev,
        struct snd_soc_dai_driver *dai_drv)
{
    struct snd_soc_dai *dai;

    dai = kzalloc(sizeof(struct snd_soc_dai), GFP_KERNEL);
    if (dai == NULL)
        return -ENOMEM;

    /* create DAI component name */
    dai->name = fmt_single_name(dev, &dai->id);
    if (dai->name == NULL) {
        kfree(dai);
        return -ENOMEM;
    }

    dai->dev = dev;
    dai->driver = dai_drv;
    if (!dai->driver->ops)
        dai->driver->ops = &null_dai_ops;

    mutex_lock(&client_mutex);
    list_add(&dai->list, &dai_list);
    mutex_unlock(&client_mutex);

    return 0;
}

注意dai操作函数的实现是cpu_dai驱动的主体,主要工作是配置好相关寄存器让硬件正常运转,因此这依赖于具体平台的cpu dai specification。snd_soc_dai_ops字段的详细说明见Codec audio operations章节。

cpu_dai驱动应该算是这个系列中最简单的一环,因此不多花费笔墨在这里了。倒是某些平台上,dma设备信息(设备的总线地址、通道号、传输单元大小)是在这里初始化的,这点要留意,这些dma设备信息在pcm_dma驱动中用到。

以Exynos平台为例,代码位置sound/soc/samsung/i2s.c。

Samsung Exynos平台的音频dma设备信息用s3c_dma_params结构体描述:

struct s3c_dma_params {
    struct s3c2410_dma_client *client;  /* stream identifier */
    int channel;                /* Channel ID */
    dma_addr_t dma_addr;
    int dma_size;           /* Size of the DMA transfer */
    unsigned ch;
    struct samsung_dma_ops *ops;
};

·          client:流标识符

·          channel:通道号

·          dma_addr:设备的总线地址,这里通常指向I2S tx FIFO或I2S rx FIFO地址

·          dma_size:dma传输单元大小

·          ops:平台dma操作函数

sound/soc/samsung/i2s.c中设置dma设备信息的相关代码片段:

struct i2s_dai {
    // ...
    /* Driver for this DAI */
    struct snd_soc_dai_driver i2s_dai_drv;
    /* DMA parameters */
    struct s3c_dma_params dma_playback; // playback dma描述信息
    struct s3c_dma_params dma_capture;  // capture dma描述信息
    struct s3c_dma_params idma_playback;// playback idma描述信息,idma仅用于回放,用于三星平台的LPA(低功耗音频)模式
    // ...
};

static __devinit int samsung_i2s_probe(struct platform_device *pdev)
{
    // ...
    // 从platform_device中取得resource,得到playback dma通道号
    res = platform_get_resource(pdev, IORESOURCE_DMA, 0);
    if (!res) {
        dev_err(&pdev->dev, "Unable to get I2S-TX dma resource\n");
        return -ENXIO;
    }
    dma_pl_chan = res->start; // dma_pl_chan中的pl是playback简写

    // 从platform_device中取得resource,得到capture dma通道号
    res = platform_get_resource(pdev, IORESOURCE_DMA, 1);
    if (!res) {
        dev_err(&pdev->dev, "Unable to get I2S-RX dma resource\n");
        return -ENXIO;
    }
    dma_cp_chan = res->start; // dma_cp_chan中的cp是capture的简写

    // 从platform_device中取得resource,得到playback idma通道号
    res = platform_get_resource(pdev, IORESOURCE_DMA, 2);
    if (res)
        dma_pl_sec_chan = res->start;
    else
        dma_pl_sec_chan = 0;

    // 从platform_device中取得resource,得到I2S的基地址
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(&pdev->dev, "Unable to get I2S SFR address\n");
        return -ENXIO;
    }

    if (!request_mem_region(res->start, resource_size(res),
                            "samsung-i2s")) {
        dev_err(&pdev->dev, "Unable to request SFR region\n");
        return -EBUSY;
    }
    regs_base = res->start;
    
    // ...
    pri_dai->dma_playback.dma_addr = regs_base + I2STXD; // 设置playback dma设备地址为I2S tx FIFO地址
    pri_dai->dma_capture.dma_addr = regs_base + I2SRXD; // 设置capture dma设备地址为I2S rx FIFO地址
    pri_dai->dma_playback.client =
        (struct s3c2410_dma_client *)&pri_dai->dma_playback;
    pri_dai->dma_capture.client =
        (struct s3c2410_dma_client *)&pri_dai->dma_capture;
    pri_dai->dma_playback.channel = dma_pl_chan; // 设置playback dma通道号
    pri_dai->dma_capture.channel = dma_cp_chan; // 设置capture dma通道号
    pri_dai->src_clk = i2s_cfg->src_clk;
    pri_dai->dma_playback.dma_size = 4; // 设置playback dma传输单元大小为4个字节
    pri_dai->dma_capture.dma_size = 4; // 设置capture dma传输单元大小为4个字节
我们再看看Board初始化时,如何设定platform_device的resource,文件arch/arm/mach-exynos/dev-audio.c:
static struct resource exynos4_i2s0_resource[] = {
    [0] = {
        .start  = EXYNOS4_PA_I2S0, // start字段保存的是I2S基地址
        .end    = EXYNOS4_PA_I2S0 + 0x100 - 1,
        .flags  = IORESOURCE_MEM,  // 置该resource标识为MEM资源
    },
    [1] = {
        .start  = DMACH_I2S0_TX,   // start字段保存的是用于回放的dma通道号
        .end    = DMACH_I2S0_TX,
        .flags  = IORESOURCE_DMA,  // 置该resource标识为DMA资源
    },
    [2] = {
        .start  = DMACH_I2S0_RX,   // start字段保存的是用于录制的dma通道号
        .end    = DMACH_I2S0_RX,
        .flags  = IORESOURCE_DMA,  // 置该resource标识为DMA资源
    },
    [3] = {
        .start  = DMACH_I2S0S_TX,  // start字段保存的是用于回放的idma通道号
        .end    = DMACH_I2S0S_TX,
        .flags  = IORESOURCE_DMA,  // 置该resource标识为DMA资源
    },
};

struct platform_device exynos4_device_i2s0 = {
    .name = "samsung-i2s", // platform_device名称标识为"samsung-i2s",将与i2s.c中的samsung_i2s_driver匹配
    .id = 0,
    .num_resources = ARRAY_SIZE(exynos4_i2s0_resource),
    .resource = exynos4_i2s0_resource,
    .dev = {
        .platform_data = &i2sv5_pdata,
    },
};

当samsung_i2s_driver初始化时,通过platform_get_resource()函数来获取platform_device声明的resource。


struct resource结构中我们通常关心start、end和flags这3个字段,分别标明资源的开始值、结束值和类型。flags可以为IORESOURCE_IO、IORESOURCE_MEM、IORESOURCE_IRQ、IORESOURCE_DMA等。start、end的含义会随着flags而变更,如当flags为IORESOURCE_MEM时,start、end分别表示该platform_device占据的内存的开始地址和结束地址;当flags为IORESOURCE_IRQ时,start、end分别表示该platform_device使用的中断号的开始值和结束值,如果只使用了1个中断号,开始和结束值相同。对于同种类型的资源而言,可以有多份,譬如说某设备占据了2个内存区域,则可以定义2个IORESOURCE_MEM资源。
以上内容摘自:http://21cnbao.blog.51cto.com/109393/337609


pcm dma


pcm数据管理可以说是ALSA系统中最核心的部分,这部分的工作有两个:

·          copy_from_user把用户态的音频数据拷贝到dma buffer;

·          启动dma把音频数据从dma buffer传送到I2S tx FIFO,这是回放的情形。

当数据送到I2S tx FIFO后,剩下的是启动I2S把数据传送到codec,然后DAC把数字PCM信号转换成模拟信号,送给SPK/HS输出,关于I2S(cpu_dai)和codec,在以上两章已经描述清楚了。

为什么要使用dma传输?两个原因:首先在数据传输过程中,不需要cpu的参与,节省了cpu的开销;其次传输速度快,提高硬件设备的吞吐量。对于Arm,它不能直接把数据从A地址搬运到B地址,只能把数据从A地址搬运到一个寄存器,然后再从这个寄存器搬运到B地址;而dma有突发(Burst)传输能力,这种模式下一次能传输几个甚至十几个字节的数据,尤其适合大数据的高速传输。一个dma传输块里面,可以划分若干个帧,每传输完一帧都可以产生一个中断。

写这个文档的初衷是为了描述清楚pcm数据流向,这里先剖析pcm dma驱动,以便后面pcm native的分析。

以Exynos平台为例,代码位置sound/soc/samsung/dma.c。


·          浅绿色:pcm_dma驱动共有的实现

·          浅灰色:samsung exynos平台的pcm_dma驱动特有的实现

·          浅紫色:pcm native关键结构

·          浅橙色:snd_soc_platform是pcm_dma注册时所创建的platform实例


我们先看看dma相关的结构,对于回放来说,dma把内存缓冲区的音频数据传送到I2S tx FIFO;对于录制来说,dma把I2S rx FIFO的音频数据传送到内存缓存区。因此在dma传输之前,必须确定data buffer和I2S FIFO信息。

snd_dma_buffer:数据缓存区,用于保存从用户态拷贝过来的音频数据,包括dma buffer的物理首地址,虚拟首地址、大小等信息;其中物理地址用于设定dma传输的源地址(回放情形)或目的地址(录制情形),虚拟地址用于与用户态的音频数据拷贝。

s3c_dma_params:dma设备描述,包括设备总线地址(回放的情形下为I2S tx FIFO首地址,设置为dma传输的目的地址)、dma通道号、dma传输数据单元大小,这些信息在i2s.c中获取。

runtime_data:dma运行期信息

·          state:记录dma状态,启动或停止;

·          dma_loaded:dma装载计数,每当启动一次dma传输,该计数加一;每当完成一次dma传输,该计数减一;

·          dma_period:dma周期数据大小;

·          dma_start:指向dma buffer的物理首地址;

·          dma_pos:记录dma buffer当前指针位置,当dma每传输一次数据,都会更新该指针;

·          dma_end:dma buffer结束位置;

·          params:dma设备描述信息,包括设备总线地址、dma通道号、传输单元大小。


pcm operations


pcm_dma操作函数的实现是本模块实现的主体,它用snd_pcm_ops结构体描述:

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 (*copy)(struct snd_pcm_substream *substream, int channel,
            snd_pcm_uframes_t pos,
            void __user *buf, snd_pcm_uframes_t count);
    int (*silence)(struct snd_pcm_substream *substream, int channel, 
               snd_pcm_uframes_t pos, snd_pcm_uframes_t count);
    struct page *(*page)(struct snd_pcm_substream *substream,
                 unsigned long offset);
    int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
    int (*ack)(struct snd_pcm_substream *substream);
};

下面介绍几个重要的回调函数:

·          open:打开pcm逻辑设备时,会回调该函数,用于为runtime设定硬件约束,为runtime的private_data申请一个私有结构,保存dma资源如通道号、传输单元、缓冲区信息、IO设备信息等。代码如下:

static const struct snd_pcm_hardware dma_hardware = {
    .info           = SNDRV_PCM_INFO_INTERLEAVED |
                    SNDRV_PCM_INFO_BLOCK_TRANSFER |
                    SNDRV_PCM_INFO_MMAP |
                    SNDRV_PCM_INFO_MMAP_VALID |
                    SNDRV_PCM_INFO_PAUSE |
                    SNDRV_PCM_INFO_RESUME,
    .formats        = SNDRV_PCM_FMTBIT_S16_LE |
                    SNDRV_PCM_FMTBIT_U16_LE |
                    SNDRV_PCM_FMTBIT_U8 |
                    SNDRV_PCM_FMTBIT_S8,
    .channels_min       = 2,
    .channels_max       = 2,
    .buffer_bytes_max   = 128*1024,
    .period_bytes_min   = PAGE_SIZE,
    .period_bytes_max   = PAGE_SIZE*2,
    .periods_min        = 2,
    .periods_max        = 128,
    .fifo_size      = 32,
};

static int dma_open(struct snd_pcm_substream *substream)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct runtime_data *prtd;

    pr_debug("Entered %s\n", __func__);

    // 设置substream运行期信息中的hw字段,可以把dma_hardware看成该platform的硬件约束
    snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
    snd_soc_set_runtime_hwparams(substream, &dma_hardware);

    // 为runtime_data分配内存,用于保存dma资源,包括缓冲区信息、IO设备信息、通道号、传输单元大小 
    prtd = kzalloc(sizeof(struct runtime_data), GFP_KERNEL);
    if (prtd == NULL)
        return -ENOMEM;

    spin_lock_init(&prtd->lock);

    // runtime中的private_data字段指向runtime_data 
    runtime->private_data = prtd;
    return 0;
}

·          hw_params:设置pcm硬件参数时(cmd:SNDRV_PCM_IOCTL_HW_PARAMS),会回调该函数,一般用于初始化dma资源,包括通道号、传输单元、缓冲区信息、IO设备信息等。代码如下:

static int dma_hw_params(struct snd_pcm_substream *substream,
    struct snd_pcm_hw_params *params)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct runtime_data *prtd = runtime->private_data;
    struct snd_soc_pcm_runtime *rtd = substream->private_data;
    unsigned long totbytes = params_buffer_bytes(params); 
    struct s3c_dma_params *dma =
        snd_soc_dai_get_dma_data(rtd->cpu_dai, substream); // 从cpu dai驱动i2s.c取得dma设备资源
    struct samsung_dma_info dma_info;

    /* return if this is a bufferless transfer e.g.
     * codec <--> BT codec or GSM modem -- lg FIXME */
    if (!dma)
        return 0;

    /* this may get called several times by oss emulation
     * with different params -HW */
    if (prtd->params == NULL) {
        /* prepare DMA */
        prtd->params = dma; // 该字段保存的是dma设备资源,如I2S tx FIFO地址、dma通道号、dma传输单元等

        prtd->params->ops = samsung_dma_get_ops(); // dma操作函数集,这些操作函数实现见:arch/arm/plat-samsung/dma-ops.c

        //...
        prtd->params->ch = prtd->params->ops->request(
                prtd->params->channel, &dma_info);
    }

    snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); // 这里把dma buffer相关信息赋给substream runtime,dma_buffer在创建pcm逻辑设备时分配

    runtime->dma_bytes = totbytes;

    spin_lock_irq(&prtd->lock);
    prtd->dma_loaded = 0;
    prtd->dma_period = params_period_bytes(params);
    prtd->dma_start = runtime->dma_addr; // dma buffer的物理首地址
    prtd->dma_pos = prtd->dma_start;
    prtd->dma_end = prtd->dma_start + totbytes;
    spin_unlock_irq(&prtd->lock);

    return 0;
}

 

·          prepare:当数据已准备好时(cmd:SNDRV_PCM_IOCTL_PREPARE),会回调该函数告知dma数据已就绪。代码如下:

static int dma_prepare(struct snd_pcm_substream *substream)
{
    struct runtime_data *prtd = substream->runtime->private_data;
    int ret = 0;

    pr_debug("Entered %s\n", __func__);

    /* return if this is a bufferless transfer e.g.
     * codec <--> BT codec or GSM modem -- lg FIXME */
    if (!prtd->params)
        return 0;

    /* flush the DMA channel */
    prtd->params->ops->flush(prtd->params->ch);

    prtd->dma_loaded = 0; // 初始化dma装载计数
    prtd->dma_pos = prtd->dma_start; // 设置dma buffer当前指针为dma buffer首地址

    /* enqueue dma buffers */
    dma_enqueue(substream); // 插入到dma传输队列中

    return ret;
}

dma_enqueue()函数,把当前dma buffer插入到dma传输队列中,当触发trigger()启动dma传输后,dma系统将会把dma buffer数据传送到FIFO(回放的情形)。

注意:每次dma传输完成后,都要调用snd_pcm_period_elapsed()告知pcm native一个周期的数据已经传送到FIFO上了,然后再次调用dma_enqueue(),dma传输...如此循环,直到触发trigger()停止dma传输。

·          trigger:pcm数据传送开始、停止、暂停、恢复时,会回调该函数启动或停止dma传输(补充:当上层第一次调用pcm_write()时,触发trigger启动dma传输;当上层调用pcm_stop()或pcm_drop()时,触发trigger停止dma传输)。trigger函数里面的操作必须是原子的,不能有可能引起睡眠的操作,并且应尽量简单。代码如下:

static int dma_trigger(struct snd_pcm_substream *substream, int cmd)
{
    struct runtime_data *prtd = substream->runtime->private_data;
    int ret = 0;

    pr_debug("Entered %s\n", __func__);

    spin_lock(&prtd->lock);

    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
    case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
        prtd->state |= ST_RUNNING;
        prtd->params->ops->trigger(prtd->params->ch); // 启动dma传输
        break;

    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
    case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
        prtd->state &= ~ST_RUNNING;
        prtd->params->ops->stop(prtd->params->ch); // 停止dma传输
        break;

    default:
        ret = -EINVAL;
        break;
    }

    spin_unlock(&prtd->lock);

    return ret;
}

·          pointer:该回调函数返回传输数据的当前位置。当dma每完成一次传输后,都会调用该函数获得传输数据的当前位置,这样pcm native可根据它来计算dma buffer指针位置及可用空间。该函数也是原子的。代码如下:

static snd_pcm_uframes_t
dma_pointer(struct snd_pcm_substream *substream)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct runtime_data *prtd = runtime->private_data;
    unsigned long res;

    pr_debug("Entered %s\n", __func__);

    res = prtd->dma_pos - prtd->dma_start; // 当前位置减去首地址,其实就是已传输数据的size

    pr_debug("Pointer offset: %lu\n", res);

    /* we seem to be getting the odd error from the pcm library due
     * to out-of-bounds pointers. this is maybe due to the dma engine
     * not having loaded the new values for the channel before being
     * called... (todo - fix )
     */

    if (res >= snd_pcm_lib_buffer_bytes(substream)) {
        if (res == snd_pcm_lib_buffer_bytes(substream))
            res = 0;
    }

    return bytes_to_frames(substream->runtime, res); // 单位转化为frames
}


dma buffer allocation


pcm operations小节,数次提及dma buffer,即dma数据缓冲区,用于保存上层拷贝过来的音频数据。dma buffer的分配,一般发生在pcm_dma驱动初始化阶段(probe)或pcm逻辑设备创建阶段(pcm_new)。代码如下:

static int preallocate_dma_buffer(struct snd_pcm *pcm, int stream)
{
    struct snd_pcm_substream *substream = pcm->streams[stream].substream;
    struct snd_dma_buffer *buf = &substream->dma_buffer;
    size_t size = dma_hardware.buffer_bytes_max; // dma buffer大小不得超过hardware的buffer_bytes_max

    buf->dev.type = SNDRV_DMA_TYPE_DEV;
    buf->dev.dev = pcm->card->dev;
    buf->private_data = NULL;
    buf->area = dma_alloc_writecombine(pcm->card->dev, size,
                       &buf->addr, GFP_KERNEL); // area字段是dma buffer的虚拟首地址,addr字段是dma buffer的物理首地址
    if (!buf->area)
        return -ENOMEM;
    buf->bytes = size;
    return 0;
}

static int dma_new(struct snd_soc_pcm_runtime *rtd)
{
    struct snd_card *card = rtd->card->snd_card;
    struct snd_pcm *pcm = rtd->pcm;
    int ret = 0;

    if (!card->dev->dma_mask)
        card->dev->dma_mask = &dma_mask;
    if (!card->dev->coherent_dma_mask)
        card->dev->coherent_dma_mask = DMA_BIT_MASK(32);

    if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) {
        ret = preallocate_dma_buffer(pcm, // 为playback stream分配的dma buffer
            SNDRV_PCM_STREAM_PLAYBACK);
        if (ret)
            goto out;
    }

    if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) {
        ret = preallocate_dma_buffer(pcm, // 为capture stream分配的dma buffer
            SNDRV_PCM_STREAM_CAPTURE);
        if (ret)
            goto out;
    }
out:
    return ret;
}

static struct snd_soc_platform_driver samsung_asoc_platform = {
    .ops        = &dma_ops,
    .pcm_new    = dma_new,
    .pcm_free   = dma_free_dma_buffers,
};

当soc-core调用soc_new_pcm()创建pcm逻辑设备时,会回调pcm_new()完成dma buffer的分配,注意回放子流和录制子流都有着各自的dma buffer。


pcm dma register


上两个小节,我们介绍了pcm_dma操作函数的作用及实现和dma buffer的分配,本小节分析pcm_dma注册过程。

当platform_driver:

static struct platform_driver asoc_dma_driver = {
    .driver = {
        .name = "samsung-audio",
        .owner = THIS_MODULE,
    },
    .probe = samsung_asoc_platform_probe,
    .remove = __devexit_p(samsung_asoc_platform_remove),
};

name = "samsung-audio"的platform_device(该platform_device在arch/arm/plat-samsung/devs.c中创建)匹配后,立即回调samsung_asoc_platform_probe()注册platform:

static struct snd_soc_platform_driver samsung_asoc_platform = {
    .ops        = &dma_ops,
    .pcm_new    = dma_new,
    .pcm_free   = dma_free_dma_buffers,
};

static int __devinit samsung_asoc_platform_probe(struct platform_device *pdev)
{
    return snd_soc_register_platform(&pdev->dev, &samsung_asoc_platform);
}

snd_soc_register_platform:将platform_drv注册到asoc-core。

·          创建一个snd_soc_platform实例,包含platform_drv(snd_soc_platform_driver)的相关信息,封装给asoc-core使用。

·          把以上创建并初始化好的platform插入到platform_list链表上,Machine驱动初始化时会遍历该链表,以找到dai_link声明的platform并绑定。

代码实现:

/**
 * snd_soc_register_platform - Register a platform with the ASoC core
 *
 * @platform: platform to register
 */
int snd_soc_register_platform(struct device *dev,
        struct snd_soc_platform_driver *platform_drv)
{
    struct snd_soc_platform *platform;

    platform = kzalloc(sizeof(struct snd_soc_platform), GFP_KERNEL);
    if (platform == NULL)
        return -ENOMEM;

    /* create platform component name */
    platform->name = fmt_single_name(dev, &platform->id);
    if (platform->name == NULL) {
        kfree(platform);
        return -ENOMEM;
    }

    platform->dev = dev;
    platform->driver = platform_drv;
    platform->dapm.dev = dev;
    platform->dapm.platform = platform;
    platform->dapm.stream_event = platform_drv->stream_event;
    mutex_init(&platform->mutex);

    mutex_lock(&client_mutex);
    list_add(&platform->list, &platform_list);
    mutex_unlock(&client_mutex);

    return 0;
}


至此,完成了platform驱动的实现,回放的情形下,pcm_dma负责把dma buffer中的数据搬运到I2S tx FIFO,I2S负责把FIFO中的数据传送到codec。

至于alsa如何把音频数据从userspace拷贝到dma buffer,如何管理dma buffer,如何启动I2S和DMA传输,见后续pcm native分析。

``` import os import matplotlib.ticker as mticker import netCDF4 as nc import matplotlib.path as mpath import cmaps import matplotlib.pyplot as plt import numpy as np import matplotlib as mpl import cartopy.crs as ccrs import cartopy.feature as cfeature from netCDF4 import Dataset from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER import regionmask import cartopy.io.shapereader as shpreader #-------------------------------- 1. 读取NC文件 --------------------------------# nc_file = r"D:\biyelunwen\data\sat_swe_flow.nc" ds = Dataset(nc_file) # 假设数据变量名为'snow',经纬度为'lon'和'lat' # 请根据实际NC文件结构调整变量名称和维度顺序 lon = ds.variables['longitude'][:] # 读取经度数组 lat = ds.variables['latitude'][:] # 读取纬度数组 data1 = ds.variables['flow_sat_to_swe'][:] # 读取数据,假设为三维数组(time, lat, lon) data2 = ds.variables['flow_swe_to_sat'][:] # 示例:取第一个时间步和创建示例数据 dss = np.array([data1, data2]) # 生成两个数据层用于绘图演示 #-------------------------------- 2. 图形参数设置 --------------------------------# mpl.rcParams["font.family"] = 'Arial' mpl.rcParams["mathtext.fontset"] = 'cm' mpl.rcParams["font.size"] = 12 mpl.rcParams["axes.linewidth"] = 1 proj = ccrs.NorthPolarStereo(central_longitude=0) # 北半球极地投影 leftlon, rightlon, lowerlat, upperlat = (-180, 180, 20, 90) img_extent = [leftlon, rightlon, lowerlat, upperlat] #-------------------------------- 3. 创建画布和子图 --------------------------------# fig = plt.figure(figsize=(14,6), dpi=600) title = np.array(['(a) inf from SAT to SWE', '(b) inf from SWE to SAT']).reshape(2, 1) for j in range(2): # 设置子图位置 left = 0.1 + j * 0.25 ax = fig.add_axes([left, 0.5, 0.35, 0.4], projection=proj) # 添加标题 ax.set_title(f'{title[j, 0]}', loc='center', fontsize=14) #-------------------------------- 4. 设置地图要素 --------------------------------# # 添加网格线 gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=1, color='grey', linestyle='--', xlocs=[-180, -90, 0, 90, 180]) gl.xformatter = LONGITUDE_FORMATTER # 经度格式 gl.yformatter = LATITUDE_FORMATTER # 纬度格式 gl.rotate_labels = False # 禁止旋转标签 ax.set_extent(img_extent, ccrs.PlateCarree()) # 设置地图范围 ax.add_feature(cfeature.COASTLINE.with_scale('110m')) # 添加海岸线 #-------------------------------- 5. 裁剪圆形边界 --------------------------------# theta = np.linspace(0, 2*np.pi, 100) center, radius = [0.5, 0.5], 0.5 verts = np.vstack([np.sin(theta), np.cos(theta)]).T circle = mpath.Path(verts * radius + center) ax.set_boundary(circle, transform=ax.transAxes) #-------------------------------- 6. 添加青藏高原轮廓 --------------------------------# tp_shape = shpreader.Reader(r"D:\biyelunwen\TPBoundary_new(2021)\TPBoundary_new(2021).shp").geometries() ax.add_geometries(tp_shape, crs=ccrs.PlateCarree(), edgecolor='red', facecolor='none', linewidth=1.5, linestyle='--', alpha=0.8) #-------------------------------- 7. 数据处理与可视化 --------------------------------# # 创建二维经纬度网格 lon2d, lat2d = np.meshgrid(lon, lat) # 绘制填色图 pcm = ax.contourf(lon, lat, transform=ccrs.PlateCarree(), cmap='RdBu_r', levels=np.arange(-4, 4, 0.4), extend='neither') #-------------------------------- 8. 添加颜色条 --------------------------------# cax = fig.add_axes([0.2, 0.4, 0.4, 0.03]) # 调整颜色条位置 cbar = fig.colorbar(pcm, cax=cax, orientation='horizontal') cbar.set_label('Snow Depth (m)') # 设置颜色条标签 plt.show()```如何画出data1,data2的北极极地投影图
最新发布
03-31
<think>好的,我现在需要帮助用户解决如何用他们提供的Python代码画出data1和data2的北极极地投影图。首先,我需要仔细阅读他们的代码,理解每个部分的作用,然后找出可能存在的问题,并提供修改建议。 用户提供的代码大致分为几个部分:读取NC文件、图形参数设置、创建画布和子图、地图要素设置、裁剪圆形边界、添加青藏高原轮廓、数据处理与可视化、添加颜色条。看起来他们已经搭建了基本的绘图框架,但可能存在一些问题导致无法正确显示数据。 首先,用户提到要绘制data1和data2的数据,但在循环中他们使用的是dss数组,而dss是通过将data1和data2堆叠创建的。这里需要确认是否正确处理了这两个数据集。例如,在循环中,代码应该遍历dss的两个层,分别绘制data1和data2。但目前的代码在循环内部,可能没有正确索引到每个数据层,特别是在绘图部分,ax.contourf的参数是否正确传递了数据? 接下来,注意到在绘图部分,ax.contourf的参数是lon, lat,但实际上应该传入lon2d和lat2d,因为contourf通常需要二维的经纬度网格。用户代码中生成了lon2d和lat2d,但在调用contourf时没有使用,这可能导致数据无法正确映射到地图上。这是一个可能的错误点。 另外,颜色条的添加部分,用户可能在循环外部添加了一个颜色条,但循环中每次绘制都会覆盖pcm变量,导致颜色条只对应最后一个子图的数据。正确的做法应该是在每个子图绘制后分别添加颜色条,或者确保两个子图使用相同的色标范围,然后添加一个共享的颜色条。但当前代码中颜色条的位置和设置可能需要调整,例如位置是否合适,标签是否正确。 还有,数据处理的步骤中,用户是否正确地处理了时间维度?例如,data1和data2是否是三维数组(时间,纬度,经度)?如果是,那么在绘图时可能需要选择一个时间步,比如data1[0]和data2[0],或者取时间平均。用户示例中取第一个时间步,但代码中的dss生成是否正确?需要确认dss的结构是否符合预期。 另外,青藏高原轮廓的添加是否正确?用户提供的shp文件路径是否存在,是否有权限访问?如果路径错误,可能导致程序崩溃或轮廓无法显示。不过,用户的问题更多是绘图本身,所以可能暂时忽略文件路径的问题,除非代码运行时报错。 还有,极地投影的设置是否正确?北半球极地投影通常需要调整地图的边界,以圆形显示。用户通过set_boundary方法使用了一个圆形的路径,这应该正确。但需要确认是否所有的地理要素都被正确裁剪,比如海岸线、网格线是否出现在圆形边界之外。 颜色映射和色阶的设置是否合理?用户使用了'RdBu_r',这对于显示正负变化的数据是合适的,但色阶范围设置为-44,步长0.4,需要确认数据范围是否适合。如果数据的实际范围超过这个,可能会导致颜色饱和,或者未覆盖全部数据范围,需要调整levels参数。 此外,子图的布局可能需要调整,例如fig.add_axes中的参数,left的位置是否正确计算,避免子图重叠或间距不当。当前的left计算是0.1 + j*0.25,两个子图的left位置分别为0.1和0.35,而宽度是0.35,可能导致第二个子图的右侧超出画布,因为0.35+0.35=0.7,但画布宽度是14英寸,可能实际位置需要重新计算。或者用户可能希望并排显示两个子图,此时需要调整left和子图宽度,确保它们不重叠。 最后,颜色条的标签是否正确?用户设置为'Snow Depth (m)',但数据变量名是flow_sat_to_swe和flow_swe_to_sat,可能需要更合适的标签,比如'Influence Strength'或其他相关名称。 总结可能的问题点: 1. 在contourf中没有使用lon2d和lat2d,导致数据无法正确映射。 2. 循环中未正确索引dss的数据层,可能导致两个子图绘制相同的数据。 3. 颜色条只添加了一次,且可能对应最后一个子图的数据,而非两者共享。 4. 子图的位置和尺寸可能导致重叠或布局问题。 5. 数据维度处理是否正确,比如是否选择了正确的时间步。 6. 颜色映射的参数是否适合数据范围。 接下来,我需要逐步分析代码,并针对这些问题提出修改建议,确保data1和data2能够正确显示在北极极地投影图上。</think>以下是逐步修改代码以正确绘制北极投影图的说明,关键修改点已用$\boxed{}$标出: --- ### **步骤解析与代码修正** $\boxed{1}$ **数据索引问题** ```python # 原代码错误:循环中未正确索引数据层 # 修改后:通过dss[j]分别读取两个数据层 for j in range(2): ... pcm = ax.contourf(lon2d, lat2d, dss[j][0], # 索引dss的第j层数据 transform=ccrs.PlateCarree(), cmap='RdBu_r', levels=np.arange(-4,4,0.4), extend='both') # 扩展色阶范围 ``` $\boxed{2}$ **二维坐标网格未正确使用** ```python # 原代码错误:contourf使用一维lon/lat而非二维网格 # 修改后:使用meshgrid生成的二维坐标 lon2d, lat2d = np.meshgrid(lon, lat) # 创建二维网格 pcm = ax.contourf(lon2d, lat2d, dss[j][0], ...) # 传入二维坐标 ``` $\boxed{3}$ **颜色条共享设置** ```python # 原代码错误:颜色条仅关联最后一个子图 # 修改后:创建统一色标范围并添加全局颜色条 vmin, vmax = -4, 4 # 定义统一色阶范围 levels = np.linspace(vmin, vmax, 21) # 生成21个等间距色阶 cbar = fig.colorbar(mpl.cm.ScalarMappable(norm=mpl.colors.Normalize(vmin, vmax), cmap='RdBu_r'), cax=cax, orientation='horizontal', extend='both') cbar.set_label('Information Flow Strength') # 修改更准确的标签 ``` $\boxed{4}$ **子图布局优化** ```python # 调整子图位置参数避免重叠 left = 0.05 + j * 0.45 # 增加间距从0.25改为0.45 ax = fig.add_axes([left, 0.1, 0.4, 0.8]) # 调整高度占比 ``` --- ### **完整修正代码** ```python import os import matplotlib.ticker as mticker import netCDF4 as nc import matplotlib.path as mpath import cmaps import matplotlib.pyplot as plt import numpy as np import matplotlib as mpl import cartopy.crs as ccrs import cartopy.feature as cfeature from netCDF4 import Dataset from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER import regionmask import cartopy.io.shapereader as shpreader #-------------------------------- 1. 读取NC文件 --------------------------------# nc_file = r"D:\biyelunwen\data\sat_swe_flow.nc" ds = Dataset(nc_file) lon = ds.variables['longitude'][:] lat = ds.variables['latitude'][:] data1 = ds.variables['flow_sat_to_swe'][:] # 三维数组(time, lat, lon) data2 = ds.variables['flow_swe_to_sat'][:] # 生成示例数据:取第一个时间步 dss = np.array([data1[0], data2[0]]) # $\boxed{数据索引修正}$ #-------------------------------- 2. 图形参数设置 --------------------------------# mpl.rcParams.update({ "font.family": 'Arial', "mathtext.fontset": 'cm', "font.size": 12, "axes.linewidth": 1 }) proj = ccrs.NorthPolarStereo(central_longitude=0) img_extent = [-180, 180, 20, 90] #-------------------------------- 3. 创建画布 --------------------------------# fig = plt.figure(figsize=(14,6), dpi=200) title_list = ['(a) inf from SAT to SWE', '(b) inf from SWE to SAT'] # 定义统一色标 vmin, vmax = -4, 4 levels = np.linspace(vmin, vmax, 21) #-------------------------------- 4. 主绘图循环 --------------------------------# for j in range(2): # 创建子图并调整位置 left = 0.05 + j * 0.45 # $\boxed{布局优化}$ ax = fig.add_axes([left, 0.1, 0.4, 0.8], projection=proj) # 设置标题 ax.set_title(title_list[j], fontsize=14, pad=20) # 添加地理要素 ax.set_extent(img_extent, ccrs.PlateCarree()) ax.add_feature(cfeature.COASTLINE.with_scale('50m'), lw=0.5) ax.add_feature(cfeature.LAND, facecolor='lightgray') # 绘制网格线 gl = ax.gridlines(draw_labels=True, linestyle='--', color='grey') gl.xlocator = mticker.FixedLocator([-180, -90, 0, 90, 180]) gl.ylocator = mticker.FixedLocator(np.arange(20, 91, 10)) gl.ylabel_style = {'size': 10} gl.xformatter = LONGITUDE_FORMATTER gl.yformatter = LATITUDE_FORMATTER # 创建二维网格 lon2d, lat2d = np.meshgrid(lon, lat) # $\boxed{坐标网格生成}$ # 绘制填色图 cf = ax.contourf(lon2d, lat2d, dss[j], levels=levels, cmap='RdBu_r', transform=ccrs.PlateCarree(), extend='both') # 添加青藏高原边界 tp_shp = shpreader.Reader(r"D:\biyelunwen\TPBoundary_new(2021)\TPBoundary_new(2021).shp") ax.add_geometries(tp_shp.geometries(), crs=ccrs.PlateCarree(), edgecolor='red', facecolor='none', linewidth=1.5, linestyle='--') # 裁剪圆形边界 theta = np.linspace(0, 2*np.pi, 100) verts = np.vstack([np.sin(theta), np.cos(theta)]).T circle = mpath.Path(verts * 0.5 + [0.5, 0.5]) ax.set_boundary(circle, transform=ax.transAxes) #-------------------------------- 5. 添加颜色条 --------------------------------# cax = fig.add_axes([0.25, 0.05, 0.5, 0.03]) # 调整位置 cbar = fig.colorbar(mpl.cm.ScalarMappable( norm=mpl.colors.Normalize(vmin, vmax), cmap='RdBu_r'), cax=cax, orientation='horizontal', extend='both') cbar.set_label('Information Flow Strength (×10⁻³)') plt.show() ``` --- ### **关键改进说明** | 改进点 | 原代码问题 | 修正方案 | |----------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------| | 数据索引 | 未正确提取data1/data2的特定时间步 | 使用`dss = np.array([data1[0], data2[0]])`明确指定时间步 | | 二维坐标网格 | 直接使用一维经纬度数组导致投影错位 | 通过`meshgrid`生成二维网格,传入`contourf` | | 颜色条标准化 | 各子图色阶独立导致对比不一致 | 定义统一`vmin/vmax`,使用`Normalize`标准化 | | 青藏高原边界显示 | 未处理文件读取错误可能导致崩溃 | 添加异常处理(代码中未显示,建议用`try-except`块包裹) | | 投影边界裁剪 | 圆形边界可能未完全覆盖北半球 | 调整`circle`半径和中心点参数 | 建议运行前检查:1)NC文件路径是否存在;2)青藏高原shp文件路径有效性;3)数据维度是否与坐标数组匹配。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值