OpenHarmony(鸿蒙南向开发)——标准系统方案之瑞芯微RK3568移植案例(上)

发布于:2024-09-19 ⋅ 阅读:(11) ⋅ 点赞:(0)

往期知识点记录:

本文章是基于瑞芯微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":{}
  }
}

主要的配置内容包括:

1.product_device:配置所使用的SOC。
2.type:配置系统的级别, 这里直接standard即可。
3.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

调用make-boot.sh 的修改可以参考如下pr:

https://gitee.com/openharmony/build/pulls/569/files

INIT配置

init相关配置请参考 启动子系统的规范要求即可

音频

RK3568 Audio总体结构图

ADM适配方案介绍

RK3568平台适配ADM框架图

  1. ADM Drivers adapter

主要完成Codec/DMA/I2S驱动注册,使得ADM可以加载驱动节点;并注册ADM与Drivers交互的接口函数

  1. ADM Drivers impl

主要完成ADM Drivers adapter接口函数的实现,以及Codec_config.hcs/dai_config.hcs等配置信息的获取,并注册到对应的设备

  1. 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结构,明确数据流与控制流通路。

  1. 针对RK3568平台,Audio的结构相对简单见RK3568 Audio总体结构图,Codec作为一个独立设备。I2C完成对设备的控制,I2S完成Codec设备与CPU之间的交互。
  2. 结合原理图整理I2S通道号,对应的引脚编号;I2C的通道号,地址等硬件信息。
  3. 获取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";
                }
            }
            ......
        }

c

根据接入的设备,选择Codec节点还是Accessory节点,配置硬件设备对应的私有属性(包含寄存器首地址,相关control寄存器地址)涉及Codec_config.hcs和DAI_config.hcs

配置相关介绍见 Audio hcs配置章节以及ADM框架的audio_parse模块代码。

codec/accessory模块

1.将驱动注册到HDF框架中,代码片段如下,启动moduleName与HCS文件的中moduleName一致

struct HdfDriverEntry g_codecDriverEntry = {
   .moduleVersion = 1,
   .moduleName = "CODEC_HI3516",
   .Bind = CodecDriverBind,
   .Init = CodecDriverInit,
   .Release = CodecDriverRelease,
};
HDF_INIT(g_codecDriverEntry);

2.Codec模块需要填充:

g_codecData:codec设备的操作函数集和私有数据集。
g_codecDaiDeviceOps:codecDai的操作函数集,包括启动传输和参数配置等函数接口。
g_codecDaiData:codec的数字音频接口的操作函数集和私有数据集。

3.完成 bind、init和release函数的实现
4.验证

在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模块
1.将I2S驱动注册到HDF框架中,代码片段如下,启动moduleName与HCS文件的中moduleName一致

struct HdfDriverEntry g_daiDriverEntry = {
    .moduleVersion = 1,
    .moduleName = "DAI_RK3568",
    .Bind = DaiDriverBind,
    .Init = DaiDriverInit,
    .Release = DaiDriverRelease,
};
HDF_INIT(g_daiDriverEntry);

2.DAI模块填充:

struct AudioDaiOps g_daiDeviceOps = {
    .Startup = Rk3568DaiStartup,
    .HwParams = Rk3568DaiHwParams,
    .Trigger = Rk3568NormalTrigger,
};

struct DaiData g_daiData = {
    .Read = Rk3568DeviceReadReg,
    .Write = Rk3568DeviceWriteReg,
    .DaiInit = Rk3568DaiDeviceInit,
    .ops = &g_daiDeviceOps,
};

3.完成 bind、init和release函数的实现

4.验证

在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模块
1.将DMA驱动注册到HDF框架中,代码片段如下,启动moduleName与HCS文件的中moduleName一致

struct HdfDriverEntry g_platformDriverEntry = {
    .moduleVersion = 1,
    .moduleName = "DMA_RK3568",
    .Bind = PlatformDriverBind,
    .Init = PlatformDriverInit,
    .Release = PlatformDriverRelease,
};
HDF_INIT(g_platformDriverEntry);

2.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,
};

3.完成 bind、init和release函数的实现

4.验证

在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模块
  1. 读取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@fe410000");
     if(dmaOfNode == NULL) {
         AUDIO_DEVICE_LOG_ERR("of_node is NULL.");
     }
     struct platform_device *platformdev = of_find_device_by_node(dmaOfNode);
     struct rk3568_i2s_tdm_dev *i2s_tdm = dev_get_drvdata(&platformdev->dev);
     
     (void)regBase;
     if (regmap_read(i2s_tdm->regmap, reg, val)) {
         AUDIO_DEVICE_LOG_ERR("read register fail: [%04x]", reg);
         return HDF_FAILURE;
     }
     return HDF_SUCCESS;
 }

 int32_t Rk3568DeviceWriteReg(unsigned long regBase, uint32_t reg, uint32_t value)
 {    
     AUDIO_DEVICE_LOG_ERR("entry");
     (void)regBase;
     struct device_node *dmaOfNode = of_find_node_by_path("/i2s@fe410000");
     if(dmaOfNode == NULL) {
         AUDIO_DEVICE_LOG_ERR("of_node is NULL.");
     }
     struct platform_device *platformdev = of_find_device_by_node(dmaOfNode);
     struct rk3568_i2s_tdm_dev *i2s_tdm = dev_get_drvdata(&platformdev->dev);
     if (regmap_write(i2s_tdm->regmap, reg, value)) {
         AUDIO_DEVICE_LOG_ERR("write register fail: [%04x] = %04x", reg, value);
         return HDF_FAILURE;
     }
     return HDF_SUCCESS;
 }

2.其他ops函数

  • Rk3568DaiDeviceInit 原始框架,主要完成DAI_config.hcs参数列表的读取,与HwParams结合,完成参数的设置。
  • Rk3568DaiHwParams 主要完成I2S MCLK/BCLK/LRCLK时钟配置。

1.根据不同采样率计算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;
    }

2.根据获取的mclk,计算BCLK/LRclk分频系数

  • Rk3568NormalTrigger 根据输入输出类型,以及cmd(启动/停止/暂停/恢复),完成一系列配置:

1.mclk的启停
2.DMA搬运的启停
3.传输的启停 详细实现见代码,参考Linux原生I2s驱动对应接口函数

    // 启动/恢复流程
    if (streamType == AUDIO_RENDER_STREAM) {
        clk_prepare_enable(i2s_tdm->mclk_tx);
        regmap_update_bits(i2s_tdm->regmap, I2S_DMACR,
                   I2S_DMACR_TDE_ENABLE,
                   I2S_DMACR_TDE_ENABLE);
    } else {
        clk_prepare_enable(i2s_tdm->mclk_rx);
        regmap_update_bits(i2s_tdm->regmap, I2S_DMACR,
                   I2S_DMACR_RDE_ENABLE,
                   I2S_DMACR_RDE_ENABLE);
        if (regmap_read(i2s_tdm->regmap, I2S_DMACR, &val)) {
            AUDIO_DEVICE_LOG_ERR("read register fail: [%04x]", I2S_DMACR);
            return ;
            }
        AUDIO_DEVICE_LOG_ERR("i2s reg: 0x%x = 0x%x ", I2S_DMACR, val);
    }

    if (atomic_inc_return(&i2s_tdm->refcount) == 1) {
        regmap_update_bits(i2s_tdm->regmap, I2S_XFER,
                   I2S_XFER_TXS_START |
                   I2S_XFER_RXS_START,
                   I2S_XFER_TXS_START |
                   I2S_XFER_RXS_START);
        if (regmap_read(i2s_tdm->regmap, I2S_XFER, &val)) {
            AUDIO_DEVICE_LOG_ERR("read register fail: [%04x]", I2S_XFER);
            return ;
            }
        AUDIO_DEVICE_LOG_ERR("i2s reg: 0x%x = 0x%x ", I2S_XFER, val);
    }

Platform(DMA)模块
ops函数相关函数

1.Rk3568DmaBufAlloc/Rk3568DmaBufFree

获取DMA设备节点,参考I2s设备获取方式,使用系统函数dma_alloc_wc/dma_free_wc,完成DMA虚拟内存与物理内存的申请/释放

2.Rk3568DmaRequestChannel

使用Linux DMA原生接口函数获取DMA传输通道,dma_request_slave_channel

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;
   }
  1. Rk3568DmaPause

使用Linux DMA原生接口函数dmaengine_terminate_async,停止DMA传输

     dmaengine_terminate_async(dmaChan);
  1. 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文件

Camera

基本概念

OpenHarmony相机驱动框架模型对上实现相机HDI接口,对下实现相机Pipeline模型,管理相机各个硬件设备。各层的基本概念如下。

  1. HDI实现层:对上实现OHOS相机标准南向接口。

  2. 框架层:对接HDI实现层的控制、流的转发,实现数据通路的搭建、管理相机各个硬件设备等功能。

  3. 适配层:屏蔽底层芯片和OS差异,支持多平台适配。

Camera驱动框架介绍

源码框架介绍

Camera 驱动框架所在的仓为:drivers_peripheral,源码目录为:“drivers/peripheral/camera”。

|-- README_zh.md
|-- figures
|  -- logic-view-of-modules-related-to-this-repository_zh.png
|-- hal
|  |-- BUILD.gn               #Camera驱动框架构建入口
|  |-- adapter                 #平台适配层,适配平台
|  |-- buffer_manager
|  |-- camera.gni               #定义组件所使用的全局变量
|  |-- device_manager
|  |-- hdi_impl
|  |-- include
|  |-- init                   #demo sample
|  |-- pipeline_core
|  |-- test                   #测试代码
|  |-- utils
|-- hal_c                    #为海思平台提供专用C接口
|  |-- BUILD.gn
|  |-- camera.gni
|  |-- hdi_cif
|  |-- include
|-- interfaces                  #HDI接口
  |-- hdi_ipc
|-- hdi_passthrough
   |-- include

Camera hcs文件是每个chipset可配置的。所以放在chipset相关的仓下。以rk3568为例。仓名为: vendor_hihope,源码目录为:“vendor/hihope/rk3568/hdf_config/uhdf/camera”。

├── hdi_impl
│   └── camera_host_config.hcs
└── pipeline_core
    ├── config.hcs
    ├── ipp_algo_config.hcs
    └── params.hcs

Camera chipset 相关代码路径以3568为例仓名为:device_hihope。路径为:device/board/hihope/rk3568/camera/

├── BUILD.gn
├── demo
│   └── include
│       └── project_camera_demo.h
├── device_manager
│   ├── BUILD.gn
│   ├── include
│   │   ├── imx600.h
│   │   ├── project_hardware.h
│   │   └── rkispv5.h
│   └── src
│       ├── imx600.cpp
│       └── rkispv5.cpp
├── driver_adapter
│   └── test
│       ├── BUILD.gn
│       ├── unittest
│       │   ├── include
│       │   │   └── utest_v4l2_dev.h
│       │   └── src
│       │       └── utest_v4l2_dev.cpp
│       └── v4l2_test
│           └── include
│               └── project_v4l2_main.h
└── pipeline_core
    ├── BUILD.gn
    └── src
        ├── ipp_algo_example
        │   └── ipp_algo_example.c
        └── node
            ├── rk_codec_node.cpp
            └── rk_codec_node.h     

Camera 驱动框架配置

RK3568 配置文件路径:

“vendor/hihope/rk3568/hdf_config/uhdf/device_info.hcs”。说明:其他平台可参考RK3568适配。

        hdi_server :: host {
            hostName = "camera_host";
            priority = 50;
            caps = ["DAC_OVERRIDE", "DAC_READ_SEARCH"];
            camera_device :: device {
                 device0 :: deviceNode {
                     policy = 2;
                     priority = 100;
                     moduleName = "libcamera_hdi_impl.z.so";
                     serviceName = "camera_service";
                 }
             }
            ...
        }

参数说明: Host:一个host节点即为一个独立进程,如果需要独立进程,新增属于自己的host节点。 Policy: 服务发布策略,HDI服务请设置为“2” moduleName: 驱动实现库名。 serviceName:服务名称,请保持全局唯一性。

Camera_host驱动实现入口

文件路径:drivers/peripheral/camera/interfaces/hdi_ipc/server/src/camera_host_driver.cpp

分发设备服务消息 cmd Id:请求消息命令字。 Data:其他服务或者IO请求数据。 Reply:存储返回消息内容数据。

static int32_t CameraServiceDispatch(struct HdfDeviceIoClient *client, int cmdId,
    struct HdfSBuf *data, struct HdfSBuf *reply)
{
    HdfCameraService *hdfCameraService = CONTAINER_OF(client->device->service, HdfCameraService, ioservice);
      return CameraHostServiceOnRemoteRequest(hdfCameraService->instance, cmdId, data, reply);
 }

绑定设备服务:初始化设备服务对象和资源对象。

int HdfCameraHostDriverBind(HdfDeviceObject *deviceObject)
{
    HDF_LOGI("HdfCameraHostDriverBind enter!");
    if (deviceObject == nullptr) {
        HDF_LOGE("HdfCameraHostDriverBind: HdfDeviceObject is NULL !");
        return HDF_FAILURE;
}

驱动初始化函数: 探测并初始化驱动程序

int HdfCameraHostDriverInit(struct HdfDeviceObject *deviceObject)
{
      return HDF_SUCCESS;
}

驱动资源释放函数 : 如已经绑定的设备服务对象

  void HdfCameraHostDriverRelease(HdfDeviceObject *deviceObject)
  {
          if (deviceObject == nullptr || deviceObject->service == nullptr) {
          HDF_LOGE("%{public}s deviceObject or deviceObject->service  is NULL!", __FUNCTION__);
                return;
      }
          HdfCameraService *hdfCameraService = CONTAINER_OF(deviceObject->service, HdfCameraService, ioservice);
      if (hdfCameraService == nullptr) {
           HDF_LOGE("%{public}s hdfCameraService is NULL!", __FUNCTION__);
           return;
       }

定义驱动描述符:将驱动代码注册给驱动框架。

 struct HdfDriverEntry g_cameraHostDriverEntry = {
      .moduleVersion = 1,
      .moduleName = "camera_service",
      .Bind = HdfCameraHostDriverBind,
      .Init = HdfCameraHostDriverInit,
      .Release = HdfCameraHostDriverRelease,
  };   

Camera配置信息介绍

Camera模块内部,所有配置文件使用系统支持的HCS类型的配置文件,HCS类型的配置文件,在编译时,会转成HCB文件,最终烧录到开发板里的配置文件即为HCB格式,代码中通过HCS解析接口解析HCB文件,获取配置文件中的信息。

 hc_gen("build_camera_host_config") {
    sources = [ rebase_path(
                  "$camera_product_name_path/hdf_config/uhdf/camera/hdi_impl/camera_host_config.hcs") ]
  }
        
  ohos_prebuilt_etc("camera_host_config.hcb") {
          deps = [ ":build_camera_host_config" ]
    hcs_outputs = get_target_outputs(":build_camera_host_config")
          source = hcs_outputs[0]
    relative_install_dir = "hdfconfig"
          install_images = [ chipset_base_dir ]
    subsystem_name = "hdf"
          part_name = "camera_device_driver"
  }

Camera适配介绍

新产品平台适配简介

drivers/peripheral/camera/hal/camera.gni 文件中可根据编译时传入的product_company product_name和device_name调用不同chipset的product.gni

  if (defined(ohos_lite)) {
          import("//build/lite/config/component/lite_component.gni")
    import(
              "//device/soc/hisilicon/common/hal/media/camera/hi3516dv300/linux_standard/camera/product.gni")
  } else {
          import("//build/ohos.gni")
    if ("${product_name}" == "ohos-arm64") {
            import(
          "//drivers/peripheral/camera/hal/adapter/chipset/rpi/rpi3/device/camera/product.gni")
          } else if ("${product_name}" == "Hi3516DV300") {
      import(
                "//device/soc/hisilicon/common/hal/media/camera/hi3516dv300/linux_standard/camera/product.gni")
    } else if ("${product_name}" == "watchos") {
            import(
          "//device/soc/hisilicon/common/hal/media/camera/hi3516dv300/linux_standard/camera/product.gni")
          } else {
      import(
                "//device/board/${product_company}/${device_name}/camera/product.gni")
    }
        }

在如下路径的product.gni指定了编译不同chipset相关的代码的路径:

 device/${product_company}/${device_name}/camera/

如下是rk3568的product.gni:

  camera_device_name_path = "//device/board/${product_company}/${device_name}"
        is_support_v4l2 = true
  if (is_support_v4l2) {
          is_support_mpi = false
    defines += [ "SUPPORT_V4L2" ]
          chipset_build_deps = "$camera_device_name_path/camera/:chipset_build"
    camera_device_manager_deps =
              "$camera_device_name_path/camera/src/device_manager:camera_device_manager"
    camera_pipeline_core_deps =
              "$camera_device_name_path/camera/src/pipeline_core:camera_pipeline_core"
  }

product.gni中指定了chipset_build_deps camera_device_manager_deps 和 camera_pipeline_core_deps 三个代码编译路径。该路径在drivers/peripheral/camera/hal/BUILD.gn中会被使用

框架适配介绍

以V4l2为例,pipeline的连接方式是在HCS配置文件中配置连接,数据源我们称之为SourceNode,主要包括硬件设备的控制、数据流的轮转等。 ISPNode可根据需要确定是否添加此Node,因为在很多操作上其都可以和SensorNode统一为SourceNode。SinkNode为pipeline中数据传输的重点,到此处会将数据传输回buffer queue中。

pipeline中的Node是硬件/软件模块的抽象,所以对于其中硬件模块Node,其是需要向下控制硬件模块的,在控制硬件模块前,需要先获取其对应硬件模块的deviceManager,通过deviceManager向下传输控制命令/数据buffer,所以deviceManager中有一个v4l2 device manager抽象模块,用来创建各个硬件设备的manager、controller.如上sensorManager、IspManager,sensorController等,所以v4l2 device manager其实是各个硬件设备总的一个管理者。

deviceManager中的controller和驱动适配层直接交互。

基于以上所描述,如需适配一款以linux v4l2框架的芯片平台,只需要修改适配如上图中颜色标记模块及HCS配置文件(如为标准v4l2框架,基本可以延用当前已适配代码),接下来单独介绍修改模块。

主要适配添加如下目录:

“vendor/hihope/rk3568/hdf_config/uhdf/camera/”:当前芯片产品的HCS配置文件目录。

“device/hihope/rk3568/camera/”:当前芯片产品的代码适配目录。

“drivers/peripheral/camera/hal/adapter/platform/v4l2”:平台通用公共代码。

HCS配置文件适配介绍
  ├── hdi_impl
  │   └── camera_host_config.hcs
  └── pipeline_core
      ├── config.hcs
      ├── ipp_algo_config.hcs
      └── params.hcs

以RK3568开发板为例,其hcs文件应该放在对应的路径中。

 vendor/${product_company}/${product_name}/ hdf_config/uhdf/camera/  

template ability {
  logicCameraId = "lcam001";
  physicsCameraIds = [
  "CAMERA_FIRST",
  "CAMERA_SECOND"
  ];
metadata {
   aeAvailableAntiBandingModes = [
       "OHOS_CONTROL_AE_ANTIBANDING_MODE_OFF",
       "OHOS_CONTROL_AE_ANTIBANDING_MODE_50HZ",
       "OHOS_CONTROL_AE_ANTIBANDING_MODE_60HZ",
       "OHOS_CONTROL_AE_ANTIBANDING_MODE_AUTO"
        ];

hdi_impl下的“camera_host_config.hcs”为物理/逻辑Camera配置、能力配置,此处的物理/逻辑Camera配置,需要在hal内部使用,逻辑Camera及能力配置需要上报给上层,请按照所适配的芯片产品添加其能力配置。其中所用的能力值为键值对,定义在//drivers/peripheral/camera/hal/hdi_impl/include/camera_host/metadata_enum_map.h中。

      normal_preview :: pipeline_spec {
      name = "normal_preview";
            v4l2_source :: node_spec {
          name = "v4l2_source#0";
                status = "new";
          out_port_0 :: port_spec {
                    name = "out0";
              peer_port_name = "in0";
                    peer_port_node_name = "sink#0";
              direction = 1;
                    width = 0;
              height = 0;
                    format = 0;
          }
            }
      sink :: node_spec {
                name = "sink#0";
          status = "new";
                stream_type = "preview";
          in_port_0 :: port_spec {
                    name = "in0";
              peer_port_name = "out0";
                    peer_port_node_name = "v4l2_source#0";
              direction = 0;
                }
      }
    }

pipeline_core下的“config.hcs”为pipeline的连接方式,按场景划分每一路流由哪些Node组成,其连接方式是怎样的。

上面为preview场景的示例,normal_preview为该场景的名称,source和sink为Node,source为数据数据源端,sink为末端,source为第一个node,node的名称是source#0,status、in/out_port分别为Node状态及输入/输出口的配置。

以in_port_0为例,name = “in0”代表它的输入为“port0”,它的对端为source node的port口out0口,direction为它的源Node和对端Node是否为直连方式。如新添加芯片产品,必须按实际连接方式配置此文件。

新增功能node时需继承NodeBase类,且在cpp文件中注册该node。具体可参考//drivers/peripheral/camera/hal/pipeline_core/nodes/src下已经实现的node。

 root {
  module = "";
        template stream_info {
      id = 0;
            name = "";
  }
        template scene_info {
      id = 0;
            name = "";
  }
        preview :: stream_info {
      id = 0;
            name = "preview";
  }
        video :: stream_info {
      id = 1;
            name = "video";
  }

param.hcs为场景、流类型名及其id定义,pipeline内部是以流id区分流类型的,所以此处需要添加定义。

Chipset 和Platform适配介绍

platform为平台性公共代码,如linux标准v4l2适配接口定义,为v4l2框架适配的通用node.以及为v4l2框架适配的通用device_manager等。目录结构如下:

  drivers/peripheral/camera/hal/adapter/platform
  ├── mpp
  │   └── src
  │       ├── device_manager
  │       └── pipeline_core
  └── v4l2
      └── src
          ├── device_manager
          ├── driver_adapter
          └── pipeline_core

“platform”目录下的“v4l2”包含了“src”, “src”中“driver_adapter”为linux v4l2标准适配接口,如有定制化功能需求,可继承driver_adapter,将定制化的具体功能接口放在chipset中实现。如无芯片定制化功能,可直接使用已有的driver_adapter。

platform目录下的Nodes为依据linux v4l2标准实现的硬件模块v4l2_source_node和uvc_node(usb热插拔设备,此模块也为linux标准接口,可直接使用),如下图为v4l2_source_node的接口声明头文件。

 namespace OHOS::Camera {
  class V4L2SourceNode : public SourceNode {
        public:
      V4L2SourceNode(const std::string& name, const std::string& type);
            ~V4L2SourceNode() override;
      RetCode Init(const int32_t streamId) override;
            RetCode Start(const int32_t streamId) override;
      RetCode Flush(const int32_t streamId) override;
            RetCode Stop(const int32_t streamId) override;
      RetCode GetDeviceController();
            void SetBufferCallback() override;
      RetCode ProvideBuffers(std::shared_ptr<FrameSpec> frameSpec) override;
        
  private:
            std::mutex                              requestLock_;
      std::map<int32_t, std::list<int32_t>>   captureRequests_ = {};
            std::shared_ptr<SensorController>       sensorController_ = nullptr;
      std::shared_ptr<IDeviceManager>     deviceManager_ = nullptr;
        };
  } // namespace OHOS::Camera

Init接口为模块初始化接口。

Start为使能接口,比如start stream功能等。

Stop为停止接口。

GetDeviceController为获取deviceManager对应的controller接口。

chipset为具体某芯片平台相关代码,例如,如和“rk3568”开发板 为例。device_manager目录下可存放该开发板适配过的sensor的相关配置文件。pipeline_core路径下可以存放由chipset开发者为满足特点需求增加的pipeline node等。

 device/board/hihope/rk3568/camera
  ├── BUILD.gn
  ├── camera_demo
  │   └── project_camera_demo.h
  ├── include
  │   └── device_manager
  ├── product.gni
  └── src
      ├── device_manager
      ├── driver_adapter
      └── pipeline_core

device/board/hihope/rk3568/camera/目录包含了“include”和“src”,“camera_demo”“src”中“device­­_manager”中包含了chipset 适配的sensor的文件,配合platform下device_manager的设备管理目录,主要对接pipeline,实现平台特有的硬件处理接口及数据buffer的下发和上报、metadata的交互。

下图为device_manager的实现框图,pipeline控制管理各个硬件模块,首先要获取对应设备的manager,通过manager获取其对应的controller,controller和对应的驱动进行交互 。

deviceManager中需要实现关键接口介绍。

      class SensorController : public IController {
      public:
          SensorController();
          explicit SensorController(std::string hardwareName);
          virtual ~SensorController();
          RetCode Init();
          RetCode PowerUp();
          RetCode PowerDown();
          RetCode Configure(std::shared_ptr<CameraStandard::CameraMetadata> meta);
          RetCode Start(int buffCont, DeviceFormat& format);
          RetCode Stop();
          RetCode SendFrameBuffer(std::shared_ptr<FrameSpec> buffer);
          void SetNodeCallBack(const NodeBufferCb cb);
          void SetMetaDataCallBack(const MetaDataCb cb);
          void BufferCallback(std::shared_ptr<FrameSpec> buffer);
          void SetAbilityMetaDataTag(std::vector<int32_t> abilityMetaDataTag);
    } 

PowerUp为上电接口,OpenCamera时调用此接口进行设备上电操作。 PowerDown为下电接口,CloseCamera时调用此接口进行设备下电操作。 Configures为Metadata下发接口,如需设置metadata参数到硬件设备,可实现此接口进行解析及下发。 Start为硬件模块使能接口,pipeline中的各个node进行使能的时候,会去调用,可根据需要定义实现,比如sensor的起流操作就可放在此处进行实现。 Stop和Start为相反操作,可实现停流操作。 SendFrameBuffer为每一帧buffer下发接口,所有和驱动进行buffer交互的操作,都是通过此接口进行的。 SetNodeCallBack为pipeline,通过此接口将buffer回调函数设置到devicemanager。 SetMetaDataCallBack为metadata回调接口,通过此接口将从底层获取的metadata数据上报给上层。 BufferCallback上传每一帧已填充数据buffer的接口,通过此接口将buffer上报给pipeline。 SetAbilityMetaDataTag设置需要从底层获取哪些类型的metadata数据,因为框架支持单独获取某一类型或多类型的硬件设备信息,所以可以通过此接口,获取想要的metadata数据。

其余接口可参考“drivers/peripheral/camera/hal/adapter/platform/v4l2/src/device_manager/”

IPP适配介绍

IPP是pipeline 中的一个算法插件模块,由ippnode加载,对流数据进行算法处理,ippnode支持同时多路数据输入,只支持一路数据输出。ippnode加载算法插件通过如下hcs文件指定: vendor/ p r o d u c t c o m p a n y / {product_company}/ productcompany/{product_name}/hdf_config/uhdf/camera/pipeline_core/ipp_algo_config.hcs 其中:

  root {
     module="sample";
     ipp_algo_config {
     algo1 {
          name = "example";
          description = "example algorithm";
          path = "libcamera_ipp_algo_example.z.so";
          mode = "IPP_ALGO_MODE_NORMAL";
     }
     }
  }

name:算法插件名称 description:描述算法插件的功能 path:算法插件所在路径 mode:算法插件所运行的模式

算法插件可运行的模式由 drivers/peripheral/camera/hal/pipeline_core/ipp/include/ipp_algo.h中的IppAlgoMode提供,可以根据需要进行扩展。

  enum IppAlgoMode {
      IPP_ALGO_MODE_BEGIN,
      IPP_ALGO_MODE_NORMAL = IPP_ALGO_MODE_BEGIN,
      IPP_ALGO_MODE_BEAUTY,
      IPP_ALGO_MODE_HDR,
      IPP_ALGO_MODE_END
  };

算法插件由gn文件 device/ p r o d u c t c o m p a n y / {product_company}/ productcompany/{device_name}/camera/BUILD.gn进行编译,算法插件需实现如下接口(接口由ipp_algo.h指定)供ippnode调用:

  typedef struct IppAlgoFunc {
      int (*Init)(IppAlgoMeta* meta);
      int (*Start)();
      int (*Flush)();
      int (*Process)(IppAlgoBuffer* inBuffer[], int inBufferCount, IppAlgoBuffer* outBuffer, IppAlgoMeta* meta);
      int (*Stop)();
  } IppAlgoFunc;

1) Init : 算法插件初始化接口,在起流前被ippnode 调用,其中IppAlgoMeta 定义在ipp_algo.h 中,为ippnode和算法插件提供非图像数据的传递通道,如当前运行的场景,算法处理后输出的人脸坐标等等,可根据实际需求进行扩展。
2) Start:开始接口,起流时被ippnode 调用
3) Flush:刷新数据的接口,停流之前被ippnode 调用。此接口被调用时,算法插件需尽可能快地停止处理。
4) Process: 数据处理接口,每帧数据都通过此接口输入至算法插件进行处理。inBuffer是一组输入buffer,inBufferCount是输入buffer 的个数,outBuffer是输出buffer,meta是算法处理时产生的非图像数据,IppAlgoBuffer在ipp_algo.h中定义
5) Stop:停止处理接口,停流时被ippnode调用

typedef struct IppAlgoBuffer {
      void* addr;
      unsigned int width;
      unsigned int height;
      unsigned int stride;
      unsigned int size;
      int id;
  } IppAlgoBuffer;

其中上边代码中的id指的是和ippnode对应的port口id,比如inBuffer[0]的id为0,则对应的是ippnode 的第0个输入port口。需要注意的是outBuffer可以为空,此时其中一个输入buffer 被ippnode作为输出buffer传递到下个node,inBuffer至少有一个buffer不为空。输入输出buffer 由pipeline配置决定。 比如在普通预览场景无算法处理且只有一路拍照数据传递到ippnode的情况下,输入buffer只有一个,输出buffer为空,即对于算法插件输入buffer 进行了透传; 比如算法插件进行两路预览图像数据进行合并的场景,第一路buffer需要预览送显示。把第二路图像拷贝到第一路的buffer即可,此时输入buffer有两个,输出buffer为空; 比如在算法插件中进行预览数据格式转换的场景,yuv转换为RGBA,那么只有一个yuv格式的输入buffer的情况下无法完成RGBA格式buffer的输出,此时需要一个新的buffer,那么ippnode的输出port口buffer作为outBuffer传递到算法插件。也即输入buffer只有一个,输出buffer也有一个。

ippnode的port口配置请查看3.3小节的config.hcs的说明。

适配V4L2驱动实例

本章节目的是在v4l2框架下适配RK3568开发板。

区分V4L2 platform相关代码并将其放置“drivers/peripheral/camera/hal/adapter/platform/v4l2”目录下,该目录中包含了“device_manager”“driver_adapter”和“pipeline_core”三个目录。其中“driver_adapter”目录中存放着v4l2协议相关代码。可通过它们实现与v4l2底层驱动交互。该目录下“Pipeline_core”目录与“drivers/peripheral/camera/hal/pipeline_core”中代码组合为pipeline框架。v4l2_source_node 和 uvc_node为v4l2专用Node。device_manager目录存放着向北与pipeline向南与v4l2 adapter交互的代码

  drivers/peripheral/camera/hal/adapter/platform/v4l2/src/
  ├── device_manager
  │   ├── enumerator_manager.cpp
  │   ├── flash_controller.cpp
  │   ├── flash_manager.cpp
  │   ├── idevice_manager.cpp
  │   ├── include
  │   ├── isp_controller.cpp
  │   ├── isp_manager.cpp
  │   ├── sensor_controller.cpp
  │   ├── sensor_manager.cpp
  │   └── v4l2_device_manager.cpp
  ├── driver_adapter
  │   ├── BUILD.gn
  │   ├── include
  │   ├── main_test
  │   └── src
  └── pipeline_core
      └── nodes

区分V4L2 chipset相关代码并将其放置在“device/ p r o d u c t c o m p a n y / {product_company}/ productcompany/{device_name} /camera”目录下。

  ├── BUILD.gn
  ├── camera_demo
  │   └── project_camera_demo.h
  ├── include
  │   └── device_manager
  ├── product.gni
  └── src
      ├── device_manager
      ├── driver_adapter
      └── pipeline_core

其中“driver_adapter”目录中包含了关于RK3568 driver adapter的测试用例头文件。Camera_demo目录存放了camera hal 中demo测试用例的chipset相关的头文件。device_manager存放了RK3568适配的camera sensor 读取设备能力的代码 其中,project_hardware.h 比较关键,存放了device_manager支持当前chipset的设备列表。如下:

 namespace OHOS::Camera {
    std::vector<HardwareConfiguration> hardware = {
        {CAMERA_FIRST, DM_M_SENSOR, DM_C_SENSOR, (std::string) "rkisp_v5"},
        {CAMERA_FIRST, DM_M_ISP, DM_C_ISP, (std::string) "isp"},
        {CAMERA_FIRST, DM_M_FLASH, DM_C_FLASH, (std::string) "flash"},
        {CAMERA_SECOND, DM_M_SENSOR, DM_C_SENSOR, (std::string) "Imx600"},
        {CAMERA_SECOND, DM_M_ISP, DM_C_ISP, (std::string) "isp"},
        {CAMERA_SECOND, DM_M_FLASH, DM_C_FLASH, (std::string) "flash"}
   };
  } // namespace OHOS::Camera

修改编译选项来达到根据不同的编译chipset来区分v4l2和其他框架代码编译。增加device/ p r o d u c t c o m p a n y / {product_company}/ productcompany/{device_name}/camera/product.gni

  camera_product_name_path = "//vendor/${product_company}/${product_name}"
  camera_device_name_path = "//device/board/${product_company}/${device_name}"
  is_support_v4l2 = true
  if (is_support_v4l2) {
      is_support_mpi = false
      defines += [ "SUPPORT_V4L2" ]
      chipset_build_deps = "$camera_device_name_path/camera/:chipset_build"
      camera_device_manager_deps =
          "$camera_device_name_path/camera/src/device_manager:camera_device_manager"
      camera_pipeline_core_deps =
          "$camera_device_name_path/camera/src/pipeline_core:camera_pipeline_core"
  }

当“product.gni”被// drivers/peripheral/camera/hal/camera.gni加载,就说明要编译v4l2相关代码。在//drivers/peripheral/camera/hal/camera.gni中根据编译时传入的product_name和device_name名来加载相应的gni文件。

  import("//build/ohos.gni")
  if ("${product_name}" == "ohos-arm64") {
    import(
        "//drivers/peripheral/camera/hal/adapter/chipset/rpi/rpi3/device/camera/product.gni")
  } else if ("${product_name}" == "Hi3516DV300") {
    import(
        "//device/soc/hisilicon/common/hal/media/camera/hi3516dv300/linux_standard/camera/product.gni")

“drivers/peripheral/camera/hal/BUILD.gn”中会根据 chipset_build_deps camera_device_manager_deps 和 camera_pipeline_core_deps来编译不同的chipset

     print("product_name : , ${product_name}")
      group("camera_hal") {
        if (is_standard_system) {
          deps = [
            "$camera_path/../interfaces/hdi_ipc/client:libcamera_client",
            "buffer_manager:camera_buffer_manager",
            "device_manager:camera_device_manager",
            "hdi_impl:camera_hdi_impl",
            "init:ohos_camera_demo",
            "pipeline_core:camera_pipeline_core",
            "utils:camera_utils",
          ]
          deps += [ "${chipset_build_deps}" ]
        }

Camera hal层向下屏蔽了平台及芯片差异,对外(Camera service或者测试程序)提供统一接口,其接口定义在“drivers/peripheral/camera/interfaces/include”目录下:

        ├── icamera_device_callback.h
        ├── icamera_device.h
        ├── icamera_host_callback.h
        ├── icamera_host.h
        ├── ioffline_stream_operator.h
        ├── istream_operator_callback.h
        ├── istream_operator.h

测试时,只需要针对所提供的对外接口进行测试,即可完整测试Camera hal层代码,具体接口说明,可参考“drivers/peripheral/camera/interfaces”目录下的“README_zh.md”和头文件接口定义。具体的调用流程,可参考测试demo:drivers/peripheral/camera/hal/init。

camera适配过程中问题以及解决方案

修改SUBWINDOW_TYPE和送显format

修改RGBA888送显,模式由video 改为 SUBWINDOW_TYPE为normal模式:

由于openharmony 较早实现的是3516平台camera, 该平台采用PIXEL_FMT_YCRCB_420_SP格式送显,而RK3568需将预览流由yuv420转换为PIXEL_FMT_RGBA_8888送上屏幕才可被正确的显示。具体需修改foundation/ace/ace_engine/frameworks/core/components/camera/standard_system/camera.cpp 文件中如下内容,该文件被编译在libace.z.so中

  #ifdef PRODUCT_RK
      previewSurface_->SetUserData(SURFACE_FORMAT, std::to_string(PIXEL_FMT_RGBA_8888));
      previewSurface_->SetUserData(CameraStandard::CameraManager::surfaceFormat,
                                   std::to_string(OHOS_CAMERA_FORMAT_RGBA_8888));
  #else
      previewSurface_->SetUserData(SURFACE_FORMAT, std::to_string(PIXEL_FMT_YCRCB_420_SP));
      previewSurface_->SetUserData(CameraStandard::CameraManager::surfaceFormat,
                                   std::to_string(OHOS_CAMERA_FORMAT_YCRCB_420_SP));
  #endif

foundation/multimedia/camera_standard/services/camera_service/src/hstream_repeat.cpp 文件中如下内容,该文件被编译在libcamera_service.z.so中

void HStreamRepeat::SetStreamInfo(std::shared_ptr<Camera::StreamInfo> streamInfo)
    {
        int32_t pixelFormat;
        auto it = g_cameraToPixelFormat.find(format_);
        if (it != g_cameraToPixelFormat.end()) {
            pixelFormat = it->second;
        } else {
    #ifdef RK_CAMERA
            pixelFormat = PIXEL_FMT_RGBA_8888;
    #else
            pixelFormat = PIXEL_FMT_YCRCB_420_SP;
    #endif

如上3516平台是使用VO通过VO模块驱动直接送显,所以在ace中配置的subwindows模式为SUBWINDOW_TYPE_VIDEO. 需在foundation/ace/ace_engine/frameworks/core/components/camera/standard_system/camera.cpp文件中做如下修改,该文件被编译在libace.z.so中

  #ifdef PRODUCT_RK
      option->SetWindowType(SUBWINDOW_TYPE_NORMAL);
  #else
      option->SetWindowType(SUBWINDOW_TYPE_VIDEO);
  #endif

增加rk_codec_node

在该node中完成rgb转换,jpeg和h264压缩编解码前文讲过camera hal的pipeline模型的每一个node都是camera数据轮转过程中的一个节点,由于当前camera hal v4l2 adapter只支持一路流进行数据轮转,那么拍照和录像流就必须从单一的预览流中拷贝。现阶段openharmony也没有专门的服务端去做codec和rgb转换jpeg压缩的工作。那么只能在camera hal中开辟一个专有node去做这些事情,也就是rk_codec_node。 Hcs中增加rk_codec_node连接模型: 修改vendor/hihope/rk3568/hdf_config/uhdf/camera/pipeline_core/config.hcs文件

          normal_preview_snapshot :: pipeline_spec {
                name = "normal_preview_snapshot";
                v4l2_source :: node_spec {
                    name = "v4l2_source#0";
                    status = "new";
                    out_port_0 :: port_spec {
                        name = "out0";
                        peer_port_name = "in0";
                        peer_port_node_name = "fork#0";
                        direction = 1;
                    }
                }
                fork :: node_spec {
                    name = "fork#0";
                    status = "new";
                    in_port_0 :: port_spec {
                        name = "in0";
                        peer_port_name = "out0";
                        peer_port_node_name = "v4l2_source#0";
                        direction = 0;
                    }
                    out_port_0 :: port_spec {
                        name = "out0";
                        peer_port_name = "in0";
                        peer_port_node_name = "RKCodec#0";
                        direction = 1;
                    }
                    out_port_1 :: port_spec {
                        name = "out1";
                        peer_port_name = "in0";
                        peer_port_node_name = "RKCodec#1";
                        direction = 1;
                    }
                }
                RKCodec_1 :: node_spec {
                    name = "RKCodec#0";
                    status = "new";
                    in_port_0 :: port_spec {
                        name = "in0";
                        peer_port_name = "out0";
                        peer_port_node_name = "fork#0";
                        direction = 0;
                    }
                    out_port_0 :: port_spec {
                        name = "out0";
                        peer_port_name = "in0";
                        peer_port_node_name = "sink#0";
                        direction = 1;
                    }
                }
                RKCodec_2 :: node_spec {
                    name = "RKCodec#1";

以预览加拍照双路流为列,v4l2_source_node为数据源,流向了fork_node,rork_node将预览数据直接送给RKCodec node, 将拍照数据流拷贝一份也送给RKCodec node进行转换。转换完成的数据将送给sink node后交至buffer的消费端。

device/board/hihope/rk3568/camera/src/pipeline_core/BUILD.gn中添加rk_codec_node.cpp和相关依赖库的编译。其中librga为yuv到rgb格式转换库,libmpp为yuv到H264编解码库,libjpeg为yuv到jpeg照片的压缩库。

    ohos_shared_library("camera_pipeline_core") {
        sources = [
          "$camera_device_name_path/camera/src/pipeline_core/node/rk_codec_node.cpp",
          "$camera_path/adapter/platform/v4l2/src/pipeline_core/nodes/uvc_node/uvc_node.cpp",
                  "$camera_path/adapter/platform/v4l2/src/pipeline_core/nodes/v4l2_source_node/v4l2_source_node.cpp",
           deps = [
            "$camera_path/buffer_manager:camera_buffer_manager",
            "$camera_path/device_manager:camera_device_manager",
            "//device/soc/rockchip/hardware/mpp:libmpp",
            "//device/soc/rockchip/hardware/rga:librga",
            "//foundation/multimedia/camera_standard/frameworks/native/metadata:metadata",
            "//third_party/libjpeg:libjpeg_static",

openharmony/device/board/hihope/rk3568/camera/src/pipeline_core/node/rk_codec_node.cpp主要接口:

   void RKCodecNode::DeliverBuffer(std::shared_ptr<IBuffer>& buffer)
    {
        if (buffer == nullptr) {
            CAMERA_LOGE("RKCodecNode::DeliverBuffer frameSpec is null");
            return;
        }
    
        int32_t id = buffer->GetStreamId();
        CAMERA_LOGE("RKCodecNode::DeliverBuffer StreamId %{public}d", id);
        if (buffer->GetEncodeType() == ENCODE_TYPE_JPEG) {
            Yuv420ToJpeg(buffer);
        } else if (buffer->GetEncodeType() == ENCODE_TYPE_H264) {
            Yuv420ToH264(buffer);
        } else {
            Yuv420ToRGBA8888(buffer);
        }

由fork_node出来的数据流将会被deliver到rk_codec_node的DeliverBuffer接口中,该接口会根据不同的EncodeType去做不同的转换处理。经过转换过的buffers再deliver到下一级node中处理。直到deliver到buffer消费者手中。

H264帧时间戳和音频时间戳不同步问题。

问题点:Ace在CreateRecorder时会同时获取音频和视频数据并将他们合成为.mp4文件。但在实际合成过程当中需要检查音视频信息中的时间戳是否一致,如不一致将会Recorder失败。表现出的现象是camera app点击录像按钮后无法正常停止,强行停止后发现mp4文件为空。

解决方法:首先需找到audio模块对于音频时间戳的获取方式。

   int32_t AudioCaptureAsImpl::GetSegmentInfo(uint64_t &start)
    {
        CHECK_AND_RETURN_RET(audioCapturer_ != nullptr, MSERR_INVALID_OPERATION);
        AudioStandard::Timestamp timeStamp;
        auto timestampBase = AudioStandard::Timestamp::Timestampbase::MONOTONIC;
        CHECK_AND_RETURN_RET(audioCapturer_->GetAudioTime(timeStamp, timestampBase), MSERR_UNKNOWN);
        CHECK_AND_RETURN_RET(timeStamp.time.tv_nsec >= 0 && timeStamp.time.tv_sec >= 0, MSERR_UNKNOWN);
        if (((UINT64_MAX - timeStamp.time.tv_nsec) / SEC_TO_NANOSECOND) <= static_cast<uint64_t>(timeStamp.time.tv_sec)) {
            MEDIA_LOGW("audio frame pts too long, this shouldn't happen");
        }
        start = timeStamp.time.tv_nsec + timeStamp.time.tv_sec * SEC_TO_NANOSECOND;
        MEDIA_LOGI("timestamp from audioCapturer: %{public}" PRIu64 "", start);
        return MSERR_OK;
    }

可以看到,audio_capture_as_impl.cpp 文件中。audio模块用的是CLOCK_MONOTONIC,即系统启动时开始计时的相对时间。而camera 模块使用的是CLOCK_REALTIME,即系统实时时间。

            mppStatus_ = 1;
            buf_size = ((MpiEncTestData *)halCtx_)->frame_size;
    
            ret = hal_mpp_encode(halCtx_, dma_fd, (unsigned char *)buffer->GetVirAddress(), &buf_size);
            SearchIFps((unsigned char *)buffer->GetVirAddress(), buf_size, buffer);
    
            buffer->SetEsFrameSize(buf_size);
            clock_gettime(CLOCK_MONOTONIC, &ts);
            timestamp = ts.tv_nsec + ts.tv_sec * TIME_CONVERSION_NS_S;
            buffer->SetEsTimestamp(timestamp);
            CAMERA_LOGI("RKCodecNode::Yuv420ToH264 video capture on\n");

解决方法:修改camera hal中rk_codec_node.cpp中的获取时间类型为CLOCK_MONOTONIC即可解决问题。

time_t改为64位以后匹配4.19 kernel问题。

背景介绍:RK3568在遇到这个问题时的环境是上层运行的32位系统,底层是linux4.19 64位kernel。在32位系统环境下time_t这个typedef是long类型的,也就是32位。但在下面这个提交中将time_t 改成_Int64位。这样就会导致camera v4l2在ioctl时发生错误。

  TYPEDEF _Int64 time_t;
  TYPEDEF _Int64 suseconds_t;    

具体错误以及临时修改方案:

1,发生错误时在hilog中搜索camera_host 会发现在V4L2AllocBuffer接口中下发VIDIOC_QUERYBUF的CMD时上报了一个Not a tty的错误。如下:

V4L2AllocBuffer error:ioctl VIDIOC_QUERYBUF failed: Not a tty

RetCode HosV4L2Buffers::V4L2AllocBuffer(int fd, const std::shared_ptr<FrameSpec>& frameSpec)
{
    struct v4l2_buffer buf = {};
    struct v4l2_plane planes[1] = {};
    CAMERA_LOGD("V4L2AllocBuffer\n");

    if (frameSpec == nullptr) {
        CAMERA_LOGE("V4L2AllocBuffer frameSpec is NULL\n");
        return RC_ERROR;
    }

    switch (memoryType_) {
        case V4L2_MEMORY_MMAP:
            // to do something
            break;
        case V4L2_MEMORY_USERPTR:
            buf.type = bufferType_;
            buf.memory = memoryType_;
            buf.index = (uint32_t)frameSpec->buffer_->GetIndex();

            if (bufferType_ == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) {
                buf.m.planes = planes;
                buf.length = 1;
            }
            CAMERA_LOGD("V4L2_MEMORY_USERPTR Print the cnt: %{public}d\n", buf.index);

            if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
                CAMERA_LOGE("error: ioctl VIDIOC_QUERYBUF failed: %{public}s\n", strerror(errno));
                return RC_ERROR;

2,我们知道,一般ioctl系统调用的CMD都是以第三个参数的sizeof为CMD值主要组成传递进内核去寻找内核中相对应的switch case. 如下图,v4l2_buffer为VIDIOC_QUERYBUF宏的值得主要组成部分,那么v4l2_buffer的size发生变化,VIDIOC_QUERYBUF的值也会发生变化。

  #define VIDIOC_S_FMT        _IOWR('V',  5, struct v4l2_format)
  #define VIDIOC_REQBUFS      _IOWR('V',  8, struct v4l2_requestbuffers)
  #define VIDIOC_QUERYBUF     _IOWR('V',  9, struct v4l2_buffer)
  #define VIDIOC_G_FBUF        _IOR('V', 10, struct v4l2_framebuffer)

3,当kernel 打开CONFIG_COMPAT这个宏时,可以实现32位系统到64位kernel的兼容,对于32位系统下发的ioctl会先进入下面截图中的接口里去做cmd值由32到64位的转换。

  long v4l2_compat_ioctl32(struct file *file, unsigned int cmd, unsigned long arg)
  {
      struct video_device *vdev = video_devdata(file);
      long ret = -ENOIOCTLCMD;
  
      if (!file->f_op->unlocked_ioctl)
          return ret;
  
      if (_IOC_TYPE(cmd) == 'V' && _IOC_NR(cmd) < BASE_VIDIOC_PRIVATE)
          ret = do_video_ioctl(file, cmd, arg);
      else if (vdev->fops->compat_ioctl32)
          ret = vdev->fops->compat_ioctl32(file, cmd, arg);

4.那么在kernel中会定义一个kernel认为的VIDIOC_QUERYBUF的值。

    #define VIDIOC_S_FMT32      _IOWR('V',  5, struct v4l2_format32)
    #define VIDIOC_QUERYBUF32   _IOWR('V',  9, struct v4l2_buffer32)
    #define VIDIOC_QUERYBUF32_TIME32 _IOWR('V',  9, struct v4l2_buffer32_time32)

5,前文提到过,上层musl中time_t已经由32位被改为64位,v4l2_buffer结构体中的struct timeval中就用到了time_t。那么应用层的v4l2_buffer的大小就会跟kernel层的不一致,因为kernel的struct timeval 中编译时使用的是kernel自己在time.h中定义的 kernel_time_t。这就导致应用和驱动层对于v4l2_buffer的sizeof计算不一致从而调用到内核态后找不到cmd的错误。

   struct v4l2_buffer {
            __u32           index;
             __u32           type;
            __u32           bytesused;
            __u32           flags;
            __u32           field;
            struct timeval      timestamp;
            struct v4l2_timecode    timecode;
            __u32           sequence;

6,临时解决方案是修改videodev2.h中的struct timeval为自己临时定义的结构体, 保证上下层size一致。如下:

            struct timeval1 {
                long tv_sec;
                long tv_usec;
            }
            struct v4l2_buffer {
                __u32           index;
                __u32           type;
                __u32           bytesused;
                __u32           flags;
                __u32           field;
                struct timeval1      timestamp;
                struct v4l2_timecode    timecode;

根本解决方案:

如需要根本解决这个问题,只有两种方法。第一将系统升级为64位系统,保证用户态和内核态对于time_t变量的size保持一致。第二,升级5.10之后版本的kernel 因为5.10版本的kernel在videodev2.h文件中解决了这个情况。目前我们已在5.10的kernel上验证成功,如下图,可以看到在编译kernel时考虑到了64位time_t的问题。

struct v4l2_buffer {
            __u32           index;
            __u32           type;
            __u32           bytesused;
            __u32           flags;
            __u32           field;
        #ifdef __KERNEL__
            struct __kernel_v4l2_timeval timestamp;
        #else
            struct timeval      timestamp;
        #endif
            struct v4l2_timecode    timecode;
 }

 struct __kernel_v4l2_timeval {
      long long   ._sec;
  #if defined(__sparc__) && defined(__arch64__)
      int     tv_usec;
      int     __pad;
  #else
      long long   tv_usec;
  #endif
  };

H264 关键帧获取上报

H264除了需要上报经过编解码的数据外,还需上报关键帧信息。即这一帧是否为关键帧?mp4编码时需要用到这些信息,那么怎么分析那一帧是关键帧那?主要是分析NALU头信息。Nalu type & 0x1f就代表该帧的类型。Nalu头是以0x00000001或0x000001为起始标志的。 该图为nal_unit_type为不同数值时的帧类型。我们主要关心type为5也就是IDR帧信息。

rk_cedec_node.cpp文件里对IDR帧分析进行了代码化:

    static constexpr uint32_t nalBit = 0x1F;
    #define NAL_TYPE(value)             ((value) & nalBit)
    void RKCodecNode::SearchIFps(unsigned char* buf, size_t bufSize, std::shared_ptr<IBuffer>& buffer)
    {
        size_t nalType = 0;
        size_t idx = 0;
        size_t size = bufSize;
        constexpr uint32_t nalTypeValue = 0x05;
    
        if (buffer == nullptr || buf == nullptr) {
            CAMERA_LOGI("RKCodecNode::SearchIFps parameter == nullptr");
            return;
        }
    
        for (int i = 0; i < bufSize; i++) {
            int ret = findStartCode(buf + idx, size);
            if (ret == -1) {
                idx += 1;
                size -= 1;
            } else {
                nalType = NAL_TYPE(buf[idx + ret]);
                CAMERA_LOGI("ForkNode::ForkBuffers nalu == 0x%{public}x buf == 0x%{public}x \n", nalType, buf[idx + ret]);

每经过一个h264转换过的buffer都会被传入SearchIFps接口中寻找IDR帧。其中findStartCode()接口会对buffer中的内容逐个字节扫描,知道寻找出NALU头来

   int RKCodecNode::findStartCode(unsigned char *data, size_t dataSz)
      {
          constexpr uint32_t dataSize = 4;
          constexpr uint32_t dataBit2 = 2;
          constexpr uint32_t dataBit3 = 3;
      
          if (data == nullptr) {
              CAMERA_LOGI("RKCodecNode::findStartCode parameter == nullptr");
              return -1;
          }
      
          if ((dataSz > dataSize) && (data[0] == 0) && (data[1] == 0) && \
              (data[dataBit2] == 0) && (data[dataBit3] == 1)) {
              return 4; // 4:start node
          }
      
          return -1;
      }

当找到NALU头后就会对&0x1F 找出nal_unit_type,如果type为5标记关键帧信息并通过buffer->SetEsKeyFrame(1);接口上报。

经常有很多小伙伴抱怨说:不知道学习鸿蒙开发哪些技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?

为了能够帮助到大家能够有规划的学习,这里特别整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

在这里插入图片描述

《鸿蒙 (Harmony OS)开发学习手册》(共计892页):https://gitcode.com/HarmonyOS_MN/733GH/overview

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

在这里插入图片描述

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

在这里插入图片描述

鸿蒙开发面试真题(含参考答案):https://gitcode.com/HarmonyOS_MN/733GH/overview

在这里插入图片描述

OpenHarmony 开发环境搭建

图片

《OpenHarmony源码解析》:https://gitcode.com/HarmonyOS_MN/733GH/overview

  • 搭建开发环境
  • Windows 开发环境的搭建
  • Ubuntu 开发环境搭建
  • Linux 与 Windows 之间的文件共享
  • ……
  • 系统架构分析
  • 构建子系统
  • 启动流程
  • 子系统
  • 分布式任务调度子系统
  • 分布式通信子系统
  • 驱动子系统
  • ……

图片

OpenHarmony 设备开发学习手册:https://gitcode.com/HarmonyOS_MN/733GH/overview

图片
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到