目录
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 方向的应用。