Tina5 Linux开发
准备开发环境
首先准备一台 Ubuntu 20.04 / Ubuntu 18.04 / Ubuntu 16.04 / Ubuntu 14.04 的虚拟机或实体机,其他系统没有测试过出 BUG 不管。
更新系统,安装基础软件包
sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install build-essential subversion git libncurses5-dev zlib1g-dev gawk flex bison quilt libssl-dev xsltproc libxml-parser-perl mercurial bzr ecj cvs unzip lsof python3 python2 python3-dev android-tools-mkbootimg python2 libpython3-dev
安装完成后还需要安装 i386 支持,SDK 有几个打包固件使用的程序是 32 位的,如果不安装就等着 Segment fault
吧。
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt install gcc-multilib
sudo apt install libc6:i386 libstdc++6:i386 lib32z1
下载 AWOL Tina Linux BSP
注册一个 AWOL 账号
下载 SDK 需要使用 AWOL 的账号,前往 https://bbs.aw-ol.com/
注册一个就行。其中需要账号等级为 LV2,可以去这个帖子:https://bbs.aw-ol.com/topic/4158/share/1 水四条回复就有 LV2 等级了。
安装 repo 管理器
BSP 使用 repo
下载,首先安装 repo
,这里建议使用国内镜像源安装
mkdir -p ~/.bin
PATH="${HOME}/.bin:${PATH}"
curl https://mirrors.bfsu.edu.cn/git/git-repo > ~/.bin/repo
chmod a+rx ~/.bin/repo
请注意这里使用的是临时安装,安装完成后重启终端就没有了,需要再次运行下面的命令才能使用,如何永久安装请自行百度。
PATH="${HOME}/.bin:${PATH}"
安装使用 repo
的过程中会遇到各种错误,请百度解决。repo 是谷歌开发的,repo 的官方服务器是谷歌的服务器,repo 每次运行时需要检查更新然后卡死,这是很正常的情况,所以在国内需要更换镜像源提高下载速度。将如下内容复制到你的~/.bashrc
里
echo export REPO_URL='https://mirrors.bfsu.edu.cn/git/git-repo' >> ~/.bashrc
source ~/.bashrc
如果您使用的是 dash、hash、 zsh 等 shell,请参照 shell 的文档配置。环境变量配置一个 REPO_URL
的地址
配置一下 git 身份认证,设置保存 git 账号密码不用每次都输入。
git config --global credential.helper store
新建文件夹保存 SDK
使用 mkdir
命令新建文件夹,保存之后需要拉取的 SDK,然后 cd
进入到刚才新建的文件夹中。
mkdir tina-v853-open
cd tina-v853-open
初始化 repo 仓库
使用 repo init
命令初始化仓库,tina-v853-open
的仓库地址是 https://sdk.aw-ol.com/git_repo/V853Tina_Open/manifest.git
需要执行命令:
repo init -u https://sdk.aw-ol.com/git_repo/V853Tina_Open/manifest.git -b master -m tina-v853-open.xml
拉取 SDK
repo sync
创建开发环境
repo start devboard-v853-tina-for-awol --all
适配 TinyVision 板子
刚才下载到的 SDK 只支持一个板子,售价 1999 的 V853-Vision
开发板,这里要添加自己的板子的适配。
下载支持包:
或者可以在:https://github.com/YuzukiHD/TinyVision/tree/main/tina 下载到文件,不过这部分没预先下载软件包到 dl 文件夹所以编译的时候需要手动下载。
放到 SDK 的主目录下
运行解压指令
tar xvf tina-bsp-tinyvision.tar.gz
即可使 Tina SDK 支持 TinyVision 板子
初始化 SDK 环境
每次开发之前都需要初始化 SDK 环境,命令如下
source build/envsetup.sh
然后按 1 选择 TinyVision
适配 ISP
Tina SDK 内置一个 libAWispApi 的包,支持在用户层对接 ISP,但是很可惜这个包没有适配 V85x 系列,这里就需要自行适配。其实适配很简单,SDK 已经提供了 lib 只是没提供编译支持。我们需要加上这个支持。
前往 openwrt/package/allwinner/vision/libAWIspApi/machinfo
文件夹中,新建一个文件夹 v851se
,然后新建文件 build.mk
写入如下配置:
ISP_DIR:=isp600
对于 v851s,v853 也可以这样操作,然后 m menuconfig
勾选上这个包
开启 camerademo 测试摄像头
进入 m menuconfig
进入如下页面进行配置。
Allwinner --->
Vision --->
<*> camerademo........................................ camerademo test sensor --->
[*] Enabel vin isp support
编译系统然后烧录系统,运行命令 camerademo
,可以看到是正常拍摄照片的
适配板载 SD NAND
设备树中的 SDC2 节点修改如下,之后线刷即可
&sdc2 {
non-removable;
bus-width = <4>;
mmc-ddr-3_3v;
/*mmc-hs200-1_8v;*/
/*mmc-hs400-1_8v;*/
no-sdio;
/delete-property/ no-sd;
no-mmc;
ctl-spec-caps = <0x8>;
cap-mmc-highspeed;
sunxi-signal-vol-sw-without-pmu;
sunxi-power-save-mode;
/*sunxi-dis-signal-vol-sw;*/
max-frequency = <50000000>;
/*vmmc-supply = <®_dcdc1>;*/
/*emmc io vol 3.3v*/
/*vqmmc-supply = <®_aldo1>;*/
/*emmc io vol 1.8v*/
/*vqmmc-supply = <®_eldo1>;*/
status = "okay";
};
适配 OpenCV
勾选 OpenCV 包
m menuconfig
进入软件包配置,勾选
OpenCV --->
<*> opencv....................................................... opencv libs
[*] Enabel sunxi vin isp support
OpenCV 适配过程
本部分的操作已经包含在 tina-bsp-tinyvision.tar.gz 中了,已经适配好了,如果不想了解如何适配 OpenCV 可以直接跳过这部分
OpenCV 的多平面视频捕获支持
一般来说,如果不适配 OpenCV 直接开摄像头,会得到一个报错:
[ 702.464977] [VIN_ERR]video0 has already stream off
[ 702.473357] [VIN_ERR]gc2053_mipi is not used, video0 cannot be close!
VIDEOIO ERROR: V4L2: Unable to capture video memory.VIDEOIO ERROR: V4L: can't open camera by index 0
/dev/video0 does not support memory mapping
Could not open video device.
这是由于 OpenCV 的 V4L2 实现是使用的 V4L2_CAP_VIDEO_CAPTURE
标准,而 sunxi-vin
驱动的 RAW Sensor 平台使用的是 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
,导致了默认 OpenCV 的配置错误。
V4L2_CAP_VIDEO_CAPTURE_MPLANE
和V4L2_BUF_TYPE_VIDEO_CAPTURE
是 Video4Linux2(V4L2)框架中用于视频捕获的不同类型和能力标志。
V4L2_CAP_VIDEO_CAPTURE_MPLANE
: 这个标志指示设备支持多平面(multi-plane)视频捕获。在多平面捕获中,图像数据可以分解成多个平面(planes),每个平面包含不同的颜色分量或者图像数据的不同部分。这种方式可以提高效率和灵活性,尤其适用于处理涉及多个颜色分量或者多个图像通道的视频流。V4L2_BUF_TYPE_VIDEO_CAPTURE
: 这个类型表示普通的单平面(single-plane)视频捕获。在单平面捕获中,图像数据以单个平面的形式存储,即所有的颜色分量或者图像数据都保存在一个平面中。
因此,区别在于支持的数据格式和存储方式。V4L2_CAP_VIDEO_CAPTURE_MPLANE
表示设备支持多平面视频捕获,而V4L2_BUF_TYPE_VIDEO_CAPTURE
表示普通的单平面视频捕获。
这里就需要通过检查capability.capabilities
中是否包含V4L2_CAP_VIDEO_CAPTURE
标志来确定是否支持普通的视频捕获类型。如果支持,那么将type
设置为V4L2_BUF_TYPE_VIDEO_CAPTURE
。
如果不支持普通的视频捕获类型,那么通过检查capability.capabilities
中是否包含V4L2_CAP_VIDEO_CAPTURE_MPLANE
标志来确定是否支持多平面视频捕获类型。如果支持,那么将type
设置为V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
。
例如如下修改:
- form.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
- form.fmt.pix.pixelformat = palette;
- form.fmt.pix.field = V4L2_FIELD_ANY;
- form.fmt.pix.width = width;
- form.fmt.pix.height = height;
+ if (capability.capabilities & V4L2_CAP_VIDEO_CAPTURE) {
+ form.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ form.fmt.pix.pixelformat = palette;
+ form.fmt.pix.field = V4L2_FIELD_NONE;
+ form.fmt.pix.width = width;
+ form.fmt.pix.height = height;
+ } else if (capability.capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE) {
+ form.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
+ form.fmt.pix_mp.width = width;
+ form.fmt.pix_mp.height = height;
+ form.fmt.pix_mp.pixelformat = palette;
+ form.fmt.pix_mp.field = V4L2_FIELD_NONE;
+ }
这段代码是在设置视频捕获的格式和参数时进行了修改。
原来的代码中,直接设置了form.type
为V4L2_BUF_TYPE_VIDEO_CAPTURE
,表示使用普通的视频捕获类型。然后设置了其他参数,如像素格式(pixelformat
)、帧字段(field
)、宽度(width
)和高度(height
)等。
修改后的代码进行了条件判断,根据设备的能力选择合适的视频捕获类型。如果设备支持普通的视频捕获类型(V4L2_CAP_VIDEO_CAPTURE
标志被设置),则使用普通的视频捕获类型并设置相应的参数。如果设备支持多平面视频捕获类型(V4L2_CAP_VIDEO_CAPTURE_MPLANE
标志被设置),则使用多平面视频捕获类型并设置相应的参数。
对于普通的视频捕获类型,设置的参数与原来的代码一致,只是将帧字段(field
)从V4L2_FIELD_ANY
改为V4L2_FIELD_NONE
,表示不指定特定的帧字段。
对于多平面视频捕获类型,设置了新的参数,如多平面的宽度(pix_mp.width
)、高度(pix_mp.height
)、像素格式(pix_mp.pixelformat
)和帧字段(pix_mp.field
)等。
通过这个修改,可以根据设备的能力选择适当的视频捕获类型,并设置相应的参数,以满足不同设备的要求。
OpenCV 的 ISP 支持
OpenCV 默认不支持开启 RAW Sensor,不过现在需要配置为 OpenCV 开启 RAW Sensor 抓图,然后通过 OpenCV 送图到之前适配的 libAWispApi 库进行 ISP 处理。在这里增加一个函数作为 RAW Sensor 抓图的处理。
#ifdef __USE_VIN_ISP__
bool CvCaptureCAM_V4L::RAWSensor()
{
struct v4l2_control ctrl;
struct v4l2_queryctrl qc_ctrl;
memset(&ctrl, 0, sizeof(struct v4l2_control));
memset(&qc_ctrl, 0, sizeof(struct v4l2_queryctrl));
ctrl.id = V4L2_CID_SENSOR_TYPE;
qc_ctrl.id = V4L2_CID_SENSOR_TYPE;
if (-1 == ioctl (deviceHandle, VIDIOC_QUERYCTRL, &qc_ctrl)){
fprintf(stderr, "V4L2: %s QUERY V4L2_CID_SENSOR_TYPE failed\n", deviceName.c_str());
return false;
}
if (-1 == ioctl(deviceHandle, VIDIOC_G_CTRL, &ctrl)) {
fprintf(stderr, "V4L2: %s G_CTRL V4L2_CID_SENSOR_TYPE failed\n", deviceName.c_str());
return false;
}
return ctrl.value == V4L2_SENSOR_TYPE_RAW;
}
#endif
这段代码的功能是检查V4L2摄像头设备的传感器类型是否为RAW格式。它使用了V4L2的ioctl函数来查询和获取传感器类型信息。具体步骤如下:
- 定义了两个v4l2_control结构体变量
ctrl
和qc_ctrl
,并初始化为零 - 将
ctrl.id
和qc_ctrl.id
分别设置为V4L2_CID_SENSOR_TYPE
,表示要查询的控制和查询ID - 使用
ioctl
函数的VIDIOC_QUERYCTRL命令来查询传感器类型的控制信息,并将结果保存在qc_ctrl
中 - 如果查询失败(
ioctl
返回-1),则输出错误信息并返回false - 使用
ioctl
函数的VIDIOC_G_CTRL命令来获取传感器类型的当前值,并将结果保存在ctrl
中 - 如果获取失败(
ioctl
返回-1),则输出错误信息并返回false - 检查
ctrl.value
是否等于V4L2_SENSOR_TYPE_RAW
,如果相等,则返回true,表示传感器类型为RAW格式;否则返回false
并且使用了#ifdef __USE_VIN_ISP__
指令。这表示只有在定义了__USE_VIN_ISP__
宏时,才会编译和执行这段代码
然后在 OpenCV 的 bool CvCaptureCAM_V4L::streaming(bool startStream)
捕获流函数中添加 ISP 处理
#ifdef __USE_VIN_ISP__
RawSensor = RAWSensor();
if (startStream && RawSensor) {
int VideoIndex = -1;
sscanf(deviceName.c_str(), "/dev/video%d", &VideoIndex);
IspPort = CreateAWIspApi();
IspId = -1;
IspId = IspPort->ispGetIspId(VideoIndex);
if (IspId >= 0)
IspPort->ispStart(IspId);
} else if (RawSensor && IspId >= 0 && IspPort) {
IspPort->ispStop(IspId);
DestroyAWIspApi(IspPort);
IspPort = NULL;
IspId = -1;
}
#endif
这段代码是在条件编译__USE_VIN_ISP__
的情况下进行了修改。
-
首先,它创建了一个
RawSensor
对象,并检查startStream
和RawSensor
是否为真。如果满足条件,接下来会解析设备名称字符串,提取出视频索引号。 -
然后,它调用
CreateAWIspApi()
函数创建了一个AWIspApi对象,并初始化变量IspId
为-1。接着,通过调用ispGetIspId()
函数获取指定视频索引号对应的ISP ID,并将其赋值给IspId
。如果IspId
大于等于0,表示获取到有效的ISP ID,就调用ispStart()
函数启动ISP流处理。 -
如果不满足第一个条件,即
startStream
为假或者没有RawSensor
对象,那么会检查IspId
是否大于等于0并且IspPort
对象是否存在。如果满足这些条件,说明之前已经启动了ISP流处理,此时会调用ispStop()
函数停止ISP流处理,并销毁IspPort
对象。最后,将IspPort
置为空指针,将IspId
重置为-1。
这段代码主要用于控制图像信号处理(ISP)的启动和停止。根据条件的不同,可以选择在开始视频流捕获时启动ISP流处理,或者在停止视频流捕获时停止ISP流处理,以便对视频数据进行处理和增强。
至于其他包括编译脚本的修改,全局变量定义等操作,可以参考补丁文件 openwrt/package/thirdparty/vision/opencv/patches/0004-support-sunxi-vin-camera.patch
使用 OpenCV 捕获摄像头并且输出到屏幕上
快速测试
这个 DEMO 也已经包含在 tina-bsp-tinyvision.tar.gz 中了,可以快速测试这个 DEMO
运行 m menuconfig
OpenCV --->
<*> opencv....................................................... opencv libs
[*] Enabel sunxi vin isp support
<*> opencv_camera.............................opencv_camera and display image
源码详解
编写一个程序,使用 OpenCV 捕获摄像头输出并且显示到屏幕上,程序如下:
#include <fcntl.h>
#include <fstream>
#include <iostream>
#include <linux/fb.h>
#include <signal.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <opencv2/opencv.hpp>
#define DISPLAY_X 240
#define DISPLAY_Y 240
static cv::VideoCapture cap;
struct framebuffer_info {
uint32_t bits_per_pixel;
uint32_t xres_virtual;
};
struct framebuffer_info get_framebuffer_info(const char* framebuffer_device_path)
{
struct framebuffer_info info;
struct fb_var_screeninfo screen_info;
int fd = -1;
fd = open(framebuffer_device_path, O_RDWR);
if (fd >= 0) {
if (!ioctl(fd, FBIOGET_VSCREENINFO, &screen_info)) {
info.xres_virtual = screen_info.xres_virtual;
info.bits_per_pixel = screen_info.bits_per_pixel;
}
}
return info;
};
/* Signal handler */
static void terminate(int sig_no)
{
printf("Got signal %d, exiting ...\n", sig_no);
cap.release();
exit(1);
}
static void install_sig_handler(void)
{
signal(SIGBUS, terminate);
signal(SIGFPE, terminate);
signal(SIGHUP, terminate);
signal(SIGILL, terminate);
signal(SIGINT, terminate);
signal(SIGIOT, terminate);
signal(SIGPIPE, terminate);
signal(SIGQUIT, terminate);
signal(SIGSEGV, terminate);
signal(SIGSYS, terminate);
signal(SIGTERM, terminate);
signal(SIGTRAP, terminate);
signal(SIGUSR1, terminate);
signal(SIGUSR2, terminate);
}
int main(int, char**)
{
const int frame_width = 480;
const int frame_height = 480;
const int frame_rate = 30;
install_sig_handler();
framebuffer_info fb_info = get_framebuffer_info("/dev/fb0");
cap.open(0);
if (!cap.isOpened()) {
std::cerr << "Could not open video device." << std::endl;
return 1;
}
std::cout << "Successfully opened video device." << std::endl;
cap.set(cv::CAP_PROP_FRAME_WIDTH, frame_width);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, frame_height);
cap.set(cv::CAP_PROP_FPS, frame_rate);
std::ofstream ofs("/dev/fb0");
cv::Mat frame;
cv::Mat trams_temp_fream;
cv::Mat yuv_frame;
while (true) {
cap >> frame;
if (frame.depth() != CV_8U) {
std::cerr << "Not 8 bits per pixel and channel." << std::endl;
} else if (frame.channels() != 3) {
std::cerr << "Not 3 channels." << std::endl;
} else {
cv::transpose(frame, frame);
cv::flip(frame, frame, 0);
cv::resize(frame, frame, cv::Size(DISPLAY_X, DISPLAY_Y));
int framebuffer_width = fb_info.xres_virtual;
int framebuffer_depth = fb_info.bits_per_pixel;
cv::Size2f frame_size = frame.size();
cv::Mat framebuffer_compat;
switch (framebuffer_depth) {
case 16:
cv::cvtColor(frame, framebuffer_compat, cv::COLOR_BGR2BGR565);
for (int y = 0; y < frame_size.height; y++) {
ofs.seekp(y * framebuffer_width * 2);
ofs.write(reinterpret_cast<char*>(framebuffer_compat.ptr(y)), frame_size.width * 2);
}
break;
case 32: {
std::vector<cv::Mat> split_bgr;
cv::split(frame, split_bgr);
split_bgr.push_back(cv::Mat(frame_size, CV_8UC1, cv::Scalar(255)));
cv::merge(split_bgr, framebuffer_compat);
for (int y = 0; y < frame_size.height; y++) {
ofs.seekp(y * framebuffer_width * 4);
ofs.write(reinterpret_cast<char*>(framebuffer_compat.ptr(y)), frame_size.width * 4);
}