PYNQ 框架 - OV5640驱动 + Linux 驱动分析

目录

1. 简介

1.1 博文要点

1.2 V4L2

2. 极简 Char 驱动

2.1 源码

2.2 Makefile

2.3 加载驱动

2.4 设备文件

2.5 测试驱动程序

2.6 卸载驱动程序

2.7 自动创建设备文件

2.8 日志等级

3. 极简 V4L2 驱动

3.1 源码

3.2 Makefile

3.3 设备节点类型

3.4 测试 V4L2

3.5 Probe / 直接注册

4. OV5640 驱动

4.1 查找驱动.ko

4.2 主要功能分析

4.3 V4L2 API

4.4 IIC 相关函数

4.4.1 ov5640_write_reg

4.4.2 ov5640_read_reg

4.5 时钟计算

4.5.1 时钟树

4.5.2 Cal Sys_Div_clk

4.5.3 Get SCLK

4.5.4 ov5640_set_mipi_pclk

4.6 曝光控制

4.6.1 自动曝光

4.6.2 获取曝光值

4.6.3 设置自动曝光

4.6.4 自动曝光阈值

4.6.5 设定手动曝光值

4.7 增益控制

4.7.1 获取当前配置

4.7.2 设置自动增益

4.8 Light Frequency

4.8.1 获取 Light Freq

4.8.2 设置自动/手动 

 4.8.3 Band Filter

4.9 Night Mode

4.10 获取 VTS/HTS

4.11 Virtual Channel

5. 总结


1. 简介

1.1 博文要点

  • 分析一个极简的 Char 驱动
  • 分享实时查看日志文件的方法
  • 分析一个极简的 V4L2 驱动
  • 分析 OV5640 的 V4L2 驱动
  • 提取 OV5640 V4L2 驱动代码并转换为 PYNQ 框架代码

1.2 V4L2

V4L2 (Video for Linux 2) 是 Linux 内核中用于视频设备驱动的框架。它为应用层提供统一的接口,并支持各种复杂硬件的灵活扩展。V4L2 主要用于实时视频捕捉,支持许多 USB 摄像头、电视调谐器等设备。

  • V4L2 框架包括以下几个主要模块:
  • v4l2-core:核心模块,处理设备实例数据。
  • media framework:媒体框架,集成了 V4L2 设备节点和子设备。
  • videobuf:视频缓冲区管理模块。

这个框架使得开发者可以更容易地为 Linux 系统添加视频支持,简化了设备实例、子设备和视频节点的设置。

2. 极简 Char 驱动

2.1 源码

// simple_char_driver.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "simple_char_device"
#define BUFFER_SIZE 1024

static char buffer[BUFFER_SIZE];
static int major_number;

// 打开设备
static int device_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "simple_char_device opened\n");
    return 0;
}

// 关闭设备
static int device_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "simple_char_device closed\n");
    return 0;
}

// 读取设备
static ssize_t device_read(struct file *file, char __user *user_buffer, size_t len, loff_t *offset) {
    int bytes_read = 0;

    if (*offset >= BUFFER_SIZE) {
        return 0; // EOF
    }

    while (len && (*offset < BUFFER_SIZE)) {
        put_user(buffer[*offset], user_buffer++);
        len--;
        (*offset)++;
        bytes_read++;
    }

    printk(KERN_INFO "Read %d bytes from simple_char_device\n", bytes_read);
    return bytes_read;
}

// 写入设备
static ssize_t device_write(struct file *file, const char __user *user_buffer, size_t len, loff_t *offset) {
    int i;

    if (len > BUFFER_SIZE) {
        len = BUFFER_SIZE; // 限制写入长度
    }

    for (i = 0; i < len; i++) {
        get_user(buffer[i], user_buffer + i);
    }

    printk(KERN_INFO "Wrote %zu bytes to simple_char_device\n", len);
    return len;
}

// 文件操作结构体
static struct file_operations fops = {
    .open = device_open,
    .release = device_release,
    .read = device_read,
    .write = device_write,
};

// 模块加载
static int __init simple_char_driver_init(void) {
    major_number = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_number < 0) {
        printk(KERN_ALERT "Failed to register character device\n");
        return major_number;
    }
    printk(KERN_INFO "simple_char_device registered with major number %d\n", major_number);
    return 0;
}

// 模块卸载
static void __exit simple_char_driver_exit(void) {
    unregister_chrdev(major_number, DEVICE_NAME);
    printk(KERN_INFO "simple_char_device unregistered\n");
}

module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_VERSION("0.1");

2.2 Makefile

# Makefile
obj-m += simple_char_driver.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

注意:Makefile 中,命令行前面必须使用 制表符(Tab),不能是空格,否则会报错 missing separator。

清理生成的文件:make clean。

执行 make 命令,终端输出如下:

make -C /lib/modules/5.15.0-1037-xilinx-zynqmp/build M=/home/ubuntu/simple_driver modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-1037-xilinx-zynqmp'
  CC [M]  /home/ubuntu/simple_driver/simple_char_driver.o
  MODPOST /home/ubuntu/simple_driver/Module.symvers
  CC [M]  /home/ubuntu/simple_driver/simple_char_driver.mod.o
  LD [M]  /home/ubuntu/simple_driver/simple_char_driver.ko
  BTF [M] /home/ubuntu/simple_driver/simple_char_driver.ko
Skipping BTF generation for /home/ubuntu/simple_driver/simple_char_driver.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-1037-xilinx-zynqmp'

解析输出:

1)make -C /lib/modules/5.15.0-1037-xilinx-zynqmp/build M=/home/ubuntu/simple_driver modules

  • 这个命令告诉 make 在指定的路径 /lib/modules/5.15.0-1037-xilinx-zynqmp/build 中查找 Linux 内核的构建文件。
  • M=/home/ubuntu/simple_driver 指定了你的模块源代码所在的目录。
  • modules 是目标,表示要编译内核模块。

2)make[1]: Entering directory '/usr/src/linux-headers-5.15.0-1037-xilinx-zynqmp'

  • 这行表示 make 进入了 Linux 内核头文件的目录,这个目录包含了构建内核模块所需的所有文件。

3)CC [M]  /home/ubuntu/simple_driver/simple_char_driver.o

  • CC 表示编译器正在编译一个源文件。
  • [M] 表示这是一个模块(而不是内核的内置部分)。
  • /home/ubuntu/simple_driver/simple_char_driver.o 是生成的目标文件的路径(即编译后的对象文件)。

4)MODPOST /home/ubuntu/simple_driver/Module.symvers

  • MODPOST 是一个步骤,负责生成模块符号版本信息,并将其写入 Module.symvers 文件。这个文件包含了模块间的符号信息,帮助内核在加载模块时解决符号依赖。

5)CC [M]  /home/ubuntu/simple_driver/simple_char_driver.mod.o

  • 这是另一个编译步骤,生成模块的描述文件(.mod.o),这个文件包含了模块的元数据。

6)LD [M]  /home/ubuntu/simple_driver/simple_char_driver.ko

  • LD 表示链接器正在将编译的对象文件链接成最终的内核模块文件(.ko 文件)。
  • /home/ubuntu/simple_driver/simple_char_driver.ko 是生成的内核模块的路径。

7)BTF [M] /home/ubuntu/simple_driver/simple_char_driver.ko

  • BTF 表示生成 BPF 类型格式(BPF Type Format)信息,这是一种为 BPF 程序提供类型信息的格式。
  • 这行表示正在为生成的模块创建 BTF 信息。

8)Skipping BTF generation for /home/ubuntu/simple_driver/simple_char_driver.ko due to unavailability of vmlinux

  • 这行表示由于找不到 vmlinux 文件,因此跳过了 BTF 的生成。vmlinux 是内核的完整可执行映像,通常在内核构建时生成。
  • 尽管这条消息提示 BTF 信息没有生成,但这并不影响模块的正常使用。

9)make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-1037-xilinx-zynqmp'

  • 这行表示 make 完成了在内核源代码目录中的操作,并即将返回到原来的目录。

2.3 加载驱动

1)加载驱动程序:

sudo insmod simple_char_driver.ko

2)查看是否加载成功:

lsmod | grep simple_char_driver
---
simple_char_driver     20480  0

3) 查看内核日志,获取主设备号:

加载驱动程序后,使用 dmesg 命令查看内核日志,查找主设备号的相关信息:

sudo dmesg | tail
---
...
[ 3112.860151] simple_char_device registered with major number 507

507 就是主设备号。

2.4 设备文件

1)创建 nod

使用 mknod 命令手动创建一个设备文件:

sudo mknod /dev/simple_char_device c <major_number> 0

2)确认设备文件: 

ll /dev/simple_char_device
---
crw-r--r-- 1 root root 507, 0 Nov 13 10:51 /dev/simple_char_device

3)设置设备文件权限:

确保设备文件具有适当的权限,以便用户或进程可以访问它。 

sudo chmod 666 /dev/simple_char_device

2.5 测试驱动程序

可以使用 echo 和 cat 命令来测试读写:

echo "Hello, World!" > /dev/simple_char_device
cat /dev/simple_char_device

2.6 卸载驱动程序

sudo rmmod simple_char_driver
  • 设备驱动程序:是内核的一部分,负责处理与硬件或虚拟设备的交互。当你使用 rmmod 命令卸载驱动程序时,你是在告诉内核停止使用该驱动程序,并释放与该驱动程序相关的资源和内核模块。
  • 设备文件:是一个特殊的文件,通常位于 /dev 目录下,用于提供用户空间程序通过文件系统接口与设备驱动程序交互的方式。这些文件是由系统管理员或通过udev系统自动创建的,并不会因为卸载驱动程序而自动删除。

因此,即使卸载了驱动程序,/dev/simple_char_device 文件仍然存在,因为它只是一个文件系统上的节点。如果你尝试对其进行读写操作,操作将失败,因为没有相应的驱动程序来处理这些请求。

2.7 自动创建设备文件

步骤 2.4 设备文件 中,为手动创建设备文件。

通过在驱动程序中实现 udev 规则或编写额外的代码来自动创建设备文件。

这涉及到内核编程的更高级部分,如使用device_create和class_create函数。这些函数可以在设备注册时自动创建设备文件,而无需步骤一的手动创建。例如:

static struct class*  simple_char_class  = NULL;
static struct device* simple_char_device = NULL;

// 在初始化函数中
simple_char_class = class_create(THIS_MODULE, "simple_char");
if (IS_ERR(simple_char_class)) { return PTR_ERR(simple_char_class); }

simple_char_device = device_create(simple_char_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
if (IS_ERR(simple_char_device)) {
    class_destroy(simple_char_class);
    return PTR_ERR(simple_char_device);
}

// 在退出函数中
device_destroy(simple_char_class, MKDEV(major_number, 0));
class_destroy(simple_char_class);

2.8 日志等级

1)控制日志级别:

#define KERN_EMERG      "<0>"  /* 系统不可用 */
#define KERN_ALERT      "<1>"  /* 需要立即采取行动 */
#define KERN_CRIT       "<2>"  /* 临界条件 */
#define KERN_ERR        "<3>"  /* 错误条件 */
#define KERN_WARNING    "<4>"  /* 警告条件 */
#define KERN_NOTICE     "<5>"  /* 正常但重要的条件 */
#define KERN_INFO       "<6>"  /* 信息性消息 */
#define KERN_DEBUG      "<7>"  /* 调试级消息 */

2)显示当前日志级别:

cat /proc/sys/kernel/printk

3)修改显示级别:

sudo sh -c 'echo "8 4 1 7" > /proc/sys/kernel/printk'

这里使用 sudo sh -c 是因为直接使用 sudo echo "6 4 1 7" > /proc/sys/kernel/printk 无法成功,因为重定向操作 (>) 不会被 sudo 影响,所以还是以普通用户权限执行。使用 sh -c 允许整个子shell命令字符串以超级用户权限执行,包括重定向操作。

4)实时查看日志文件

sudo tail -f /var/log/kern.log

or

sudo dmesg -w

可以新建一个终端,然后执行实时查看日志,另一个终端开发驱动,这样可以实时得到结果。 

5)查看终端编号

ubuntu@kria: w
---

11:39:04 up 39 min,  2 users,  load average: 0.07, 0.08, 0.11

USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
ubuntu   tty1     -                11:04   32:40   0.54s  0.26s -bash
ubuntu   pts/1    192.168.101.225  11:14    0.00s  0.69s  0.01s w

tty(Teletypewriter):指的是物理终端设备,例如直接连接到计算机的键盘和显示器。

pts(Pseudo Terminal Slave):指的是伪终端设备,通常用于远程登录会话,例如通过SSH连接的会话。

3. 极简 V4L2 驱动

3.1 源码

这个驱动程序创建了一个虚拟的视频设备,实现了基本的打开、关闭、查询功能,但是省略了帧捕捉和缓冲区管理的复杂部分。为了完整实现一个功能性的 V4L2 驱动,需要进一步处理缓冲区管理、帧的生成或捕捉等。涉及到更复杂的内核编程技术,如 DMA (直接内存访问) 和中断处理等。 

//simple_v4l2_driver.c
#include <linux/module.h>
#include <linux/videodev2.h>
#include <media/videobuf2-vmalloc.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-fh.h>
#include <media/v4l2-event.h>
#include <media/v4l2-common.h>

#define VIDEO_DEVICE_NAME "simple_v4l2_device"

static struct v4l2_device v4l2_dev;
static struct video_device vdev;

static int v4l2_open(struct file *file)
{
    printk(KERN_INFO "simple V4L2 device opened\n");
    return 0;
}

static int v4l2_close(struct file *file)
{
    printk(KERN_INFO "simple V4L2 device closed\n");
    return 0;
}

static int v4l2_mmap(struct file *file, struct vm_area_struct *vma)
{
    // For simplicity, mmap handling is not implemented here.
    return -EINVAL;
}

static ssize_t v4l2_read(struct file *file, char __user *buffer, size_t size, loff_t *offset)
{
    // For simplicity, read handling is not implemented here.
    return -EINVAL;
}

static const struct v4l2_file_operations v4l2_fops = {
    .owner = THIS_MODULE,
    .open = v4l2_open,
    .release = v4l2_close,
    .mmap = v4l2_mmap,
    .read = v4l2_read,
};

static int v4l2_querycap(struct file *file, void *fh, struct v4l2_capability *cap)
{
    strscpy(cap->driver, "simple_v4l2_driver", sizeof(cap->driver));
    strscpy(cap->card, "simple V4L2 device", sizeof(cap->card));
    strscpy(cap->bus_info, "platform:simple_v4l2", sizeof(cap->bus_info));
    cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE | V4L2_CAP_STREAMING; // report capabilities
    cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
    return 0;
}

static const struct v4l2_ioctl_ops v4l2_ioctl_ops = {
    .vidioc_querycap = v4l2_querycap,
};

static int __init v4l2_driver_init(void)
{
    int ret;

    //init v4l2 name, version
    strlcpy(v4l2_dev.name, "sv", sizeof("sv"));
    ret = v4l2_device_register(NULL, &v4l2_dev);
    if (ret)
        return ret;

    //setup video
    strlcpy(vdev.name, VIDEO_DEVICE_NAME, sizeof(VIDEO_DEVICE_NAME));
    vdev.v4l2_dev = &v4l2_dev;
    vdev.fops = &v4l2_fops;
    vdev.ioctl_ops = &v4l2_ioctl_ops;
    vdev.release = video_device_release_empty;

    ret = video_register_device(&vdev, VFL_TYPE_SUBDEV, -1);
    if (ret < 0) {
        v4l2_device_unregister(&v4l2_dev);
        return ret;
    }

    printk(KERN_INFO "simple V4L2 driver loaded\n");
    return 0;
}

static void __exit v4l2_driver_exit(void)
{
    video_unregister_device(&vdev);
    v4l2_device_unregister(&v4l2_dev);
    printk(KERN_INFO "simple V4L2 driver unloaded\n");
}

module_init(v4l2_driver_init);
module_exit(v4l2_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple V4L2 Driver");
MODULE_VERSION("0.1");

3.2 Makefile

obj-m += simple_v4l2_driver.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

install:
	insmod simple_v4l2_driver.ko

remove:
	rmmod simple_v4l2_driver

编译成功后:

make
---
make -C /lib/modules/5.15.0-1037-xilinx-zynqmp/build M=/home/ubuntu/simple_v4l2_driver modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-1037-xilinx-zynqmp'
  CC [M]  /home/ubuntu/simple_v4l2_driver/simple_v4l2_driver.o
  MODPOST /home/ubuntu/simple_v4l2_driver/Module.symvers
  CC [M]  /home/ubuntu/simple_v4l2_driver/simple_v4l2_driver.mod.o
  LD [M]  /home/ubuntu/simple_v4l2_driver/simple_v4l2_driver.ko
  BTF [M] /home/ubuntu/simple_v4l2_driver/simple_v4l2_driver.ko
Skipping BTF generation for /home/ubuntu/simple_v4l2_driver/simple_v4l2_driver.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-1037-xilinx-zynqmp'

3.3 设备节点类型

1)VFL_TYPE_VIDEO(视频输入/输出设备)

这类设备主要用于视频捕捉和视频输出。例如,摄像头、电视卡等。它们可以用来录制视频或者将视频信号输出到其他设备。

2)VFL_TYPE_VBI(垂直消隐数据)

VBI(Vertical Blanking Interval)是电视信号中的一部分,用于传输非图像信息,如闭路字幕和电视文本(Teletext)。这类设备用于解码或编码这些信息。

3)VFL_TYPE_RADIO(无线电调谐器)

这类设备用于接收和处理无线电信号,例如FM/AM广播接收器。用户可以通过这些设备收听广播。

4)VFL_TYPE_SUBDEV(V4L2子设备)

子设备通常是指不独立于主设备存在的组件,例如摄像头模块中的图像传感器或镜头控制器。它们通常作为更大系统的一部分,由主设备控制。

5)VFL_TYPE_SDR(软件定义无线电调谐器)

6)VFL_TYPE_TOUCH(触摸传感器)

3.4 测试 V4L2

1)加载/卸载驱动模块:

  • 使用 insmod 命令加载编译好的内核模块:
sudo make install
sudo make remove
  • 确认模块已加载:
lsmod | grep simple_v4l2_driver
---
simple_v4l2_driver     16384  0

2)创建设备文件:

  • 创建一个设备节点(设备文件)。
sudo mknod /dev/video0 c <major_number> 0
sudo chmod 666 /dev/video0
ls -l /dev | grep video
---
crw-rw-rw- 1 root root 92, 0 Nov 14 08:44 /dev/video0
  • V4L2 的主设备号为81:
cat /proc/devices
------
...
81 video4linux
...

3)使用 v4l2-ctl 工具测试:

  • 安装 v4l2-ctl 工具(如果尚未安装):
sudo apt install v4l-utils
  • 使用 v4l2-ctl 工具查询设备信息:
v4l2-ctl --all
---
VIDIOC_SUBDEV_QUERYCAP: failed: Inappropriate ioctl for device
Driver Info:
        Driver version   : 0.0.0
        Capabilities     : 0x00000000
  • 你应该能看到设备的详细信息,包括驱动名称、卡名称、总线信息等。

3.5 Probe / 直接注册

1)probe 方法

通常被称为设备探测方法。它用于在设备驱动程序中检测和初始化设备。当设备与驱动程序匹配时,内核会调用 probe 方法来执行设备的初始化工作。

适用于更复杂的设备,通常依赖于设备树平台代码来匹配设备和驱动程序。

2)直接注册方法

直接注册方法适用于简单设备,直接在 module_init 和 module_exit 中进行设备的注册和注销。

驱动程序直接在 module_init 和 module_exit 函数中注册和注销设备。

4. OV5640 驱动

4.1 查找驱动.ko

检查 Ubuntu 系统中是否包含特定的驱动(即使驱动没有被加载),应该查看系统中的可用内核模块。

find /lib/modules/$(uname -r) -type f -name "*ov5640*.ko"
---
/lib/modules/5.15.0-1037-xilinx-zynqmp/kernel/drivers/media/i2c/ov5640.ko

查看模块信息:

modinfo /lib/modules/5.15.0-1037-xilinx-zynqmp/kernel/drivers/media/i2c/ov5640.ko
---
filename:       /lib/modules/5.15.0-1037-xilinx-zynqmp/kernel/drivers/media/i2c/ov5640.ko
license:        GPL
description:    OV5640 MIPI Camera Subdev Driver
srcversion:     E8CCF4859B81AD94897A18F
alias:          i2c:ov5640
alias:          of:N*T*Covti,ov5640C*
alias:          of:N*T*Covti,ov5640
depends:
intree:         Y
name:           ov5640
vermagic:       5.15.0-1037-xilinx-zynqmp SMP mod_unload modversions aarch64
sig_id:         PKCS#7
signer:         Build time autogenerated kernel key
sig_key:        1E:C8:8A:35:C8:65:47:6C:7B:10:E0:2A:AB:7C:2C:A9:A2:FC:52:4A
sig_hashalgo:   sha512
signature:      ...
parm:           virtual_channel:MIPI CSI-2 virtual channel (0..3), default 0 (uint)

4.2 主要功能分析

1)枚举类型

定义了一些枚举类型,如 `ov5640_mode_id`、`ov5640_frame_rate` 和 `ov5640_pixel_rate_id`,用于表示不同的图像模式、帧率和像素率。

2)数据结构

  • ov5640_pixfmt: 定义了像素格式的结构体,包括格式代码、色彩空间、每个像素的字节数等。
  • ov5640_dev: 定义了传感器设备的主要结构体,包含了 I2C 客户端、V4L2 子设备、媒体 pad、时钟、GPIO、调节器、锁、格式、模式、流状态等信息。
  • reg_value: 用于表示寄存器地址、值、掩码和延迟的结构体。

3)功能实现

  • 初始化和配置: 包括初始化传感器、设置寄存器、配置时钟等。函数如 `ov5640_init_slave_id`、`ov5640_write_reg`、`ov5640_read_reg` 等用于与硬件进行交互。
  • 控制流: 通过 `v4l2_ctrl` 相关的函数实现对传感器的控制,如曝光、增益、白平衡等。`ov5640_set_ctrl_*` 函数用于设置各种控制属性。
  • 流控制: 通过 `ov5640_set_stream` 函数控制视频流的开启和关闭。
  • 格式和分辨率设置: 通过 `ov5640_set_fmt` 和 `ov5640_try_fmt_internal` 函数设置和尝试不同的图像格式和分辨率。

4)驱动的挂载和卸载

  • ov5640_probe: 驱动的探测函数,初始化设备,获取 GPIO 和时钟,设置控制器。
  • ov5640_remove: 驱动的卸载函数,释放资源,关闭设备。

5)设备树和 I2C 驱动

  • 支持通过设备树配置(`of_device_id`)和 I2C 设备(`i2c_device_id`)进行驱动注册和识别。

6) 运行时电源管理

  • 使用 `pm_runtime` API 实现设备的电源管理,确保在不使用时可以降低功耗。

4.3 V4L2 API

1)V4L2 子设备操作

  • ov5640_get_fmt: 获取当前的格式设置。此函数可以被用户空间应用程序用来查询当前的图像格式。
  • ov5640_set_fmt: 设置图像格式。用户空间可以通过此函数请求更改图像的格式。
  • ov5640_get_selection: 获取选择区域的信息,通常用于获取裁剪区域或原生尺寸。
  • ov5640_set_frame_interval: 设置帧间隔(即帧率)。
  • ov5640_get_frame_interval: 获取当前的帧间隔设置。
  • ov5640_enum_frame_size: 枚举支持的帧大小,可以让用户空间查询传感器支持的分辨率。
  • ov5640_enum_frame_interval: 枚举支持的帧间隔,允许用户空间查询每个分辨率下支持的帧率。
  • ov5640_enum_mbus_code: 枚举支持的媒体总线代码,允许用户空间查询支持的像素格式。

2)控制操作

  • ov5640_set_ctrl: 设置各种控制参数(如曝光、增益、白平衡等)。这些控制通常是通过 V4L2 控制接口暴露的。
  • ov5640_g_volatile_ctrl: 获取可变控制值,如当前增益和曝光值。这可以用于监视传感器状态。

3)流控制

  • ov5640_s_stream: 启动或停止视频流。用户空间应用程序可以通过此函数控制数据流的开始和结束。

4)设备管理

  • ov5640_probe: 驱动的探测函数,虽然通常不直接暴露给用户空间,但它是驱动初始化的一部分,确保设备正确设置。
  • ov5640_remove: 驱动的移除函数,用于释放资源,通常在设备卸载时调用。

5)运行时电源管理

  • ov5640_sensor_suspend 和 ov5640_sensor_resume : 用于控制设备的电源状态,通常在设备进入休眠或唤醒时调用。

4.4 IIC 相关函数

4.4.1 ov5640_write_reg

static int ov5640_write_reg(struct ov5640_dev *sensor, u16 reg, u8 val)
{
	struct i2c_client *client = sensor->i2c_client;
	struct i2c_msg msg;
	u8 buf[3];
	int ret;

	buf[0] = reg >> 8;
	buf[1] = reg & 0xff;
	buf[2] = val;

	msg.addr = client->addr;
	msg.flags = client->flags;
	msg.buf = buf;
	msg.len = sizeof(buf);

	ret = i2c_transfer(client->adapter, &msg, 1);
	if (ret < 0) {
		dev_err(&client->dev, "%s: error: reg=%x, val=%x\n",
			__func__, reg, val);
		return ret;
	}

	return 0;
}

4.4.2 ov5640_read_reg

static int ov5640_read_reg(struct ov5640_dev *sensor, u16 reg, u8 *val)
{
	struct i2c_client *client = sensor->i2c_client;
	struct i2c_msg msg[2];
	u8 buf[2];
	int ret;

	buf[0] = reg >> 8;
	buf[1] = reg & 0xff;

	msg[0].addr = client->addr;
	msg[0].flags = client->flags;
	msg[0].buf = buf;
	msg[0].len = sizeof(buf);

	msg[1].addr = client->addr;
	msg[1].flags = client->flags | I2C_M_RD;
	msg[1].buf = buf;
	msg[1].len = 1;

	ret = i2c_transfer(client->adapter, msg, 2);
	if (ret < 0) {
		dev_err(&client->dev, "%s: error: reg=%x\n",
			__func__, reg);
		return ret;
	}

	*val = buf[0];
	return 0;
}

4.5 时钟计算

4.5.1 时钟树

4.5.2 Cal Sys_Div_clk

def ov5640_compute_sys_clk(pll_mult, sysdiv):
    '''
                   +----------------+        +----------------+            +----------------+
     xclk          | PRE_DIV0       |        | Mult (4~252)   |  PLL1_clk  | Sys divider    | sys_div_clk
     +-------+-----> 3037[3:0]      +--------> 3036[7:0]      +------------> 3035[7:4]      +-------------->
     12MHz   |     | 3 (fixed)      |        | pll_mult       |            | sysdiv         |
             |     +----------------+        +----------------+            +----------+-----+
    '''
    xclk_freq = 12_000_000
    OV5640_PLL_PREDIV = 3

    PLL1_clk = xclk_freq // OV5640_PLL_PREDIV * pll_mult # PLL1 CLK Out

    # PLL1输出不能超过 1GHz
    if PLL1_clk // 1_000_000 > 1_000:
        return 0

    sys_div_clk = PLL1_clk // sysdiv # Sys divider clk out

    return sys_div_clk

def ov5640_calc_sys_clk(sys_div_clk):
    best_sys_div_clk = 2**32 - 1
    best_sysdiv = 1
    best_mult   = 1

    for _sysdiv in range(1, 16 + 1):
        for _pll_mult in range(4, 252 + 1):

            if _pll_mult > 127 and (_pll_mult % 2 != 0):
                continue # 如果 PLL 乘数超过 127 且为奇数,则跳过内循环,_pll_mult自增一

            _sys_div_clk = ov5640_compute_sys_clk(_pll_mult, _sysdiv)

            if _sys_div_clk == 0:
                break # 如果达到 PLL1 输出的最大允许值,则跳过外循环,sysdiv自增一

            # 更倾向于选择高于期望时钟率的值,即使这意味着精度较低
            if _sys_div_clk < sys_div_clk:
                continue

            if abs(sys_div_clk - _sys_div_clk) < abs(sys_div_clk - best_sys_div_clk):
                best_sys_div_clk = _sys_div_clk
                best_sysdiv      = _sysdiv
                best_mult        = _pll_mult

            if _sys_div_clk == sys_div_clk:
                break

    return best_sysdiv, best_mult, best_sys_div_clk

#test
#sysdiv, pll_mult, best_sys_div_clk = ov5640_calc_sys_clk(48_000_000) # 期望的时钟率
#print(f"best_sysdiv: {sysdiv}, best_pll_mult: {pll_mult}, Sys divider clk: {best_sys_div_clk}")

4.5.3 Get SCLK

def ov5640_get_SCLK():
    '''
              +----------------+        +------------------+         +---------------------+
XVCLK         | PRE_DIV0       |        | Mult (4~252)     |         | Sys divider (0=16)  |
+-------------> 3037[3:0]=0001 +--------> 3036[7:0]=0x38   +---------> 3035[7:4]=0001      +
12MHz         | / 1            | 12MHz  | * 56             | 672MHz  | / 1                 |
              +----------------+        +------------------+         +----------+----------+
                                                                                |
                                                                                |
                                                                                |
                                                                     +----------v----------+
                                                                     | PLL R divider       |
                                                                     | 3037[4]=1 (+1)      |
                                                                     | / 2                 |
                                                                     +----------+----------+
                                                                                |
                                                                                |
                                                                                |
                                                                     +----------v----------+        +---------------------+
                                                                     | BIT div (MIPI 8/10) |        | SCLK divider        | SCLK
                                                                     | 3034[3:0]=0x8)      +--------> 3108[1:0]=01 (2^)   +------->
                                                                     | / 2                 |        | / 2                 | 84MHz
                                                                     +----------+----------+        +---------------------+
    '''
    # Calculate sysclk
    xvclk = 12_000_000
    sclk_rdiv_map = [1, 2, 4, 8]
    bit_div2x = 1

    temp1 = read_cam_dat(0x3034, 1) # OV5640_REG_SC_PLL_CTRL0
    temp2 = temp1[0] & 0x0f
    if temp2 == 8 or temp2 == 10:
        bit_div2x = temp2 // 2

    temp1 = read_cam_dat(0x3035, 1) # OV5640_REG_SC_PLL_CTRL1
    sysdiv = temp1[0] >> 4
    if sysdiv == 0: # 0x3035[7:4]=0x0, sysdiv=16
        sysdiv = 16

    temp1 = read_cam_dat(0x3036, 1) # OV5640_REG_SC_PLL_CTRL2
    multiplier = temp1[0]

    temp1 = read_cam_dat(0x3037, 1) # OV5640_REG_SC_PLL_CTRL3
    prediv = temp1[0] & 0x0f
    pll_rdiv = ((temp1[0] >> 4) & 0x01) + 1

    temp1 = read_cam_dat(0x3108, 1) # OV5640_REG_SYS_ROOT_DIVIDER
    temp2 = temp1[0] & 0x03
    sclk_rdiv = sclk_rdiv_map[temp2]

    PLL1_clk = xvclk * multiplier // prediv
    _SCLK = PLL1_clk // sysdiv // pll_rdiv * 2 // bit_div2x // sclk_rdiv

    return _SCLK

SCLK = ov5640_get_SCLK()
print(f"SCLK: {SCLK} Hz")

4.5.4 ov5640_set_mipi_pclk

ov5640_csi2_link_freqs = [
    992000000, 888000000, 768000000, 744000000, 672000000, 672000000, #  0 - 5
    592000000, 592000000, 576000000, 576000000, 496000000, 496000000, #  6 - 11
    384000000, 384000000, 384000000, 336000000, 296000000, 288000000, # 12 - 17
    248000000, 192000000, 192000000, 192000000, 96000000,             # 18 - 22
]

def ov5640_set_mipi_pclk():

    # Use the link frequency calculated in ov5640_update_pixel_rate()
    link_freq = ov5640_csi2_link_freqs[13]

    if link_freq > 490_000_000:
        mipi_div = 1
    else:
        mipi_div = 2 # 0x0305[3:0]

    sysclk = link_freq * mipi_div
    prediv, mult, sysdiv = ov5640_calc_sys_clk(sysclk)

    root_div   = 1    # PLL_ROOT_DIV = 2, 0x3037[4], fixed
    bit_div    = 0x08 # 0x3034[3:0], 0x08 = 8bit-mode, 0x0A = 10bit-mode
    pclk_div   = 0    # PCLK_ROOT_DIV = 1, 0x3108[5:4], fixed
    sclk_div   = 1    # SCLK root divider = 2, 0x3108[1:0], fixed
    sclk2x_div = 0    # sclk2x root divider = 1, 0x3108[1:0], fixed

    num_lanes = 2
    sample_rate = (link_freq * mipi_div * num_lanes * 2) // 16
    pclk_period = 2000000000 // sample_rate # Period of pixel clock, 0x4837[7:0]

    ov5640_mod_reg(0x3034, 0x0f, 0x08)                      # OV5640_REG_SC_PLL_CTRL0 = 0x3034
    ov5640_mod_reg(0x3035, 0xff, (sysdiv << 4) | mipi_div)  # OV5640_REG_SC_PLL_CTRL1 = 0x3035
    ov5640_mod_reg(0x3036, 0xff, mult)                      # OV5640_REG_SC_PLL_CTRL2 = 0x3036
    ov5640_mod_reg(0x3037, 0x1f, (root_div << 4) | prediv)  # OV5640_REG_SC_PLL_CTRL3 = 0x3037
    ov5640_mod_reg(0x3108, 0x3f, (pclk_div << 4) | (sclk2x_div << 2) | sclk_div) # OV5640_REG_SYS_ROOT_DIVIDER = 0x3108
    write_cam_dat(0x4837, pclk_period)                      # OV5640_REG_PCLK_PERIOD  = 0x4837

    print(f"link_freq: {link_freq}, PLL1 out: {sysclk}, sample_rate: {sample_rate}")

ov5640_set_mipi_pclk()

4.6 曝光控制

4.6.1 自动曝光

基于平均值的自动曝光控制(AEC)系统通过使用一系列特定寄存器来调整图像的亮度和曝光。这些寄存器包括:

  • 寄存器(0x3A0F)和(0x3A10):分别设置图像亮度的高阈值和低阈值。
  • 寄存器(0x3A1B)和(0x3A1E):控制图像从稳定状态到不稳定状态的亮度阈值。

当图像的平均亮度(由寄存器 0x56A1 测量)处于特定的阈值范围内时,AEC 会维持当前的曝光和增益。如果平均亮度超出这些阈值,AEC 将自动调整曝光和增益,以使亮度回到设定的范围内。

AEC系统提供手动和自动两种速度调整模式:

  • 手动模式允许用户选择正常或快速调整曝光。
  • 自动模式下,系统会根据目标亮度和当前亮度之间的差异自动调整步进速度。

此外,还有两个寄存器(0x3A11)和(0x3A1F)用于在手动模式下快速调整 AEC 的范围。这使得 AEC 能够根据实际情况灵活调整,以实现图像的最佳曝光。

这个基于平均值的 AEC 系统通过一系列精细的控制和自动调整机制,确保图像在不同亮度条件下保持最佳曝光,从而达到稳定和高质量的图像输出。

在自动模式下,AEC 将根据目标值和当前值之间的差异自动计算所需的调整步骤。因此,对于这种模式来说,外部控制区域是无意义的。

4.6.2 获取曝光值

def get_exposure():

    val = read_cam_dat(0x56A1, 1)
    print(f"Image luminance average value AVG Readout: {val[0]}")

    val = read_cam_dat(0x3500, 3)
    exp = (val[0] & 0x0F)<<16 | val[1]<<8 | val[2]
    exp = exp >> 4

    temp = read_cam_dat(0x3503, 1)
    if temp[0] & 0x01: # 0x3503[0]=1
        print(f"Manual Exposure Mode: {exp}")
    else: # 0x3503[0]=0
        temp = read_cam_dat(0x350C, 2)
        aec_vts = temp[0]<<8 | temp[1]

        print(f"Auto Exposure Mode: {exp}")
        print(f"AEC VTS Output: {aec_vts}")

get_exposure()

---
Image luminance average value AVG Readout: 110
Auto Exposure Mode: 1770
AEC VTS Output: 0

4.6.3 设置自动曝光

def set_autoexposure(aec):
    ov5640_mod_reg(0x3503, 0x01, 1 if aec else 0x00);

set_autoexposure(1) # 0=Enable AEC; 1=Disable AEC

4.6.4 自动曝光阈值

def ov5640_set_ae_target(target):

    ae_low  = target * 23 // 25  # 0.92
    ae_high = target * 27 // 25  # 1.08

    fast_high = ae_high << 1
    if (fast_high > 255):
        fast_high = 255

    fast_low = ae_low >> 1

    write_cam_dat(0x3A0F, ae_high)
    write_cam_dat(0x3A10, ae_low)
    write_cam_dat(0x3A1B, ae_high)
    write_cam_dat(0x3A1E, ae_low)
    write_cam_dat(0x3A11, fast_high)
    write_cam_dat(0x3A1F, fast_low)

ov5640_set_ae_target(52) # in ov5640.c @line-3873

4.6.5 设定手动曝光值

def set_exposure(exp):
    exp = exp << 4
    write_cam_dat(0x3500, (exp>>16)&0x0F)
    write_cam_dat(0x3501, (exp>>8 )&0xFF)
    write_cam_dat(0x3502,  exp     &0xF0)

set_exposure(200)

4.7 增益控制

4.7.1 获取当前配置

def get_gain():
    
    val  = read_cam_dat(0x350A, 2)
    gain = val[0]*256 + val[1]
    gain &= 0x3ff
    
    temp = read_cam_dat(0x3503, 1)
    
    if temp[0] & 0x02:
        print(f"Manual Gain Mode: {gain}") # 0x3503[1]=1
    else:
        print(f"Auto Gain Mode: {gain}")   # 0x3503[1]=0

get_gain()

---
Auto Gain Mode: 19

4.7.2 设置自动增益

def set_autogain(on):
    ov5640_mod_reg(0x3503, 0x02, 0x02 if on else 0x00)

set_autogain(1) # 0=Enable AGC, 1=Disable AGC

4.8 Light Frequency

4.8.1 获取 Light Freq

def ov5640_get_light_freq():
    # 获取带宽滤波值
    val = read_cam_dat(0x3C01, 1) # 0x3C01[7], 0=Auto, 1=Manual
    AM = val[0] & 0x80

    if AM:
        print("Banding filter: Manual Mode")

        val = read_cam_dat(0x3C00, 1)
        light_freq = 50 if (val[0] & 0x04) else 60
    else:
        print("Banding filter: Auto Mode")

        val = read_cam_dat(0x3C0C, 1)
        light_freq = 50 if (val[0] & 0x01) else 60

    return light_freq

ov5640_get_light_freq()

---
Banding filter: Auto Mode
50

4.8.2 设置自动/手动 

def ov5640_set_ctrl_light_am(AM):

    ov5640_mod_reg(0x3C01, 0x80, AM << 7) # AM=0, Auto; AM=1, Manual
    ov5640_mod_reg(0x3A00, 0x20, AM << 5)

def ov5640_set_ctrl_light_freq(LINE_FREQ):
    
    ov5640_mod_reg(0x3C00, 0x04, (1<<2) if LINE_FREQ else 0) # LINE_FREQ=1,50Hz; LINE_FREQ=0,60Hz

ov5640_set_ctrl_light_am(0)  # AM=0, Auto; AM=1, Manual
ov5640_set_ctrl_light_freq(1) # 0=60Hz; 1=50Hz;

 4.8.3 Band Filter

val = read_cam_dat(0x3A00, 1) # Bit[5]: Band function enable

print(f"Band function enable: {'Yes' if (val[0] & 0x20) else 'No'}")

4.9 Night Mode

val = read_cam_dat(0x3A00, 1) # Bit[2]: Night Mode

print(f"Night Mode: {'Yes' if (val[0] & 0x04) else 'No'}")

4.10 获取 VTS/HTS

val = read_cam_dat(0x380E, 2) # Total vertical size
vts = val[0]*256 + val[1]

val = read_cam_dat(0x380C, 2) # Total vertical size
hts = val[0]*256 + val[1]

print(f"Total Size: {hts} x {vts}")

---
Total Size: 1896 x 984

4.11 Virtual Channel

def get_virtual_channel():
    temp = read_cam_dat(0x4814, 1)
    channel = temp[0] >> 6; # 保留第6和第7位
    
    return channel

def set_virtual_channel(channel):
    if (channel > 3):
        print("Wrong virtual_channel parameter, expected (0..3).")
        return 0

    temp = read_cam_dat(0x4814, 1)
    temp = temp[0] & (~(3 << 6)) # 清除第6和第7位
    temp |= (channel << 6)       # 设置第6和第7位
    
    write_cam_dat(0x4814, temp)

    # 返回修改后的值
    return temp

vc = get_virtual_channel()
print(f"Virtual Channel: {vc}")

5. 总结

OV5640 驱动非常复杂,通过一系列博文分享其调试过程,暂时告一段落,继续研究 KV260 在 AI 方向的应用。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RNN详解及其实现

目录 概述为什么需要 RNN&#xff1f;RNN 理解及其简单实现RNN 完成文本分类任务RNN 存在的问题 概述 提及 RNN&#xff0c;绝大部分人都知道他是一个用于序列任务的神经网络&#xff0c;会提及他保存了时序信息&#xff0c;但是&#xff0c;为什么需要考虑时序的信息&#xf…

Redis开发03:常见的Redis命令

1.输入以下命令&#xff0c;启动redis。 sudo service redis-server start 如果你是直接安装在WSL的&#xff0c;搜索栏搜索Ubuntu或者点击左下角Windows图表找到U那一栏&#xff0c;直接打开Ubentu&#xff0c;输入账密后&#xff0c;输入“sudo service redis-server start”…

【webrtc】 mediasoup中m77的IntervalBudget及其在AlrDetector的应用

IntervalBudget 用于带宽控制和流量整形 mediasoup中m77 代码的IntervalBudget ,版本比较老IntervalBudget 在特定时间间隔内的比特预算管理,从而实现带宽控制和流量整形。 一。 pacedsender 执行周期: 下一次执行的时间的动态可变的 int64_t PacedSender::TimeUntilNextPr…

Docker for Everyone Plus——No Enough Privilege

直接告诉我们flag在/flag中&#xff0c;访问第一小题&#xff1a; sudo -l查看允许提权执行的命令&#xff1a; 发现有image load命令 题目指明了有rz命令&#xff0c;可以用ZMODEM接收文件&#xff0c;看到一些write up说可以用XShell、MobaXterm、Tabby Terminal等软件连接上…

SpringMVC工作原理【流程图+文字详解SpringMVC工作原理】

SpringMVC工作原理 前端控制器&#xff1a;DispactherServlet处理器映射器&#xff1a;HandlerMapping处理器适配器&#xff1a;HandlerAdapter处理器&#xff1a;Handler&#xff0c;视图解析器&#xff1a;ViewResolver视图&#xff1a;View 首先用户通过浏览器发起HTTP请求…

网络安全 社会工程学 敏感信息搜集 密码心理学攻击 密码字典生成

网络安全 社会工程学 敏感信息搜集 密码心理学攻击 理解社会工程学的概念掌握获取敏感信息的方法提高自我信息保护的意识和方法理解密码心理学的概念理解密码特征分析掌握黑客猜解密码的切入方法掌握如何提高密码强壮性 敏感信息搜集 「注」由于对实验环境的限制&#xff0c;…

【机器学习】机器学习的基本分类-监督学习-逻辑回归-对数似然损失函数(Log-Likelihood Loss Function)

对数似然损失函数&#xff08;Log-Likelihood Loss Function&#xff09; 对数似然损失函数是机器学习和统计学中广泛使用的一种损失函数&#xff0c;特别是在分类问题&#xff08;例如逻辑回归、神经网络&#xff09;中应用最为广泛。它基于最大似然估计原理&#xff0c;通过…

SQL基础入门 —— SQL概述

目录 1. 什么是SQL及其应用场景 SQL的应用场景 2. SQL数据库与NoSQL数据库的区别 2.1 数据模型 2.2 查询语言 2.3 扩展性 2.4 一致性与事务 2.5 使用场景 2.6 性能与扩展性 总结 3. 常见的SQL数据库管理系统&#xff08;MySQL, PostgreSQL, SQLite等&#xff09; 3.…

力扣--LCR 149.彩灯装饰记录I

题目 代码 /** Definition for a binary tree node. public class TreeNode { int val;TreeNode left;TreeNode right;TreeNode() {}TreeNode(int val) { this.val val; }TreeNode(int val, TreeNode left, TreeNode right) {this.val val;this.left left;this.right ri…

Admin.NET框架使用宝塔面板部署步骤

文章目录 Admin.NET框架使用宝塔面板部署步骤&#x1f381;框架介绍部署步骤1.Centos7 部署宝塔面板2.部署Admin.NET后端3.部署前端Web4.访问前端页面 Admin.NET框架使用宝塔面板部署步骤 &#x1f381;框架介绍 Admin.NET 是基于 .NET6 (Furion/SqlSugar) 实现的通用权限开发…

软通动力携子公司鸿湖万联、软通教育助阵首届鸿蒙生态大会成功举办

11月23日中国深圳&#xff0c;首届鸿蒙生态大会上&#xff0c;软通动力及软通动力子公司鸿湖万联作为全球智慧物联网联盟&#xff08;GIIC&#xff09;理事单位、鸿蒙生态服务&#xff08;深圳&#xff09;有限公司战略合作伙伴&#xff0c;联合软通教育深度参与了大会多项重磅…

利用若依代码生成器实现课程管理模块开发

目录 前言1. 环境准备1.1 数据库表设计与导入 2. 使用若依代码生成器生成模块代码2.1 导入数据库表2.2 配置生成规则2.2.1 基本信息配置2.2.2 字段信息配置2.2.3 生成信息配置 3. 下载与集成生成代码3.1 解压与集成3.2 启动项目并验证 4. 优化与扩展4.1 前端优化4.2 后端扩展 结…

MySQL Linux 离线安装

下载 进入官网&#xff0c;下载对应的需要MySQL版本&#xff0c;这里是历史版本。 官网 选择第一个MySQL Community Sever社区版&#xff0c;因为这个是免费的。 选择需要的对应版本&#xff1a; 安装 1.将下载好的安装包上传到服务器端 使用FinalShell 客户端连接服务器 …

Milvus 2.5:全文检索上线,标量过滤提速,易用性再突破!

01. 概览 我们很高兴为大家带来 Milvus 2.5 最新版本的介绍。 在 Milvus 2.5 里&#xff0c;最重要的一个更新是我们带来了“全新”的全文检索能力&#xff0c;之所以说“全新”主要是基于以下两点&#xff1a; 第一&#xff0c;对于全文检索基于的 BM25 算法&#xff0c;我们采…

think php处理 异步 url 请求 记录

1、需求 某网站 需要 AI生成音乐&#xff0c;生成mp3文件的时候需要等待&#xff0c;需要程序中实时监听mp3文件是否生成 2、用的开发框架 为php 3、文件结构 配置路由设置 Route::group(/music, function () {Route::post(/musicLyrics, AiMusic/musicLyrics);//Ai生成歌词流式…

27 基于51单片机的方向盘模拟系统

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于STC89C52单片机&#xff0c;采用两个MPX4115压力传感器作为两路压力到位开关电路&#xff0c; 采用滑动变阻器连接数模转换器模拟重力加速度传感器电路&#xff1b; 一个按键控制LED灯的点亮与…

【亚马逊云科技】使用Amazon Lightsail搭建nginx服务

文章目录 前言一、为什么选择Amazon Lightsail二、创建账号与登录注册亚马逊账号登录控制台 三、创建Amazon Lightsail进入控制台创建实例登录服务器部署nginx服务关闭防火墙 总结 前言 不论是个人名片还是官方网站都离不开网站建设工作。计算机技术经历漫长的发展&#xff0c…

YOLOv8实战无人机视角目标检测

本文采用YOLOv8作为核心算法框架&#xff0c;结合PyQt5构建用户界面&#xff0c;使用Python3进行开发。YOLOv8以其高效的实时检测能力&#xff0c;在多个目标检测任务中展现出卓越性能。本研究针对无人机目标数据集进行训练和优化&#xff0c;该数据集包含丰富的无人机目标图像…

Java设计模式——职责链模式:解锁高效灵活的请求处理之道

嘿&#xff0c;各位 Java 编程大神和爱好者们&#xff01;今天咱们要一同深入探索一种超厉害的设计模式——职责链模式。它就像一条神奇的“处理链”&#xff0c;能让请求在多个对象之间有条不紊地传递&#xff0c;直到找到最合适的“处理者”。准备好跟我一起揭开它神秘的面纱…

【四轴】利用PWM捕获解析接收机信号

在学习这部分之间&#xff0c;建议大家先看之前这篇博客&#xff0c;里面包含对PWM一些重要概念的基本介绍。 【四轴】利用PWM输出驱动无刷电机-CSDN博客 1. 基本原理 1.1 PWM是什么 这一部分可以看我之前的博客&#xff0c;已经对PWM有了基本的介绍。 1.2 什么叫捕获PWM波&…