----------------------------------------------------------------------------------------------------------------------------
开发板 :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:
![](https://img-blog.csdnimg.cn/img_convert/95e985a56d53623026c3030a4768f765.gif)
![](https://img-blog.csdnimg.cn/img_convert/d5d187c9aed53a5143a086bf4e56dec9.gif)
/**
* 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;
}
(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