基于i.MX6ULL开发板与OV5640摄像头实现QT相机应用开发

1.硬件软件要求

正点原子i.MX6ULL开发板+800*480显示屏,OV5640摄像头,内核版本(NXP:linux-imx-rel_imx_4.1.15_2.1.0_ga),qt版本(qt5.12.9)

2.驱动层

2.1获取OV5640驱动

推荐使用正点原子官方内核中的OV5640驱动文件,因为NXP内核中的OV5640驱动文件缺少部分适配i.MX6ULL开发板的格式配置,需要进行一些修改。在官方资料中找到正点原子Linux出厂源码,将内核文件进行下载解压,在路径drivers/media/platform/mxc/subdev找到mx6s_capture.c,ov5640.c和ov5640af.h,新建new_ov5640目录将三个文件拷贝到该目录下并新建ov5640.h和Makefile

Makefile

bash 复制代码
ARCH = arm
CROSS_COMPILE = arm-linux-gnueabihf-
//自己内核路径
KERNELDIR := /home/linux/imx6ull_dev/linux_kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
    obj-m := ov5640.o mx6s_capture.o

build: kernel_modules

kernel_modules:
        $(MAKE) -C $(KERNELDIR) \
                ARCH=$(ARCH) \
                CROSS_COMPILE=$(CROSS_COMPILE) \
                M=$(CURRENT_PATH) modules
clean:
        $(MAKE) -C $(KERNELDIR) \
                ARCH=$(ARCH) \
                CROSS_COMPILE=$(CROSS_COMPILE) \
                M=$(CURRENT_PATH) clean

在当前目录下进行make

bash 复制代码
linux@linux-virtual-machine:~/imxcull_dev/nfs/ubuntu_sys/ubuntu_rootfs/camera_demo$ make
make -C /home/linux/imxcull_dev/linux_kernel_bak/linux-imx-rel_imx_4.1.15_2.1.0_ga \
	ARCH=arm \
	CROSS_COMPILE=arm-linux-gnueabihf- \
	M=/home/linux/imxcull_dev/nfs/ubuntu_sys/ubuntu_rootfs/camera_demo modules
make[1]: 进入目录"/home/linux/imxcull_dev/linux_kernel_bak/linux-imx-rel_imx_4.1.15_2.1.0_ga"
  CC [M]  /home/linux/imxcull_dev/nfs/ubuntu_sys/ubuntu_rootfs/camera_demo/ov5640.o
  CC [M]  /home/linux/imxcull_dev/nfs/ubuntu_sys/ubuntu_rootfs/camera_demo/mx6s_capture.o
  Building modules, stage 2.
  MODPOST 2 modules
  CC      /home/linux/imxcull_dev/nfs/ubuntu_sys/ubuntu_rootfs/camera_demo/mx6s_capture.mod.o
  LD [M]  /home/linux/imxcull_dev/nfs/ubuntu_sys/ubuntu_rootfs/camera_demo/mx6s_capture.ko
  CC      /home/linux/imxcull_dev/nfs/ubuntu_sys/ubuntu_rootfs/camera_demo/ov5640.mod.o
  LD [M]  /home/linux/imxcull_dev/nfs/ubuntu_sys/ubuntu_rootfs/camera_demo/ov5640.ko
make[1]: 离开目录"/home/linux/imxcull_dev/linux_kernel_bak/linux-imx-rel_imx_4.1.15_2.1.0_ga"
linux@linux-virtual-machine:~/imxcull_dev/nfs/ubuntu_sys/ubuntu_rootfs/camera_demo$ ls
Makefile       Module.symvers  mx6s_capture.ko     mx6s_capture.mod.o  ov5640af.h  ov5640.h   ov5640.mod.c  ov5640.o
modules.order  mx6s_capture.c  mx6s_capture.mod.c  mx6s_capture.o      ov5640.c    ov5640.ko  ov5640.mod.o

其中mx6s_capture.ko和ov5640.ko就是可加载的驱动

2.2设备树修改

在内核目录arch/arm/boot/dts路径下找到imx6ull-14x14-evk.dts设备树文件,在i2c和CSI节点下添加ov5640摄像头描述信息

bash 复制代码
        ov5640: ov5640@3c {
                compatible = "ovti,ov5640";
                reg = <0x3c>;
                pinctrl-names = "default";
                pinctrl-0 = <&pinctrl_csi1
                             &csi_pwn_rst>;
                clocks = <&clks IMX6UL_CLK_CSI>;
                clock-names = "csi_mclk";
                pwn-gpios = <&gpio1 4 1>;
                rst-gpios = <&gpio1 2 0>;
        //      pwn-gpios = <&gpio_spi 6 1>;
        //      rst-gpios = <&gpio_spi 5 0>;
                csi_id = <0>;
                mclk = <24000000>;
                mclk_source = <0>;
                status = "okay";
                port {
                        ov5640_ep: endpoint {
                                remote-endpoint = <&csi1_ep>;
                        };
                };
        };

在csi节点下添加描述信息

bash 复制代码
&csi {
	status = "okay";
 
	port {
		csi1_ep: endpoint {
			remote-endpoint = <&ov5640_ep>;
		};
	};
};

并配置ov5640的电源和复位管脚

bash 复制代码
                csi_pwn_rst: csi_pwn_rstgrp {
                        fsl,pins = <
                                MX6UL_PAD_GPIO1_IO02__GPIO1_IO02        0x10b0
                                MX6UL_PAD_GPIO1_IO04__GPIO1_IO04        0x10b0
                        >;
                };

然后找找看有没有和ov5640管脚冲突的管脚,比如我的设备树中的电阻屏设备

bash 复制代码
                //与ov5640管脚冲突
                pinctrl_tsc: tscgrp {
                        fsl,pins = <
                                MX6UL_PAD_GPIO1_IO01__GPIO1_IO01        0xb0
                                MX6UL_PAD_GPIO1_IO02__GPIO1_IO02        0xb0
                                MX6UL_PAD_GPIO1_IO03__GPIO1_IO03        0xb0
                                MX6UL_PAD_GPIO1_IO04__GPIO1_IO04        0xb0
                        >;
                };

直接找到tsc节点将okay改为disabled(因为我们使用的是电容屏,所以不影响)

bash 复制代码
&tsc {
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_tsc>;
        xnur-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
        measure-delay-time = <0xffff>;
        pre-charge-time = <0xfff>;
        //设置"disabled"
        status = "disabled";
};

修改完后进行保存

2.3内核配置

在内核目录下输入命令make ARCH=arm menuconfig进入图形配置界面,找到Device Drivers --->Multimedia support --->V4L platform devices --->MXC Video For Linux Video Capture勾选

再进入MXC Camera/V4L2 PRP Features support --->OmniVision ov5640 camera support勾选

然后进行save保存

2.4编译

编译内核:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j10 > /dev/null (-j10 > /dev/null:10核编译且仅输出错误信息)

编译设备树:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

2.5测试

获取正点原子官方提供的ov5640应用层的测试代码,用来检测设备树,内核和驱动是否正常。

先将上面编译好的mx6s_capture.ko和ov5640.ko拷贝到开发板的根目录下,通过insmod命令进行加载驱动

bash 复制代码
insmod mx6s_capture.ko
insmod ov5640.ko

然后检查dev目录下是否有video1设备文件(USB摄像头是video0)

编译测试代码

bash 复制代码
/***************************************************************
 Copyright ? ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
 文件名 : v4l2_camera.c
 作者 : 邓涛
 版本 : V1.0
 描述 : V4L2摄像头应用编程实战
 其他 : 无
 论坛 : www.openedv.com
 日志 : 初版 V1.0 2021/7/09 邓涛创建
 ***************************************************************/
 
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/fb.h>
#include <time.h>
#include <linux/rtc.h>
#include <sys/time.h>
 
#define FB_DEV "/dev/fb0"   // LCD设备节点
#define FRAMEBUFFER_COUNT 3 // 帧缓冲数量
#define RTC_DEV "/dev/rtc0" // RTC设备节点,fd名称
/*** 摄像头像素格式及其描述信息 ***/
typedef struct camera_format
{
    unsigned char description[32]; // 字符串描述信息
    unsigned int pixelformat;      // 像素格式
} cam_fmt;
 
/*** 描述一个帧缓冲的信息 ***/
typedef struct cam_buf_info
{
    unsigned short *start; // 帧缓冲起始地址
    unsigned long length;  // 帧缓冲长度
} cam_buf_info;
 
unsigned short timeBUuffer[1024 * 64 * 2]; // 存放时间的像素点信息
 
static int width;                          // LCD宽度
static int height;                         // LCD高度
static unsigned short *screen_base = NULL; // LCD显存基地址
static int fb_fd = -1;                     // LCD设备文件描述符
static int v4l2_fd = -1;                   // 摄像头设备文件描述符
static cam_buf_info buf_infos[FRAMEBUFFER_COUNT];
static cam_fmt cam_fmts[10];
static int frm_width, frm_height; // 视频帧宽度和高度
 
 
static int fb_dev_init(void)
{
    struct fb_var_screeninfo fb_var = {0};
    struct fb_fix_screeninfo fb_fix = {0};
    unsigned long screen_size;
 
    /* 打开framebuffer设备 */
    fb_fd = open(FB_DEV, O_RDWR);
    if (0 > fb_fd)
    {
        fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
        return -1;
    }
 
    /* 获取framebuffer设备信息 */
    ioctl(fb_fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fb_fd, FBIOGET_FSCREENINFO, &fb_fix);
 
    screen_size = fb_fix.line_length * fb_var.yres;
    width = fb_var.xres;
    height = fb_var.yres;
 
    /* 内存映射 */
    screen_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    if (MAP_FAILED == (void *)screen_base)
    {
        perror("mmap error");
        close(fb_fd);
        return -1;
    }
 
    /* LCD背景刷白 */
    memset(screen_base, 0xF800, screen_size);
    return 0;
}
 
static int v4l2_dev_init(const char *device)
{
    struct v4l2_capability cap = {0};
 
    /* 打开摄像头 */
    v4l2_fd = open(device, O_RDWR);
    if (0 > v4l2_fd)
    {
        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
        return -1;
    }
 
    /* 查询设备功能 */
    ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap);
 
    /* 判断是否是视频采集设备 */
    if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities))
    {
        fprintf(stderr, "Error: %s: No capture video device!\n", device);
        close(v4l2_fd);
        return -1;
    }
 
    return 0;
}
 
static void v4l2_enum_formats(void)
{
    struct v4l2_fmtdesc fmtdesc = {0};
 
    /* 枚举摄像头所支持的所有像素格式以及描述信息 */
    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FMT, &fmtdesc))
    {
 
        // 将枚举出来的格式以及描述信息存放在数组中
        cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;
        strcpy(cam_fmts[fmtdesc.index].description, fmtdesc.description);
        fmtdesc.index++;
    }
}
 
static void v4l2_print_formats(void)
{
    struct v4l2_frmsizeenum frmsize = {0};
    struct v4l2_frmivalenum frmival = {0};
    int i;
 
    frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    for (i = 0; cam_fmts[i].pixelformat; i++)
    {
 
        printf("format<0x%x>, description<%s>\n", cam_fmts[i].pixelformat,
               cam_fmts[i].description);
 
        /* 枚举出摄像头所支持的所有视频采集分辨率 */
        frmsize.index = 0;
        frmsize.pixel_format = cam_fmts[i].pixelformat;
        frmival.pixel_format = cam_fmts[i].pixelformat;
        while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize))
        {
 
            printf("size<%d*%d> ",
                   frmsize.discrete.width,
                   frmsize.discrete.height);
            frmsize.index++;
 
            /* 获取摄像头视频采集帧率 */
            frmival.index = 0;
            frmival.width = frmsize.discrete.width;
            frmival.height = frmsize.discrete.height;
            while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival))
            {
 
                printf("<%dfps>", frmival.discrete.denominator /
                                      frmival.discrete.numerator);
                frmival.index++;
            }
            printf("\n");
        }
        printf("\n");
    }
}
 
static int v4l2_set_format(void)
{
    struct v4l2_format fmt = {0};
    struct v4l2_streamparm streamparm = {0};
 
    /* 设置帧格式 */
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // type类型
    fmt.fmt.pix.width = width;              // 视频帧宽度
    fmt.fmt.pix.height = height;            // 视频帧高度
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
    // V4L2_PIX_FMT_RGB565; // 像素格式
    // fmt.fmt.pix.field = V4L2_FIELD_ANY;
    if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt))
    {
        fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));
        return -1;
    }
 
    /*** 判断是否已经设置为我们要求的RGB565像素格式
    如果没有设置成功表示该设备不支持RGB565像素格式 */
    if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat)
    {
        fprintf(stderr, "Error: the device does not support RGB565 format!\n");
        return -1;
    }
 
    frm_width = fmt.fmt.pix.width;   // 获取实际的帧宽度
    frm_height = fmt.fmt.pix.height; // 获取实际的帧高度
    printf("视频帧大小<%d * %d>\n", frm_width, frm_height);
 
    /* 获取streamparm */
    streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);
 
    /** 判断是否支持帧率设置 **/
    if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability)
    {
        streamparm.parm.capture.timeperframe.numerator = 1;
        streamparm.parm.capture.timeperframe.denominator = 30; // 30fps
        if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm))
        {
            fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
            return -1;
        }
    }
 
    return 0;
}
 
static int v4l2_init_buffer(void)
{
    struct v4l2_requestbuffers reqbuf = {0};
    struct v4l2_buffer buf = {0};
 
    /* 申请帧缓冲 */
    reqbuf.count = FRAMEBUFFER_COUNT; // 帧缓冲的数量
    reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuf.memory = V4L2_MEMORY_MMAP;
    if (0 > ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf))
    {
        fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
        return -1;
    }
 
    /* 建立内存映射 */
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++)
    {
 
        ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);
        buf_infos[buf.index].length = buf.length;
        buf_infos[buf.index].start = mmap(NULL, buf.length,
                                          PROT_READ | PROT_WRITE, MAP_SHARED,
                                          v4l2_fd, buf.m.offset);
        if (MAP_FAILED == buf_infos[buf.index].start)
        {
            perror("mmap error");
            return -1;
        }
    }
 
    /* 入队 */
    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++)
    {
 
        if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf))
        {
            fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));
            return -1;
        }
    }
 
    return 0;
}
 
static int v4l2_stream_on(void)
{
    /* 打开摄像头、摄像头开始采集数据 */
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 
    if (0 > ioctl(v4l2_fd, VIDIOC_STREAMON, &type))
    {
        fprintf(stderr, "ioctl error: VIDIOC_STREAMON: %s\n", strerror(errno));
        return -1;
    }
 
    return 0;
}
 
static void v4l2_read_data(void)
{
    struct v4l2_buffer buf = {0};
    unsigned short *base;
    unsigned short *start;
    int min_w, min_h;
    int j,i;
 
    if (width > frm_width)
        min_w = frm_width;
    else
        min_w = width;
    if (height > frm_height)
        min_h = frm_height;
    else
        min_h = height;
 
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    for ( ; ; ) {
 
        for(buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {
 
            ioctl(v4l2_fd, VIDIOC_DQBUF, &buf);     //出队
            for (j = 0, base=screen_base, start=buf_infos[buf.index].start;
                        j < min_h; j++) {
 
                memcpy(base, start, min_w * 2); //RGB565 一个像素占2个字节
                #if 0
                for (i = 0; i < 1024;i++)
                {
                    printf("%d\r\n", base[i]);
                }
                #endif
                base += width; // LCD显示指向下一行
                start += frm_width;//指向下一行数据
            }
 
            // 数据处理完之后、再入队、往复
            ioctl(v4l2_fd, VIDIOC_QBUF, &buf);
        }
    }
}
 
int main(int argc, char *argv[])
{
 
    if (2 != argc)
    {
        fprintf(stderr, "Usage: %s <video_dev>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
 
    /* 初始化LCD */
    if (fb_dev_init())
        exit(EXIT_FAILURE);
 
    /* 初始化摄像头 */
    if (v4l2_dev_init(argv[1]))
        exit(EXIT_FAILURE);
 
    /* 枚举所有格式并打印摄像头支持的分辨率及帧率 */
    v4l2_enum_formats();
    v4l2_print_formats();
 
    /* 设置格式 */
    if (v4l2_set_format())
        exit(EXIT_FAILURE);
 
    /* 初始化帧缓冲:申请、内存映射、入队 */
    if (v4l2_init_buffer())
        exit(EXIT_FAILURE);
 
    /* 开启视频采集 */
    if (v4l2_stream_on())
        exit(EXIT_FAILURE);
 
    /* 读取数据:出队 */
    v4l2_read_data(); // 在函数内循环采集数据、将其显示到LCD屏
 
    exit(EXIT_SUCCESS);
}

编译运行画面如下

3.应用层

3.1概述

基于v4l2应用编程结合正点原子官方的代码在qt中创建一个单独的线程去获取摄像头图像数据,图像数据格式为RGB565,尺寸640x480,以帧格式在队列中缓存,留出160x480的显示屏做qt的ui按键交互界面,主界面为相机界面,接收摄像头数据并显示在界面左侧640x480,右侧为拍照和相册俩个按键,拍的照片存储在当前目录下的opt/photos目录,副界面为相册界面,负责显示存储在opt/photos目录下的照片,并配有切换照片、删除照片和返回相机按键,具体项目代码如下:

3.2代码实现

qt_camera.pro
bash 复制代码
QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    albumwindow.cpp \
    camerathread.cpp \
    main.cpp \
    mainwindow.cpp \
    v4l2_camera.cpp

HEADERS += \
    albumwindow.h \
    camerathread.h \
    mainwindow.h \
    v4l2_camera.h

FORMS += \
    mainwindow.ui

# 启用JPG/PNG图片支持
DEFINES += QT_NO_WARNING_OUTPUT

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
albumwindow.h (相册)
cpp 复制代码
#ifndef ALBUMWINDOW_H
#define ALBUMWINDOW_H

#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QFileInfoList>

class AlbumWindow : public QWidget
{
    Q_OBJECT
public:
    explicit AlbumWindow(QWidget *parent = nullptr);

protected:
    void showEvent(QShowEvent *event) override;
    void hideEvent(QHideEvent *event) override;

private slots:
    void prevPhoto();
    void nextPhoto();
    void deletePhoto();
    void backToCamera();

private:
    void loadPhotoList();
    void showCurrentPhoto();
    void updateButtonState();

    QLabel *m_showLab;
    QPushButton *m_prevBtn;
    QPushButton *m_nextBtn;
    QPushButton *m_delBtn;
    QPushButton *m_backBtn;

    QFileInfoList m_photoList;
    int m_currentIndex;
};

#endif
albumwindow.c
cpp 复制代码
#include "albumwindow.h"
#include <QDir>
#include <QFile>
#include <QDebug>
#include <QPixmap>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include "v4l2_camera.h"

AlbumWindow::AlbumWindow(QWidget *parent) : QWidget(parent)
{
    // 窗口大小800*480
    this->setFixedSize(800, 480);
    this->setWindowTitle("Album");
    this->setAutoFillBackground(true);
    this->setStyleSheet("background-color: black;");

    // ===================== 照片显示区(640*480)=====================
    m_showLab = new QLabel(this);
    m_showLab->setFixedSize(640, 480);
    m_showLab->setStyleSheet(
        "background-color: black;"
        "color: white;"
        "font-size: 24px;"
        "border: none;"
        );
    m_showLab->setAlignment(Qt::AlignCenter);
    m_showLab->setScaledContents(false);

    // ===================== 右侧按键区(160*480)=====================
    QWidget *btnWidget = new QWidget(this);
    btnWidget->setFixedSize(160, 480);
    btnWidget->setStyleSheet("background-color: #f0f0f0;");

    QVBoxLayout *btnLayout = new QVBoxLayout(btnWidget);
    btnLayout->setSpacing(20);
    btnLayout->setContentsMargins(10, 40, 10, 40);

    m_prevBtn = new QPushButton("上一张");
    m_nextBtn = new QPushButton("下一张");
    m_delBtn = new QPushButton("删除");
    m_backBtn = new QPushButton("返回");

    // 按键ui
    m_prevBtn->setFixedHeight(60);
    m_nextBtn->setFixedHeight(60);
    m_delBtn->setFixedHeight(60);
    m_backBtn->setFixedHeight(60);

    btnLayout->addWidget(m_prevBtn);
    btnLayout->addWidget(m_nextBtn);
    btnLayout->addWidget(m_delBtn);
    btnLayout->addWidget(m_backBtn);
    btnLayout->addStretch();

    // ===================== 水平布局=====================
    QHBoxLayout *mainLayout = new QHBoxLayout(this);
    mainLayout->addWidget(m_showLab);
    mainLayout->addWidget(btnWidget);
    mainLayout->setContentsMargins(0, 0, 0, 0);
    mainLayout->setSpacing(0);
    this->setLayout(mainLayout);

    // 信号绑定
    connect(m_prevBtn, &QPushButton::clicked, this, &AlbumWindow::prevPhoto);
    connect(m_nextBtn, &QPushButton::clicked, this, &AlbumWindow::nextPhoto);
    connect(m_delBtn, &QPushButton::clicked, this, &AlbumWindow::deletePhoto);
    connect(m_backBtn, &QPushButton::clicked, this, &AlbumWindow::backToCamera);

    // 初始化
    m_currentIndex = 0;
    loadPhotoList();
    showCurrentPhoto();
    updateButtonState();

    qDebug() << "Album interface initialized (layout fixed)";
}

// 打开相册
void AlbumWindow::showEvent(QShowEvent *event)
{
    QWidget::showEvent(event);
    g_enable_render = false;

    loadPhotoList();
    showCurrentPhoto();
    updateButtonState();

    qDebug() << "Album: Stop camera render + refresh photos";
}


void AlbumWindow::hideEvent(QHideEvent *event)
{
    QWidget::hideEvent(event);
    g_enable_render = true;
    qDebug() << "Camera: Resume camera render";
}

// 加载相册照片列表
void AlbumWindow::loadPhotoList()
{
    QDir dir("/opt/photos/");
    QStringList filters = {"*.jpg", "*.png", "*.bmp"};
    m_photoList = dir.entryInfoList(filters, QDir::Files, QDir::Time);
}

// 显示照片
void AlbumWindow::showCurrentPhoto()
{
    if (m_photoList.isEmpty()) {
        m_showLab->setText("暂无照片");
        return;
    }

    // 索引保护
    if (m_currentIndex < 0) m_currentIndex = 0;
    if (m_currentIndex >= m_photoList.size()) m_currentIndex = m_photoList.size() - 1;

    QString path = m_photoList[m_currentIndex].filePath();
    QPixmap pix(path);
    if (!pix.isNull()) {
        pix = pix.scaled(640, 480, Qt::KeepAspectRatio, Qt::SmoothTransformation);
        m_showLab->setPixmap(pix);
        qDebug() << "Display photo (640*480):" << path;
    } else {
        m_showLab->setText("Image Error");
    }
}

// 更新按钮状态
void AlbumWindow::updateButtonState()
{
    bool enable = !m_photoList.isEmpty();
    m_prevBtn->setEnabled(enable);
    m_nextBtn->setEnabled(enable);
    m_delBtn->setEnabled(enable);
}

// 上一张照片
void AlbumWindow::prevPhoto()
{
    if (m_photoList.isEmpty()) return;
    m_currentIndex = (m_currentIndex - 1 + m_photoList.size()) % m_photoList.size();
    showCurrentPhoto();
    qDebug() << "Switch to previous photo";
}

// 下一张照片
void AlbumWindow::nextPhoto()
{
    if (m_photoList.isEmpty()) return;
    m_currentIndex = (m_currentIndex + 1) % m_photoList.size();
    showCurrentPhoto();
    qDebug() << "Switch to next photo";
}

// 删除当前照片并显示下一张
void AlbumWindow::deletePhoto()
{
    if (m_photoList.isEmpty()) return;

    QString path = m_photoList[m_currentIndex].filePath();
    QFile::remove(path);
    qDebug() << "Delete photo:" << path;

    loadPhotoList();
    showCurrentPhoto();
    updateButtonState();
}

// 返回相机界面
void AlbumWindow::backToCamera()
{
    this->hide();
    qDebug() << "Back to camera interface";
}
camerathread.h (摄像头采集线程)
cpp 复制代码
#ifndef CAMERATHREAD_H
#define CAMERATHREAD_H

#include <QThread>

// 摄像头采集线程
class CameraThread : public QThread
{
    Q_OBJECT
public:
    explicit CameraThread(QObject *parent = nullptr);

protected:
    void run() override; // 线程入口函数
};

#endif // CAMERATHREAD_H
camerathread.c
cpp 复制代码
#include "camerathread.h"
#include "v4l2_camera.h"
#include <QDebug>

CameraThread::CameraThread(QObject *parent) : QThread(parent)
{
}

void CameraThread::run()
{
    qDebug() << "Camera thread started, device:/dev/video1";

    // 初始化OV5640摄像头
    int ret = camera_init("/dev/video1");
    if (ret < 0) {
        qDebug() << "Camera init failed!";
        return;
    }

    qDebug() << "Camera init success, start capturing...";
    // 启动采集
    camera_start_capture();
}
mainwindow.h (相机主界面)
cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QPushButton>
#include <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include "camerathread.h"
#include "albumwindow.h"

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void capturePhoto(); // 拍照
    void openAlbum();    // 打开相册

private:
    QWidget *m_centralWidget;
    QLabel *m_previewLab; // 预览占位区
    QPushButton *m_captureBtn;
    QPushButton *m_albumBtn;

    CameraThread *m_camThread; // 摄像头采集线程
    AlbumWindow *m_albumWin;   // 相册窗口
};

#endif // MAINWINDOW_H
mainwindow.c
cpp 复制代码
#include "mainwindow.h"
#include <QLabel>
#include <QDateTime>
#include <QFile>
#include <QDebug>
#include "v4l2_camera.h"
#include <QDir>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    // 固定窗口大小
    this->setFixedSize(800, 480);
    this->setWindowTitle("OV5640 Camera");

    // 中心部件
    m_centralWidget = new QWidget(this);
    this->setCentralWidget(m_centralWidget);

    // 1. 左侧预览区
    m_previewLab = new QLabel(m_centralWidget);
    m_previewLab->setFixedSize(640, 480);
    m_previewLab->setStyleSheet("background-color:black;");
    m_previewLab->setText("Camera Preview");
    m_previewLab->setAlignment(Qt::AlignCenter);

    // 2. 右侧按键区
    QWidget *btnWidget = new QWidget(m_centralWidget);
    btnWidget->setFixedSize(160, 480);
    QVBoxLayout *btnLayout = new QVBoxLayout(btnWidget);
    btnLayout->setSpacing(40);
    btnLayout->setContentsMargins(10, 80, 10, 80);

    m_captureBtn = new QPushButton("拍照");
    m_albumBtn = new QPushButton("相册");
    m_captureBtn->setFixedHeight(80);
    m_albumBtn->setFixedHeight(80);

    btnLayout->addWidget(m_captureBtn);
    btnLayout->addWidget(m_albumBtn);
    btnLayout->addStretch();

    // 主布局
    QHBoxLayout *mainLayout = new QHBoxLayout(m_centralWidget);
    mainLayout->addWidget(m_previewLab);
    mainLayout->addWidget(btnWidget);
    mainLayout->setContentsMargins(0,0,0,0);
    mainLayout->setSpacing(0);
    m_centralWidget->setLayout(mainLayout);

    // 绑定信号槽
    connect(m_captureBtn, &QPushButton::clicked, this, &MainWindow::capturePhoto);
    connect(m_albumBtn, &QPushButton::clicked, this, &MainWindow::openAlbum);

    // 初始化摄像头线程
    m_camThread = new CameraThread(this);
    m_camThread->start(); // 启动采集线程

    // 初始化相册窗口
    m_albumWin = new AlbumWindow();

    qDebug() << "Main window created, camera thread started";
}

MainWindow::~MainWindow()
{
    m_camThread->quit();
    m_camThread->wait();
    delete m_albumWin;
}

//拍照
void MainWindow::capturePhoto()
{
    if (!screen_base) {
        qDebug() << "Capture failed: camera not initialized";
        return;
    }

    QDir dir;
    if (!dir.exists("/opt/photos")) {
        dir.mkpath("/opt/photos");
        qDebug() << "Create photo directory: /opt/photos";
    }


    QString fileName = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".jpg";
    QString filePath = "/opt/photos/" + fileName;


    QImage image((uchar *)screen_base, 640, 480, 800*2, QImage::Format_RGB16);
    bool save_ret = image.save(filePath, "JPG", 100);

    if (save_ret) {
        qDebug() << "Capture success! Save to:" << filePath;
    } else {
        qDebug() << "Capture failed! Cannot save image";
    }
}

// 打开相册
void MainWindow::openAlbum()
{
    m_albumWin->show();
    qDebug() << "Open album window";
}
v4l2_camera.h (v4l2解析摄像头数据)
cpp 复制代码
#ifndef V4L2_CAMERA_H
#define V4L2_CAMERA_H

#include <stdint.h>

extern unsigned short *screen_base;
// 是否允许渲染画面到LCD(true=允许,false=禁止)
extern bool g_enable_render;

#ifdef __cplusplus
extern "C" {
#endif

int camera_init(const char *device);
void camera_start_capture(void);

#ifdef __cplusplus
}
#endif

#endif
v4l2_camera.c
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/fb.h>
#include <time.h>
#include <linux/rtc.h>
#include <sys/time.h>
#include "v4l2_camera.h"

#define FB_DEV "/dev/fb0"   // LCD设备节点
#define FRAMEBUFFER_COUNT 3 // 帧缓冲数量
#define RTC_DEV "/dev/rtc0" // RTC设备节点

bool g_enable_render = true;  // 默认允许渲染

/*** 摄像头像素格式及其描述信息 ***/
typedef struct camera_format
{
    unsigned char description[32];
    unsigned int pixelformat;
} cam_fmt;

/*** 帧缓冲信息 ***/
typedef struct cam_buf_info
{
    unsigned short *start;
    unsigned long length;
} cam_buf_info;

unsigned short timeBUuffer[1024 * 64 * 2];

static int width = 640;    // 固定:左预览区宽度640
static int height = 480;   // 固定:预览区高度480
unsigned short *screen_base = NULL; // LCD显存基地址(删除static,对外暴露)
static int fb_fd = -1;
static int v4l2_fd = -1;
static cam_buf_info buf_infos[FRAMEBUFFER_COUNT];
static cam_fmt cam_fmts[10];
static int frm_width, frm_height;

// LCD初始化
static int fb_dev_init(void)
{
    struct fb_var_screeninfo fb_var = {0};
    struct fb_fix_screeninfo fb_fix = {0};
    unsigned long screen_size;

    fb_fd = open(FB_DEV, O_RDWR);
    if (0 > fb_fd)
    {
        fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
        return -1;
    }

    ioctl(fb_fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fb_fd, FBIOGET_FSCREENINFO, &fb_fix);

    screen_size = fb_fix.line_length * fb_var.yres;
    // 保留原LCD分辨率,仅渲染640*480区域
    width = 640;
    height = 480;

    screen_base = (unsigned short *)mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    if (MAP_FAILED == (void *)screen_base)
    {
        perror("mmap error");
        close(fb_fd);
        return -1;
    }

    memset(screen_base, 0xF800, screen_size);
    return 0;
}

// 摄像头设备初始化
static int v4l2_dev_init(const char *device)
{
    struct v4l2_capability cap = {0};

    v4l2_fd = open(device, O_RDWR);
    if (0 > v4l2_fd)
    {
        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
        return -1;
    }

    ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap);

    if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities))
    {
        fprintf(stderr, "Error: %s: No capture video device!\n", device);
        close(v4l2_fd);
        return -1;
    }

    return 0;
}

// 枚举摄像头格式
static void v4l2_enum_formats(void)
{
    struct v4l2_fmtdesc fmtdesc = {0};

    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FMT, &fmtdesc))
    {
        cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;
        strcpy((char *)cam_fmts[fmtdesc.index].description, (char *)fmtdesc.description);
        fmtdesc.index++;
    }
}

// 打印摄像头格式
static void v4l2_print_formats(void)
{
    struct v4l2_frmsizeenum frmsize = {0};
    struct v4l2_frmivalenum frmival = {0};
    int i;

    frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    for (i = 0; cam_fmts[i].pixelformat; i++)
    {
        printf("format<0x%x>, description<%s>\n", cam_fmts[i].pixelformat,
               cam_fmts[i].description);

        frmsize.index = 0;
        frmsize.pixel_format = cam_fmts[i].pixelformat;
        frmival.pixel_format = cam_fmts[i].pixelformat;
        while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize))
        {
            printf("size<%d*%d> ",
                   frmsize.discrete.width,
                   frmsize.discrete.height);
            frmsize.index++;

            frmival.index = 0;
            frmival.width = frmsize.discrete.width;
            frmival.height = frmsize.discrete.height;
            while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival))
            {
                printf("<%dfps>", frmival.discrete.denominator /
                                      frmival.discrete.numerator);
                frmival.index++;
            }
            printf("\n");
        }
        printf("\n");
    }
}

// 设置摄像头格式(640*480 RGB565)
static int v4l2_set_format(void)
{
    struct v4l2_format fmt = {0};
    struct v4l2_streamparm streamparm = {0};

    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 640;    // 固定宽度
    fmt.fmt.pix.height = 480;   // 固定高度
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;

    if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt))
    {
        fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));
        return -1;
    }

    if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat)
    {
        fprintf(stderr, "Error: the device does not support RGB565 format!\n");
        return -1;
    }

    frm_width = fmt.fmt.pix.width;
    frm_height = fmt.fmt.pix.height;
    printf("Video size<%d * %d>\n", frm_width, frm_height);

    streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);

    if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability)
    {
        streamparm.parm.capture.timeperframe.numerator = 1;
        streamparm.parm.capture.timeperframe.denominator = 30;
        if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm))
        {
            fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
            return -1;
        }
    }

    return 0;
}

// 初始化帧缓冲
static int v4l2_init_buffer(void)
{
    struct v4l2_requestbuffers reqbuf = {0};
    struct v4l2_buffer buf = {0};

    reqbuf.count = FRAMEBUFFER_COUNT;
    reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuf.memory = V4L2_MEMORY_MMAP;
    if (0 > ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf))
    {
        fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
        return -1;
    }

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++)
    {
        ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);
        buf_infos[buf.index].length = buf.length;
        buf_infos[buf.index].start = (unsigned short *)mmap(NULL, buf.length,
                                                             PROT_READ | PROT_WRITE, MAP_SHARED,
                                                             v4l2_fd, buf.m.offset);
        if (MAP_FAILED == buf_infos[buf.index].start)
        {
            perror("mmap error");
            return -1;
        }
    }

    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++)
    {
        if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf))
        {
            fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));
            return -1;
        }
    }

    return 0;
}

// 开启视频流
static int v4l2_stream_on(void)
{
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    if (0 > ioctl(v4l2_fd, VIDIOC_STREAMON, &type))
    {
        fprintf(stderr, "ioctl error: VIDIOC_STREAMON: %s\n", strerror(errno));
        return -1;
    }

    return 0;
}

static void v4l2_read_data(void)
{
    struct v4l2_buffer buf = {0};
    unsigned short *base;
    unsigned short *start;
    int min_w, min_h;
    int j,i;

    if (width > frm_width)
        min_w = frm_width;
    else
        min_w = width;
    if (height > frm_height)
        min_h = frm_height;
    else
        min_h = height;

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;

    for ( ; ; )
    {
        for(buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++)
        {
            ioctl(v4l2_fd, VIDIOC_DQBUF, &buf);  // 出队(必须一直执行)


            if(g_enable_render)
            {
                for (j = 0, base=screen_base, start=buf_infos[buf.index].start; j < min_h; j++) {
                    memcpy(base, start, min_w * 2);
                    base += 800;
                    start += frm_width;
                }
            }

            ioctl(v4l2_fd, VIDIOC_QBUF, &buf);  // 入队(必须一直执行)
        }
    }
}
// 对外接口:摄像头初始化
int camera_init(const char *device)
{
    // 初始化LCD
    if (fb_dev_init())
        return -1;

    // 初始化摄像头
    if (v4l2_dev_init(device))
        return -1;

    // 枚举格式
    v4l2_enum_formats();
    v4l2_print_formats();

    // 设置格式
    if (v4l2_set_format())
        return -1;

    // 初始化缓冲
    if (v4l2_init_buffer())
        return -1;

    // 开启流
    if (v4l2_stream_on())
        return -1;

    return 0;
}

// 对外接口:开始采集
void camera_start_capture(void)
{
    v4l2_read_data();
}
main.cpp
cpp 复制代码
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

编译运行之后显示画面如下:

相机主界面

相册界面

相关推荐
传奇开心果编程9 分钟前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程21 分钟前
【Compose Multiplatform 跨端开发学与练】第5课 网络与数据层
android·网络·学习·ui·ios·kotlin·composer
扶尔魔ocy1 小时前
【qt openGL入门笔记】FPS相机
qt·opengl
yt004yt1 小时前
园区 VOC 绿岛集中治理项目,管网与监测系统设计要点
大数据·学习
一条破秋裤1 小时前
06_从设备读数据_字符设备read_write流程
学习
m4Rk_2 小时前
【论文阅读】Agent 记忆机制(92):EMPO²——让 Memory 从经验复用走向主动探索
论文阅读·人工智能·学习·开源·github
欣欣之王来了3 小时前
前端开发学习路线图:2026年Vue方向最新版
前端·学习·架构·项目·vue3教程
wdfk_prog3 小时前
LWIP教程 00:从网线上的电信号到 lwIP——PHY、MAC、DMA 与 `netif` 的完整边界
运维·网络·笔记·学习·ip
扶尔魔ocy3 小时前
qt openGL入门笔记】FPS相机和轨道相机
qt·opengl
尘客-追梦3 小时前
qmake / jom / 影子构建:把编译管起来
开发语言·c++·qt