📝往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)
🚩 鸿蒙(HarmonyOS)北向开发知识点记录~
🚩 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~
🚩 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
🚩 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
🚩 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
🚩 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
🚩 记录一场鸿蒙开发岗位面试经历~
📃 持续更新中……
本文章是基于瑞芯微 RK3568 芯片的 DAYU200 开发板,进行标准系统相关功能的移植,主要包括产品配置添加,内核启动、升级,音频 ADM 化,Camera,TP,LCD,WIFI,BT,vibrator、sensor、图形显示模块的适配案例总结,以及相关功能的适配。
产品配置和目录规划
产品配置
在产品 //productdefine/common/device
目录下创建以 rk3568 名字命名的 json 文件,并指定 CPU 的架构。//productdefine/common/device/rk3568.json
配置如下:
{
"device_name": "rk3568",
"device_company": "rockchip",
"target_os": "ohos",
"target_cpu": "arm",
"kernel_version": "",
"device_build_path": "device/board/hihope/rk3568",
"enable_ramdisk": true, //是否支持ramdisk二级启动
"build_selinux": true // 是否支持selinux权限管理
}
在 //productdefine/common/products
目录下创建以产品名命名的 rk3568.json 文件。该文件用于描述产品所使用的 SOC 以及所需的子系统。配置如下
{
"product_name": "rk3568",
"product_company" : "hihope",
"product_device": "rk3568",
"version": "2.0",
"type": "standard",
"parts":{
"ace:ace_engine_standard":{},
"ace:napi":{},
...
"xts:phone_tests":{}
}
}
主要的配置内容包括:
- product_device:配置所使用的 SOC。
- type:配置系统的级别, 这里直接 standard 即可。
- parts:系统需要启用的子系统。子系统可以简单理解为一块独立构建的功能块。
已定义的子系统可以在//build/subsystem_config.json
中找到。当然你也可以定制子系统。
这里建议先拷贝 Hi3516DV300 开发板的配置文件,删除掉 hisilicon_products 这个子系统。这个子系统为 Hi3516DV300 SOC 编译内核,不适合 rk3568。
目录规划
参考 Board 和 SoC 解耦的设计思路 ,并把芯片适配目录规划为:
device
├── board --- 单板厂商目录
│ └── hihope --- 单板厂商名字:
│ └── rk3568 --- 单板名:rk3568,主要放置开发板相关的驱动业务代码
└── soc --- SoC厂商目录
└── rockchip --- SoC厂商名字:rockchip
└── rk3568 --- SoC Series名:rk3568,主要为芯片原厂提供的一些方案,以及闭源库等
vendor
└── hihope
└── rk3568 --- 产品名字:产品、hcs以及demo相关
内核启动
二级启动
二级启动简单来说就是将之前直接挂载 sytem,从 system 下的 init 启动,改成先挂载 ramdsik,从 ramdsik 中的 init 启动,做些必要的初始化动作,如挂载 system,vendor 等分区,然后切到 system 下的 init 。
Rk3568 适配主要是将主线编译出来的 ramdisk 打包到 boot_linux.img 中,主要有以下工作:
1.使能二级启动
在 productdefine/common/device/rk3568.json 中使能 enable_ramdisk。
{
"device_name": "rk3568",
"device_company": "hihope",
"target_os": "ohos",
"target_cpu": "arm",
"kernel_version": "",
"device_build_path": "device/hihope/build",
"enable_ramdisk": true,
"build_selinux": true
}
2.把主线编译出来的 ramdsik.img 打包到 boot_linux.img
配置:
由于 rk 启动 uboot 支持从 ramdisk 启动,只需要在打包 boot_linux.img 的配置文件中增加 ramdisk.img ,因此没有使用主线的 its 格式,具体配置就是在内核编译脚本 make-ohos.sh 中增加:
function make_extlinux_conf()
{
dtb_path=$1
uart=$2
image=$3
echo "label rockchip-kernel-5.10" > ${EXTLINUX_CONF}
echo " kernel /extlinux/${image}" >> ${EXTLINUX_CONF}
echo " fdt /extlinux/${TOYBRICK_DTB}" >> ${EXTLINUX_CONF}
if [ "enable_ramdisk" == "${ramdisk_flag}" ]; then
echo " initrd /extlinux/ramdisk.img" >> ${EXTLINUX_CONF}
fi
cmdline="append earlycon=uart8250,mmio32,${uart} root=PARTUUID=614e0000-0000-4b53-8000-1d28000054a9 rw rootwait rootfstype=ext4"
echo " ${cmdline}" >> ${EXTLINUX_CONF}
}
打包
增加了打包 boot 镜像的脚本 make-boot.sh,供编译完 ramdisk,打包 boot 镜像时调用, 主要内容:
genext2fs -B ${blocks} -b ${block_size} -d boot_linux -i 8192 -U boot_linux.img
INIT 配置
init 相关配置请参考 启动子系统的规范要求 即可
音频
RK3568 Audio 总体结构图
ADM 适配方案介绍
RK3568 平台适配 ADM 框架图
- ADM Drivers adapter
主要完成 Codec/DMA/I2S 驱动注册,使得 ADM 可以加载驱动节点;并注册 ADM 与 Drivers 交互的接口函数 - ADM Drivers impl
主要完成 ADM Drivers adapter 接口函数的实现,以及 Codec_config.hcs/dai_config.hcs 等配置信息的获取,并注册到对应的设备 - Linux Drivers
ADM Drivers impl 可以直接阅读硬件手册,完成驱动端到端的配置;也可以借用 Linux 原生驱动实现与接口,减少开发者工作量。
目录结构
./device/board/hihope/rk3568/audio_drivers
├── codec
│ └── rk809_codec
│ ├── include
│ │ ├── rk809_codec_impl.h
│ │ └── rk817_codec.h
│ └── src
│ ├── rk809_codec_adapter.c
│ ├── rk809_codec_linux_driver.c
│ └── rk809_codec_ops.c
├── dai
│ ├── include
│ │ ├── rk3568_dai_linux.h
│ │ └── rk3568_dai_ops.h
│ └── src
│ ├── rk3568_dai_adapter.c
│ ├── rk3568_dai_linux_driver.c
│ └── rk3568_dai_ops.c
├── dsp
│ ├── include
│ │ └── rk3568_dsp_ops.h
│ └── src
│ ├── rk3568_dsp_adapter.c
│ └── rk3568_dsp_ops.c
├── include
│ ├── audio_device_log.h
│ └── rk3568_audio_common.h
└── soc
├── include
│ └── rk3568_dma_ops.h
└── src
├── rk3568_dma_adapter.c
└── rk3568_dma_ops.c
RK3568 适配 ADM 详细过程
梳理平台 Audio 框架
梳理目标平台的 Audio 结构,明确数据流与控制流通路。
- 针对 RK3568 平台,Audio 的结构相对简单见 RK3568 Audio 总体结构图,Codec 作为一个独立设备。I2C 完成对设备的控制,I2S 完成 Codec 设备与 CPU 之间的交互。
- 结合原理图整理 I2S 通道号,对应的引脚编号;I2C 的通道号,地址等硬件信息。
- 获取 Codec 对应的 datasheet,以及 RK3568 平台的 Datasheet(包含 I2S/DMA 通道等寄存器的介绍)。
熟悉并了解 ADM 结构
ADM 结构框图如下,Audio Peripheral Drivers 和 Platform Drivers 为平台适配需要完成的工作。
结合第 1 步梳理出来的 Audio 结构分析,Audio Peripheral Drivers 包含 Rk809 的驱动,Platform Drivers 包含 DMA 驱动和 I2S 驱动。
需要适配的驱动 | ADM 对应模块 | 接口文件路径 |
---|---|---|
RK809 驱动 | Accessory | drivers/framework/include/audio/audio_accessory_if.h |
DMA 驱动 | platform | drivers/framework/include/audio/audio_platform_if.h |
I2S 驱动 | DAI | drivers/framework/include/audio/audio_dai_if.h.h |
搭建驱动代码框架
配置 HCS 文件
在 device_info.hcs 文件中 Audio 下注册驱动节点
audio :: host {
hostName = "audio_host";
priority = 60;
device_dai0 :: device {
device0 :: deviceNode {
policy = 1;
priority = 50;
preload = 0;
permission = 0666;
moduleName = "DAI_RK3568";
serviceName = "dai_service";
deviceMatchAttr = "hdf_dai_driver";
}
}
device_codec :: device {
device0 :: deviceNode {
policy = 1;
priority = 50;
preload = 0;
permission = 0666;
moduleName = "CODEC_RK809";
serviceName = "codec_service_0";
deviceMatchAttr = "hdf_codec_driver";
}
}
device_codec_ex :: device {
device0 :: deviceNode {
policy = 1;
priority = 50;
preload = 0;
permission = 0666;
moduleName = "CODEC_RK817";
serviceName = "codec_service_1";
deviceMatchAttr = "hdf_codec_driver_ex";
}
}
device_dsp :: device {
device0 :: deviceNode {
policy = 1;
priority = 50;
preload = 0;
permission = 0666;
moduleName = "DSP_RK3568";
serviceName = "dsp_service_0";
deviceMatchAttr = "hdf_dsp_driver";
}
}
device_dma :: device {
device0 :: deviceNode {
policy = 1;
priority = 50;
preload = 0;
permission = 0666;
moduleName = "DMA_RK3568";
serviceName = "dma_service_0";
deviceMatchAttr = "hdf_dma_driver";
}
}
......
}
根据接入的设备,选择 Codec 节点还是 Accessory 节点,配置硬件设备对应的私有属性(包含寄存器首地址,相关 control 寄存器地址)涉及 Codec_config.hcs 和 DAI_config.hcs 配置相关介绍见 Audio hcs 配置章节以及 ADM 框架的 audio_parse 模块代码。
codec/accessory 模块
- 将驱动注册到 HDF 框架中,代码片段如下,启动 moduleName 与 HCS 文件的中 moduleName 一致
struct HdfDriverEntry g_codecDriverEntry = {
.moduleVersion = 1,
.moduleName = "CODEC_HI3516",
.Bind = CodecDriverBind,
.Init = CodecDriverInit,
.Release = CodecDriverRelease,
};
HDF_INIT(g_codecDriverEntry);
- Codec 模块需要填充:
g_codecData:codec 设备的操作函数集和私有数据集。
g_codecDaiDeviceOps:codecDai 的操作函数集,包括启动传输和参数配置等函数接口。
g_codecDaiData:codec 的数字音频接口的操作函数集和私有数据集。 - 完成 bind、init 和 release 函数的实现
- 验证
在 bind 和 init 函数加调试日志,编译版本并获取系统系统日志:
[ 1.548624] [E/"rk809_codec_adapter"] [Rk809DriverBind][line:258]: enter
[ 1.548635] [E/"rk809_codec_adapter"] [Rk809DriverBind][line:260]: success
[ 1.548655] [E/"rk809_codec_adapter"] [Rk809DriverInit][line:270]: enter
[ 1.549050] [E/"rk809_codec_adapter"] [GetServiceName][line:226]: enter
[ 1.549061] [E/"rk809_codec_adapter"] [GetServiceName][line:250]: success
[ 1.549072] [E/"rk809_codec_adapter"] [Rk809DriverInit][line:316]: g_chip->accessory.drvAccessoryName = codec_service_1
[ 1.549085] [E/audio_core] [AudioSocRegisterDai][line:86]: Register [accessory_dai] success.
[ 1.549096] [E/audio_core] [AudioRegisterAccessory][line:120]: Register [codec_service_1] success.
[ 1.549107] [E/"rk809_codec_adapter"] [Rk809DriverInit][line:323]: success!
DAI 模块
- 将 I2S 驱动注册到 HDF 框架中,代码片段如下,启动 moduleName 与 HCS 文件的中 moduleName 一致
struct HdfDriverEntry g_daiDriverEntry = {
.moduleVersion = 1,
.moduleName = "DAI_RK3568",
.Bind = DaiDriverBind,
.Init = DaiDriverInit,
.Release = DaiDriverRelease,
};
HDF_INIT(g_daiDriverEntry);
- DAI 模块填充:
struct AudioDaiOps g_daiDeviceOps = {
.Startup = Rk3568DaiStartup,
.HwParams = Rk3568DaiHwParams,
.Trigger = Rk3568NormalTrigger,
};
struct DaiData g_daiData = {
.Read = Rk3568DeviceReadReg,
.Write = Rk3568DeviceWriteReg,
.DaiInit = Rk3568DaiDeviceInit,
.ops = &g_daiDeviceOps,
};
- 完成 bind、init 和 release 函数的实现
- 验证
在 bind/init 函数加调试日志,编译版本并获取系统系统日志
[ 1.549193] [I/device_node] launch devnode dai_service
[ 1.549204] [E/HDF_LOG_TAG] [DaiDriverBind][line:38]: entry!
[ 1.549216] [E/HDF_LOG_TAG] [DaiDriverBind][line:55]: success!
[ 1.549504] [E/audio_core] [AudioSocRegisterDai][line:86]: Register [dai_service] success.
[ 1.549515] [E/HDF_LOG_TAG] [DaiDriverInit][line:116]: success.
Platform 模块
- 将 DMA 驱动注册到 HDF 框架中,代码片段如下,启动 moduleName 与 HCS 文件的中 moduleName 一致
struct HdfDriverEntry g_platformDriverEntry = {
.moduleVersion = 1,
.moduleName = "DMA_RK3568",
.Bind = PlatformDriverBind,
.Init = PlatformDriverInit,
.Release = PlatformDriverRelease,
};
HDF_INIT(g_platformDriverEntry);
- DMA 模块需要填充:
struct AudioDmaOps g_dmaDeviceOps = {
.DmaBufAlloc = Rk3568DmaBufAlloc,
.DmaBufFree = Rk3568DmaBufFree,
.DmaRequestChannel = Rk3568DmaRequestChannel,
.DmaConfigChannel = Rk3568DmaConfigChannel,
.DmaPrep = Rk3568DmaPrep,
.DmaSubmit = Rk3568DmaSubmit,
.DmaPending = Rk3568DmaPending,
.DmaPause = Rk3568DmaPause,
.DmaResume = Rk3568DmaResume,
.DmaPointer = Rk3568PcmPointer,
};
struct PlatformData g_platformData = {
.PlatformInit = AudioDmaDeviceInit,
.ops = &g_dmaDeviceOps,
};
- 完成 bind、init 和 release 函数的实现
- 验证
在 bind 和 init 函数加调试日志,编译版本并获取系统系统日志
[ 1.548469] [E/rk3568_platform_adapter] [PlatformDriverBind][line:42]: entry!
[ 1.548481] [E/rk3568_platform_adapter] [PlatformDriverBind][line:58]: success!
[ 1.548492] [E/rk3568_platform_adapter] [PlatformDriverInit][line:100]: entry.
[ 1.548504] [E/rk3568_platform_adapter] [PlatformGetServiceName][line:67]: entry!
[ 1.548515] [E/rk3568_platform_adapter] [PlatformGetServiceName][line:91]: success!
[ 1.548528] [E/audio_core] [AudioSocRegisterPlatform][line:63]: Register [dma_service_0] success.
[ 1.548536] [E/rk3568_platform_adapter] [PlatformDriverInit][line:119]: success.
驱动适配
code/accessory 模块
- 读取 DTS 文件,获取到对应设备节点,使用 Linux 原生的驱动注册函数,获取到对应 device。
static int rk817_platform_probe(struct platform_device *pdev) {
rk817_pdev = pdev;
dev_info(&pdev->dev, "got rk817-codec platform_device");
return 0;
}
static struct platform_driver rk817_codec_driver = {
.driver = {
.name = "rk817-codec", // codec node in dts file
.of_match_table = rk817_codec_dt_ids,
},
.probe = rk817_platform_probe,
.remove = rk817_platform_remove,
};
2.读写寄存器函数封装
根据上述获取到的 device, 使用 Linux 的 regmap 函数,开发者不需要获取模块的基地址
获取 rk817 的 regmap 代码段
g_chip = devm_kzalloc(&rk817_pdev->dev, sizeof(struct Rk809ChipData), GFP_KERNEL);
if (!g_chip) {
AUDIO_DEVICE_LOG_ERR("no memory");
return HDF_ERR_MALLOC_FAIL;
}
g_chip->pdev = rk817_pdev;
struct rk808 *rk808 = dev_get_drvdata(g_chip->pdev->dev.parent);
if (!rk808) {
AUDIO_DEVICE_LOG_ERR("%s: rk808 is NULL\n", __func__);
ret = HDF_FAILURE;
RK809ChipRelease();
return ret;
}
g_chip->regmap = devm_regmap_init_i2c(rk808->i2c,
&rk817_codec_regmap_config);
if (IS_ERR(g_chip->regmap)) {
AUDIO_DEVICE_LOG_ERR("failed to allocate regmap: %ld\n", PTR_ERR(g_chip->regmap));
RK809ChipRelease();
return ret;
}
寄存器读写函数代码段
int32_t Rk809DeviceRegRead(uint32_t reg, uint32_t *val)
{
if (regmap_read(g_chip->regmap, reg, val)) {
AUDIO_DRIVER_LOG_ERR("read register fail: [%04x]", reg);
return HDF_FAILURE;
}
return HDF_SUCCESS;
}
int32_t Rk809DeviceRegWrite(uint32_t reg, uint32_t value) {
if (regmap_write(g_chip->regmap, reg, value)) {
AUDIO_DRIVER_LOG_ERR("write register fail: [%04x] = %04x", reg, value);
return HDF_FAILURE;
}
return HDF_SUCCESS;
}
int32_t Rk809DeviceRegUpdatebits(uint32_t reg, uint32_t mask, uint32_t value) {
if (regmap_update_bits(g_chip->regmap, reg, mask, value)) {
AUDIO_DRIVER_LOG_ERR("update register bits fail: [%04x] = %04x", reg, value);
return HDF_FAILURE;
}
return HDF_SUCCESS;
}
3.寄存器初始化函数
因为使用 Linux 的 regmap 函数,所以需要自行定义 RegDefaultInit 函数,读取 hcs 中 initSeqConfig 的寄存器以及数值来进行配置
RK809RegDefaultInit 代码段
int32_t RK809RegDefaultInit(struct AudioRegCfgGroupNode **regCfgGroup)
{
int32_t i;
struct AudioAddrConfig *regAttr = NULL;
if (regCfgGroup == NULL || regCfgGroup[AUDIO_INIT_GROUP] == NULL ||
regCfgGroup[AUDIO_INIT_GROUP]->addrCfgItem == NULL || regCfgGroup[AUDIO_INIT_GROUP]->itemNum <= 0) {
AUDIO_DEVICE_LOG_ERR("input invalid parameter.");
return HDF_ERR_INVALID_PARAM;
}
regAttr = regCfgGroup[AUDIO_INIT_GROUP]->addrCfgItem;
for (i = 0; i < regCfgGroup[AUDIO_INIT_GROUP]->itemNum; i++) {
Rk809DeviceRegWrite(regAttr[i].addr, regAttr[i].value);
}
return HDF_SUCCESS;
}
4.封装控制接口的读写函数
设置控制读写函数为 RK809CodecReadReg 和 RK809CodecWriteReg
struct CodecData g_rk809Data = {
.Init = Rk809DeviceInit,
.Read = RK809CodecReadReg,
.Write = RK809CodecWriteReg,
};
struct AudioDaiOps g_rk809DaiDeviceOps = {
.Startup = Rk809DaiStartup,
.HwParams = Rk809DaiHwParams,
.Trigger = RK809NormalTrigger,
};
struct DaiData g_rk809DaiData = {
.DaiInit = Rk809DaiDeviceInit,
.ops = &g_rk809DaiDeviceOps,
};
封装控制接口的读写函数
因为原来的读写原型,涉及三个参数(unsigned long virtualAddress,uint32_t reg, uint32_t *val),其中 virtualAddress 我们并不需要用到,所以封装个接口即可,封装如下
int32_t RK809CodecReadReg(unsigned long virtualAddress,uint32_t reg, uint32_t *val)
{
if (val == NULL) {
AUDIO_DRIVER_LOG_ERR("param val is null.");
return HDF_FAILURE;
}
if (Rk809DeviceRegRead(reg, val)) {
AUDIO_DRIVER_LOG_ERR("read register fail: [%04x]", reg);
return HDF_FAILURE;
}
ADM_LOG_ERR("read reg 0x[%02x] = 0x[%02x]",reg,*val);
return HDF_SUCCESS;
}
int32_t RK809CodecWriteReg(unsigned long virtualAddress,uint32_t reg, uint32_t value)
{
if (Rk809DeviceRegWrite(reg, value)) {
AUDIO_DRIVER_LOG_ERR("write register fail: [%04x] = %04x", reg, value);
return HDF_FAILURE;
}
ADM_LOG_ERR("write reg 0x[%02x] = 0x[%02x]",reg,value);
return HDF_SUCCESS;
}
5.其他 ops 函数
- Rk809DeviceInit,读取 hcs 文件,初始化 Codec 寄存器,同时将对应的 control 配置(/* reg, rreg, shift, rshift, min, max, mask, invert, value */添加到 kcontrol,便于 dispatch contro 进行控制
- Rk809DaiStartup, 读取 hcs 文件,配置可选设备(codec/accessory)的控制寄存器
- Rk809DaiHwParams, 根据 hal 下发的 audio attrs(采样率、format、channel 等),配置对应的寄存器
- RK809NormalTrigger,根据 hal 下发的操作命令码,操作对应的寄存器,实现 Codec 的启动停止、录音和放音的切换等
DAI(i2s)模块
1.读写寄存器函数
思路与 Codec 模块的一致,读取 Linux DTS 文件,使用 Linux 的 regmap 函数完成寄存器的读写操作
int32_t Rk3568DeviceReadReg(unsigned long regBase, uint32_t reg, uint32_t *val)
{
AUDIO_DEVICE_LOG_ERR("entry");
(void)regBase;
struct device_node *dmaOfNode = of_find_node_by_path("/i2s@
2.其他 ops 函数
-
Rk3568DaiDeviceInit
原始框架,主要完成 DAI_config.hcs 参数列表的读取,与 HwParams 结合,完成参数的设置。 -
Rk3568DaiHwParams
主要完成 I2S MCLK/BCLK/LRCLK 时钟配置。- 根据不同采样率计算 MCLK
int32_t RK3568I2sTdmSetSysClk(struct rk3568_i2s_tdm_dev *i2s_tdm, const struct AudioPcmHwParams *param)
{
/* Put set mclk rate into rockchip_i2s_tdm_set_mclk() */
uint32_t sampleRate = param->rate;
uint32_t mclk_parent_freq = 0;
switch (sampleRate) {
case AUDIO_DEVICE_SAMPLE_RATE_8000:
case AUDIO_DEVICE_SAMPLE_RATE_16000:
case AUDIO_DEVICE_SAMPLE_RATE_24000:
case AUDIO_DEVICE_SAMPLE_RATE_32000:
case AUDIO_DEVICE_SAMPLE_RATE_48000:
case AUDIO_DEVICE_SAMPLE_RATE_64000:
case AUDIO_DEVICE_SAMPLE_RATE_96000:
mclk_parent_freq = i2s_tdm->bclk_fs * AUDIO_DEVICE_SAMPLE_RATE_192000;
break;
case AUDIO_DEVICE_SAMPLE_RATE_11025:
case AUDIO_DEVICE_SAMPLE_RATE_22050:
case AUDIO_DEVICE_SAMPLE_RATE_44100:
mclk_parent_freq = i2s_tdm->bclk_fs * AUDIO_DEVICE_SAMPLE_RATE_176400;
break;
default:
AUDIO_DEVICE_LOG_ERR("Invalid LRCK freq: %u Hz\n", sampleRate);
return HDF_FAILURE;
}
i2s_tdm->mclk_tx_freq = mclk_parent_freq;
i2s_tdm->mclk_rx_freq = mclk_parent_freq;
return HDF_SUCCESS;
}
-
根据获取的 mclk,计算 BCLK/LRclk 分频系数
-
Rk3568NormalTrigger 根据输入输出类型,以及 cmd(启动/停止/暂停/恢复),完成一系列配置:
-
mclk 的启停
-
DMA 搬运的启停
-
传输的启停
详细实现见代码,参考 Linux 原生 I2s 驱动对应接口函数
// 启动/恢复流程
if (streamType == AUDIO_RENDER_STREAM) {
clk_prepar
Platform(DMA)模块
ops 函数相关函数
1.Rk3568DmaBufAlloc/Rk3568DmaBufFree
获取 DMA 设备节点,参考 I2s 设备获取方式,使用系统函数 dma_alloc_wc/dma_free_wc,完成 DMA 虚拟内存与物理内存的申请/释放
2.Rk3568DmaRequestChannel
使用 Linux DMA 原生接口函数获取 DMA 传输通道,
dmaRtd->dmaChn[streamType] = dma_request_slave_channel(dmaDevice, dmaChannelNames[streamType]);
3.Rk3568DmaConfigChannel
//设置通道配置参数
// 放音通道参数配置
slave_config.direction = DMA_MEM_TO_DEV;
slave_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
slave_config.dst_addr = I2S1_ADDR + I2S_TXDR;
slave_config.dst_maxburst = 8;
// 录音通道参数配置
slave_config.direction = DMA_DEV_TO_MEM;
slave_config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
slave_config.src_addr = I2S1_ADDR + I2S_RXDR;
slave_config.src_maxburst = 8;
//使用Linux DMA原生接口函数完成DMA通道配置
ret = dmaengine_slave_config(dmaChan, &slave_config);
if (ret != 0) {
AUDIO_DEVICE_LOG_ERR("dmaengine_slave_config failed");
return HDF_FAILURE;
}
4.Rk3568DmaSubmit/Rk3568DmaPending
使用 Linux DMA 原生接口函数 dmaengine_prep_dma_cyclic,初始化一个具体的周期性的 DMA 传输描述符 dmaengine_submit 接口将该描述符放到传输队列上,然后调用 dma_async_issue_pending 接口,启动传输。
5.Rk3568PcmPointer
第 4 步完成之后,ADM 框架调用 Rk3568PcmPointer,循环写 cirBuf,计算 pointer
dma_chn = dmaRtd->dmaChn[DMA_TX_CHANNEL];
buf_size = data->renderBufInfo.cirBufSize;
dmaengine_tx_status(dma_chn, dmaRtd->cookie[DMA_TX_CHANNEL], &dma_state);
if (dma_state.residue) {
currentPointer = buf_size - dma_state.residue;
*pointer = BytesToFrames(data->pcmInfo.frameSize, currentPointer);
} else {
*pointer = 0;
}
6.Rk3568DmaPause
使用 Linux DMA 原生接口函数 dmaengine_terminate_async,停止 DMA 传输
dmaengine_terminate_async(dmaChan);
7.Rk3568DmaResume
暂停使用的 DMA 停止函数,对应恢复,相当于重启 DMA 传输,执行 Rk3568DmaSubmit/Rk3568DmaPending 相关操作即可完成
适配中遇到问题与解决方案
1.播放一段时间后,停止播放,持续有尖锐的很小的声音
问题原因:播放停止后,Codec 相关器件没有下电
解决方案:注册 Codec 的 trigger 函数,当接收到 Cmd 为 Stop 时,对 Codec 进行下电
2.播放一段时间后,停止播放,然后重新播放没有声音
问题原因:DMA 驱动的 PAUSE 接口函数,并未停止 DMA 传输
解决方案:暂停状态不再使用 DMA 的 PAUSE 函数,而是使用 DAM 传输停止接口; 相对应的,恢复函数的业务逻辑相当于重启 DMA 传输,执行 Rk3568DmaSubmit/Rk3568DmaPending 相关操作即可完成
3.播放存在杂音
问题原因:DMA 数据搬运 pointer 位置不正确
解决方案:Rk3568PcmPointer 函数返回值为 DMA 搬运的内存位置,用缓存区 buf 与 dma_state.residue 的差值计算
4.可以放音,但 Mclk 引脚没有时钟信号
问题原因:DTS 文件 pin-ctrl 没有配置 mclk 的引脚
解决方案:修改 DTS 文件