往期知识点记录:
- 鸿蒙(HarmonyOS)应用层开发(北向)知识点汇总
- 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~
- 持续更新中……
Camera
基本概念
OpenHarmony 相机驱动框架模型对上实现相机 HDI 接口,对下实现相机 Pipeline 模型,管理相机各个硬件设备。各层的基本概念如下。
- HDI 实现层:对上实现 OHOS 相机标准南向接口。
- 框架层:对接 HDI 实现层的控制、流的转发,实现数据通路的搭建、管理相机各个硬件设备等功能。
- 适配层:屏蔽底层芯片和 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 = 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);接口上报。