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

news2024/9/27 6:03:01

往期知识点记录:

  • 鸿蒙(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 = 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);接口上报。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2169143.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows安装Vim,并在PowerShell中直接使用vim

大家好啊&#xff0c;我是豆小匠。 这期介绍下怎么在windows的PowerShell上使用vim&#xff0c;方便在命令行里修改配置文件等。 先上效果图&#xff1a; 1、下载Vim GitHub传送门&#xff1a;https://github.com/vim/vim-win32-installer/releases 选择win-64的版本下载即可&…

【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器

文章目录 C list 容器详解&#xff1a;从入门到精通前言第一章&#xff1a;C list 容器简介1.1 C STL 容器概述1.2 list 的特点 第二章&#xff1a;list 的构造方法2.1 常见构造函数2.1.1 示例&#xff1a;不同构造方法2.1.2 相关文档 第三章&#xff1a;list 迭代器的使用3.1 …

[大语言模型] 情感认知在大型语言模型中的近期进展-2024-09-26

[大语言模型] 情感认知在大型语言模型中的近期进展-2024-09-26 论文信息 Title: Recent Advancement of Emotion Cognition in Large Language Models Authors: Yuyan Chen, Yanghua Xiao https://arxiv.org/abs/2409.13354 情感认知在大型语言模型中的近期进展 《Recent A…

ElasticSearch安装分词器与整合SpringBoot

ElasticSearch安装分词器与整合SpringBoot 如果还没安装的点击安装ElasticSearch查看怎么安装 分词器 1.分词器 在Elasticsearch中&#xff0c;分词器&#xff08;Tokenizer&#xff09;是分析器&#xff08;Analyzer&#xff09;的一部分&#xff0c;它的主要职责是将文本输入…

MySql简介及发展

MySql简介及发展 1、MySql起源和分支 MySQL 是最流行的关系型数据库软件之一&#xff0c;由于其体积小、速度快、开源免费、简单易用、维护成本 低等&#xff0c;在集群架构中易于扩展、高可用&#xff0c;因此深受开发者和企业的欢迎。 Oracle和MySQL是世界市场占比最高的两…

C#图像处理学习笔记(屏幕截取,打开保存图像、旋转图像、黑白、马赛克、降低亮度、浮雕)

1、创建Form窗体应用程序 打开VS&#xff0c;创建新项目-语言选择C#-Window窗体应用&#xff08;.NET Framework) 如果找不到&#xff0c;检查一下有没有安装.NET 桌面开发模块&#xff0c;如果没有&#xff0c;需要下载&#xff0c;记得勾选相关开发工具 接上一步&#xff0c;…

【ARM 嵌入式 编译系列 10.4 -- GNU Binary Utilies】

文章目录 GNU Binary Utilities 详细介绍常用工具介绍1. arm-none-eabi-objcopy2. arm-none-eabi-readelf3. arm-none-eabi-size4. arm-none-eabi-objdump5. arm-none-eabi-nm6. arm-none-eabi-strip7. arm-none-eabi-ld8. arm-none-eabi-as9. arm-none-eabi-addr2line10. arm-…

linux内核双向链表使用list klist

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、list和klist是什么&#xff1f;二、代码示例1.list2.klist 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; linux内核中大量使…

Spring Boot打造甘肃非遗文化传承网站

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本甘肃非物质文化网站就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信…

如何像专家一样修复任何 iPhone 上的“iPhone 已禁用”错误

“我忘记了密码&#xff0c;并且我的 iPhone 在多次输入错误密码后就被禁用了&#xff0c;如何再次访问我的手机&#xff1f;” 作为最安全的数字设备之一&#xff0c;iPhone 必须使用正确的密码解锁。即使您可以使用 Face ID 或 Touch ID 访问您的设备&#xff0c;在充电或重…

交警车辆通入城行证管理建设方案和必要性-———未来之窗行业应用跨平台架构

一、系统目标 建立一个高效、便捷、规范的车辆入城通行证管理系统&#xff0c;提高交警部门的管理效率&#xff0c;优化城市交通流量&#xff0c;减少交通拥堵&#xff0c;保障城市交通安全。 二、系统功能模块 1. 通行证申请模块 - 提供在线申请入口&#xff0c;申请人填…

【MySQL】聚合函数、group by子句

目录 聚合函数 count([distinct] column) sum([distinct] column) avg([distinct] column) max([distinct] column) min([distinct] column) group by子句 1.如何显示每个部门的平均薪资和最高薪资 2.显示每个部门每种岗位的平均薪资和最低薪资 3.显示平均工资低于200…

maven给springboot项目打成jar包 maven springboot打包配置

基于maven的spring boot 打包分离依赖及配置文件 使用springCloud或springboot的过程中&#xff0c;发布到生产环境的网速受限&#xff0c;如果每次将60,70M甚至更大的jar包上传&#xff0c;速度太慢了&#xff0c;采取jar包和配置文件分离的方式可以极大的压缩jar包大小&#…

npm 安装newman时idealTree:vue: sill idealTree buildDeps卡住了(实测成功)

删除 C:\Users\{账户}\ 文件夹中的 .npmrc 文件在命令提示符里执行 npm cache verify 在命令提示符里执行 npm config set registry https://registry.npmmirror.com 切换淘宝源来源&#xff1a; https://blog.csdn.net/weixin_39245942/article/details/135748323

【Verilog学习日常】—牛客网刷题—Verilog企业真题—VL62

序列发生器 描述 编写一个模块&#xff0c;实现循环输出序列001011。 模块的接口信号图如下&#xff1a; 要求使用Verilog HDL实现&#xff0c;并编写testbench验证模块的功能。 输入描述&#xff1a; clk&#xff1a;时钟信号 rst_n&#xff1a;复位信号&#xff0c;低电平…

【学习笔记】手写 Tomcat 七

目录 一、优化 Dao 1. 设置 UserDaoImpl 为单例模式 2. 创建 Dao 工厂 3. 在 Service 层获取 UserDao 的实例 二、优化 Service 1. 设置 UserServiceImpl 为单例模式 2. 创建 Service 工厂 3. 在 Servlet 层获取 Service 实现类的对象 三、优化 Servlet 1. 使用配置…

Map+Set

我们前面接触过的string、vector、list这些都算序列式容器&#xff0c;它们都有一定的关联性&#xff0c;即使随便换位置也无伤大雅&#xff0c;因为是它们靠位置顺序来保存的。但是今天的MapSet就不是了&#xff0c;它们算关联式容器&#xff0c;两个位置之间有紧密的联系&…

Linux-df命令使用方法

Linux-df&#xff08;disk filesystem&#xff09;命令 df 命令是 Unix 和 Linux 系统中用于报告文件系统磁盘空间使用情况的工具。 df [OPTION]... [FILE]...OPTION 常用选项&#xff08;博主一般df -h用的较多&#xff0c;可读性较好&#xff09; -h&#xff1a;以人类可读的…

Uniapp 微信小程序 最新 获取用户头像 和 昵称 方法 有效可用

文章目录 前言代码实现运行效果技术分析 前言 同事有个需求 授权获取用户头像 和 昵称 。之前做过线上小程序发版上线流程 就实现了下 最新的方法和 api 有些变化 记录下 代码实现 先直接上代码 <template><view class"container"><buttonclass&qu…

如何部署北斗定位应用,基于国产自主架构LS2K1000LA-i处理器平台

北斗卫星导航系统(以下简称北斗系统)是着眼于国内经济社会发展需要,自主建设、独立运行的卫星导航系统。经过多年发展,北斗系统已成为面向全球用户提供全天候、全天时、高精度定位、导航与授时服务的重要新型基础设施。 图 1 北斗定位系统的应用优势 强可控:北斗系统是国…