关于海思HI3559AV100及SDK的一些使用经验

[TOC]

概述

我们绝对不会从0开始,所以,如果你想从0开始使用海思平----无论是做视频/音频采集还是做人工智能项目----,那得考虑一下

  • 你是否能得到海思的技术支持
  • 你是否熟练掌握各种规范
  • 你是否有足够多的时间去阅读手册
  • 你是否足够聪明去猜测海思开发人员的思路

如果你的答案都是否,那么还是放弃吧。因为使用海思平台,你需要从头至尾了解所有的硬件,对于linux,你反倒不用太在乎;正所谓海思不在乎。

海思从来不在乎小生意,FAE据说都在大客户那里,这种有钱就是爹的尿性正是太令人喜欢了。相反,nvidia跟你隔着N个时区,你在论坛上提的问题都有人详细跟踪,你只需要注册一个账号就行了。

Video

如果是从各路IMX传感器来,那么请使用海思提供的sample,如果是采集YUV视频,那么请继续往下读。
海思提供了不少的样例,每一个样例的友好度无限接近于0。看着海思自以为聪明的命名方式,自以为聪明的代码排版,自以为聪明的结构设计,你无从下手。所以从现在开始,忘记你在使用linux,调整到面向海思SDK编程模式。

MIPI-Rx

如果你采用2-lane的方式,那么我劝你放弃。原因如下:

  • 不稳定,总是莫名其妙的错误,手动调整DESCREW也没用
  • 几乎不可能靠自己的力量解决问题,然而小公司不可能得到技术支持,各路代理商属于你问100个问题,他只回答一个的那种,而且他经常回答“我们也没用过”、“我们也不知道”、“你可以试试”、“没有”。
  • 海思提供的寄存器不足以让你找到问题,比如:MIPI(N)_PKT_INTR_STMIPI(N)_PKT_INTR2_STMIPI(N)_FRAME_INTR_ST会告诉你发生了错误,但你难以进一步跟踪,比如此时你想捕获一个包头看看,一个包头正好是4字节,海思没有提供寄存器,所幸,可以读PHY\_PH\_MIPI\_LINK(P),因为你只有2-lane,所以你只能遗憾的得到头2个字节,总之如果你的CSI不稳定,你很难从硬件上找到问题并解决。而如果选用4-lane,那么你正好可以从PHY_PH_MIPI_LINK(P)读出包头。

为了调试2-lane,我废了很多工夫,海思提供的himm读写不好用,我这里提供一个还不错的。

//
// (c) 2020 chiv
//
#include "utils/flags.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string>

qDefineFlag(uint32, phy, 0, "phy link, [0..3]");
qDefineFlag(uint32, chn, 0, "mipi rx chn, [0..7]");
qDefineFlag(bool, wr, true, "read");
qDefineFlag(bool, dump, true, "dump status register only");
qDefineFlag(uint32, addr, 0, "register offset");
qDefineFlag(uint32, value, 0, "if write mode was enabled, this is the value to write");
qDefineFlag(bool, relative, true, "use offset");

uint32_t regaddr(uint32_t pa_offset, uint32_t addr, uint32_t reg_addr)
{
    return addr - pa_offset + reg_addr;
}

void dump_mipi_rx_regs(int mem_fd, int phy, int chn)
{
    size_t len = 0x10000;
    uint32_t offset = 0x11a40000;
    uint32_t pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1);
    printf("mipi rx, pa_offset = %08x\n", pa_offset);
    uint32_t* base = (uint32_t*)mmap(NULL, len + offset - pa_offset, PROT_READ | PROT_WRITE,
        MAP_SHARED, mem_fd, pa_offset);
    if (base) {
        uint32_t pkt_intr_st = 0x1060 + 0x1000 * chn;
        uint32_t pkt_intr_st2 = 0x1070 + 0x1000 * chn;
        uint32_t frm_intr_st = 0x1080 + 0x1000 * chn;
        printf("PKT_INTR_ST :\033[31m%08x\033[0m\n", *(base + regaddr(pa_offset, offset, pkt_intr_st) / 4));
        printf("PKT_INTR_ST2:\033[31m%08x\033[0m\n", *(base + regaddr(pa_offset, offset, pkt_intr_st2) / 4));
        printf("FRM_INTR_ST :\033[31m%08x\033[0m\n", *(base + regaddr(pa_offset, offset, frm_intr_st) / 4));
        //
        munmap((void*)base, len + offset - pa_offset);
    }
}

void process_read(int mem_fd, uint32_t addr)
{
    if (qFlag(relative)) {
        addr += 0x11a40000;
    }
    size_t len = 4;
    uint32_t offset = addr;
    uint32_t pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1);
    printf("pa_offset = %08x\n", pa_offset);
    uint32_t* base = (uint32_t*)mmap(NULL, len + offset - pa_offset, PROT_READ | PROT_WRITE,
        MAP_SHARED, mem_fd, pa_offset);
    if (base) {
        printf("Reg:%08x, Value:%08x\n", addr, *(base + regaddr(pa_offset, addr, 0) / 4));
        //
        munmap((void*)base, len + offset - pa_offset);
    }
}

void process_write(int mem_fd, uint32_t addr, uint32_t value)
{
    if (qFlag(relative)) {
        addr += 0x11a40000;
    }
    size_t len = 4;
    uint32_t offset = addr;
    uint32_t pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1);
    printf("pa_offset = %08x\n", pa_offset);
    uint32_t* base = (uint32_t*)mmap(NULL, len + offset - pa_offset, PROT_READ | PROT_WRITE,
        MAP_SHARED, mem_fd, pa_offset);
    if (base) {
        *(base + regaddr(pa_offset, offset, 0) / 4) = value;
        //
        munmap((void*)base, len + offset - pa_offset);
    }
}

int main(int argc, char** argv)
{
    // usage
    std::string usage = std::string() + "\n" +
        argv[0] + "--dump=1, dump mipi_rx status registers\n" +
        argv[0] + "--dump=0 --wr=1 --addr=0x1060, read the mipi_rx register\n" +
        argv[0] + "--dump=0 --wr=1 --addr=0x11a41060 --relative=false, read any register\n" +
        argv[0] + "--dump=0 --wr=0 --addr=0x1064 --value=0x10081008, write the mipi_rx register\n" +
        argv[0] + "--dump=0 --wr=0 --addr=0x10601064 --value=0 --relative=false, write any register\n";
    //printf("%s\n", usage.c_str());
    google::SetUsageMessage(usage);
    qInitializeFlags(&argc, &argv);
    
    int mem_fd = open("/dev/mem", O_RDWR, 0666);
    if (mem_fd < 0) {
        printf("can't open device:/dev/mem\n");
        return -1;
    }
    if (qFlag(dump)) {
        dump_mipi_rx_regs(mem_fd, qFlag(phy), qFlag(chn));
    } else {
        if (qFlag(wr)) { // read mode
            process_read(mem_fd, qFlag(addr));
        } else {
            process_write(mem_fd, qFlag(addr), qFlag(value));
        }
    }
    //
    close(mem_fd);

    return 0;
}

为了使用时方便,通常可以放入脚本中执行

#!/bin/sh

# read mipi register
reg_addr=0x20
if [ $1 != "" ]; then
    reg_addr=$1
    echo read $reg_addr
fi

./dump_mipi_rx_regs --dump=0 --wr=1 --addr=$reg_addr
#!/bin/sh

# write value to mipi register
if [ $# != 2 ]; then
    echo "Usage:$0 <mipi_rx_reg_addr> <value>"

    exit
fi

./dump_mipi_rx_regs --dump=0 --wr=0 --addr=$1 --value=$2

例如,读取link0寄存器PHY_PH_MIPI_LINK(P)

./mipir 0x1c

程序中内置了mipi-rx的寄存器基地址,默认情况下输入都被当做相对地址,使用--relative=0可启用绝对地址。最常用的地址就是0x1c[PHY_PH_MIPI_LINK(P)],可以从中读出MIPI的包头,即Type-ID,Word-Count,ECC。不带参数执行程序,可以直接输出MIPI_Rx的几个状态寄存器,无错误时都为0。如果这些都没有错误,那么cat /proc/umap/hi_mipi可以获得可阅读的更多信息,否则还是拿上你的数据手册打印寄存器的值看看吧。

YUV

我们只关注YUV,也即sensor输出的数据为YUV格式。海思提供有限支持,在文档《HiMPP V4.0 媒体处理软件 FAQ》的3.3.4节有介绍,这也是唯一的配置方法,我也是从这里抄的。海思的各个硬件之间可以互相倒腾数据,这个工作可以由SDK完成。使用YUV时要旁路ISP,那么此时我们可以粗略的得到一个工作流程图。

graph TB
    id1[MIPI_Rx]-->id2(VI_CAP)
    subgraph Video input #0
        id2==>id3[PIPE0, YUV422]
        id2-.->id4[PIPE2-4]
        id3==>id5[Channel #0, YUV420 SP]
        id5-.->id6[Ext channel#0-8]
        id5==>id7(Encoder)
    end
    subgraph Video Encoder #0
        id7==>id8[Channel #0, H265]
        id8==>id9[Query frame]
    end
    subgraph live555
        id9==>id10[Send to network]
    end

对于HI3559AV100而言,Video和Mipi的对应关系不需要设置,天然对应;PIPE可动态绑定,每个PIPE有至少1个物理通道和几个扩展通道,它们可以完成一些诸如裁切、缩放、旋转之类的工作。理清了视频的流向,那么程序也就有了思路,不要看海思的样例,按照我们自己的思路走。

第一个YUV采集程序

硬件连接

4-lane不像2-lane一样,在设计原理图时不容易迷惑。HI3559AV100有16个lane,4个link,4-lane的连接方式可以接入4路设备。

graph LR
    id1["Sensor0"]==>id2["Lane0~3"]
    id3["Sensor1"]==>id4["Lane4~7"]
    id5["Sensor2"]==>id6["Lane8~Lane11"]
    id7["Sensor3"]==>id9["Lane12~Lane15"]

海思SDK中的资源描述

海思的SDK中把路视频输抽象为VI(Video input),并给它们分配不同的设备号,HI3559AV100最多可接入8路视频,因此VI的设备号范围为0~7。当采用2-lane的方式时,可接入8路Sensor,VI设备号对应0~7,但是当采用4-lane的连接时,设备号只能由4个,但范围并不是0~3,而是0,2,4,6。对于MIPI,海思SDK也采用了类似的抽象方式,其设备号范围为对应Vi的设备号。

开始编写程序

首先,使用海思的SDK就不要想着多进程了。其次,使用相应的功能之前需要初始化。

第一步,建立硬件描述

硬件的连接已经非常清楚了,我们专门建立模块来描述它们。

//
// (c) 2020 chiv
//
#pragma once

#include "mpi_vi.h"
#include "hi_mipi.h"

// all definitions of Hisilicon modules
namespace lowlevel {
namespace desc {
//---------------------------------------------------------------------------------------
//                                             Audio
//---------------------------------------------------------------------------------------

//---------------------------------------------------------------------------------------
//                                             Video
//---------------------------------------------------------------------------------------
// VI_DEV_ATTR_S
// VI_PIPE_ATTR_S
// VI_PIPE_CHN_ATTR_S
struct ViAttrs {
    VI_DEV_ATTR_S DevAttr;
    VI_PIPE_ATTR_S PipeAttr;
    VI_CHN_ATTR_S ChnAttr;
};
ViAttrs MakeMipiYuvAttrs_YUV422_8bits_UYVY(int width, int height);
//
// 4 input signals connected to mipi rx
combo_dev_attr_t MakeMipiAttrs_Input_0(int width, int height);
combo_dev_attr_t MakeMipiAttrs_Input_1(int width, int height);
combo_dev_attr_t MakeMipiAttrs_Input_2(int width, int height);
combo_dev_attr_t MakeMipiAttrs_Input_3(int width, int height);
//
static const int kNumberOfInputs = 4;
//
combo_dev_t MakeComboDev(int idx);
VI_DEV MakeViDev(int idx);
VI_PIPE MakeViPipe(int idx);
//
static inline combo_dev_attr_t MakeMipiAttrs(int idx, int cx, int cy)
{
    switch (idx) {
    case 0: return MakeMipiAttrs_Input_0(cx, cy);
    case 1: return MakeMipiAttrs_Input_1(cx, cy);
    case 2: return MakeMipiAttrs_Input_2(cx, cy);
    case 3: return MakeMipiAttrs_Input_3(cx, cy);
    default:printf("\033[31mMipiDevNo[%d] is not valid\033[0m", MakeComboDev(idx)); abort();break;
    }
}
}
}
#include "HardwareDesc.h"

namespace lowlevel {

//---------------------------------------------------------------------------------------
//                               Audio
//---------------------------------------------------------------------------------------

//---------------------------------------------------------------------------------------
//                               Video
//---------------------------------------------------------------------------------------
static combo_dev_attr_t MIPI_YUV422_ATTR_BASE = {
    .devno = 0,
    .input_mode = INPUT_MODE_MIPI,
    .data_rate = MIPI_DATA_RATE_X1,
    .img_rect = { 0, 0, 1920, 1080 },
    { .mipi_attr = {
          DATA_TYPE_YUV422_8BIT,
          HI_MIPI_WDR_MODE_NONE,
          { -1, -1, -1, -1, -1, -1, -1, -1 } } }
};

static VI_DEV_ATTR_S DEV_MIPI_YUV422_ATTR_BASE = {
    VI_MODE_MIPI_YUV422,
    VI_WORK_MODE_1Multiplex,
    { 0xFF000000, 0x00FF0000 },
    VI_SCAN_PROGRESSIVE,
    { -1, -1, -1, -1 },
    VI_DATA_SEQ_UVUV,
    { VI_VSYNC_PULSE, VI_VSYNC_NEG_LOW, VI_HSYNC_VALID_SINGNAL,
        VI_HSYNC_NEG_HIGH, VI_VSYNC_VALID_SINGAL,
        VI_VSYNC_VALID_NEG_HIGH,
        { 0, 1920, 0,
            0, 1080, 0,
            0, 0, 0 } },
    VI_DATA_TYPE_YUV,
    HI_FALSE,
    { 1920, 1080 },
    { {
          { 1920, 1080 },
      },
        { VI_REPHASE_MODE_NONE,
            VI_REPHASE_MODE_NONE } },
    { WDR_MODE_NONE,
        1080 },
    DATA_RATE_X1
};
static VI_PIPE_ATTR_S PIPE_YUV422_ATTR_BASE = {
    VI_PIPE_BYPASS_NONE, HI_FALSE, HI_TRUE,
    1920, 1080,
    PIXEL_FORMAT_YVU_SEMIPLANAR_422,
    COMPRESS_MODE_NONE,
    DATA_BITWIDTH_8,
    HI_FALSE,
    { PIXEL_FORMAT_YVU_SEMIPLANAR_422,
        DATA_BITWIDTH_8,
        VI_NR_REF_FROM_RFR,
        COMPRESS_MODE_NONE },
    HI_FALSE,
    { -1, -1 },
    HI_FALSE
};
static VI_CHN_ATTR_S CHN_YUV422_ATTR_BASE {
    { 1920, 1080 },
    PIXEL_FORMAT_YVU_SEMIPLANAR_420, // CHN ---> VENC, VENC need YVU420 semiplanar
    DYNAMIC_RANGE_SDR8,
    VIDEO_FORMAT_LINEAR,
    COMPRESS_MODE_NONE,
    HI_FALSE, HI_FALSE,
    0,
    { -1, -1 }
};
namespace desc {
    //
    combo_dev_t MakeComboDev(int idx)
    {
        return idx * 2;
    }
    VI_DEV MakeViDev(int idx)
    {
        return idx * 2;
    }
    VI_PIPE MakeViPipe(int idx)
    {
        return idx * 2;
    }
    // YUV 422 8bit
    ViAttrs MakeMipiYuvAttrs_YUV422_8bits_UYVY(int width, int height)
    {
        ViAttrs attrs = {
            .DevAttr = DEV_MIPI_YUV422_ATTR_BASE,
            .PipeAttr = PIPE_YUV422_ATTR_BASE,
            .ChnAttr = CHN_YUV422_ATTR_BASE
        };
        // No need for YUV
        attrs.DevAttr.stSynCfg.stTimingBlank = VI_TIMING_BLANK_S {
            0, (HI_U32)width, 0,
            0, (HI_U32)height, 0,
            0, 0, 0
        };
        attrs.DevAttr.stBasAttr.stSacleAttr.stBasSize = SIZE_S { (HI_U32)width, (HI_U32)height };
        attrs.DevAttr.stSize = SIZE_S { (HI_U32)width, (HI_U32)height };
        attrs.DevAttr.stWDRAttr.u32CacheLine = height;
        attrs.PipeAttr.u32MaxW = width;
        attrs.PipeAttr.u32MaxH = height;
        attrs.ChnAttr.stSize = SIZE_S { (HI_U32)width, (HI_U32)height };
        return attrs;
    }

    combo_dev_attr_t MakeMipiAttrs_Input_0(int width, int height)
    {
        combo_dev_attr_t attr = MIPI_YUV422_ATTR_BASE;
        attr.devno = 0;
        attr.img_rect = img_rect_t { 0, 0, (HI_U32)width, (HI_U32)height };
        attr.mipi_attr.lane_id[0] = 0;
        attr.mipi_attr.lane_id[1] = 1;
        attr.mipi_attr.lane_id[2] = 2;
        attr.mipi_attr.lane_id[3] = 3;
        attr.mipi_attr.lane_id[4] = -1;
        attr.mipi_attr.lane_id[5] = -1;
        attr.mipi_attr.lane_id[6] = -1;
        attr.mipi_attr.lane_id[7] = -1;
        return attr;
    }
    combo_dev_attr_t MakeMipiAttrs_Input_1(int width, int height)
    {
        combo_dev_attr_t attr = MIPI_YUV422_ATTR_BASE;
        attr.devno = 2;
        attr.img_rect = img_rect_t { 0, 0, (HI_U32)width, (HI_U32)height };
        attr.mipi_attr.lane_id[0] = 4;
        attr.mipi_attr.lane_id[1] = 5;
        attr.mipi_attr.lane_id[2] = 6;
        attr.mipi_attr.lane_id[3] = 7;
        attr.mipi_attr.lane_id[4] = -1;
        attr.mipi_attr.lane_id[5] = -1;
        attr.mipi_attr.lane_id[6] = -1;
        attr.mipi_attr.lane_id[7] = -1;
        return attr;
    }
    combo_dev_attr_t MakeMipiAttrs_Input_2(int width, int height)
    {
        combo_dev_attr_t attr = MIPI_YUV422_ATTR_BASE;
        attr.devno = 4;
        attr.img_rect = img_rect_t { 0, 0, (HI_U32)width, (HI_U32)height };
        attr.mipi_attr.lane_id[0] = 8;
        attr.mipi_attr.lane_id[1] = 9;
        attr.mipi_attr.lane_id[2] = 10;
        attr.mipi_attr.lane_id[3] = 11;
        attr.mipi_attr.lane_id[4] = -1;
        attr.mipi_attr.lane_id[5] = -1;
        attr.mipi_attr.lane_id[6] = -1;
        attr.mipi_attr.lane_id[7] = -1;
        return attr;
    }
    combo_dev_attr_t MakeMipiAttrs_Input_3(int width, int height)
    {
        combo_dev_attr_t attr = MIPI_YUV422_ATTR_BASE;
        attr.devno = 6;
        attr.img_rect = img_rect_t { 0, 0, (HI_U32)width, (HI_U32)height };
        attr.mipi_attr.lane_id[0] = 12;
        attr.mipi_attr.lane_id[1] = 13;
        attr.mipi_attr.lane_id[2] = 14;
        attr.mipi_attr.lane_id[3] = 15;
        attr.mipi_attr.lane_id[4] = -1;
        attr.mipi_attr.lane_id[5] = -1;
        attr.mipi_attr.lane_id[6] = -1;
        attr.mipi_attr.lane_id[7] = -1;
        return attr;
    }

} // namespace desc
} // namespace lowlevel

这部分代码的目的是用一个连续的数字代替视频输入,例如:0代表第一路输入,1代表第二路输入,等等,硬件如何实现不重要,重要的是要把硬件的输入与idx对应起来,故而对用户而言,只存在video0、video1这样的设备,不存在理解上的偏差。

第二步,初始化系统

使用视频采集需要VB(Video buffer)模块,Sys是必须的,无用赘言。

// 初始化Video buffer
static bool _InitVb()
{
    struct {
        int Width;
        int Height;
        PIXEL_FORMAT_E PixFmt;
        DATA_BITWIDTH_E BitWidth;
        COMPRESS_MODE_E CompressMode;
        HI_U32 Align;
    } fmts[] = {
        { 1920, 1080, PIXEL_FORMAT_YVU_SEMIPLANAR_422, DATA_BITWIDTH_8, COMPRESS_MODE_NONE, DEFAULT_ALIGN },
        { 1280, 720, PIXEL_FORMAT_YVU_SEMIPLANAR_422, DATA_BITWIDTH_8, COMPRESS_MODE_NONE, DEFAULT_ALIGN }
    };
    const int kFmtsCnt = sizeof(fmts) / sizeof(fmts[0]);
    //-------------------------------------------------------------------
    VB_CONFIG_S vb_conf;
    bzero(&vb_conf, sizeof(vb_conf));
    vb_conf.u32MaxPoolCnt = kFmtsCnt;
    for (int k = 0; k < kFmtsCnt; ++k) {
        HI_U32 frmsz = COMMON_GetPicBufferSize(fmts[k].Width, fmts[k].Height,
            fmts[k].PixFmt, fmts[k].BitWidth, fmts[k].CompressMode, fmts[k].Align);
        vb_conf.astCommPool[k].u32BlkCnt = 8;
        vb_conf.astCommPool[k].u64BlkSize = frmsz;
    }
    MPI_CHECK(HI_MPI_VB_SetConfig(&vb_conf), return false);
    MPI_CHECK(HI_MPI_VB_Init(), return false);

    return true;
}
// 初始化System
static bool _InitSys()
{
    MPI_CHECK(HI_MPI_SYS_Init(), return false);
    return true;
}

我们把这个过程总结为一个函数

bool Initialize()
{
    bool ok = _InitVb() && _InitSys();
    return ok;
}

void Shutdown()
{
    HI_MPI_SYS_Exit();
    HI_MPI_VB_Exit();
}
第三步,启动MIPI
// 模式7, 4-lane

void RunOnce()
{
    lane_divide_mode_t ld_mode = LANE_DIVIDE_MODE_7;
    Mipi::Inst()->IoControl(HI_MIPI_SET_HS_MODE, &ld_mode);
}
// idx 可取0,1,2,3
bool StartMipi(int idx, int cx, int cy)
{
    bool ok = true;
    combo_dev_t devno = desc::MakeComboDev(idx);
    ok = ok && Mipi::Inst()->IoControl(HI_MIPI_ENABLE_MIPI_CLOCK, &devno);
    ok = ok && Mipi::Inst()->IoControl(HI_MIPI_RESET_MIPI, &devno);
    auto dev_attr = desc::MakeMipiAttrs(idx, cx, cy);
    ok = ok && Mipi::Inst()->IoControl(HI_MIPI_SET_DEV_ATTR, &dev_attr);
    ok = ok && Mipi::Inst()->IoControl(HI_MIPI_UNRESET_MIPI, &devno);
    //
    return ok;
}
第四步,创建VI

创建VI包括启用VI_DEV,绑定PIPE,设置CHANNEL等工作。

// idx 可取 0,1,2,3
bool StartVi(int idx, int cx, int cy)
{
    VI_DEV devno = desc::MakeViDev(idx);
    VI_PIPE pipeno = desc::MakeViPipe(idx);
    // step 1. enable vi dev
    auto attrs = desc::MakeMipiYuvAttrs_YUV422_8bits_UYVY(cx, cy);
    MPI_CHECK(HI_MPI_VI_SetDevAttr(devno, &attrs.DevAttr), return false);
    MPI_CHECK(HI_MPI_VI_EnableDev(devno), return false);
    // step 2. start pipe
    VI_DEV_BIND_PIPE_S binded_pipe;
    bzero(&binded_pipe, sizeof(binded_pipe));
    binded_pipe.PipeId[0] = pipeno;
    binded_pipe.u32Num = 1;
    MPI_CHECK(HI_MPI_VI_SetDevBindPipe(devno, &binded_pipe), return false);
    MPI_CHECK(HI_MPI_VI_CreatePipe(pipeno, &attrs.PipeAttr), return false);
    MPI_CHECK(HI_MPI_VI_StartPipe(pipeno), return false);
    // step 3. start chn
    // Pipe has 2 channel, we use channel 0.
    VI_CHN chnno = 0;
    MPI_CHECK(HI_MPI_VI_SetChnAttr(pipeno, chnno, &attrs.ChnAttr), return false);
    MPI_CHECK(HI_MPI_VI_EnableChn(pipeno, chnno), return false);
    //
    return true;
}
第五步,创建编码通道
static HI_S32 CreateH265Channel(VENC_CHN chnno, int cx, int cy, int fps)
{
    VENC_CHN_ATTR_S chn_attr;
    bzero(&chn_attr, sizeof(chn_attr));
    // 这个函数从SDK的sample中抄的.
    SAMPLE_COMM_VENC_GetGopAttr(VENC_GOPMODE_NORMALP, &chn_attr.stGopAttr);
    // rc attr , variable bitrate
    HI_U32 stat_time = 1, gop = 30;
    if (chn_attr.stGopAttr.enGopMode == VENC_GOPMODE_ADVSMARTP) {
        stat_time = chn_attr.stGopAttr.stSmartP.u32BgInterval / gop;
    } else if (chn_attr.stGopAttr.enGopMode == VENC_GOPMODE_SMARTP) {
        stat_time = chn_attr.stGopAttr.stSmartP.u32BgInterval / gop;
    }
    // vbr
#if 0
    chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H265VBR;
    chn_attr.stRcAttr.stH265Vbr.u32Gop = gop;
    chn_attr.stRcAttr.stH265Vbr.u32StatTime = stat_time;
    chn_attr.stRcAttr.stH265Vbr.u32SrcFrameRate = fps;
    chn_attr.stRcAttr.stH265Vbr.fr32DstFrameRate = std::min(25, fps);
    chn_attr.stRcAttr.stH265Vbr.u32MaxBitRate = 900; // 900k
#endif
#if 1 // 选用固定码率
    chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H265CBR;
    chn_attr.stRcAttr.stH265Cbr.u32Gop = gop;
    chn_attr.stRcAttr.stH265Cbr.u32StatTime = stat_time;
    chn_attr.stRcAttr.stH265Cbr.u32SrcFrameRate = fps;
    chn_attr.stRcAttr.stH265Cbr.fr32DstFrameRate = std::min(25, fps);
    chn_attr.stRcAttr.stH265Cbr.u32BitRate = 500;//1024*2 + 2048*fps/gop;
#endif
    // enc attr
    chn_attr.stVencAttr.enType = PT_H265;
    chn_attr.stVencAttr.u32MaxPicWidth = cx;
    chn_attr.stVencAttr.u32MaxPicHeight = cy;
    chn_attr.stVencAttr.u32PicWidth = cx;
    chn_attr.stVencAttr.u32PicHeight = cy;
    chn_attr.stVencAttr.u32BufSize = cx * cy * 2;
    chn_attr.stVencAttr.u32Profile = 0; // H265 mainprofile
    chn_attr.stVencAttr.bByFrame = HI_TRUE;
    chn_attr.stVencAttr.stAttrH265e.bRcnRefShareBuf = HI_FALSE;
    // create channel
    MPI_CHECK(HI_MPI_VENC_CreateChn(chnno, &chn_attr), return HI_FAILURE);
    //
    VENC_RECV_PIC_PARAM_S recv_param;
    recv_param.s32RecvPicNum = -1;
    MPI_CHECK(HI_MPI_VENC_StartRecvFrame(chnno, &recv_param), return HI_FAILURE);
    //
    return HI_SUCCESS;
}
第六步,建立视频到编码器的管道
void BuildPipeline(int idx)
{
    MPP_CHN_S src;
    src.enModId = HI_ID_VI;
    src.s32DevId = desc::MakeViDev(idx);
    src.s32ChnId = 0;
    MPP_CHN_S dst;
    dst.enModId = HI_ID_VENC;
    dst.s32DevId = 0;
    dst.s32ChnId = idx;
    MPI_CHECK(HI_MPI_SYS_Bind(&src, &dst), return -1);
}
所以完整的流程是

因为是记录过程,因此失败后的处理代码不赘述了。

int main()
{
    // 初始化必要的模块
    Initialize();
    // 只需要执行一次的内容, 这里是选择MIPI的工作模式
    RunOnce();
    // 创建4个采集通道
    for (int i = 0; i < 4; ++i) {
        StartMipi(i, 1920, 1080);
        StartVi(i, 1920, 1080);
        CreateH265Channel(i, 1920, 1080, 60);
        BuildPipeline(i);
    }
    // 取编码数据
    while (1) {
        for (int i = 0; i < 4; ++i) {
            auto out = QueryFrame(i, 100);
            if (!out.empty()) {
                Dump(i, out);
            }
        }
    }
    // release your resources here
}
// 从编码器里取数据
std::vector<char> QueryFrame(int idx, int timeout)
{
    std::vector<char> out; if (idx < 0) return out; // return empty for invalid idx.
    //
    VENC_CHN_STATUS_S chn_status;
    MPI_CHECK(HI_MPI_VENC_QueryStatus(idx, &chn_status), return out);
    if (chn_status.u32CurPacks > 0) {
        VENC_STREAM_S stream;
        bzero(&stream, sizeof(stream));
        stream.pstPack = new VENC_PACK_S[chn_status.u32CurPacks];
        stream.u32PackCount = chn_status.u32CurPacks;
        MPI_CHECK(HI_MPI_VENC_GetStream(idx, &stream, timeout),
                  delete[] stream.pstPack;
                  return out);
        HI_U32 len = 0;
        for (HI_U32 k = 0; k < stream.u32PackCount; ++k) {
            len += stream.pstPack[k].u32Len - stream.pstPack[k].u32Offset;
        }
        out = std::vector<char>(len);
        len = 0;
        for (HI_U32 k = 0; k < stream.u32PackCount; ++k) {
            memcpy(out.data() + len, stream.pstPack[k].pu8Addr + stream.pstPack[k].u32Offset,
                stream.pstPack[k].u32Len - stream.pstPack[k].u32Offset);
            len += stream.pstPack[k].u32Len - stream.pstPack[k].u32Offset;
        }
        //
        MPI_CHECK(HI_MPI_VENC_ReleaseStream(idx, &stream), );
        delete[] stream.pstPack;
    }
    return out;
}

吐槽时间

  • 视频采集,linux有v4l2系统,为什么不用?
  • 音频采集,linux有alsa,为什么不用?
  • 以及我什么也没说

虚拟通道的处理

如果采用了虚拟通道,即在4-lane上传输多路视频,那么需要绑定对应数目的PIPE,并使用HI_MPI_VI_SetPipeVCNumber为绑定的PIPE指定虚拟通道号,在使能PIPE的物理通道时,要设置物理通道的队列深度大于0,这样之后可以使用HI_MPI_VI_GetChnFrame从物理通道里获取YUV420 SP格式的数据,并调用HI_MPI_VENC_SendFrame把数据送入编码器编码。

Video output

海思SDK用VO来抽象视频输出。HI3559AV100有三种物理输出,HDMI、BT1120和MIPI-Tx。VO的目的用途是产生视频帧、产生正确的时序,驱动物理接口输出。

graph LR
subgraph "HI3559AV100 VO"
    id1["DHD0"]---id2["VHD0, Super HD"]
    id1["DHD0"]---id3["VHD2, HD"]
    id1["DHD0"]---id6["G0,G3"]
    id4["DHD1"]---id5["VHD1, HD"]
    id4["DHD1"]---id7["G1,G3"]
end

DHD0支持超高清输出,DHD1支持高清输出。VHD、G可以叠加,DHD0和DHD1都可以与G3叠加,故而G3可以用来实现鼠标层。

海思SDK中内置了一些常用的分辨率,使用它们时不需要自己设置时序,否则就需要自定义时序。自定义时序时,请餐卡vesa标准,linux下有一些工具方便进行计算,例如gtf、cvt,nvidia的显卡驱动中也带了计算工具,vesa官方提供了一个excel表格,也可以进行计算,总之还是不难的。

程序流程

// 初始化模块
bool Initialize()
{
    Shutdown();
    //
    bool ok = _InitVb() && _InitSys() && Hdmi::Initialize();
    // should be ok
    return ok;
}
// 初始化输出物理设备
bool Hdmi::Open(int cx, int cy, int fps)
{
    // init, register callback
    auto devid = _FromIdx(mIdx);
    MPI_CHECK(HI_MPI_HDMI_Open(devid), return false);
    // register callback
    MPI_CHECK(HI_MPI_HDMI_RegCallbackFunc(devid, mRc->HdmiCb()), );
    //
    HI_HDMI_ATTR_S attr;
    MPI_CHECK(HI_MPI_HDMI_GetAttr(devid, &attr), return false);
    attr.bEnableHdmi = HI_TRUE;
    attr.bEnableVideo = HI_TRUE;
    attr.enVideoFmt = _FindVideoFmt(cx, cy, fps);
    if (attr.enVideoFmt == HI_HDMI_VIDEO_FMT_VESA_CUSTOMER_DEFINE) {
        qLog(ERROR) << "Dose not support user defined timing yet" << std::endl;
        return false;
    }
    attr.enVidInMode = HI_HDMI_VIDEO_MODE_YCBCR444;
    attr.enVidOutMode = HI_HDMI_VIDEO_MODE_YCBCR444;
    attr.enDeepColorMode = HI_HDMI_DEEP_COLOR_24BIT;
    attr.bxvYCCMode = HI_FALSE;
    attr.enOutCscQuantization = HDMI_QUANTIZATION_LIMITED_RANGE;
    attr.bEnableAudio = HI_FALSE;
    attr.enSoundIntf = HI_HDMI_SND_INTERFACE_I2S;
    attr.bIsMultiChannel = HI_FALSE;
    attr.enBitDepth = HI_HDMI_BIT_DEPTH_16;
    attr.bEnableAviInfoFrame = HI_TRUE;
    attr.bEnableAudInfoFrame = HI_TRUE;
    attr.bEnableSpdInfoFrame = HI_FALSE;
    attr.bEnableMpegInfoFrame = HI_FALSE;
    attr.bDebugFlag = HI_FALSE;
    attr.bHDCPEnable = HI_FALSE;
    attr.b3DEnable = HI_FALSE;
    attr.enDefaultMode = HI_HDMI_FORCE_HDMI;
    MPI_CHECK(HI_MPI_HDMI_SetAttr(devid, &attr), return false);
    MPI_CHECK(HI_MPI_HDMI_Start(devid), return false);
    //
    return true;
}
// 初始化输出
bool InitVO()
{
    MPI_CHECK(HI_MPI_VO_Disable(vodev), );
    MPI_CHECK(HI_MPI_VO_SetPubAttr(vodev, &pub_attr), return false);
    MPI_CHECK(HI_MPI_VO_Enable(vodev), return false);
    // 启用层
    VO_VIDEO_LAYER_ATTR_S vlayer_attr;
    vlayer_attr.bClusterMode = HI_FALSE;
    vlayer_attr.bDoubleFrame = HI_FALSE;
    vlayer_attr.enDstDynamicRange = DYNAMIC_RANGE_SDR8;
    vlayer_attr.enPixFormat = PIXEL_FORMAT_YVU_SEMIPLANAR_422;
    vlayer_attr.stImageSize = SIZE_S { (HI_U32)cx, (HI_U32)cy };
    vlayer_attr.stDispRect = RECT_S { 0, 0, (HI_U32)cx, (HI_U32)cy };
    vlayer_attr.u32DispFrmRt = fps;
    MPI_CHECK(HI_MPI_VO_SetDisplayBufLen(mVoLayer, 3), return false);
    MPI_CHECK(HI_MPI_VO_SetVideoLayerAttr(mVoLayer, &vlayer_attr), return false);
    MPI_CHECK(HI_MPI_VO_EnableVideoLayer(mVoLayer), return false);
}

海思的SDK是通用的,所以每一个用户都需要根据硬件的情况设计应用。哎,带一个图形界面多好,这部分工作交给小厂家去做很费劲的。如果仅仅基于framebuffer做一个GUI,那效率堪忧。瑞芯微3399上有一个linux发行版,用起来就是卡卡的,看着性能与nvidia的jetson不差,但是jetson的GUI那是可以办公用的。

Audio

To be continued

GPU

按照手册测试,有的不能运行,能运行的结果不对。哎,好好的,你整个X windows实现不行吗?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342