Rockchip RK3399 - Codec驱动( Realtek ALC5651)

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

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

Rockchip RK3399 - ASoC Codec驱动基础中我们介绍了Codec驱动涉及到的数据结构以及核心API。并且已经了解到每个Codec driver必须提供以下功能:

  • Codec DAI和PCM的配置信息:通过struct snd_soc_dai_driver描述,包括dai的能力描述和操作接口;
  • Codec的控制接口:其控制接口一般是I2C或SPI。控制接口用于读写codec的寄存器。在struct snd_soc_component_driver结构体中,有大量字段描述codec的控制接口,比如read、write等;
  • Mixer和其它音频控件;
  • Codec的音频操作:通过结构体struct snd_soc_dai_ops描述;
  • DAPM描述信息;
  • DAPM事件处理程序;

本节我们将会以rt5651驱动为例进行分析,驱动源码位于sound/soc/codecs/rt5651.c文件。

一、设备树配置

1.1 设备节点rt5651

我们在arch/arm64/boot/dts/rockchip/rk3399-evb.dts文件添加rt5651设备节点,该节点位于i2c1节点下:

&i2c1 {
    status = "okay";
    i2c-scl-rising-time-ns = <300>;
    i2c-scl-falling-time-ns = <15>;

    rt5651: rt5651@1a {
        #sound-dai-cells = <0>;
        compatible = "rockchip,rt5651";
        reg = <0x1a>;
        clocks = <&cru SCLK_I2S_8CH_OUT>;
        clock-names = "mclk";
        status = "okay";
    };
};

 其中:

  • status :指定设备状态为“正常”,表示该设备状态为正常运行;
  • i2c-scl-rising-time-ns:定义了SCL信号上升时间的最小值,单位是纳秒;
  • i2c-scl-falling-time-ns:定义了SCL信号下降时间的最小值,单位是纳秒;

接着定义I2C从设备节点rt5651,即音频编解码器的设备节点,其名称为 rt5651,I2C从设备7位地址为0x1a;

  • compatible:指定设备驱动程序的兼容性,即告诉内核该设备可以被哪些驱动程序所使用;
  • reg:指定了rt5651设备在I2C控制器上的设备地址;
  • clock-names:指定时钟名称,"mclk"表示MCLK时钟;
  • clocks:mclk时钟来自SCLK_I2S_8CH_OUT;
  • status :指定设备状态为“正常”,表示该设备状态为正常运行;

关于rt5651设备节点更多属性可以参考文档:Documentation/devicetree/bindings/sound/rt5651.txt。

i2c1设备节点定义在arch/arm64/boot/dts/rockchip/rk3399.dtsi,内容如下:

i2c1: i2c@ff110000 {
        compatible = "rockchip,rk3399-i2c";
        reg = <0x0 0xff110000 0x0 0x1000>;
        assigned-clocks = <&cru SCLK_I2C1>;
        assigned-clock-rates = <200000000>;
        clocks = <&cru SCLK_I2C1>, <&cru PCLK_I2C1>;
        clock-names = "i2c", "pclk";
        interrupts = <GIC_SPI 59 IRQ_TYPE_LEVEL_HIGH 0>;
        pinctrl-names = "default";
        pinctrl-0 = <&i2c1_xfer>;
        #address-cells = <1>;
        #size-cells = <0>;
        status = "disabled";
};
1.2 时钟频率

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

clocks = <&cru SCLK_I2S_8CH_OUT>;
clock-names = "mclk";
1.2.1 clk_i2sout

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

COMPOSITE_NODIV(SCLK_I2S_8CH_OUT, "clk_i2sout", mux_i2sout_p, CLK_SET_RATE_PARENT,
                RK3399_CLKSEL_CON(31), 2, 1, MFLAGS,
                RK3399_CLKGATE_CON(8), 12, GFLAGS)

这是composite类型的时钟,其中COMPOSITE_NODIV宏定义在drivers/clk/rockchip/clk.h:

#define COMPOSITE_NODIV(_id, cname, pnames, f, mo, ms, mw, mf,  \
                        go, gs, gf)                             \
        {                                                       \
                .id             = _id,                          \
                .branch_type    = branch_composite,             \
                .name           = cname,                        \
                .parent_names   = pnames,                       \
                .num_parents    = ARRAY_SIZE(pnames),           \
                .flags          = f,                            \
                .muxdiv_offset  = mo,                           \
                .mux_shift      = ms,                           \
                .mux_width      = mw,                           \
                .mux_flags      = mf,                           \
                .gate_offset    = go,                           \
                .gate_shift     = gs,                           \
                .gate_flags     = gf,                           \
        }

(1) 在RK3399 datasheet中,我们可以找到名字为clk_i2sout的时钟的信息,从下图可以看到它有两个父时钟,一个ID为62,可以在datasheet表中找到62代表的是clk_i2sout_src;另一个ID为64,可以在datasheet表中找到64代表的是clk_12m;

 实际上mux_i2sout_p中存放的就是这两个父时钟的名称;

PNAME(mux_i2sout_p)  = { "clk_i2sout_src", "xin12m" };

我们可以找到时钟clk_i2sout_src的定义,它是一个多路选择类型的时钟,如下所示;

MUX(0, "clk_i2sout_src", mux_i2sch_p, CLK_SET_RATE_PARENT,
RK3399_CLKSEL_CON(31), 0, 2, MFLAGS),

而xin12m应该是一个fixed rate clock((有源晶振、无源晶振))。

(2) 宏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寄存器。

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

可以看到位12为clk_i2sout时钟使能位,低电平使能,高电平禁用。 那clk_i2sout到底是什么时钟呢?
RK3399平台有三路I2S(其中一路内部使用,可以不管),但是MCLK只有一个,也就是说I2S0、I2S1只有一路能占用,因此我猜测clk_i2sout应该就是MCLK信号线的时钟。(3) 宏RK3399_CLKSEL_CON定义在drivers/clk/rockchip/clk.h:

#define RK3399_CLKSEL_CON(x)            ((x) * 0x4 + 0x100)

通过RK3399_CLKSEL_CON(31)可以得到寄存器偏移地址31*0x04+0x100=0x17C,偏移0x17C是CRU_CLKSEL_CON31寄存器。

接着我们看一下CRU_CLKSEL_CON31寄存器,CRU_CLKSEL_CON31为Internal clock select and divide register31,其中位[2]含义如下:

可以看到位2用于clk_i2sout时钟源选择,这里需要配置为clk_i2s。

1.2.2 时钟链路

经过上面的分析,我们不难推断出clk_i2sout的时钟链路如下所示:

其中clk_i2sout_src的时钟源由clk_i2s0、clk_i2s1、clk_i2s2,其定义在mux_i2sch_p:

PNAME(mux_i2sch_p)  = { "clk_i2s0", "clk_i2s1","clk_i2s2" };

由CRU_CLKSEL_CON31寄存器的位[1:0]控制时钟源的选择:

关于时钟源clk_i2s0以及之前的时钟链路我们在Rockchip RK3399 - Platform驱动(DMA&i2s0)中介绍。

二、I2C控制器驱动

RK3399这款SOC的I2C结构,其内部有9个I2C控制器,这里我们以I2C1为例,其中I2C1_SCL连接GPIO4_A2引脚,I2C1_SDA连接GPIO4_A1引脚。

关于RK3399 I2C控制器驱动实现位于drivers/i2c/busses/i2c-rk3x.c文件,I2C控制器驱动是基于platform模型的,主要提供一个algorithm底层的I2C协议的收发函数。

在platform driver中probe函数中:

  • 动态分配i2c_adapter,并进行成员初始化,包括设置algo;
  • 初始化I2C总线所使用的的GPIO功能复用为I2C;
  • 初始化I2C控制器相关的寄存器;
  • 获取资源信息,并注册I2C中断处理函数;
  • 最后调用i2c_add_adapter将i2c_adapter注册到i2c_bus_type总线,并且注册时会:
    • 调用of_i2c_register_devices,解析I2C控制器设备节点的子设备节点,从而调用of_i2c_register_device完成I2C从设备的注册;
    • 调用i2c_scan_board_info,扫描并使用i2c_new_device注册I2C从设备。

of_i2c_register_device内部通过调用of_i2c_get_board_info函数解析设备节点rt5651可以得到如下定义的I2C从设备:

struct i2c_board_info info = {
     .type      = "rt5651",  // 会赋值给i2c_client的name字段
     .addr      = 0x1a,
     .of_node   = rt5651设备节点,
};

然后将该I2C从设备注册到系统,更多的细节在这一节我们不去研究。有关I2C驱动的内容可以先参考linux驱动移植-I2C总线设备驱动linux驱动移植-I2C适配器驱动移植linux驱动移植-I2C驱动移植(OLED SSD1306),关于RK3399 I2C控制器驱动后面有时间再单独介绍。

三、Codec驱动

3.1 模块入口函数

我们定位到sound/soc/codecs/rt5651.c文件的最后:

module_i2c_driver(rt5651_i2c_driver);
3.1.1 module_i2c_driver

module_i2c_driver宏可以展开为相应驱动模块的init和exit接口,其定义在include/linux/i2c.h:

/**
 * module_i2c_driver() - Helper macro for registering a modular I2C driver
 * @__i2c_driver: i2c_driver struct
 *
 * Helper macro for I2C drivers which do not do anything special in module
 * init/exit. This eliminates a lot of boilerplate. Each module may only
 * use this macro once, and calling it replaces module_init() and module_exit()
 */
#define module_i2c_driver(__i2c_driver) \
        module_driver(__i2c_driver, i2c_add_driver, \
                        i2c_del_driver)
3.1.2 module_driver

module_driver定义在include/linux/device/driver.h:

/**
 * module_driver() - Helper macro for drivers that don't do anything
 * special in module init/exit. This eliminates a lot of boilerplate.
 * Each module may only use this macro once, and calling it replaces
 * module_init() and module_exit().
 *
 * @__driver: driver name
 * @__register: register function for this driver type
 * @__unregister: unregister function for this driver type
 * @...: Additional arguments to be passed to __register and __unregister.
 *
 * Use this macro to construct bus specific macros for registering
 * drivers, and do not use it on its own.
 */
#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
        return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
static void __exit __driver##_exit(void) \
{ \
        __unregister(&(__driver) , ##__VA_ARGS__); \
} \
module_exit(__driver##_exit);
3.1.3 展开后

因此如下定义:

module_i2c_driver(rt5651_i2c_driver);

经过上述宏的作用之后,就成为如下形式:

static int __init ov4689_i2c_driver_init(void)
{
    return i2c_add_driver(&rt5651_i2c_driver);
}
 
static void __exit ov4689_i2c_driver_exit(void)
{
    return i2c_del_driver(&rt5651_i2c_driver);
}

其中i2c_add_driver函数用于注册I2C设备驱动。

3.2 rt5651_i2c_driver 

这里我们需要关注一下i2c_driver结构体变量rt5651_i2c_driver :

static struct i2c_driver rt5651_i2c_driver = {
        .driver = {
                .name = "rt5651",
                .acpi_match_table = ACPI_PTR(rt5651_acpi_match),
                .of_match_table = of_match_ptr(rt5651_of_match),  // 用于设备树匹配
        },
        .probe_new = rt5651_i2c_probe,
        .id_table = rt5651_i2c_id,
};

其成员:

  • driver.of_match_table:用于设备树匹配;
  • probe:当I2C驱动和I2C从设备信息匹配成功之后,就会调用probe函数;
  • id_table:id列表,用于和I2C从设备名称进行匹配;
3.3.1 rt5651_of_match

如果使用了设备树,rt5651_of_match被定义为:

#if defined(CONFIG_OF)
static const struct of_device_id rt5651_of_match[] = {
        { .compatible = "realtek,rt5651", },   // 用来匹配的I2C从设备,匹配设备节点rt5651
        {},  /* 最后一个必须为空,表示结束 */
};
MODULE_DEVICE_TABLE(of, rt5651_of_match);
#endif

由于在I2C控制器注册的时候为声卡设备注册了I2C从设备(对应数据结构struct i2c_client),其名称为rt5651,因此会与I2C从设备驱动中rt5651_of_match匹配失败。

3.3.2 rt5651_i2c_id

i2c_device_id中存放的是和I2C驱动匹配的I2C从设备的名称,以rt5651_i2c_id为例:

static const struct i2c_device_id rt5651_i2c_id[] = {
        { "rt5651", 0 },                // 用来匹配的I2C从设备
        { }  /* 最后一个必须为空,表示结束 */
};

由于在I2C控制器注册的时候为声卡设备注册了I2C从设备(对应数据结构struct i2c_client),其名称为rt5651,因此会与I2C从设备驱动中的rt5651_i2c_id匹配成功,从而进入执行probe探测函数;

3.3.3 rt5651_i2c_probe

probe探测函数rt5651_i2c_probe定义如下:

static int rt5651_i2c_probe(struct i2c_client *i2c)  // 参数为I2C从设备
{
        struct rt5651_priv *rt5651;
        int ret;
        int err;

        rt5651 = devm_kzalloc(&i2c->dev, sizeof(*rt5651),   // 动态申请内存,数据结构类型为struct rt5651_priv
                                GFP_KERNEL);
        if (NULL == rt5651)
                return -ENOMEM;

        i2c_set_clientdata(i2c, rt5651); //  i2c->dev.driver_data = rt5651 设置为驱动数据

        rt5651->regmap = devm_regmap_init_i2c(i2c, &rt5651_regmap);  // 注册regmap实例
        if (IS_ERR(rt5651->regmap)) {
                ret = PTR_ERR(rt5651->regmap);
                dev_err(&i2c->dev, "Failed to allocate register map: %d\n",
                        ret);
                return ret;
        }

    // 读取RT5651_DEVICE_ID寄存器的值,RT5651_DEVICE_ID值为0xff,寄存器地址0xff存放的是设备ID,数据位宽为16位 err = regmap_read(rt5651->regmap, RT5651_DEVICE_ID, &ret);if (err) // 读取失败 return err; if (ret != RT5651_DEVICE_ID_VALUE) { // 0x6281 dev_err(&i2c->dev, "Device with ID register %#x is not rt5651\n", ret); return -ENODEV; } regmap_write(rt5651->regmap, RT5651_RESET, 0); // 寄存器地址0x00为软件复位寄存器,写入0x00将会复位所有寄存器的 ret = regmap_register_patch(rt5651->regmap, init_list, // 用于初始化rt5651,向一组寄存器中写入值 ARRAY_SIZE(init_list)); if (ret != 0) dev_warn(&i2c->dev, "Failed to apply regmap patch: %d\n", ret); rt5651->irq = i2c->irq; // I2C从设备所使用的的中断编号; 由于rt5651设备节点中并没有配置中断,所以i2c->irq默认值为0 rt5651->hp_mute = true; INIT_DELAYED_WORK(&rt5651->bp_work, rt5651_button_press_work); // 初始化延迟的工作rt5651->bp_work,设置工作函数为rt5651_button_press_work INIT_WORK(&rt5651->jack_detect_work, rt5651_jack_detect_work); // 初始化工作rt5651->jack_detect_work,设置工作函数为rt5651_jack_detect_work /* Make sure work is stopped on probe-error / remove */ ret = devm_add_action_or_reset(&i2c->dev, rt5651_cancel_work, rt5651); if (ret) return ret; ret = devm_request_irq(&i2c->dev, rt5651->irq, rt5651_irq, // 申请I2C中断,中断处理函数为rt5651_irq,;因为没有配置中断,所以这里中断会申请失败 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT | IRQF_NO_AUTOEN, "rt5651", rt5651); if (ret) { dev_warn(&i2c->dev, "Failed to reguest IRQ %d: %d\n", rt5651->irq, ret); rt5651->irq = -ENXIO; } ret = devm_snd_soc_register_component(&i2c->dev, // 注册component &soc_component_dev_rt5651, rt5651_dai, ARRAY_SIZE(rt5651_dai)); return ret; }

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

(2) 调用devm_regmap_init_i2c注册regmap实例,这样i2c驱动驱动就可以正常调用regmap_write和regmap_read函数进行i2c数据传输了;

(3) 读取rt5651设备寄存器地址0xff的值,对于rt5651芯片寄存器地址0xff存放的是设备ID,因此读取到的为0x6281;

(4) 向rt5651软件复位寄存器地址0x00写入0,将所有寄存器的值复位;

(5) 调用regmap_register_patch向一组寄存器中写入值;其中init_list设置为:

static const struct reg_sequence init_list[] = {
        {RT5651_PR_BASE + 0x3d, 0x3e00},
};

regmap_register_patch定义在drivers/base/regmap/regmap.c:

/**
 * regmap_register_patch - Register and apply register updates to be applied
 *                         on device initialistion
 *
 * @map: Register map to apply updates to.
 * @regs: Values to update.
 * @num_regs: Number of entries in regs.
 *
 * Register a set of register updates to be applied to the device
 * whenever the device registers are synchronised with the cache and
 * apply them immediately.  Typically this is used to apply
 * corrections to be applied to the device defaults on startup, such
 * as the updates some vendors provide to undocumented registers.
 *
 * The caller must ensure that this function cannot be called
 * concurrently with either itself or regcache_sync().
 */
int regmap_register_patch(struct regmap *map, const struct reg_sequence *regs,
                          int num_regs)
{
        struct reg_sequence *p;
        int ret;
        bool bypass;

        if (WARN_ONCE(num_regs <= 0, "invalid registers number (%d)\n",
            num_regs))
                return 0;

        p = krealloc(map->patch,
                     sizeof(struct reg_sequence) * (map->patch_regs + num_regs),
                     GFP_KERNEL);
        if (p) {
                memcpy(p + map->patch_regs, regs, num_regs * sizeof(*regs));
                map->patch = p;
                map->patch_regs += num_regs;
        } else {
                return -ENOMEM;
        }

        map->lock(map->lock_arg);

        bypass = map->cache_bypass;

        map->cache_bypass = true;
        map->async = true;

        ret = _regmap_multi_reg_write(map, regs, num_regs);  // 写入多个寄存器

        map->async = false;
        map->cache_bypass = bypass;

        map->unlock(map->lock_arg);

        regmap_async_complete(map);

        return ret;
}
View Code

(6) 初始化rt5651成员irq、hp_mute;初始化延迟的工作rt5651->bp_work,设置工作函数为rt5651_button_press_work;初始化工作rt5651->jack_detect_work,设置工作函数为rt5651_jack_detect_work;

(7) 调用devm_add_action_or_reset函数Make sure work is stopped on probe-error / remove;

static void rt5651_cancel_work(void *data)
{
        struct rt5651_priv *rt5651 = data;

        cancel_work_sync(&rt5651->jack_detect_work);
        cancel_delayed_work_sync(&rt5651->bp_work);
}

(8) 申请I2C中断,中断处理函数为rt5651_irq;

static irqreturn_t rt5651_irq(int irq, void *data)
{
        struct rt5651_priv *rt5651 = data;

        queue_work(system_power_efficient_wq, &rt5651->jack_detect_work);

        return IRQ_HANDLED;
}

由于我们设备节点rt5651中并没有配置中断,因此申请中断会失败,内核启动的时候也会输出相关错误信息;其中rt5651为模块的名称,1-001a为i2c_client->dev设备的名称;

[    3.465917] rt5651 1-001a: Failed to reguest IRQ 0: -22

(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的名称为i2c从设备对应的struct device_driver、struct device实例的名字拼接而成,即rt5651.1-001a;
  • snd_soc_component的dai_list链表包含两个dai,第一个dai的名称为rt5651-aif1,第二个dai的名称为rt5651-aif2;
  • 每个dai对应一个dai driver,第一个dai driver的名称为rt5651-aif1,第二个dai driver的名称为rt5651-aif2;

四、soc_component_dev_rt5651

devm_snd_soc_register_component函数第二个参数为soc_component_dev_rt5651:

static const struct snd_soc_component_driver soc_component_dev_rt5651 = {
        .probe                  = rt5651_probe,
        .suspend                = rt5651_suspend,
        .resume                 = rt5651_resume,
        .set_bias_level         = rt5651_set_bias_level,
        .set_jack               = rt5651_set_jack,
        .controls               = rt5651_snd_controls,                 // kcontrol定义
        .num_c
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Graceful_scenery

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值