当前位置: 首页 > news >正文

【移植】标准系统方案之瑞芯微RK3568移植案例(二)

往期知识点记录:

  • 鸿蒙(HarmonyOS)应用层开发(北向)知识点汇总
  • 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~
  • 持续更新中……

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/<mjx-container class="MathJax CtxtMenu_Attached_0" jax="SVG" role="presentation" tabindex="0" ctxtmenu_counter="0" style="overflow-wrap: break-word; padding: 0px; margin: 0px; -webkit-font-smoothing: subpixel-antialiased; direction: ltr; position: relative;"><mjx-assistive-mml role="presentation" unselectable="on" display="inline" style="overflow-wrap: break-word; padding: 1px 0px 0px !important; margin: 0px; -webkit-font-smoothing: subpixel-antialiased; top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); user-select: none; position: absolute !important; border: 0px !important; display: block !important; width: auto !important; overflow: hidden !important;"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mi>p</mi><mi>r</mi><mi>o</mi><mi>d</mi><mi>u</mi><mi>c</mi><msub><mi>t</mi><mi>c</mi></msub><mi>o</mi><mi>m</mi><mi>p</mi><mi>a</mi><mi>n</mi><mi>y</mi></mrow><mrow><mo>/</mo></mrow></math></mjx-assistive-mml></mjx-container>{device_name}/camera/product.gni")
}
}

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

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

如下是 rk3568 的 product.gni:

camera_device_name_path = "//device/board/productcompany/{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/productcompany/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/peripheral/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/ productcompany/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/productcompany/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 = trueif (is_support_v4l2) {is_support_mpi = falsedefines += [ "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_CAMERApixelFormat = PIXEL_FMT_RGBA_8888;#elsepixelFormat = 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 somethingbreak;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;#elsestruct timeval      timestamp;#endifstruct v4l2_timecode    timecode;}struct __kernel_v4l2_timeval {long long   ._sec;#if defined(__sparc__) && defined(__arch64__)int     tv_usec;int     __pad;#elselong 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);接口上报。


http://www.mrgr.cn/news/37111.html

相关文章:

  • 华为源NAT技术与目的NAT技术
  • 每日一练:二叉树的右视图
  • 【yolov8】模型导出----pytorch导出为onnx模型
  • Maya没有Arnold材质球
  • 第13讲 实践:设计SLAM系统
  • dependencyManagement的作用
  • 探索词向量的奥秘:自然语言处理的基石
  • Java.动态代理
  • JS面试真题 part7
  • 数据清洗与数据治理的关系
  • [附源码]在线音乐系统+SpringBoot+Vue前后端分离
  • 新手上路:Anaconda虚拟环境创建和配置以使用PyTorch和DGL
  • 第三十篇——总结:成功的捷径是没有捷径
  • Linux 学习 awk 和sed 命令使用
  • 操作配置笔记
  • 职业技能大赛-单元测试笔记(assertThat)分享
  • 【Vue】Vue3 的初始化过程
  • 深度学习中的正则化和归一化
  • 【Python报错已解决】ModuleNotFoundError: No module named ‘psutil’
  • 智界R7订单爆了,它凭什么抢了Model Y的风头?