程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

Rockchip RK3399 - Platform驱动(DMA&i2s0)

----------------------------------------------------------------------------------------------------------------------------

开发板 :NanoPC-T4开发板
eMMC :16GB
LPDDR3 :4GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot :2023.04
linux :6.3
----------------------------------------------------------------------------------------------------------------------------

Platfrom driver提供了配置/使能SoC音频接口的能力;Plaftrom驱动分为三个部分:dma driver、cpu dai driver、dsp driver。

(1) cpu dai driver:在嵌入式系统里面通常指SoC的 I2S、PCM 总线控制器,负责把音频数据从I2S tx FIFO 搬运Codec(这是回放的情形,录制则方向相反)。每个cpu dai driver必须提供以下功能:

  • DAI描述信息;
  • DAI配置信息;
  • PCM描述信息;
  • 系统时钟配置;
  • 挂起和恢复(可选);

更多信息参考: Documentation/sound/soc/dai.rst。

(2) DMA driver :负责把dma buffer中的音频数据搬运到 I2S tx FIFO。值得留意的是:某些情形下是不需要dma操作的,比如Modem和Codec直连,因为Modem本身已经把数据送到 FIFO 了,这时只需启动codec_dai 接收数据即可;DMA driver可以参考soc/pxa/pxa2xx-pcm.c;

(3) DSP driver

每个DSP driver通常提供以下功能:

  • DAPM拓扑;
  • Mixer controls;
  • DMA IO to/from DSP buffers (if applicable);
  • Definition of DSP front end (FE) PCM devices;

更多信息参考: Documentation/sound/soc/platform.rst,https://www.kernel.org/doc/html/v6.3/sound/soc/platform.html驱动代码位于sound/soc/rockchip/rockchip_i2s.c文件。

一、设备树配置

1.1 设备节点i2s0

设备节点i2s0定义在arch/arm64/boot/dts/rockchip/rk3399.dtsi文件:

i2s0: i2s@ff880000 {
        compatible = "rockchip,rk3399-i2s", "rockchip,rk3066-i2s";
        reg = <0x0 0xff880000 0x0 0x1000>;
        rockchip,grf = <&grf>;
        interrupts = <GIC_SPI 39 IRQ_TYPE_LEVEL_HIGH 0>;
        dmas = <&dmac_bus 0>, <&dmac_bus 1>;
        dma-names = "tx", "rx";
        clock-names = "i2s_clk", "i2s_hclk";
        clocks = <&cru SCLK_I2S0_8CH>, <&cru HCLK_I2S0_8CH>;
        pinctrl-names = "bclk_on", "bclk_off";
        pinctrl-0 = <&i2s0_8ch_bus>;
        pinctrl-1 = <&i2s0_8ch_bus_bclk_off>;
        power-domains = <&power RK3399_PD_SDIOAUDIO>;
        #sound-dai-cells = <0>;
        status = "disabled";
};

这是Rockchip RK3399中I2S0设备节点描述。它包括以下属性:

  • compatible:指定设备驱动程序的兼容性,即告诉内核该设备可以被哪些驱动程序所使用;
  • reg:指定I2S0控制器的基地址和地址空间大小,从0xff880000到0xff881000共有0x1000个字节的寄存器空间,其中0xff880000为I2S0寄存器基地址;
  • rockchip,grf:设置为grf设备节点,GRF表示的是通用寄存器文件,由许多用于系统控制的寄存器组成,GRF可以分为两部分;
    • GRF:used for generalnon-secure system;
    • PMUGRF:used for always on sysyem;
  • interrupts:指定I2S控制器的中断号为GIC_SPI 39,并且取值方式为IRQ_TYPE_LEVEL_HIGH,意味着中断信号为高电平触发;
  • dmas:指定数据传输时使用的DMA控制器,第一个表示tx数据使用的DMA控制器,第二个表示rx数据使用的DMA控制器;
  • dma-names:分别对应"tx"和"rx"的DMA名称;
  • clock-names:指定时钟名称,"i2s_clk"表示I2S0控制器时钟,"i2s_hclk" 表示I2S总线时钟;
  • clocks:i2s_clk时钟来自SCLK_I2S0_8CH,i2s_hclk时钟来自 HCLK_I2S0_8CH;
  • pinctrl-names:指定设备pinctrl配置集合;
  • pinctrl-0:设置bclk_on状态对应的引脚配置为i2s0_8ch_bus,这里主要配置I2S0相关引脚复用为I2S功能;
  • pinctrl-1:设置bclk_off状态对应的引脚配置为i2s0_8ch_bus_bclk_off,这里主要配置I2S0相关引脚复用为I2S功能,但是SCLK被设置为了GPIO,因此此时I2S0功能是禁用的;
  • power-domains:指定设备隶属于的电源域,这里是 RK3399_PD_SDIOAUDIO;
  • #sound-dai-cells:表示定义这个节点的sound DAI数据单元格的数量,这里为0表示没有单元格;
  • status:表示设备状态,这里 "disabled" 表示该设备当前是禁用状态;

其中设备节点grf定义如下:

grf: syscon@ff770000 {
        compatible = "rockchip,rk3399-grf", "syscon", "simple-mfd";
        reg = <0x0 0xff770000 0x0 0x10000>;  // GRF基地址0xff770000 大小64kb
        #address-cells = <1>;
        #size-cells = <1>;

        io_domains: io-domains {
                compatible = "rockchip,rk3399-io-voltage-domain";
                status = "disabled";
        };
        ......
}

我们需要在arch/arm64/boot/dts/rockchip/rk3399-evb.dts文件添加如下属性,启用I2S0控制器:

&i2s0 {
        rockchip,playback-channels = <8>;
        rockchip,capture-channels = <8>;
        status = "okay";
};

其中:

  • rockchip,playback-channels:为最大播放通道数;
  • rockchip,capture-channels:为最大录音通道数;

这里配置为8,以播放音频为例我谈一谈我个人的理解,应该是RK3399 I2S0有4条数据线可以用来传输收音频数据,I2S0_SDI0、I2S0_SDI1、I2S0_SDI02、I2S0_SDI03;由于在每条数据线上传输左通道、右通道的信号,因此4*2=8,一共就是8个通道;

关于设备节点属性可以参考文档Documentation/devicetree/bindings/sound/rockchip-i2s.yaml。

而RK3399 I2S控制器驱动代码位于sound/soc/rockchip/rockchip_i2s.c文件。

1.2 引脚配置节点i2s0_8ch_bus

引脚配置节点i2s0_8ch_bus和i2s0_8ch_bus_bclk_off定义在pinctrl设备节点下:

i2s0 {
        i2s0_2ch_bus: i2s0-2ch-bus {
                rockchip,pins =
                        <3 RK_PD0 1 &pcfg_pull_none>,
                        <3 RK_PD1 1 &pcfg_pull_none>,
                        <3 RK_PD2 1 &pcfg_pull_none>,
                        <3 RK_PD3 1 &pcfg_pull_none>,
                        <3 RK_PD7 1 &pcfg_pull_none>,
                        <4 RK_PA0 1 &pcfg_pull_none>;
        };

        i2s0_8ch_bus: i2s0-8ch-bus {
                rockchip,pins =
                        <3 RK_PD0 1 &pcfg_pull_none>,
                        <3 RK_PD1 1 &pcfg_pull_none>,
                        <3 RK_PD2 1 &pcfg_pull_none>,
                        <3 RK_PD3 1 &pcfg_pull_none>,
                        <3 RK_PD4 1 &pcfg_pull_none>,
                        <3 RK_PD5 1 &pcfg_pull_none>,
                        <3 RK_PD6 1 &pcfg_pull_none>,
                        <3 RK_PD7 1 &pcfg_pull_none>,
                        <4 RK_PA0 1 &pcfg_pull_none>;
        };

        i2s0_8ch_bus_bclk_off: i2s0-8ch-bus-bclk-off {
                rockchip,pins =
                        <3 RK_PD0 RK_FUNC_GPIO &pcfg_pull_none>,
                        <3 RK_PD1 1 &pcfg_pull_none>,
                        <3 RK_PD2 1 &pcfg_pull_none>,
                        <3 RK_PD3 1 &pcfg_pull_none>,
                        <3 RK_PD4 1 &pcfg_pull_none>,
                        <3 RK_PD5 1 &pcfg_pull_none>,
                        <3 RK_PD6 1 &pcfg_pull_none>,
                        <3 RK_PD7 1 &pcfg_pull_none>,
                        <4 RK_PA0 1 &pcfg_pull_none>;
        };
}; 

这里我们只关注i2s0_8ch_bus引脚配置节点,这里定义了9个引脚,管脚与具体的功能和电气特性如下:

  • GPIO3_PD0:功能复用为I2S0_SCLK,电气特性配置为pcfg_pull_none;
  • GPIO3_PD1:功能复用为I2S0_LRCK_RX,电气特性配置为pcfg_pull_none;
  • GPIO3_PD2:功能复用为I2S0_LRCK_TX,电气特性配置为pcfg_pull_none;
  • GPIO3_PD3:功能复用为I2S0_SDI0,电气特性配置为pcfg_pull_none;
  • GPIO3_PD4:功能复用为I2S0_SDI1/I2S0_SDO3,电气特性配置为pcfg_pull_none;
  • GPIO3_PD5:功能复用为I2S0_SDI2/I2S0_SDO2,电气特性配置为pcfg_pull_none;
  • GPIO3_PD6:功能复用为I2S0_SDI3/I2S0_SDO1,电气特性配置为pcfg_pull_none;
  • GPIO3_PD7:功能复用为I2S0_SDO0,电气特性配置为pcfg_pull_none;
  • GPIO4_PA0:功能复用为I2S0_MCLK,电气特性配置为pcfg_pull_none;

1.3 时钟频率

这里我们看看一下时钟频率配置:

clock-names = "i2s_clk", "i2s_hclk";
clocks = <&cru SCLK_I2S0_8CH>, <&cru HCLK_I2S0_8CH>;

SCLK_I2S0_8CH、HCLK_I2S0_8CH为平台为时钟分配的特定的id,定义在drivers/clk/rockchip/clk-rk3399.c:

GATE(SCLK_I2S0_8CH, "clk_i2s0", "clk_i2s0_mux", CLK_SET_RATE_PARENT,
                RK3399_CLKGATE_CON(8), 5, GFLAGS),
GATE(HCLK_I2S0_8CH, "hclk_i2s0", "hclk_perilp1", 0, RK3399_CLKGATE_CON(34), 0, GFLAGS),

他们都是gate类型的时钟,其中GATE宏定义在drivers/clk/rockchip/clk.h:

#define GATE(_id, cname, pname, f, o, b, gf)                    \
        {                                                       \
                .id             = _id,                          \
                .branch_type    = branch_gate,                  \
                .name           = cname,                        \
                .parent_names   = (const char *[]){ pname },    \
                .num_parents    = 1,                            \
                .flags          = f,                            \
                .gate_offset    = o,                            \
                .gate_shift     = b,                            \
                .gate_flags     = gf,                           \
        }
1.3.1 hclk_i2s0

我们以GATE(HCLK_I2S0_8CH, "hclk_i2s0", "hclk_perilp1", 0, RK3399_CLKGATE_CON(34), 0, GFLAGS)为例:

  • id表示平台为时钟特定分配的id,这里被设置为了HCLK_I2S0_8CH;定义在include/dt-bindings/clock/rk3399-cru.h,值为468;
  • name表示时钟的名称,这里设置为hclk_i2s0;
  • parent_name为父时钟的名称,这里设置为hclk_perilp1;
  • gate_offset表示控制时钟开关的寄存器偏移地址,这里设置为RK3399_CLKGATE_CON(34);
  • gate_shift表示控制时钟开关bit位,这里设置为0;

宏RK3399_CLKGATE_CON定义在drivers/clk/rockchip/clk.h:

#define RK3399_CLKGATE_CON(x)           ((x) * 0x4 + 0x300)

通过RK3399_CLKGATE_CON(34)可以得到寄存器偏移地址34*0x04+0x300=0x388,偏移0x388是CRU_CLKGATE_CON34寄存器。

在RK3399 datasheet中,我们可以找到名字为hclk_i2s0的时钟的信息,从下图可以看到其父时钟为ID为162(datasheet里这个ID和程序里id并不是相同的),可以在datasheet表中找到162代表的是hclk_perilp1

接着我们看一下CRU_CLKGATE_CON34寄存器,CRU_CLKGATE_CON34为Internal clock gating register34,其中位[0]含义如下:

可以看到位0为hclk_i2s0时钟使能位,低电平使能,高电平禁用。 那hclk_i2s0到底是什么时钟呢?官方给出的解释是I2S BUS时钟;

系统在播放音频文件时, 我们可以通过以下命令查看时钟频率,可以看到输出的频率为100MHz;

root@rk3399:/# cd /
root@rk3399:/# aplay AbuduOffice.wav
Playing WAVE 'AbuduOffice.mp3' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
root@rk3399:/# cat /sys/kernel/debug/clk/hclk_i2s0/clk_rate
100000000
1.3.2 clk_i2s0

我们以GATE(SCLK_I2S0_8CH, "clk_i2s0", "clk_i2s0_mux", CLK_SET_RATE_PARENT, RK3399_CLKGATE_CON(8), 5, GFLAGS)为例:

  • id表示平台为时钟特定分配的id,这里被设置为了SCLK_I2S0_8CH;定义在include/dt-bindings/clock/rk3399-cru.h,值为86;
  • name表示时钟的名称,这里设置为clk_i2s0;
  • parent_name为父时钟的名称,这里设置为clk_i2s0_mux;
  • gate_offset表示控制时钟开关的寄存器偏移地址,这里设置为RK3399_CLKGATE_CON(8);
  • gate_shift表示控制时钟开关bit位,这里设置为5;

宏RK3399_CLKGATE_CON定义在drivers/clk/rockchip/clk.h:

#define RK3399_CLKGATE_CON(x)           ((x) * 0x4 + 0x300)

通过RK3399_CLKGATE_CON(8)可以得到寄存器偏移地址8*0x04+0x300=0x320,偏移0x320是CRU_CLKGATE_CON8寄存器。

在RK3399 datasheet中,我们可以找到名字为clk_i2s0的时钟的信息,从下图可以看到其父时钟为ID为52,可以在datasheet表中找到52代表的是clk_i2s0_mux;

接着我们看一下CRU_CLKGATE_CON8寄存器,CRU_CLKGATE_CON8为Internal clock gating register8,其中位[5]含义如下:

可以看到位5为clk_i2s0时钟使能位,低电平使能,高电平禁用。 那clk_i2s0到底是什么时钟呢?官方给出的解释是I2S Controller时钟,因此我猜测应该就是I2S0模块输出的MCLK时钟

系统在播放音频文件时, 我们可以通过以下命令查看时钟频率,可以看到输出的频率为11.2896MHz;

root@rk3399:/# cd /
root@rk3399:/# aplay AbuduOffice.wav
Playing WAVE 'AbuduOffice.mp3' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
root@rk3399:/# cat /sys/kernel/debug/clk/clk_i2s0/clk_rate
11289600

假设采样频率为44.1kHz,则MCLK=256*44100=11289600,可以看到这个时钟频率和通过命令查看到的是一致的,这也证明了我们的采样。

1.3.3 时钟链路

我们以时钟clk_i2s0为例,我们查找其父时钟:

root@rk3399:/# cat /sys/kernel/debug/clk/clk_i2s0/clk_parent
clk_i2s0_mux

接着我们查找clk_i2s0_mux的父时钟,因此类推,最终可以得到clk_i2s0所在的时钟链路;

  • xin24m --> pll_cpll --> cpll --> clk_i2s0_div (分频器,CRU_CLKSEL_CON28寄存器位[6:0]用于设置分频系数)--> clk_i2s0_frac(gate,CRU_CLKGATE_CON8寄存器位[4]用于时钟使能) --> clk_i2s0_mux(多路选择,CRU_CLKSEL_CON28寄存器位[9:8]用于选择时钟源) --> clk_i2s0。

Rockchip RK3399 - Codec驱动( Realtek ALC5651) 驱动中我们说过I2S0/IS1接口共用一个MCLK引脚,因此要想将clk_i2s0时钟输出到MCLK引脚还需要进行多路选择、以及与门相关的配置。

同理我们可以得到hclk_i2s0的时钟链路:xin24m --> pll_cpll --> cpll --> cpll_hclk_perilp1_src --> hclk_perilp1 --> hclk_i2s0。

二、Platform驱动

2.1 模块入口函数

我们定位到sound/soc/rockchip/rockchip_i2s.c文件的最后;

static struct platform_driver rockchip_i2s_driver = {
        .probe = rockchip_i2s_probe,
        .remove = rockchip_i2s_remove,
        .driver = {
                .name = DRV_NAME,       // rockchip-i2s
                .of_match_table = of_match_ptr(rockchip_i2s_match),  // 用于设备树匹配
                .pm = &rockchip_i2s_pm_ops,
        },
};
module_platform_driver(rockchip_i2s_driver);

module_platform_driver这个宏之前已经介绍过多次了,其展开后等价于:

static int __init rockchip_i2s_driver_init(void) 
{ 
    return platform_driver_register(&(rockchip_i2s_driver)); 
} 
module_init(rockchip_i2s_driver_init); 
static void __exit rockchip_i2s_driver_exit(void) 
{ 
    platform_driver_unregister(&(rockchip_i2s_driver)); 
} 
module_exit(rockchip_i2s_driver_exit);

看到这里是不是有点意外,这里是通过platform_driver_register函数注册了一个名称为"rockchip-i2s"的platform驱动。

2.1.1 设备树匹配

在plaftrom总线设备驱动模型中,我们知道当内核中有platform设备和platform驱动匹配,会调用到platform_driver里的成员.probe,在这里就是rockchip_i2s_probe函数。

而platform设备和驱动匹配规则包括:

  • of_driver_match_device(dev, drv);这一种是进行设备树的匹配;
  • strcmp(pdev->name, drv->name);这一种就是将platform设备的名称和platform驱动的名称进行匹配;

of_driver_match_device定义在include/linux/of_device.h:

/**
 * of_driver_match_device - Tell if a driver's of_match_table matches a device.
 * @drv: the device_driver structure to test
 * @dev: the device structure to match against
 */
static inline int of_driver_match_device(struct device *dev,
                                         const struct device_driver *drv)
{
        return of_match_device(drv->of_match_table, dev) != NULL;
}

可以看到这里是调用of_match_device函数,遍历device_driver的of_match_table数组,然后和device的of_node进行匹配,该函数定义在drivers/of/device.c:

/**
 * of_match_device - Tell if a struct device matches an of_device_id list
 * @matches: array of of device match structures to search in
 * @dev: the of device structure to match against
 *
 * Used by a driver to check whether an platform_device present in the
 * system is in its list of supported devices.
 */
const struct of_device_id *of_match_device(const struct of_device_id *matches,
                                           const struct device *dev)
{
        if ((!matches) || (!dev->of_node))
                return NULL;
        return of_match_node(matches, dev->of_node);
}
2.1.2 rockchip_i2s_match

device_driver的of_match_table数组rockchip_i2s_match定义如下:

static const struct of_device_id rockchip_i2s_match[] __maybe_unused = {
        { .compatible = "rockchip,px30-i2s", },
        { .compatible = "rockchip,rk1808-i2s", },
        { .compatible = "rockchip,rk3036-i2s", },
        { .compatible = "rockchip,rk3066-i2s", },
        { .compatible = "rockchip,rk3128-i2s", },
        { .compatible = "rockchip,rk3188-i2s", },
        { .compatible = "rockchip,rk3228-i2s", },
        { .compatible = "rockchip,rk3288-i2s", },
        { .compatible = "rockchip,rk3308-i2s", },
        { .compatible = "rockchip,rk3328-i2s", },
        { .compatible = "rockchip,rk3366-i2s", },
        { .compatible = "rockchip,rk3368-i2s", },
        { .compatible = "rockchip,rk3399-i2s", .data = &rk3399_i2s_pins },
        { .compatible = "rockchip,rv1126-i2s", },
        {},
};

因此当i2s0设备节点中的compatible与rockchip_i2s_match中某一个元素匹配,of_match_table函数将会返回true,即匹配成功。

i2s0: i2s@ff880000 {
        compatible = "rockchip,rk3399-i2s", "rockchip,rk3066-i2s";
        .......
}

2.2  rockchip_i2s_probe

当platform设备和platform驱动匹配,会调用到platform_driver里的成员.probe,在这里就是rockchip_i2s_probe函数;

static int rockchip_i2s_probe(struct platform_device *pdev)
{
        struct device_node *node = pdev->dev.of_node;
        const struct of_device_id *of_id;
        struct rk_i2s_dev *i2s;
        struct snd_soc_dai_driver *dai;
        struct resource *res;
        void __iomem *regs;
        int ret;

        i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL);  // 1. 动态申请内存,数据结构类型为struct rk_i2s_dev
        if (!i2s)
                return -ENOMEM;

        spin_lock_init(&i2s->lock);             // 初始化自旋锁
        i2s->dev = &pdev->dev;                  // 初始化dev
 
        i2s->grf = syscon_regmap_lookup_by_phandle(node, "rockchip,grf"); // 2. 等价将of_parse_phandle和syscon_node_to_regmap,实现通过regmap来访问grf寄存器
        if (!IS_ERR(i2s->grf)) {
                of_id = of_match_device(rockchip_i2s_match, &pdev->dev);
                if (!of_id || !of_id->data)
                        return -EINVAL;

                i2s->pins = of_id->data;
        }

        /* try to prepare related clocks */
        i2s->hclk = devm_clk_get(&pdev->dev, "i2s_hclk");   // 3. 根据时钟名称"i2s_hclk"获取时钟,设备节点属性clock-names、clocks、
// 指定了名字为"i2s_hclk"对应的时钟为<&cru HCLK_I2S0_8CH>
if (IS_ERR(i2s->hclk)) { dev_err(&pdev->dev, "Can't retrieve i2s bus clock\n"); return PTR_ERR(i2s->hclk); } ret = clk_prepare_enable(i2s->hclk); // 时钟使能 if (ret) { dev_err(i2s->dev, "hclock enable failed %d\n", ret); return ret; } i2s->mclk = devm_clk_get(&pdev->dev, "i2s_clk"); // 4.根据时钟名称"i2s_clk"获取时钟,设备节点属性clock-names、clocks、
// 指定了名字为"i2s_clk"对应的时钟为<&cru SCLK_I2S0_8CH>
if (IS_ERR(i2s->mclk)) { dev_err(&pdev->dev, "Can't retrieve i2s master clock\n"); ret = PTR_ERR(i2s->mclk); goto err_clk; } regs = devm_platform_get_and_ioremap_resource(pdev, 0, &res); // 5.获取I/O内存资源,并将其映射到内核虚拟地址空间中 if (IS_ERR(regs)) { ret = PTR_ERR(regs); goto err_clk; } i2s->regmap = devm_regmap_init_mmio(&pdev->dev, regs, // 6.将I2S0寄存器转化为regmap形式,这样regmap机制的regmap_write、regmap_read等API函数操作寄存器 &rockchip_i2s_regmap_config); if (IS_ERR(i2s->regmap)) { dev_err(&pdev->dev, "Failed to initialise managed register map\n"); ret = PTR_ERR(i2s->regmap); goto err_clk; } i2s->bclk_ratio = 64; i2s->pinctrl = devm_pinctrl_get(&pdev->dev); // 7.获取与设备相关联的pinctrl句柄 if (!IS_ERR(i2s->pinctrl)) { i2s->bclk_on = pinctrl_lookup_state(i2s->pinctrl, "bclk_on"); if (!IS_ERR_OR_NULL(i2s->bclk_on)) { i2s->bclk_off = pinctrl_lookup_state(i2s->pinctrl, "bclk_off"); if (IS_ERR_OR_NULL(i2s->bclk_off)) { dev_err(&pdev->dev, "failed to find i2s bclk_off\n"); ret = -EINVAL; goto err_clk; } } } else { dev_dbg(&pdev->dev, "failed to find i2s pinctrl\n"); } i2s_pinctrl_select_bclk_off(i2s); // 配置设备引脚的状态为bclk_off dev_set_drvdata(&pdev->dev, i2s); // 设置驱动数据 pm_runtime_enable(&pdev->dev); // 电源相关,使能设备的runtime pm功能 暂且忽略 if (!pm_runtime_enabled(&pdev->dev)) { ret = i2s_runtime_resume(&pdev->dev); if (ret) goto err_pm_disable; } ret = rockchip_i2s_init_dai(i2s, res, &dai); // 8. 初始化cpu dai driver if (ret) goto err_pm_disable; ret = devm_snd_soc_register_component(&pdev->dev, // 9. 注册component &rockchip_i2s_component, dai, 1); if (ret) { dev_err(&pdev->dev, "Could not register DAI\n"); goto err_suspend; } ret = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0); // 10 申请dma通道 if (ret) { dev_err(&pdev->dev, "Could not register PCM\n"); goto err_suspend; } return 0; err_suspend: if (!pm_runtime_status_suspended(&pdev->dev)) i2s_runtime_suspend(&pdev->dev); err_pm_disable: pm_runtime_disable(&pdev->dev); err_clk: clk_disable_unprepare(i2s->hclk); return ret; }

(1) 动态申请内存,数据结构类型为struct rk_i2s_dev,并调用dev_set_drvdata将其设置为驱动数据;

(2) 调用syscon_regmap_lookup_by_phandle函数,将GRF寄存器转化为regmap形式,这样regmap机制的regmap_write、regmap_read等API函数操作寄存器;GRF寄存器基地址为0xff770000,大小为64kb;;同时初始化成员pins为rk3399_i2s_pins:

static const struct rk_i2s_pins rk3399_i2s_pins = {
        .reg_offset = 0xe220,
        .shift = 11,
};

偏移0xe220为GRF_SOC_CON8寄存器,寄存器信息如下,从下表可以看到寄存器位偏移11为i2s0_sdio_oe_n,表示位控制;

BitAttr Reset ValueDescription
31:16 RW 0x0000

write_enable bit0~15 write enable

When bit 16=1, bit 0 can be written by software .

When bit 16=0, bit 0 cannot be written by software;

When bit 17=1, bit 1 can be written by software .

When bit 17=0, bit 1 cannot be written by software;

......

When bit 31=1, bit 15 can be written by software .

When bit 31=0, bit 15 cannot be written by software;

15:14 RO 0x0 reserved
13:11 RW 0x0

2s0_sdio_oe_n i2s0

sdio_oe_n bit control

10:7 RW 0x0

pcie_test_i

pci test input

6:1 RW 0x0 0

pcie_test_addr

pci test address control

0 RW 0x0

pcie_test_write

pcie test write control

1'b0: disable

1'b1: enable

(3) 调用devm_clk_get根据时钟名称"i2s_hclk"获取I2S总线时钟,设备节点属性clock-names、clocks、指定了名字为"i2s_hclk"对应的时钟为<&cru HCLK_I2S0_8CH>;并调用clk_prepare_enable使能hclk;

(4) 调用devm_clk_get根据时钟名称"i2s_clk"获取I2C控制器时钟,设备节点属性clock-names、clocks、指定了名字为"i2s_clk"对应的时钟为<&cru SCLK_I2S0_8CH>;如果pm_runtime_enabled返回false,则clk使能是通过i2s_runtime_resume完成;

static int i2s_runtime_resume(struct device *dev)
{
        struct rk_i2s_dev *i2s = dev_get_drvdata(dev);
        int ret;

        ret = clk_prepare_enable(i2s->mclk);
        if (ret) {
                dev_err(i2s->dev, "clock enable failed %d\n", ret);
                return ret;
        }

        regcache_cache_only(i2s->regmap, false);
        regcache_mark_dirty(i2s->regmap);

        ret = regcache_sync(i2s->regmap);
        if (ret)
                clk_disable_unprepare(i2s->mclk);

        return ret;
}

(5) 调用devm_platform_get_and_ioremap_resource获取I/O内存资源,并将其映射到内核虚拟地址空间中;0xff880000为I2S0寄存器基地址;

 reg = <0x0 0xff880000 0x0 0x1000>;

(6) 调用devm_regmap_init_mmio将I2S0寄存器转化为regmap形式,这样regmap机制的regmap_write、regmap_read等API函数操作寄存器;

(7) 调用devm_pinctrl_get获取与设备相关联的pinctrl句柄,并配置设备引脚的状态为bclk_off;

(8) 调用rockchip_i2s_init_dai初始化cpu dai driver;

(9) 调用devm_snd_soc_register_component注册的component,该函数会动态申请一个component,并将其添加到全局链表component_list中,同时会建立dai_driver与component的关系;

注册component完成后,snd_soc_dai,snd_soc_dai_driver、snd_soc_component、snd_soc_component_driver之间的关系如下图:

其中:

  • 新建的snd_soc_component的名称为pdev->dev设备的名称,即ff880000.i2s;
  • snd_soc_component的dai_list链表包含1个dai,dai的名称为ff880000.i2s;
  • 每dai对应的dai driver的名称为NULL,未设置;

(10) 调用devm_snd_dmaengine_pcm_register申请dma通道。

2.3 总结

我们在分析了platform驱动的源码之后,我们大致可以得到一个结论,palform驱动注册流程主要包含一下几个步骤:

(1) 构造一个struct snd_soc_component_driver实例,比如这里的rockchip_i2s_component,用于描述palform驱动;需要初始化成员name;

(2) 构造一个struct snd_soc_dai_driver,比如这里的rockchip_i2s_dai,用于描述dai和 pcm的能力和操作;需要初始化成员name、probe、playback、capture、ops等;

(3) 调用devm_snd_soc_register_component注册component;

下面我们将会具体来解析rockchip_i2s_probe函数源码,如果不感兴趣可以忽略后面章节介绍的内容。

三、相关数据结构

3.1 struct rk_i2s_dev

struct rk_i2s_dev定义在sound/soc/rockchip/rockchip_i2s.c:

struct rk_i2s_dev {
        struct device *dev;

        struct clk *hclk;
        struct clk *mclk;

        struct snd_dmaengine_dai_dma_data capture_dma_data;
        struct snd_dmaengine_dai_dma_data playback_dma_data;

        struct regmap *regmap;
        struct regmap *grf;

        bool has_capture;
        bool has_playback;

/*
 * Used to indicate the tx/rx status.
 * I2S controller hopes to start the tx and rx together,
 * also to stop them when they are both try to stop.
*/
        bool tx_start;
        bool rx_start;
        bool is_master_mode;
        const struct rk_i2s_pins *pins;
        unsigned int bclk_ratio;
        spinlock_t lock; /* tx/rx lock */
        struct pinctrl *pinctrl;
        struct pinctrl_state *bclk_on;
        struct pinctrl_state *bclk_off;
};

其中:

  • dev:设备驱动模型中的device,可以将rk_i2s_dev看做其子类;
  • hclk:I2S总线的时钟;
  • mclk:I2S控制器的时钟,即i2s0模块输出的MCLK时钟(对应的时钟名称为clk_i2s0),是采样频率的256倍;
  • capture_dma_data:capture dam data;
  • playback_dma_data:playback dma data;
  • lock:tx/rx 自旋锁;
  • regmap:实现通过regmap机制来访问I2S0寄存器;
  • grf:实现通过regmap机制来访问GRF寄存器;
  • has_capture:表明是否有音频捕获功能;
  • has_playback:表明是否有音频播放功能;
  • pinctrl:保存client device的所有引脚状态;
  • blck_on:保存blck_on引脚状态;
  • blck_off:保存blck_off引脚状态;

3.2 struct rk_i2s_pins

struct rk_i2s_pins定义在sound/soc/rockchip/rockchip_i2s.c:

struct rk_i2s_pins {
        u32 reg_offset;
        u32 shift;
};

四、相关API

4.1 i2s_pinctrl_select_bclk_off

i2s_pinctrl_select_bclk_off定义在sound/soc/rockchip/rockchip_i2s.c:

static int i2s_pinctrl_select_bclk_off(struct rk_i2s_dev *i2s)
{

        int ret = 0;

        if (!IS_ERR(i2s->pinctrl) && !IS_ERR_OR_NULL(i2s->bclk_off))
                ret = pinctrl_select_state(i2s->pinctrl, i2s->bclk_off);

        if (ret)
                dev_err(i2s->dev, "bclk disable failed %d\n", ret);

        return ret;
}

这里调用pinctrl_select_state函数来配置设备引脚状态为bclk_off。更多pinctrl子系统知识可以参考:linux设备树-pinctrl子系统 

4.2 rockchip_i2s_init_dai

rockchip_i2s_init_dai定义在sound/soc/rockchip/rockchip_i2s.c,主要是初始化dai playback/capuure stream,以及配置dai dma data;

static int rockchip_i2s_init_dai(struct rk_i2s_dev *i2s, struct resource *res,
                                 struct snd_soc_dai_driver **dp)
{
        struct device_node *node = i2s->dev->of_node;
        struct snd_soc_dai_driver *dai;
        struct property *dma_names;
        const char *dma_name;
        unsigned int val;

        of_property_for_each_string(node, "dma-names", dma_names, dma_name) { // 获取属性"dma-names"的值,依次为"tx","rx"
                if (!strcmp(dma_name, "tx"))
                        i2s->has_playback = true; // 设置标志位
                if (!strcmp(dma_name, "rx"))
                        i2s->has_capture = true;  // 设置标志位
        }

        dai = devm_kmemdup(i2s->dev, &rockchip_i2s_dai, // 动态分配内存,并将rockchip_i2s_dai内容拷贝到新分配的内存中,拷贝长度为sizeof(*dai)
                           sizeof(*dai), GFP_KERNEL);
        if (!dai)
                return -ENOMEM;

        if (i2s->has_playback) {   // 支持音频播放
                // 配置pcm playback stream
                dai->playback.stream_name = "Playback";   
                dai->playback.channels_min = 2;            // 最小通道数
                dai->playback.channels_max = 8;            // 最大通道数
                dai->playback.rates = SNDRV_PCM_RATE_8000_192000;  // 采样频率
                dai->playback.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;

                // 配置dai dma data,DMA client driver会使用到
                i2s->playback_dma_data.addr = res->start + I2S_TXDR;   // 偏移0x0024为发送FIFO数据寄存器
                i2s->playback_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;   //音频采样位宽/8,单位字节 4字节
                i2s->playback_dma_data.maxburst = 8;  // FIFO深度

                if (!of_property_read_u32(node, "rockchip,playback-channels", &val)) { // 获取属性"rockchip,playback-channels",值为8
                        if (val >= 2 && val <= 8)
                                dai->playback.channels_max = val;
                }
        }

        if (i2s->has_capture) {  // 支持录音
               // 配置pcm capture stream
                dai->capture.stream_name = "Capture";      
                dai->capture.channels_min = 2;
                dai->capture.channels_max = 8;
                dai->capture.rates = SNDRV_PCM_RATE_8000_192000;
                dai->capture.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;

                // 配置dai dma data,DMA client driver会使用到
                i2s->capture_dma_data.addr = res->start + I2S_RXDR;
                i2s->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
                i2s->capture_dma_data.maxburst = 8;

                if (!of_property_read_u32(node, "rockchip,capture-channels", &val)) { // 获取属性"rockchip,capture-channels",值为8
                        if (val >= 2 && val <= 8)
                                dai->capture.channels_max = val;
                }
        }

        if (dp)
                *dp = dai;

        return 0;
}

这里主要就是动态分配一个struct snd_soc_dai_driver ,其内容是从rockchip_i2s_dai拷贝过去的,并根据dts设备节点属性进行调整;实际上函数执行完成之后得到的dai driver和rockchip_i2s_dai内容是一致的。

并在注册component时并将其作为devm_snd_soc_register_component的第三个参数,有关rockchip_i2s_dai咱们放到最后来说,因为这个比较重要。

4.3 devm_snd_soc_register_component

devm_snd_soc_register_component函数第二个参数为rockchip_i2s_component,定义在sound/soc/rockchip/rockchip_i2s.c:

ret = devm_snd_soc_register_component(&pdev->dev,  // 9. 注册component
                                      &rockchip_i2s_component,
                                      dai, 1);

rockchip_i2s_component函数定义如下:

static const struct snd_soc_component_driver rockchip_i2s_component = {
        .name = DRV_NAME,   // rockchip-i2s
        .legacy_dai_naming = 1,  // 配置个这个之后,在注册component时创建的dai的名字设置为plat->dev的名称,平台设备是由i2s0设备节点转换来的;否则创建的dai的名字设置为dai driver的名字
};

这里并没有controls、dapm_widgets、dapm_routes相关的配置。

4.4 devm_snd_dmaengine_pcm_register

devm_snd_dmaengine_pcm_register定义在sound/soc/soc-devres.c,用于为设备注册一个dmaengine_pcm;

/**
 * devm_snd_dmaengine_pcm_register - resource managed dmaengine PCM registration
 * @dev: The parent device for the PCM device
 * @config: Platform specific PCM configuration
 * @flags: Platform specific quirks
 *
 * Register a dmaengine based PCM device with automatic unregistration when the
 * device is unregistered.
 */
int devm_snd_dmaengine_pcm_register(struct device *dev,
        const struct snd_dmaengine_pcm_config *config, unsigned int flags)    // config传入NULL,flags传入0
{
        struct device **ptr;
        int ret;

        ptr = devres_alloc(devm_dmaengine_pcm_release, sizeof(*ptr), GFP_KERNEL);  // 动态申请内存,指向一个struct *ptr
        if (!ptr)
                return -ENOMEM;

        ret = snd_dmaengine_pcm_register(dev, config, flags);   // 申请DMA通道
        if (ret == 0) {
                *ptr = dev;
                devres_add(dev, ptr);
        } else {
                devres_free(ptr);
        }

        return ret;
}
4.4.1 snd_dmaengine_pcm_register

snd_dmaengine_pcm_register中通过dmaengine_pcm_request_chan_of去申请DMA通道,定义在sound/soc/soc-generic-dmaengine-pcm.c:

/**
 * snd_dmaengine_pcm_register - Register a dmaengine based PCM device
 * @dev: The parent device for the PCM device
 * @config: Platform specific PCM configuration
 * @flags: Platform specific quirks
 */
int snd_dmaengine_pcm_register(struct device *dev,
        const struct snd_dmaengine_pcm_config *config, unsigned int flags)
{
        const struct snd_soc_component_driver *driver;
        struct dmaengine_pcm *pcm;
        int ret;

        pcm = kzalloc(sizeof(*pcm), GFP_KERNEL);        // 动态分配一个dmaengine_pcm结构
        if (!pcm)
                return -ENOMEM;

#ifdef CONFIG_DEBUG_FS
        pcm->component.debugfs_prefix = "dma";
#endif
        if (!config)
                config = &snd_dmaengine_pcm_default_config;   // 走这里
        pcm->config = config;            // 初始化pcm成员
        pcm->flags = flags;

        ret = dmaengine_pcm_request_chan_of(pcm, dev, config);
        if (ret)
                goto err_free_dma;

        if (config->process)
                driver = &dmaengine_pcm_component_process;
        else
                driver = &dmaengine_pcm_component;  // 走这里

        ret = snd_soc_component_initialize(&pcm->component, driver, dev); // 初始化component
        if (ret)
                goto err_free_dma;

        ret = snd_soc_add_component(&pcm->component, NULL, 0);  // 注册pcm component
        if (ret)
                goto err_free_dma;

        return 0;

err_free_dma:
        dmaengine_pcm_release_chan(pcm);
        kfree(pcm);
        return ret;
}

函数执行流程如下:

  • 此处分配一个dmaengine_pcm结构,然后根据传入的config和flag设置pcm,config被设置为了snd_dmaengine_pcm_default_config;
  •  调用dmaengine_pcm_request_chan_of函数获取dma的传输通道,根据传输的是否是半双工,设置pcm的通道;
  • 调用snd_soc_add_platform函数注册platform component,实际上snd_soc_component_initialize和snd_soc_add_platform合在一起就是函数snd_soc_register_component;

snd_dmaengine_pcm_default_config定义如下:

static const struct snd_dmaengine_pcm_config snd_dmaengine_pcm_default_config = {
        .prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config,
};
4.4.2 dmaengine_pcm_request_chan_of

dmaengine_pcm_request_chan_of根据flag 标志以及驱动中定义的数组获取DMA-name。为参数调用dma_request_slave_channel_reason;

static const char * const dmaengine_pcm_dma_channel_names[] = {
        [SNDRV_PCM_STREAM_PLAYBACK] = "tx",
        [SNDRV_PCM_STREAM_CAPTURE] = "rx",
};

dmaengine_pcm_request_chan_of定义如下:

static int dmaengine_pcm_request_chan_of(struct dmaengine_pcm *pcm,
        struct device *dev, const struct snd_dmaengine_pcm_config *config)
{
        unsigned int i;
        const char *name;
        struct dma_chan *chan;

        if ((pcm->flags & SND_DMAENGINE_PCM_FLAG_NO_DT) || (!dev->of_node &&
            !(config->dma_dev && config->dma_dev->of_node)))
                return 0;

        if (config->dma_dev) {
                /*
                 * If this warning is seen, it probably means that your Linux
                 * device structure does not match your HW device structure.
                 * It would be best to refactor the Linux device structure to
                 * correctly match the HW structure.
                 */
                dev_warn(dev, "DMA channels sourced from device %s",
                         dev_name(config->dma_dev));
                dev = config->dma_dev;
        }

        for_each_pcm_streams(i) {
                if (pcm->flags & SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX)
                        name = "rx-tx";
                else
                        name = dmaengine_pcm_dma_channel_names[i];
                if (config->chan_names[i])
                        name = config->chan_names[i];
                chan = dma_request_chan(dev, name);
                if (IS_ERR(chan)) {
                        /*
                         * Only report probe deferral errors, channels
                         * might not be present for devices that
                         * support only TX or only RX.
                         */
                        if (PTR_ERR(chan) == -EPROBE_DEFER)
                                return -EPROBE_DEFER;
                        pcm->chan[i] = NULL;
                } else {
                        pcm->chan[i] = chan;
                }
                if (pcm->flags & SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX)
                        break;
        }

        if (pcm->flags & SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX)
                pcm->chan[1] = pcm->chan[0];

        return 0;
}
4.4.3 dma_request_slave_channel_reason

dma_request_slave_channel_reason根据参数name并解析device tree 来申请具体的DMA 通道。一个DMA控制器有8个channel,每两个作为一组,既可以输出也可以输入,但它们的控制代码是一样的,并且在DMA注册过程中,已经使DMA处于可用状态了,这里根据传入的name 参数以及在device tree 中的描述申请具体的通道。

i2s0: i2s@ff880000 {
        dmas = <&dmac_bus 0>, <&dmac_bus 1>;
        dma-names = "tx", "rx";
        ......
};

每个需要使用DMA的client都会通过 dmas来引用DMA控制器和通道,通过dma-names实现name的匹配。最终调用of_dma_request_slave_channel。

五、rockchip_i2s_dai

cpu dai和pcm配置信息通过结构体snd_soc_dai_driver描述,包括了dai的能力描述和操作接口,这里我们介绍一下rockchip_i2s_dai,其定义在sound/soc/rockchip/rockchip_i2s.c:

static struct snd_soc_dai_driver rockchip_i2s_dai = {
        .probe = rockchip_i2s_dai_probe,
      
        // 声卡注册的时候会为其创建一个类型为snd_soc_dapm_dai_in的playback dai widget,其name以及sname均设置为"Playback"
        .playback = {
                .stream_name = "Playback",
                .channels_min = 2,
                .channels_max = 8,    
                .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),
        },
        // 声卡注册的时候会为其创建一个类型为snd_soc_dapm_dai_out的capture dai widget,其name以及sname均设置为"Capture"
        .capture = {
                .stream_name = "Capture",
                .channels_min = 1,
                .channels_max = 2,
                .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),
        },
        .ops = &rockchip_i2s_dai_ops,
        .symmetric_rates = 1,
};

其中:

  • probe:component探测函数;在Macine驱动中会进行ASoC声卡的注册,其中会执行soc_probe_link_dais(card),即进行dai driver的探测工作,soc_probe_link_dais函数内部会调用dai->driver->probe(dai);
  • name:cpu dai的名称标识,dai_link通过配置cpu dai_name来找到对应的cpu dai;
  • capture:描述capture的能力;如回放设备所支持的声道数、采样率、音频格式;非常重要的字段;
  • playback:描述playback的能力;如录制设备所支持声道数、采样率、音频格式;非常重要的字段;
  • ops:cpu dai的操作函数集,这些函数集非常重要,用于dai的时钟配置、格式配置、硬件参数配置;

在注册ASoC声卡的时候,会调用soc_probe_link_components,每一个dai创建两个音频数据流的widget:

  • 一个是类型为snd_soc_dapm_dai_in名称为Playback的播放流widget;
  • 另一个是类型为snd_soc_dapm_dai_out名称为Capture的录音流widget;

在注册ASoC声卡的时候,会调用snd_soc_dapm_connect_dai_link_widgets,构建cpu dai和codec dai之间音频播放和录音的路径;

|-----------------|                  |-----------------|  
|                 |                  |                 | 
|     Playback    |----------------->|   AIF1 Playback |   这俩Playback widget为snd_soc_dapm_dai_in类型的widget
|                 |                  |                 |   
|      Capture    |<--------- -------|   AIF1 Capture  |   这俩Capture widget为snd_soc_dapm_dai_out类型的widget            
|                 |                  |                 |  
|-----------------|                  |-----------------|  
    cpu_dai widget                    codec_dai widget

此时我们可以得到一个如下路径:

  • 从cpu dai widget作为输入端的路径:Playback (位于platform驱动中,snd_soc_dapm_dai_in类型)   --> AIF1 Playback(位于codec驱动中,snd_soc_dapm_dai_in类型) --> AIF1RX --> IF1 DAC --> IF1 DAC1 L -->.... -->HPO L Playback --> HPOL --> Headphones(位于machine驱动中) ;
  • 以cpu dai widget作为输出端的路径:Capture(位于platform驱动中,snd_soc_dapm_dai_out类型)  <--  AIF1 Capture(位于codec驱动中,snd_soc_dapm_dai_out类型)<-- AIF1TX  <--IF1 ADC1 <-- ....  <--  BST2 <--  IN2P <--  Mic Jack(位于machine驱动中) 。

5.1 rockchip_i2s_dai_probe

cpu  dai driver的探测函数被设置为了rockchip_i2s_dai_probe,定义在sound/soc/rockchip/rockchip_i2s.c:

static int rockchip_i2s_dai_probe(struct snd_soc_dai *dai)
{
        struct rk_i2s_dev *i2s = snd_soc_dai_get_drvdata(dai);

        snd_soc_dai_init_dma_data(dai,
                i2s->has_playback ? &i2s->playback_dma_data : NULL,
                i2s->has_capture  ? &i2s->capture_dma_data  : NULL);

        return 0;
}

snd_soc_dai_init_dma_data定义在include/sound/soc-dai.h:

static inline void snd_soc_dai_init_dma_data(struct snd_soc_dai *dai, void *playback, void *capture)
{
        snd_soc_dai_dma_data_set_playback(dai, playback);
        snd_soc_dai_dma_data_set_capture(dai,  capture);
}

宏snd_soc_dai_dma_data_set_playback、snd_soc_dai_dma_data_set_capture定义如下:

#define snd_soc_dai_dma_data_set_playback(dai, data)    snd_soc_dai_dma_data_set(dai, SNDRV_PCM_STREAM_PLAYBACK, data)
#define snd_soc_dai_dma_data_set_capture(dai,  data)    snd_soc_dai_dma_data_set(dai, SNDRV_PCM_STREAM_CAPTURE,  data)
#define snd_soc_dai_set_dma_data(dai, ss, data)         snd_soc_dai_dma_data_set(dai, ss->stream, data)

这两个宏最终都是调用的snd_soc_dai_dma_data_set函数,用于设置dai->stream[stream].dma_data;

static inline void snd_soc_dai_dma_data_set(struct snd_soc_dai *dai, int stream, void *data)
{
        dai->stream[stream].dma_data = data;
}

5.2 rockchip_i2s_dai_ops

音频操作接口通过结构体struct snd_soc_dai_ops描述:

static const struct snd_soc_dai_ops rockchip_i2s_dai_ops = {
        .hw_params = rockchip_i2s_hw_params,
        .set_sysclk = rockchip_i2s_set_sysclk,
        .set_fmt = rockchip_i2s_set_fmt,
        .trigger = rockchip_i2s_trigger,
};

其中:

  • set_sysclk:用于设置系统时钟,对于cpi dai来说系统时钟指的是RK3399 i2s_clk的时钟,即i2s0模块输出的MCLK时钟(对应的时钟名称为clk_i2s0);
  • set_fmt:设置数字音频接口格式,具体见 include/sound/soc-dai.h;
    • SND_SOC_DAIFMT_I2S:数字音频接口是I2S格式,常用于多媒体音频;
    • SND_SOC_DAIFMT_RIGHT_J:数字音频接口是I2S右对齐格式;
    • SND_SOC_DAIFMT_LEFT_J:数字音频接口是I2S左对齐格式;
    • SND_SOC_DAIFMT_DSP_A:数字音频接口是PCM格式,常用于语音通话;
    • SND_SOC_DAIFMT_DSP_B:数字音频接口是PCM格式,常用于语音通话;
    • SND_SOC_DAIFMT_CBM_CFM:ALC5651作为主机,BCLK 和 LRCLK由ALC5651提供;
    • SND_SOC_DAIFMT_CBS_CFS:ALC5651作为从机,BCLK和LRCLK由SoC/CPU提供;
    • .......
  • hw_params:cpu dai硬件参数设置,根据上层设定的声道数、采样率、数据格式,来配置cpu dai相关寄存器;
  • trigger:触发pcm音频操作;
5.2.1 set_sysclk

Rockchip i2s音频操作接口set_sysclk函数用于设置系统时钟频率,set_sysclk被设置为rockchip_i2s_set_sysclk,定义在sound/soc/rockchip/rockchip_i2s.c:

static int rockchip_i2s_set_sysclk(struct snd_soc_dai *cpu_dai, int clk_id, // clk_id传入的0,dir也是传入的0,freq传入的是系统时钟频率
                                   unsigned int freq, int dir)
{
        struct rk_i2s_dev *i2s = to_info(cpu_dai); // 获取rk_i2s_dev 
        int ret;

        ret = clk_set_rate(i2s->mclk, freq);  // 设置i2s0模块输出的MCLK时钟频率
        if (ret)
                dev_err(i2s->dev, "Fail to set mclk %d\n", ret);

        return ret;
}

mclk对应的时钟id为SCLK_I2S0_8CH,时钟名称为clk_i2s0,这里调用clk_set_rate设置了mclk的时钟频率为freq。其底层实现实际上就是配置时钟相关的寄存器,比如CRU_CLKGATE_CONx。

5.2.2 set_fmt 

set_fmt用于设置数字音频接口格式,set_fmt 被设置为rockchip_i2s_set_fmt,定义在sound/soc/rockchip/rockchip_i2s.c:

static int rockchip_i2s_set_fmt(struct snd_soc_dai *cpu_dai,
                                unsigned int fmt)
{
        struct rk_i2s_dev *i2s = to_info(cpu_dai);
        unsigned int mask = 0, val = 0;

        mask = I2S_CKR_MSS_MASK;    // 主/从模式选择 位[27]
        switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
        case SND_SOC_DAIFMT_CBS_CFS:       // ALC5651作为从机,BCLK和LRCLK由SoC/CPU提供   一般走这里
                /* Set source clock in Master mode */
                val = I2S_CKR_MSS_MASTER;      // 0<<27
                i2s->is_master_mode = true;
                break;
        case SND_SOC_DAIFMT_CBM_CFM:      // ALC5651作为主机,BCLK 和 LRCLK由ALC5651提供
                val = I2S_CKR_MSS_SLAVE;      // 1<<27
                i2s->is_master_mode = false;
                break;
        default:
                return -EINVAL;
        }

        regmap_update_bits(i2s->regmap, I2S_CKR, mask, val);  // 配置时钟发生寄存器,寄存器偏移为0x0008   修改主/从模式选择位

        mask = I2S_CKR_CKP_MASK;
        switch (fmt & SND_SOC_DAIFMT_INV_MASK) {      // SCLK(也叫BCLK)极性配置
        case SND_SOC_DAIFMT_NB_NF:       // normal
                val = I2S_CKR_CKP_NEG;     // 0<<26: sample data at posedge sclk and drive data at negedge sclk
                break;
        case SND_SOC_DAIFMT_IB_NF:       // invert
                val = I2S_CKR_CKP_POS;     // 1<<26: sample data at negedge sclk and drive data at posedge sclk
                break;
        default:
                return -EINVAL;
        }

        regmap_update_bits(i2s->regmap, I2S_CKR, mask, val);   // 配置时钟发生寄存器,寄存器偏移为0x0008   修改SLCK极性配置

        mask = I2S_TXCR_IBM_MASK | I2S_TXCR_TFS_MASK | I2S_TXCR_PBM_MASK;  // I2S总线模式  传输格式  PCM总线模式
        switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {  // 数字音频接口格式,0x000f
        case SND_SOC_DAIFMT_RIGHT_J:      // I2S右对齐模式
                val = I2S_TXCR_IBM_RSJM;  //2<<9
                break;
        case SND_SOC_DAIFMT_LEFT_J:      // I2S左对齐模式
                val = I2S_TXCR_IBM_LSJM; //1<<9
                break;
        case SND_SOC_DAIFMT_I2S:        // 标准I2C 
                val = I2S_TXCR_IBM_NORMAL; //0<<9
                break;
        case SND_SOC_DAIFMT_DSP_A: /* PCM no delay mode */  PCM模式A
                val = I2S_TXCR_TFS_PCM;  // 1<<5 | 0<<7
                break;
        case SND_SOC_DAIFMT_DSP_B: /* PCM delay 1 mode */  PCM模式B
                val = I2S_TXCR_TFS_PCM | I2S_TXCR_PBM_MODE(1); // 1<<5 | 1<<7
                break;
        default:
                return -EINVAL;
        }

        regmap_update_bits(i2s->regmap, I2S_TXCR, mask, val);   // 配置接收操作控制寄存器,寄存器偏移地址0x0000  修改I2S总线模式、传输格式、PCM总线模式位

        mask = I2S_RXCR_IBM_MASK | I2S_RXCR_TFS_MASK | I2S_RXCR_PBM_MASK; // I2S总线模式  传输格式  PCM总线模式
        switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {     // 数字音频接口格式,0x000f
        case SND_SOC_DAIFMT_RIGHT_J:      // I2S右对齐模式
                val = I2S_RXCR_IBM_RSJM;    //2<<9
                break;
        case SND_SOC_DAIFMT_LEFT_J:      // I2S左对齐模式
                val = I2S_RXCR_IBM_LSJM;     //1<<9
                break;
        case SND_SOC_DAIFMT_I2S:         // 标准I2C 
                val = I2S_RXCR_IBM_NORMAL;   //0<<9
                break;
        case SND_SOC_DAIFMT_DSP_A: /* PCM no delay mode */ PCM模式A
                val = I2S_RXCR_TFS_PCM;        // 1<<5 | 0<<7
                break;
        case SND_SOC_DAIFMT_DSP_B: /* PCM delay 1 mode */   PCM模式B
                val = I2S_RXCR_TFS_PCM | I2S_RXCR_PBM_MODE(1);  // 1<<5 | 1<<7
                break;
        default:
                return -EINVAL;
        }

        regmap_update_bits(i2s->regmap, I2S_RXCR, mask, val);  // 配置接收操作控制寄存器,寄存器偏移地址0x0004  修改I2S总线模式、传输格式、PCM总线模式位

        return 0;
}

这里实际上就是配置RK3399 I2S0相关的寄存器:

  • I2S_TXCR:发送操作寄存器,偏移地址为0x0000,这里主要是修改位[9]配置I2S操作模式;

  • I2S_RXCR:接收操作寄存器,偏移地址为0x0004,这里主要是修改位[9]配置I2S操作模式;

  • I2S_CLK:时钟发生寄存器,偏移地址0x0008,这里主要是修改位{27]、[26]配置主从模式,以及SLCK极性;

当我们移植完了声卡驱动,并通过开发板ubuntu系统进行音频播放的时候,我特意查看了偏移为0x00、0x04、0x08寄存器的值:

  • 0x00:传输操作控制器寄存器I2S_TXCR,寄存器值为0x0000000f;

    • 位9为0:I2S正常模式;

  • 0x04:接收操作控制器寄存器I2S_RXCR,寄存器值为0x0000000f;

    • 位9为0:I2S正常模式;

  • 0x08:时钟发生寄存器I2S_CKR,寄存器值为0x00033f3f;

    • 位27为0:主机模式(SCLK输出);

    • 位26为0:sample data at posedge sclk and drive data at negedge sclk;

5.2.3 hw_params

hw_params被设置为rockchip_i2s_hw_params,用于cpu dai硬件参数设置,根据上层设定的声道数、采样率、数据格式,来配置I2S_TXCR、I2S_RXCR、I2S_CLK寄存器;

static int rockchip_i2s_hw_params(struct snd_pcm_substream *substream,
                                  struct snd_pcm_hw_params *params,
                                  struct snd_soc_dai *dai)
{
        struct rk_i2s_dev *i2s = to_info(dai);
        struct snd_soc_pcm_runtime *rtd = substream->private_data; // 获取pcm runtime
        unsigned int val = 0;
        unsigned int mclk_rate, bclk_rate, div_bclk, div_lrck;

        if (i2s->is_master_mode) {   // 主机模式
                mclk_rate = clk_get_rate(i2s->mclk);        // 获取mclk时钟频率,如果采样频率为44.1kHz,这里应该是11.2896MHz
                bclk_rate = 2 * 32 * params_rate(params);   // 计算位时钟,SCLK=2*采样率*采样位数 采样宽度为32?
                if (bclk_rate && mclk_rate % bclk_rate)
                        return -EINVAL;

                div_bclk = mclk_rate / bclk_rate;
                div_lrck = bclk_rate / params_rate(params);
                regmap_update_bits(i2s->regmap, I2S_CKR,   // 时钟控制寄存器,偏移地址0x0008
                                   I2S_CKR_MDIV_MASK,       //设置SCLK时钟频率
                                   I2S_CKR_MDIV(div_bclk)); //div_bclk<<16

                regmap_update_bits(i2s->regmap, I2S_CKR,   // 时钟控制寄存器,偏移地址0x0008
                                   I2S_CKR_TSD_MASK |      //设置LRCK_TX时钟频率
                                   I2S_CKR_RSD_MASK,       //设置LRCK_RCK时钟频率
                                   I2S_CKR_TSD(div_lrck) | //div_lrck<<8
                                   I2S_CKR_RSD(div_lrck));  //div_lrck<<0
        }

        switch (params_format(params)) {  // 获取采样格式
        case SNDRV_PCM_FORMAT_S8:
                val |= I2S_TXCR_VDW(8);
                break;
        case SNDRV_PCM_FORMAT_S16_LE:    // 采样宽度为16,走这里
                val |= I2S_TXCR_VDW(16);
                break;
        case SNDRV_PCM_FORMAT_S20_3LE:
                val |= I2S_TXCR_VDW(20);
                break;
        case SNDRV_PCM_FORMAT_S24_LE:
                val |= I2S_TXCR_VDW(24);
                break;
        case SNDRV_PCM_FORMAT_S32_LE:
                val |= I2S_TXCR_VDW(32);
                break;
        default:
                return -EINVAL;
        }

        switch (params_channels(params)) {
        case 8:
                val |= I2S_CHN_8;
                break;
        case 6:
                val |= I2S_CHN_6;
                break;
        case 4:
                val |= I2S_CHN_4;
                break;
        case 2:                   // 双通道走这里
        case 1:
                val |= I2S_CHN_2;
                break;
        default:
                dev_err(i2s->dev, "invalid channel: %d\n",
                        params_channels(params));
                return -EINVAL;
        }

        if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
                regmap_update_bits(i2s->regmap, I2S_RXCR,  // 接收操作控制寄存器,偏移地址0x0004
                                   I2S_RXCR_VDW_MASK | I2S_RXCR_CSR_MASK,  // 设置采样位数,以及TX通道选择
                                   val);
        else
                regmap_update_bits(i2s->regmap, I2S_TXCR,  // 发送操作控制寄存器,偏移地址0x0000
                                   I2S_TXCR_VDW_MASK | I2S_TXCR_CSR_MASK,  // 设置采样位数,以及TX通道选择
                                   val);

        if (!IS_ERR(i2s->grf) && i2s->pins) {
                regmap_read(i2s->regmap, I2S_TXCR, &val);  
                val &= I2S_TXCR_CSR_MASK;

                switch (val) {
                case I2S_CHN_4:
                        val = I2S_IO_4CH_OUT_6CH_IN;
                        break;
                case I2S_CHN_6:
                        val = I2S_IO_6CH_OUT_4CH_IN;
                        break;
                case I2S_CHN_8:
                        val = I2S_IO_8CH_OUT_2CH_IN;
                        break;
                default:  
                        val = I2S_IO_2CH_OUT_8CH_IN;   // 7
                        break;
                }

                val <<= i2s->pins->shift; // val << 11,位[13:11]为i2s0_sdio_oe_n bit control
                val |= (I2S_IO_DIRECTION_MASK << i2s->pins->shift) << 16; // 位[31:16]对应这位[15:0]的写使能位,只有高位位1,才能写入低位
                regmap_write(i2s->grf, i2s->pins->reg_offset, val); // GRF_SOC_CON8寄存器,位[13:11]写入7
        }

        regmap_update_bits(i2s->regmap, I2S_DMACR, I2S_DMACR_TDL_MASK, // I2S_DMACR为DMA控制寄存器,偏移地址0x0010
                           I2S_DMACR_TDL(16));     // 位[4:0]写入16,发送数据级别控制(个人理解应该就是FIFO的深度)
        regmap_update_bits(i2s->regmap, I2S_DMACR, I2S_DMACR_RDL_MASK,
                           I2S_DMACR_RDL(16));     // 位[20:16]写入16,接收数据级别控制(个人理解应该就是FIFO的深度)

        val = I2S_CKR_TRCM_TXRX;
        if (dai->driver->symmetric_rates && rtd->dai_link->symmetric_rates)
                val = I2S_CKR_TRCM_TXONLY;

        regmap_update_bits(i2s->regmap, I2S_CKR,
                           I2S_CKR_TRCM_MASK,  // Tx and Rx Common Use,用于设置tx_lrck、rx_lrck
                           val);
        return 0;
}

当我们移植完了声卡驱动,并通过开发板ubuntu系统进行音频播放的时候,我特意查看了偏移为0x00、0x04、0x08、0x10寄存器的值:

  • 0x00:发送操作控制器寄存器I2S_TXCR,寄存器值为0x0000000f;

    • 位[4:0]为0xF:位宽度位16;

    • 位[16:15]为0x00:双通道;

  • 0x04:接收操作控制器寄存器I2S_RXCR,寄存器值为0x0000000f;

    • 位[4:0]为0xF:位宽度位16;

    • 位[16:15]为0x00:双通道;

  • 0x08:时钟发生寄存器I2S_CKR,寄存器值为0x00033f3f;

    • 位[23:16]为0x03:MDIV=3,则$F_{sclk}=\frac{F_{mclk}}{4}$;由于$F_{mclk}=256F_s$,其中$F_s$为采样频率,因此$F_{sclk}=64F_s$;这里将采样宽度设置为了32;

    • 位[15:8]为0x3F:RSD=63,则$F_{rxlrck}=\frac{F_{sclk}}{0x3F+1}=F_s$;

    • 位[7:0]位0x3F:TSD=63,则$F_{txlrck}=\frac{F_{sclk}}{0x3F+1}=F_s$;
  • 0x10:DMACR为DMA控制寄存器,寄存器的值位000f0110;

    • 位[20:16为0x0F:RDL=15,接收数据级别设置为15,The watermark level = 15+1=16;

    • 位[4:0]为0x0F:TDL=16,发送数据级别设置为16,The watermark level = 16;

5.2.4 rockchip_i2s_trigger

rockchip_i2s_trigger被设置为rockchip_i2s_trigger,用于触发pcm音频操作,比如播放音频,录音:

static int rockchip_i2s_trigger(struct snd_pcm_substream *substream,
                                int cmd, struct snd_soc_dai *dai)
{
        struct rk_i2s_dev *i2s = to_info(dai);
        int ret = 0;

        switch (cmd) {
        case SNDRV_PCM_TRIGGER_START:        // 开始 
        case SNDRV_PCM_TRIGGER_RESUME:       // 恢复
        case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:  // 释放暂停状态
                if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)  // 录音操作
                        ret = rockchip_snd_rxctrl(i2s, 1);
                else     // 播放操作
                        ret = rockchip_snd_txctrl(i2s, 1);
                if (ret < 0)
                        return ret;
                i2s_pinctrl_select_bclk_on(i2s);     // 设置I2S相关引脚状态为bclk_ok
                break;
        case SNDRV_PCM_TRIGGER_SUSPEND:    // 挂起
        case SNDRV_PCM_TRIGGER_STOP:       // 停止
        case SNDRV_PCM_TRIGGER_PAUSE_PUSH:   // 暂停
                if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) {  // 录音操作
                        if (!i2s->tx_start)   
                                i2s_pinctrl_select_bclk_off(i2s);
                        ret = rockchip_snd_rxctrl(i2s, 0);
                } else {      // 播放操作
                        if (!i2s->rx_start)
                                i2s_pinctrl_select_bclk_off(i2s);
                        ret = rockchip_snd_txctrl(i2s, 0);  
                }
                break;
        default:
                ret = -EINVAL;
                break;
        }

        return ret;
}

这里我们以播放音频为例,首先调用rockchip_snd_txctrl函数,函数内部配置I2S_XFER、I2S_DMACR寄存器以使能DMA传输;

static int rockchip_snd_txctrl(struct rk_i2s_dev *i2s, int on)  // on传入1
{
        unsigned int val = 0;
        int ret = 0;

        spin_lock(&i2s->lock);  // 获取自旋锁
        if (on) {     // 播放
                ret = regmap_update_bits(i2s->regmap, I2S_DMACR, // DMA控制寄存器
                                         I2S_DMACR_TDE_ENABLE,
                                         I2S_DMACR_TDE_ENABLE);  // 配置位8 TDE,Transmit DMA enabled
                if (ret < 0)
                        goto end;
                ret = regmap_update_bits(i2s->regmap, I2S_XFER,  // 数据传输开始寄存器 
                                         I2S_XFER_TXS_START | I2S_XFER_RXS_START,
                                         I2S_XFER_TXS_START | I2S_XFER_RXS_START); // 配置位0 TXS,开始TX传输;配置位1 RXS,开始RX传输;
                if (ret < 0)
                        goto end;
                i2s->tx_start = true;
        } else {        // 停止
                i2s->tx_start = false;

                ret = regmap_update_bits(i2s->regmap, I2S_DMACR, // DMA控制寄存器
                                         I2S_DMACR_TDE_ENABLE,
                                         I2S_DMACR_TDE_DISABLE); // 配置位8 TDE,Transmit DMA disabled
                if (ret < 0)
                        goto end;

                if (!i2s->rx_start) {
                        ret = regmap_update_bits(i2s->regmap, I2S_XFER, // 数据传输开始寄存器 
                                                 I2S_XFER_TXS_START | I2S_XFER_RXS_START,
                                                 I2S_XFER_TXS_STOP | I2S_XFER_RXS_STOP); // 配置位0 TXS,停止TX传输;配置位1 RXS,停止RX传输;
                        if (ret < 0)
                                goto end;
                        udelay(150);
                        ret = regmap_update_bits(i2s->regmap, I2S_CLR, // SCLK域裸机清除寄存器
                                                 I2S_CLR_TXC | I2S_CLR_RXC,
                                                 I2S_CLR_TXC | I2S_CLR_RXC);// 配置位0 TXC,TX逻辑清除;配置位1 RXC,RX逻辑清除;;
                        if (ret < 0)
                                goto end;
                        ret = regmap_read_poll_timeout_atomic(i2s->regmap, // 读取I2S_CLR寄存器,直至值为0x0000,超时时间位200us
                                                              I2S_CLR,
                                                              val,
                                                              val == 0,
                                                              20,
                                                              200);
                        if (ret < 0)
                                dev_warn(i2s->dev, "fail to clear: %d\n", ret);
                }
        }
end:
        spin_unlock(&i2s->lock);  // 释放自旋锁
        if (ret < 0)
                dev_err(i2s->dev, "lrclk update failed\n");

        return ret;
}

接着调用i2s_pinctrl_select_bclk_on设置I2S相关引脚状态为bclk_ok:

static int i2s_pinctrl_select_bclk_on(struct rk_i2s_dev *i2s)
{
        int ret = 0;

        if (!IS_ERR(i2s->pinctrl) && !IS_ERR_OR_NULL(i2s->bclk_on))
                ret = pinctrl_select_state(i2s->pinctrl, i2s->bclk_on);
       
        if (ret)
                dev_err(i2s->dev, "bclk enable failed %d\n", ret);

        return ret;
}
5.2.5 I2S/PCM控制流程图

这里我们回顾一下RK3399 datasheet官方给出了I2S/PCM控制传输操作的流程图(即音频播放流程);

具体流程如下:

(1) 通过向I2C_XFER寄存器第0位写入0x0,禁止I2S发送器(在rockchip_snd_txctrl函数中音频播放停止时会进行该操作);

(2) 通过向I2S_CLR寄存器第0位写入0x1来清除I2S控制逻辑(在rockchip_snd_txctrl函数中音频播放停止时会进行该操作);

(3) 读取I2S_CLR寄存器第0位,直至该位为0x0(在rockchip_snd_txctrl函数中音频播放停止时会进行该操作);

(4) 通过设置I2S_TXCR和I2S_CKR寄存器配置I2S的操作模式(rockchip_i2s_set_fmt函数与以及rockchip_i2s_hw_params函数中会进行配置);

(5) 为I2S发送器配置DMA通道,并通过I2S_TXDR寄存器传输目标地址;

(6) 向I2S_DMACR寄存器写入,以确定何时发出DMA请求;(rockchip_snd_txctrl函数中配置位TDE)

(7) 向I2S_XFER寄存器第0位写入0x01,使能I2S发送器;(rockchip_snd_txctrl函数中配置位TXS)

(8) I2S_XFER寄存器第0位不能被禁止直至数据传输完成。

六 音频播放流程分析

到了这里,实际上我们已经介绍完了machine驱动、codec驱动、以及platform驱动内容;但是你可能还是有许多的问题,比如我打开一个音频文件,machine驱动、codec驱动、以及platform驱动驱动之间是如何配合的呢?如何实现音频的播放的呢?

这里我们就简单介绍一下音频播放的流程,当然这里为了方便大家理解,我们就以uboot 2017.09的代码为例来说这件事情,因为uboot的音频驱动实际上就是内核驱动的简化版。

6.1 uboot目录结构

我们看一下uboot drivers/sound目录下的音频文件结构如下图:

其中:

  • rockchip-i2s.c:对应内核的platform驱动,定义了snd_soc_dai_ops;实现了操作集的.hw_params、.set_sysclk、.transfer方法;

  • rockchip-sound.c:对应内核的machine驱动,提供了sound_init、sound_play方法,一个用于硬件初始化工作,另一个用于音频播放;

  • rk817_codec.c、wm8994.c等:对应内核的codec驱动,定义了snd_soc_dai_ops;实现了操作集的.hw_params、.startup方法;

6.1.1 platform中的set_sysclk
static int rk_i2s_set_sysclk(struct udevice *dev, unsigned int freq)
{
        struct rk_i2s_dev *i2s = dev_get_priv(dev);
​
        clk_set_rate(&i2s->mclk, freq);
​
        return 0;
}
6.1.2 platform中的hw_params
static int rk_i2s_hw_params(struct udevice *udev, unsigned int samplerate,
                            unsigned int fmt, unsigned int channels)
{
        struct rk_i2s_dev *dev = dev_get_priv(udev);
​
        /* set fmt */
        i2s_reg_update_bits(dev, I2S_CKR,
                            I2S_CKR_MSS_MASK, I2S_CKR_MSS_MASTER);  // 主机模式
        i2s_reg_update_bits(dev, I2S_TXCR,
                            I2S_TXCR_IBM_MASK, I2S_TXCR_IBM_NORMAL); //  标准I2S
        i2s_reg_update_bits(dev, I2S_RXCR,
                            I2S_RXCR_IBM_MASK, I2S_RXCR_IBM_NORMAL); //  标准I2S
        /* set div */
        i2s_reg_update_bits(dev, I2S_CKR,
                            I2S_CKR_TSD_MASK | I2S_CKR_RSD_MASK,  // 设置LRCK_T、LRCK_RCKX时钟频率
                            I2S_CKR_TSD(64) | I2S_CKR_RSD(64));
        i2s_reg_update_bits(dev, I2S_CKR,
                            I2S_CKR_MDIV_MASK, I2S_CKR_MDIV(4));    //设置SCLK时钟频率
        /* set hwparams */
        i2s_reg_update_bits(dev, I2S_TXCR,
                            I2S_TXCR_VDW_MASK |    
                            I2S_TXCR_CSR_MASK,
                            I2S_TXCR_VDW(16) |     // 设置采样位
                            I2S_TXCR_CHN_2);       // 设置TX通道选择
        i2s_reg_update_bits(dev, I2S_RXCR,
                            I2S_RXCR_CSR_MASK |
                            I2S_RXCR_VDW_MASK,
                            I2S_TXCR_VDW(16) |    // 设置采样位
                            I2S_TXCR_CHN_2);      // 设置TX通道选择
        i2s_reg_update_bits(dev, I2S_DMACR,
                            I2S_DMACR_TDL_MASK | I2S_DMACR_RDL_MASK, 
                            I2S_DMACR_TDL(16) | I2S_DMACR_RDL(16));  // 发送数据级别控制、接收数据级别控制
return 0;
}
6.1.3 platform中的transfer
static void rk_i2s_txctrl(struct rk_i2s_dev *dev, int on)
{
        if (on) {
                i2s_reg_update_bits(dev, I2S_XFER,
                                    I2S_XFER_TXS_MASK | I2S_XFER_RXS_MASK,
                                    I2S_XFER_TXS_START | I2S_XFER_RXS_START); // 配置位0 TXS,开始TX传输;配置位1 RXS,开始RX传输;
        } else {
                i2s_reg_update_bits(dev, I2S_XFER,
                                    I2S_XFER_TXS_MASK |
                                    I2S_XFER_RXS_MASK,
                                    I2S_XFER_TXS_STOP |
                                    I2S_XFER_RXS_STOP);
​
                i2s_reg_update_bits(dev, I2S_CLR,
                                    I2S_CLR_TXC_MASK | I2S_CLR_RXC_MASK,
                                    I2S_CLR_TXC | I2S_CLR_RXC);
        }
}
​
static int rk_i2s_transfer_tx_data(struct udevice *udev, unsigned int *data,
                                   unsigned long data_size)
{
        struct rk_i2s_dev *dev = dev_get_priv(udev);
        u32 val;
​
        if (data_size < I2S_FIFO_LENGTH) {
                debug("%s : invalid data size\n", __func__);
                return -EINVAL;
        }
​
        rk_i2s_txctrl(dev, 1);
        while (data_size > 0) {
                val = i2s_reg_readl(dev, I2S_FIFOLR);  
                if (val < I2S_FIFO_LENGTH) {
                        i2s_reg_writel(dev, I2S_TXDR, *data++); // 写数据到I2S_TXDR
                        data_size--;
                }
        }
​
        return 0;
}
​

6.2 音频初始化

如果进行音频播放首先需要调用sound_init方法:

int sound_init(const void *blob)
{
        int ret;
​
        ret = uclass_get_device(UCLASS_I2S, 0, &i2s_dev);
        if (ret) {
                if (ret != -ENODEV) {
                        printf("Get i2s device failed: %d\n", ret);
                        return ret;
                }
                return 0;
        }
​
        ret = uclass_get_device(UCLASS_CODEC, 0, &codec_dev);
        if (ret) {
                if (ret != -ENODEV) {
                        printf("Get codec device failed: %d\n", ret);
                        return ret;
                }
                return 0;
        }
​
        sound_set_sysclk(i2s_dev, SAMPLERATE * 256);
        sound_hw_params(i2s_dev, SAMPLERATE, 16, 2);
        sound_hw_params(codec_dev, SAMPLERATE, 16, 2);
        sound_startup(i2s_dev);
        sound_startup(codec_dev);
​
        return ret;
}

可以看到这里依次调用:

  • platform驱动操作函数集中的set_sysclk、hw_params方法;

  • codec驱动操作函数集中的hw_params方法;

  • platform驱动操作函数集中的startup方法,这个实际上没有定义;

  • codec驱动操作函数集中的startup方法;

这里本质上就是初始化RK3399的I2S和cdoec芯片的各个寄存器。

6. 3 音频播放

初始化声卡之后,就可以进行音频播放了,这里调用的就是sound_play:

static int _sound_play(struct udevice *dev, unsigned int *data,
                       unsigned long data_size)
{
        const struct snd_soc_dai_ops *ops = dev_get_driver_ops(dev);
​
        if (!ops || !ops->transfer)
                return -ENOTSUPP;
​
        return ops->transfer(dev, data, data_size);
}
​
int sound_play(u32 msec, u32 frequency)
{
        unsigned int *buf;
        unsigned long buf_size;
        unsigned int ret = 0;
​
        buf_size = WAV_SIZE;
​
        buf = malloc(buf_size);
        if (!buf) {
                debug("%s: buf malloc failed\n", __func__);
                return -ENOMEM;
        }
        ret = load_audio_wav(buf, "boot.wav", buf_size);
        /* if boot.wav not find, use sound_create_square_wave */
        if (ret <= 0)
                sound_create_square_wave((unsigned short *)buf,
                                         buf_size / sizeof(unsigned short),
                                         frequency);
​
        ret = _sound_play(i2s_dev, buf, (buf_size / sizeof(int)));
        free(buf);
​
        return ret;
}

这里实际上就是去加载boot.wav文件,这个文件哪里来的呢?感兴趣自己看代码去吧,哈哈哈哈!

如果不存在boot.wav文件,它自己动态生成了一个音频数据,然后存放到buf里面。

最后调用sound_play进行音频播放,sound_play实际上调用的就是platform驱动操作函数集中的transfer。

6.4 总结

看到上面的代码,是不是感觉很熟悉,彷佛就是和内核代码一个模子出来的,只不过内核的代码看起来太抽象了。

实际上内核驱动中定义的cpu dai、codec dai的操作函数也会在适当时机被调用,只是不像uboot中那么明显可以看懂。

比如cpu dai、codec dai的操作函数中定义的set_sysclk、set_fmt就会在ASoC声卡注册的时候执行,而startup、hw_params、trigger会在打开pcm设备以及音频播放/录音的时候执行。具体流程可以参考Rockchip RK3399 - ALSA 声卡之PCM设备 

参考文章

[1] 理解ALSA(三):从零写ASoC驱动

[2] syscon 的使用

[3] Linux内核4.14版本——alsa框架分析(9)——PCM DMA注册

[4] AlSA驱动中的PCM DMA

posted @ 2023-07-30 14:30  大奥特曼打小怪兽  阅读(2231)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步