platform driver注册PCM driver、CPU DAI driver以及相关的操作函数、为PCM components分配缓存和设定playback、capture操作。platform driver主要包括audio DMA配置和音频接口驱动。
platform driver中有两个重要的数据结构:struct snd_soc_component_driver
和struct snd_soc_dai_driver
。前者主要管理PCM和DMA配置、后者则用于DAI的参数配置。
1.platform class中的snd_soc_component_driver
在platform部分的struct snd_soc_component_driver
中定义的功能很少,DMA相关的配置由独立的soc generic dma framework完成,所以platform对应的struct snd_soc_component_driver
中一般只需要定义name
项以及部分kcontrol定义即可。
elf2中的i2s部分的定义如下,只定义了几个成员变量。
而rockchip中对应的platform中的定义为
位于sound/soc/rockchip/rockchip_i2s_tdm.c
中
static const struct snd_soc_component_driver rockchip_i2s_tdm_component = {
.name = DRV_NAME,
.controls = rockchip_i2s_tdm_snd_controls,
.num_controls = ARRAY_SIZE(rockchip_i2s_tdm_snd_controls),
};
1.1 ASoC中的generic PCM DMA engine framework
ALSA中PCM设备是其音频处理的核心,PCM layer(属于ALSA core)负责所有的数字音频处理工作,比如初始化playback声卡和caputure声卡、启动设备的数据传输等。
PCM driver通过struct snd_pcm_ops
接口中函数指针指向的函数来实现DMA操作。这部分与ASoC中的platform是无关的,仅与SOC DMA engine中的上行接口(upstream API)交互。DMA engine然后与platform相关的DMA驱动进行交互,进行相应的DMA设置。struct snd_pcm_ops
中包含对PCM接口不同事件的回调函数。
不过在ASoC中,只要用户使用ASoC中generic PCM DMA engine framework,用户无需实例化上述结构体。ASoC core会帮助用户完成上述操作。相关的调用栈为:snd_soc_register_card->snd_soc_instantiate_card->soc_probe_link_dais->soc_new_pcm。用户只需提供必要的硬件信息,比如I2S数据收发寄存器的地址、DMA通道配置等具体的硬件参数和特定的函数即可,减少代码量。
1.2 The audio DMA interface
SoC中使用audio DMA interface提供DMA功能。audio DMA driver通过devm_snd_damengine_pcm_register()
进行注册。其函数原型为
int devm_snd_dmaengine_pcm_register(
struct device *dev,
const struct snd_dmaengine_pcm_config *config,
unsigned int flag
);
elf2中rockchip_i2s_tdm_register_platform
函数中会调用上述函数。
static int rockchip_i2s_tdm_register_platform(struct device *dev)
{
struct rk_i2s_tdm_dev *i2s_tdm = dev_get_drvdata(dev);
struct snd_soc_component *comp;
int ret = 0;
if (device_property_read_bool(dev, "rockchip,no-dmaengine")) {
dev_info(dev, "Used for Multi-DAI\n");
return 0;
}
if (device_property_read_bool(dev, "rockchip,digital-loopback")) {
ret = devm_snd_dmaengine_dlp_register(dev, &dconfig);
if (ret)
dev_err(dev, "Could not register DLP\n");
return ret;
}
if (i2s_tdm->clk_trcm) {
ret = devm_snd_dmaengine_trcm_register(dev);
if (ret) {
dev_err(dev, "Could not register TRCM PCM\n");
return ret;
}
comp = snd_soc_lookup_component(i2s_tdm->dev,
SND_DMAENGINE_TRCM_DRV_NAME);
if (!comp) {
dev_err(dev, "Could not find TRCM PCM\n");
ret = -ENODEV;
}
i2s_tdm->pcm_comp = comp;
return ret;
}
ret = devm_snd_dmaengine_pcm_register(dev, NULL, 0);
if (ret)
dev_err(dev, "Could not register PCM\n");
return ret;
}
该函数会注册strcut snd_dmaengine_pcm_config
到设备中。上述函数原型中,dev
是PCM设备的父类设备,通常来说&pdev->dev.config
是platform-specific PCM configuration,其类型为struct snd_dmaengine_pcm_config
。flags用于表示如何进行DMA channel的相关处理,一般其值设定为0。
#define SND_DMAENGINE_PCM_FLAG_COMPAT BIT(0)
#define SND_DMAENGINE_PCM_FLAG_NO_DT BIT(1)
#define SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX BIT(3)
在注册完成后,generic PCM DMA engine framework会生成相应的snd_pcm_ops
,并将其赋予component driver的的.ops
域。
Linux中DMA操作流程一般为:
dma_request_channel
:用于分配slave channel,这里的slave channel为DMA控制器的UART、SPI、I2S等外设通道。
dmaengine_slave_config
:设定slave和controller的具体参数;
dma_prep_xxxx
:生成用于传输的描述符(descriptor);
dma_cookie=dmaengine_submit(tx)
:提交传输请求并获取DMA cookie;
dma_async_issue_pending(chan)
:启动数据传输并等待完成的回调通知;
在ASoc中,设备树用于将DMA channel映射到PCM设备中。devm_snd_dmaengine_pcm_regisiter()
中通过调用dmaengine_pcm_request_chan_of()
(该函数是基于设备树的接口,dma_request_chan
属于Linux中DMA Engine申请DMA channel的函数)申请DMA channel。为完成步骤1到3,需要向PCM DMA engine core提供额外的信息。这里有两种方式,一种是通过填充strcut snd_dmaengine_pcm_config
中的数据项,传递到devm_snd_damengine_pcm_register()
函数中;另一种是编程使PCM DMA engine framework从系统的DMA engine core中提取信息。步骤4到5由PCM DMA engine core自行处理。
位于"include\sound\dmaengine_pcm.h"对struct snd_dmaengine_pcm_config
进行了定义。
struct snd_dmaengine_pcm_config {
int (*prepare_slave_config)(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct dma_slave_config *slave_config);
struct dma_chan *(*compat_request_channel)(
struct snd_soc_pcm_runtime *rtd,
struct snd_pcm_substream *substream);
int (*process)(struct snd_pcm_substream *substream,
int channel, unsigned long hwoff,
void *buf, unsigned long bytes);
dma_filter_fn compat_filter_fn;
struct device *dma_dev;
const char *chan_names[SNDRV_PCM_STREAM_LAST + 1];
const struct snd_pcm_hardware *pcm_hardware;
unsigned int prealloc_buffer_size;
};
上述结构体主要用于解决DMA channel management,buffer management以及通道配置。目前看到的瑞芯微的代码中,没有使用该结构体进行管理,而是采用第二种办法进行处理,第二种办法更为灵活,不过理解这个结构体的定义,便于学习相关概念,理解第二种方式即generic DMA framework的回调函数的方式
prepare_slave_config
:该回调函数用于填充对于单个PCM sub-stream的DMAslave_config
(其为struct dma_slave_config
,用于运行时对DMA slave channel)。其在PCM driver的hwparams
回调函数中调用。另外,用户可以使用snd_dmaengine_pcm_prepare_slave_config
,对于使用struct snd_dmaengine_dai_dma_data
表示DAI DMA数据的platform驱动,这一函数是prepare_slave_config
通用版本。该函数内部会调用snd_hwparams_to_dma_slave_config
基于hw_params
参数填充slave config的相关项,然后调用snd_dmaengine_set_config_from_dai_data
基于DAI DMA数据填充剩余的数据项。
在使用generic DMA framework提供的回调方法时,用户需要在CPU侧的DAI驱动的.probe
回调函数中调用snd_soc_dai_init_dma_data()
(给定DAI相关的capture和playback通道的DMA数据配置,用数据类型struct snd_dmaengine_dai_dma_data
表示),该函数会设定cpu_dai->playback_dma_data
和cpu_dai->capture_dma_data
数据项,或者类似以下代码片段直接赋值。目的是通过填充struct snd_dmaengine_dai_dma_data
配置playback和capture的参数。
snd_soc_dai_init_dma_data()
的定义如下,
static inline void snd_soc_dai_init_dma_data(struct snd_soc_dai *dai,void *playback,void *capture)
{
dai->playback_dma_data = playback;
dai->capture_dma_data = capture;
}
其中playback和capture虽然传参时是void类型,但是其数据类型实际为strcut snd_dmaengine_dai_dma_data
,其定义位于"include\sound\dmaengine_pcm.h"
struct snd_dmaengine_dai_dma_data {
dma_addr_t addr;
enum dma_slave_buswidth addr_width;
u32 maxburst;
unsigned int slave_id;
void *filter_data;
const char *chan_name;
unsigned int fifo_size;
unsigned int flags;
void *peripheral_config;
size_t peripheral_size;
};
在elf2中,相关的部分代码位于rockchip-i2s-tdm.c
中,这里没有使用snd_soc_dai_init_dma_data()
,而是直接赋值。
static int rockchip_i2s_tdm_dai_probe(struct snd_soc_dai *dai)
{
struct rk_i2s_tdm_dev *i2s_tdm = snd_soc_dai_get_drvdata(dai);
dai->capture_dma_data = &i2s_tdm->capture_dma_data;
dai->playback_dma_data = &i2s_tdm->playback_dma_data;
if (i2s_tdm->mclk_calibrate)
snd_soc_add_component_controls(dai->component,
&rockchip_i2s_tdm_compensation_control,
1);
return 0;
}
在实际的代码中,PCM DMA config存在调用devm_snd_dmaengine_pcm_regisiter()
函数时不传递参数的情况,其值为NULL,注册函数的调用形式为ret=devm_snd_dmaengine_pcm_regisiter(&pdev->dev,NULL,0)
,elf2中采用的就是这种注册方法。
在这种情况下,按照上述描述在CPU DAI Driver的.probe
函数中对capture和playback对应的DAI DMA channel进行配置。使用这种方式,其余的数据将从系统中提取。比如,申请DMA channel时,PCM DMA engine core将依赖device tree中的设备节点中特定的属性的值确定相关的配置,capture和playback对应的DMA channels属性值的name项设定为“rx”和“tx”,如果注册函数的flag项设定为SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX
,在这种情况下,capture和playback使用相同的DMA channel,在设备树节点的属性值的name项为“rx-tx”。
位于rk3588s.dts中可以看到其中的属性节点dmas
和dma-names
的值
i2s0_8ch: i2s@fe470000 {
compatible = "rockchip,rk3588-i2s-tdm";
reg = <0x0 0xfe470000 0x0 0x1000>;
interrupts = <GIC_SPI 180 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru MCLK_I2S0_8CH_TX>, <&cru MCLK_I2S0_8CH_RX>, <&cru HCLK_I2S0_8CH>;
clock-names = "mclk_tx", "mclk_rx", "hclk";
assigned-clocks = <&cru CLK_I2S0_8CH_TX_SRC>, <&cru CLK_I2S0_8CH_RX_SRC>;
assigned-clock-parents = <&cru PLL_AUPLL>, <&cru PLL_AUPLL>;
dmas = <&dmac0 0>, <&dmac0 1>;
dma-names = "tx", "rx";
power-domains = <&power RK3588_PD_AUDIO>;
resets = <&cru SRST_M_I2S0_8CH_TX>, <&cru SRST_M_I2S0_8CH_RX>;
reset-names = "tx-m", "rx-m";
rockchip,clk-trcm = <1>;
i2s-lrck-gpio = <&gpio1 RK_PC5 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default", "idle", "clk";
pinctrl-0 = <&i2s0_sdi0
&i2s0_sdi1
&i2s0_sdi2
&i2s0_sdi3
&i2s0_sdo0
&i2s0_sdo1>;
pinctrl-1 = <&i2s0_idle>;
pinctrl-2 = <&i2s0_lrck
&i2s0_sclk>;
#sound-dai-cells = <0>;
status = "disabled";
};
1.3 PCM hardware configuration
DMA的配置信息无法自动由PCM DMA engine core从系统的设备树中提取时或者需要修改时,platform的PCM driver需要提供PCM hardware配置,描述硬件应当如何在内存中如何组织PCM数据。这些设置通过strcut snd_pcm_hardware
数据类型传递。在rk3588源码中可以看到相关的应用代码。
位于"sound/soc/rockchip/rockchip_trcm.c"中
static int
dmaengine_pcm_set_runtime_hwparams(struct snd_soc_component *component,
struct snd_pcm_substream *substream)
{
struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
struct dmaengine_trcm *trcm = soc_component_to_trcm(component);
struct device *dma_dev = dmaengine_dma_dev(trcm, substream);
struct dma_chan *chan = trcm->chan[substream->stream];
struct snd_dmaengine_dai_dma_data *dma_data;
struct snd_pcm_hardware hw;
if (rtd->num_cpus > 1) {
dev_err(rtd->dev,
"%s doesn't support Multi CPU yet\n", __func__);
return -EINVAL;
}
dma_data = snd_soc_dai_get_dma_data(asoc_rtd_to_cpu(rtd, 0), substream);
memset(&hw, 0, sizeof(hw));
hw.info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID |
SNDRV_PCM_INFO_INTERLEAVED;
hw.periods_min = 2;
hw.periods_max = UINT_MAX;
hw.period_bytes_min = 256;
hw.period_bytes_max = dma_get_max_seg_size(dma_dev);
hw.buffer_bytes_max = SIZE_MAX;
hw.fifo_size = dma_data->fifo_size;
snd_dmaengine_pcm_refine_runtime_hwparams(substream,
dma_data,
&hw,
chan);
return snd_soc_set_runtime_hwparams(substream, &hw);
}
完成配置后,如果使用通过填充strcut snd_dmaengine_pcm_config
中的数据项,传递到devm_snd_damengine_pcm_register()
函数中的方式注册,snd_dmaengine_pcm_config.pcm_hardware
需要在调用注册函数前给定。或者参考rk3588的实现方式,调用其他接口函数,这里使用的函数是snd_dmaengine_pcm_refine_runtime_hwparams
,在运行过程中对PCM hardware的配置进行修改。
2. platform class中的snd_soc_dai_driver
与codec class类似,platform一侧同样有snd_soc_dai_driver用于管理音频接口相关的功能。包括时钟参数的设定、音频格式设定以及事件触发处理函数等。主要的结构体为snd_soc_dai_driver。
elf2中的rockchip-i2s-tdm.c
相关的代码如下
static const struct snd_soc_dai_ops rockchip_i2s_tdm_dai_ops = {
.startup = rockchip_i2s_tdm_startup,
.shutdown = rockchip_i2s_tdm_shutdown,
.hw_params = rockchip_i2s_tdm_hw_params,
.hw_free = rockchip_i2s_tdm_hw_free,
.set_sysclk = rockchip_i2s_tdm_set_sysclk,
.set_fmt = rockchip_i2s_tdm_set_fmt,
.set_tdm_slot = rockchip_dai_tdm_slot,
.trigger = rockchip_i2s_tdm_trigger,
};
static int rockchip_i2s_tdm_dai_prepare(struct platform_device *pdev,
struct snd_soc_dai_driver **soc_dai)
{
struct snd_soc_dai_driver rockchip_i2s_tdm_dai = {
.probe = rockchip_i2s_tdm_dai_probe,
.playback = {
.stream_name = "Playback",
.channels_min = 2,
.channels_max = 64,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = (SNDRV_PCM_FMTBIT_S8 |
SNDRV_PCM_FMTBIT_S16_LE |
SNDRV_PCM_FMTBIT_S20_3LE |
SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE |
SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_LE),
},
.capture = {
.stream_name = "Capture",
.channels_min = 2,
.channels_max = 64,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = (SNDRV_PCM_FMTBIT_S8 |
SNDRV_PCM_FMTBIT_S16_LE |
SNDRV_PCM_FMTBIT_S20_3LE |
SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE |
SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_LE),
},
.ops = &rockchip_i2s_tdm_dai_ops,
};
*soc_dai = devm_kmemdup(&pdev->dev, &rockchip_i2s_tdm_dai,
sizeof(rockchip_i2s_tdm_dai), GFP_KERNEL);
if (!(*soc_dai))
return -ENOMEM;
return 0;
}
3 platform驱动注册
类似于codec驱动,通过调用devm_snd_soc_register_component()
函数,将component和DAI 驱动注册到ASoC Core中,不同的地方在于,platform驱动所依赖的设备初始化函数中会额外调用devm_snd_damengine_pcm_register()
函数用于设定PCM相关的dma操作。
elf2中相关的代码位于sound/soc/rockchip/rockchip_i2s_tdm.c
中
static int rockchip_i2s_tdm_probe(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node;
const struct of_device_id *of_id;
struct rk_i2s_tdm_dev *i2s_tdm;
struct snd_soc_dai_driver *soc_dai;
struct resource *res;
void __iomem *regs;
#ifdef HAVE_SYNC_RESET
bool sync;
#endif
int ret, val, i, irq;
ret = rockchip_i2s_tdm_dai_prepare(pdev, &soc_dai);
if (ret)
return ret;
ret = rockchip_i2s_tdm_register_platform(&pdev->dev);
if (ret)
goto err_suspend;
ret = devm_snd_soc_register_component(&pdev->dev,
&rockchip_i2s_tdm_component,
soc_dai, 1);
if (ret) {
dev_err(&pdev->dev, "Could not register DAI\n");
goto err_suspend;
}
return 0;
err_suspend:
if (!pm_runtime_status_suspended(&pdev->dev))
i2s_tdm_runtime_suspend(&pdev->dev);
err_pm_disable:
pm_runtime_disable(&pdev->dev);
#if defined(HAVE_SYNC_RESET) || defined(CONFIG_SND_SOC_ROCKCHIP_I2S_TDM_MULTI_LANES)
err_unmap:
rockchip_i2s_tdm_unmap(i2s_tdm);
#endif
err_disable_hclk:
clk_disable_unprepare(i2s_tdm->hclk);
return ret;
}
static struct platform_driver rockchip_i2s_tdm_driver = {
.probe = rockchip_i2s_tdm_probe,
.remove = rockchip_i2s_tdm_remove,
.shutdown = rockchip_i2s_tdm_platform_shutdown,
.driver = {
.name = DRV_NAME,
.of_match_table = of_match_ptr(rockchip_i2s_tdm_match),
.pm = &rockchip_i2s_tdm_pm_ops,
},
};
module_platform_driver(rockchip_i2s_tdm_driver);