diff --git a/camera_framework/Kconfig b/camera_framework/Kconfig new file mode 100644 index 0000000..331ac3a --- /dev/null +++ b/camera_framework/Kconfig @@ -0,0 +1,148 @@ +menu "ov2640 camera" + + config SENSOR_USING_OV2640 + bool "Enable OV2640 Camera Module" + default y + help + Enable this option to use OV2640 as the camera sensor module. + + menu "DVP configuration" + depends on SENSOR_USING_OV2640 + + config OV2640_DVP_PINGPONG_POOL_SIZE + int "DVP PingPong Buffer Pool Size (compile-time ceiling)" + default 10240 + range 1024 65536 + help + Size of the statically allocated ping-pong buffer pool. + This is the hard upper limit for dvp_set_pingpong_size() at runtime; + you can never exceed this value without recompiling. + For SF32LB56X with OV2640_DVP_PINGPONG_USE_SECTION enabled, this + value must match DVP_PINGPONG_SIZE in custom_mem_map.h + (default: 640*8*2 = 10240). For SF32LB52X (normal BSS) set it + to the largest size you will ever need at runtime. + + config OV2640_DVP_PINGPONG_BUFFER_SIZE + int "DVP PingPong Buffer Initial Size" + default 8192 + range 1024 65536 + help + Initial active ping-pong buffer size passed to the DMA on dvp_init(). + Must not exceed OV2640_DVP_PINGPONG_POOL_SIZE. + Can be changed freely at runtime via dvp_set_pingpong_size() without + recompiling, as long as the new value stays within POOL_SIZE. + + config OV2640_DVP_PINGPONG_USE_SECTION + bool "Place DVP ping-pong buffer in a dedicated linker section (.dvp_pingpong)" + default n + help + When enabled, dvp_pingpong_buffer is placed in the ".dvp_pingpong" section. + The board link script must define a matching memory region (e.g. DVP_SRAM) + that maps this section to the correct SRAM address. + Enable for SF32LB56X boards that carve out dedicated SRAM for DVP. + Leave disabled for SF32LB52X (buffer lands in normal .bss). + + config OV2640_DVP_DATA_PIN_BASE + int "DVP Data Pin Base Numbers" + default 0 + help + DO NOT MODIFY! Only for internal use to specify the base pin number for DVP data pins (D0-D7) of the OV2640 camera module. + config OV2640_DVP_PCLK_PIN + int "DVP PCLK Pin Number (PAx index)" + default 41 + help + PAx pin index for the DVP pixel clock (PCLK) input. + The driver muxes this pin to GPTIM1_ETR automatically. + SF32LB52X default: 41 (PA41). SF32LB56X default: 77 (PA77). + + config OV2640_DVP_HSYNC_PIN + int "DVP HSYNC Pin Number (PAx index)" + default 43 + help + PAx pin index for the DVP horizontal sync (HSYNC/HREF) input. + The driver muxes this pin to GPTIM1_CH1 automatically. + SF32LB52X default: 43 (PA43). SF32LB56X default: 78 (PA78). + + config OV2640_DVP_VSYNC_PIN + int "DVP VSYNC Pin Number (PAx index)" + default 42 + help + Specify the PAx pin index connected to the DVP VSYNC signal of the OV2640 camera module. + This pin is used for GPIO interrupt. + SF32LB52X default: 42 (PA42). SF32LB56X: adjust to your board layout. + + endmenu + + menu "SCCB configuration" + depends on SENSOR_USING_OV2640 + + config OV2640_SCCB_I2C_BUS_NAME + string "SCCB I2C Bus Name" + default "i2c1" + help + Specify the I2C bus name used for SCCB communication with the OV2640 camera module. + + config OV2640_SCCB_TIMEOUT_MS + int "SCCB Timeout (ms)" + default 1000 + help + Specify the timeout value in milliseconds for SCCB I2C communication. + + config OV2640_SCCB_MAX_HZ + int "SCCB I2C Max Frequency (Hz)" + default 100000 + range 10000 400000 + help + Specify the maximum I2C bus frequency in Hz for SCCB communication. + Default is 100kHz (100000 Hz). + + endmenu + + menu "Camera configuration" + depends on SENSOR_USING_OV2640 + + config OV2640_CAMERA_READ_TIMEOUT_MS + int "Camera Read Timeout (ms)" + default 1000 + range 100 10000 + help + Specify the timeout value in milliseconds for camera read operations. + Default is 10000ms (10 second). + + config OV2640_DVP_XCLK_PIN + int "XCLK Output Pin Number" + default -1 + help + Specify the pin number for XCLK (external clock) output. + Set to -1 to disable XCLK output (use external clock source). + NOTE: User must configure this pin as GPTIM2_CH1 in main.c before opening the camera device. + Example: HAL_PIN_Set(PAD_PA00 + 44, GPTIM2_CH1, PIN_NOPULL, 1); + + choice + prompt "XCLK Output Frequency" + default OV2640_DVP_XCLK_FREQ_12M + depends on OV2640_DVP_XCLK_PIN != -1 + help + Select the XCLK output frequency for the OV2640 camera module. + GPTIM2 runs at 24MHz, so only 6MHz and 12MHz can be generated. + + config OV2640_DVP_XCLK_FREQ_6M + bool "6 MHz" + help + Generate 6MHz XCLK (24MHz / 4). + + config OV2640_DVP_XCLK_FREQ_12M + bool "12 MHz" + help + Generate 12MHz XCLK (24MHz / 2). + endchoice + + config OV2640_DVP_XCLK_FREQ + int + default 6000000 if OV2640_DVP_XCLK_FREQ_6M + default 12000000 if OV2640_DVP_XCLK_FREQ_12M + default 0 + + endmenu + +endmenu \ No newline at end of file diff --git a/camera_framework/README.md b/camera_framework/README.md new file mode 100644 index 0000000..ac9083f --- /dev/null +++ b/camera_framework/README.md @@ -0,0 +1,266 @@ +# OV2640 摄像头组件 + +[中文](README.md) | [English](README_EN.md) + +基于 RT-Thread 的 OV2640 摄像头组件,适用于 SiFli SF32 平台。当前版本已经从单一设备读图模型扩展为分层相机框架,包含传感器驱动层、DVP 数据总线层、通用 handle 层,以及面向上层的单次采集和连续采集接口。 + +## 当前能力 +- 支持SF32LB52-LCD平台,SF32LB56-wlan-core平台 +- 支持 JPEG、RGB565、YUV422、RAW8 像素格式 +- 支持单次采集 +- 支持连续采集,使用双缓冲流式取帧 +- 支持 RT-Thread 设备接口 +- 支持通用 handle 层接口,便于后续扩展多摄像头或替换底层驱动 +- 支持常见图像参数调节:分辨率、质量、镜像、翻转、曝光、白平衡、增益、特效等 + +## 目录结构 + +```text +ov2640/ +├── camera/ +│ ├── bus/ +│ │ └── data/ +│ │ ├── dvp.c +│ │ └── dvp.h +│ │ └── control/ +│ │ ├── sccb.c +│ │ └── sccb.h +│ ├── driver/ +│ │ └── ov2640/ +│ │ ├── ov2640.c +│ │ ├── ov2640.h +│ │ ├── ov2640_regs.h +│ │ ├── ov2640_settings.h +│ ├── handle/ +│ │ ├── camera_handle.c +│ │ └── camera_handle.h +│ ├── camera_device_ops.h +│ └── camera_types.h +├── examples/ +├── Kconfig +├── SConscript +├── README.md +└── README_EN.md +``` + +## 架构说明 + +```text +┌────────────────────────────────────────────────────┐ +│ 应用层 / 示例 │ +│ rt_device_* 接口 / camera_handle_* 接口 / MSH │ +└───────────────────────┬────────────────────────────┘ + │ +┌───────────────────────▼────────────────────────────┐ +│ handle 层 camera_handle │ +│ 单次采集封装、连续采集封装、流队列管理、通用适配 │ +└───────────────────────┬────────────────────────────┘ + │ +┌───────────────────────▼────────────────────────────┐ +│ OV2640 驱动层 camera/driver │ +│ 传感器寄存器配置、RT-Thread 设备接口实现 │ +└───────────────────────┬────────────────────────────┘ + │ +┌───────────────────────▼────────────────────────────┐ +│ DVP 数据层 camera/bus/data │ +│ GPIO + GPTIM + DMA、JPEG 解析、固定尺寸帧采集、重装填 │ +└───────────────────────┬────────────────────────────┘ + │ + OV2640 硬件 +``` + +## 硬件连接 + +| OV2640 引脚 | MCU 引脚 | 功能 | 说明 | +|-------------|----------|------|------| +| D0 ~ D7 | 平台固定数据引脚组 | 8-bit 并行数据 | 由 DVP 默认资源宏配置 | +| PCLK | 默认 PA41 | 像素时钟 | DVP 默认映射到 GPTIM1_ETR | +| HSYNC/HREF | 默认 PA43 | 行同步 | DVP 默认映射到 GPTIM1_CH1 | +| VSYNC | Kconfig 配置 | 帧同步 | 由中断触发一帧采集 | +| SDA | 默认 PA39 | SCCB 数据 | 用户在 open 前配置 | +| SCL | 默认 PA40 | SCCB 时钟 | 用户在 open 前配置 | +| XCLK | 默认 PA08 | 外部输入时钟 | 可选 GPTIM2_CH1 输出 | + +说明: + +- DVP 默认资源描述已收敛在 dvp.h 中,通过宏统一配置 +- I2C、PCLK、HSYNC、XCLK 的 pinmux 仍需在打开设备前由板级代码完成 + +## Kconfig 配置 + +在 menuconfig 中进入 ov2640 camera 菜单: + +### DVP 配置 + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| OV2640_DVP_PINGPONG_BUFFER_SIZE | 8192 | DVP 乒乓缓冲区大小 | +| OV2640_DVP_VSYNC_PIN | 42 | VSYNC 引脚编号 | + +### SCCB 配置 + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| OV2640_SCCB_I2C_BUS_NAME | i2c1 | I2C 总线名称 | +| OV2640_SCCB_TIMEOUT_MS | 1000 | I2C 通信超时 | +| OV2640_SCCB_MAX_HZ | 100000 | I2C 最大频率 | + +### 相机配置 + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| OV2640_CAMERA_READ_TIMEOUT_MS | 1000 | 单次采集超时 | +| OV2640_DVP_XCLK_PIN | -1 | XCLK 输出引脚,-1 表示禁用 | +| OV2640_DVP_XCLK_FREQ | 12MHz | XCLK 输出频率 | + +## 使用方式 + +### 1. 底层 RT-Thread 设备接口 + +适用于直接按设备方式控制 OV2640。 + +```c +rt_device_t dev = rt_device_find("ov2640"); +rt_device_open(dev, RT_DEVICE_OFLAG_RDWR); + +rt_device_control(dev, OV2640_CMD_SET_PIXFORMAT, (void *)(rt_ubase_t)PIXFORMAT_JPEG); +rt_device_control(dev, OV2640_CMD_SET_FRAMESIZE, (void *)(rt_ubase_t)FRAMESIZE_VGA); +rt_device_control(dev, OV2640_CMD_SET_QUALITY, (void *)(rt_ubase_t)10); + +rt_thread_mdelay(500); + +rt_size_t size = rt_device_read(dev, 0, buffer, buffer_size); + +rt_device_close(dev); +``` + +### 2. 通用 handle 层接口 + +适用于上层业务不希望直接依赖具体传感器驱动时使用。 + +```c +camera_handler_instance_t instance; +camera_handler_all_input_arg_t input_arg = { + .device_name = "ov2640", + .device_ops = ov2640_get_device_ops(), +}; + +camera_handler_instance_init(&instance, &input_arg); +camera_init(&instance); + +camera_capture_config_t cfg = { + .pixformat = PIXFORMAT_JPEG, + .framesize = FRAMESIZE_VGA, + .quality = 10, +}; +camera_change_settings(&instance, &cfg); +``` + +单次采集: + +```c +camera_capture_request_t request = { + .buffer = buffer, + .buffer_size = buffer_size, + .frame_size = 0, +}; + +camera_capture_single(&instance, &request); +``` + +连续采集: + +```c +camera_stream_config_t stream_config = { + .buffers = {buffer0, buffer1}, + .buffer_size = buffer_size, +}; + +camera_stream_frame_t frame; + +camera_start_stream(&instance, &stream_config); +camera_get_stream_frame(&instance, &frame, 5 * RT_TICK_PER_SECOND); +camera_stop_stream(&instance); +``` + +## 控制命令 + +### 常用控制命令 + +| 命令 | 值 | 参数 | 说明 | +|------|----|------|------| +| OV2640_CMD_SET_PIXFORMAT | 0x01 | pixformat_t | 设置像素格式 | +| OV2640_CMD_SET_FRAMESIZE | 0x02 | framesize_t | 设置分辨率 | +| OV2640_CMD_SET_QUALITY | 0x06 | int | 设置 JPEG 质量 | +| OV2640_CMD_START_CAPTURE | 0x10 | 无 | 启动单次采集 | +| OV2640_CMD_STOP_CAPTURE | 0x11 | 无 | 停止单次采集 | +| OV2640_CMD_GET_FRAME_SIZE | 0x12 | uint32_t * | 获取当前帧大小 | +| OV2640_CMD_SET_FRAME_BUFFER | 0x1F | void * | 设置目标帧缓冲 | +| OV2640_CMD_SET_FRAME_BUFFER_SIZE | 0x20 | rt_size_t | 设置帧缓冲大小 | +| OV2640_CMD_SET_PINGPONG_SIZE | 0x21 | uint32_t | 设置 DVP 乒乓缓冲大小 | +| OV2640_CMD_START_STREAM | 0x22 | camera_stream_start_args_t * | 启动连续采集 | +| OV2640_CMD_STOP_STREAM | 0x23 | 无 | 停止连续采集 | + +### 支持的像素格式 + +- PIXFORMAT_JPEG +- PIXFORMAT_RGB565 +- PIXFORMAT_YUV422 +- PIXFORMAT_RAW8 + +### 支持的分辨率 + +- FRAMESIZE_96X96 +- FRAMESIZE_QQVGA +- FRAMESIZE_128X128 +- FRAMESIZE_QCIF +- FRAMESIZE_HQVGA +- FRAMESIZE_240X240 +- FRAMESIZE_QVGA +- FRAMESIZE_320X320 +- FRAMESIZE_CIF +- FRAMESIZE_HVGA +- FRAMESIZE_VGA +- FRAMESIZE_SVGA +- FRAMESIZE_XGA +- FRAMESIZE_HD +- FRAMESIZE_SXGA +- FRAMESIZE_UXGA + +## 示例命令 + +集成工程 src/main.c 中可见以下命令示例: + +- take_photo:JPEG 单次拍照并保存到 SD 卡 +- take_raw_photo:RAW8 拍照并转 BMP 保存 +- take_yuv_photo:YUV422 拍照并转 BMP 保存 +- take_rgb565_photo:RGB565 拍照并转 BMP 保存 +- stream_capture:连续采集测试,只打印每帧耗时和帧大小,不保存文件 + +连续采集示例: + +```text +msh> stream_capture jpeg VGA 30 10 +msh> stream_capture rgb565 QVGA 60 +``` + +## 注意事项 + +1. 连续采集使用双缓冲,调用方需要提供两块可持续复用的帧缓冲。 +2. 单次采集和连续采集是两种互斥模式,流模式开启后不应再调用单次 camera_capture_single。 +3. JPEG 模式建议按分辨率预留足够缓冲,VGA 常见可从 300 KB 起评估,UXGA 需要更大空间。 +4. RAW8、YUV422、RGB565 属于定长帧模式,缓冲大小应按分辨率和像素格式精确计算。 +5. 修改传感器分辨率、质量等参数后,建议等待约 500 ms 再开始采集。 +6. 如果只出现首帧或持续超时,优先检查 VSYNC、PCLK、HSYNC、XCLK 配置以及 DVP 缓冲大小。 + +## 依赖项 + +- RT-Thread 设备框架 +- RT-Thread I2C 驱动框架 +- SiFli BSP HAL,含 GPIO、DMA、GPTIM +- 可用的 SCCB I2C 总线设备,默认 i2c1 + +## 相关文档 + +- 集成工程说明:工作区根目录 README.md +- 英文说明:README_EN.md diff --git a/camera_framework/README_EN.md b/camera_framework/README_EN.md new file mode 100644 index 0000000..be5ca2b --- /dev/null +++ b/camera_framework/README_EN.md @@ -0,0 +1,357 @@ +# OV2640 Camera Component + +[中文](README.md) | [English](README_EN.md) + +RT-Thread based OV2640 camera component for the SiFli SF32LB52/SF32LB56 platform. The component is built around a layered architecture: sensor driver, DVP data bus (with a generic bus-adapter abstraction), and a generic handle layer. Frame buffers are supplied by the caller — the component does not reserve any PSRAM. Upper-layer applications drive the camera through the handle-layer API or the standard RT-Thread device interface. + +## Current Capabilities + +- Supports JPEG, RGB565, YUV422, and RAW8 pixel formats +- Supports single-shot capture +- Supports continuous capture with double-buffer streaming +- Supports the RT-Thread device interface +- Supports a generic handle-layer API for future multi-camera or driver replacement scenarios +- Supports common image controls such as frame size, quality, mirror, flip, exposure, white balance, gain, and special effects + +## Directory Layout + +```text +ov2640/ +├── camera/ +│ ├── bus/ +│ │ ├── control/ +│ │ │ ├── sccb.c # SCCB (I2C) control bus +│ │ │ └── sccb.h +│ │ └── data/ +│ │ ├── data_bus_adapter.c # Generic data-bus adapter abstraction +│ │ ├── data_bus_adapter.h +│ │ ├── dvp.c # DVP DMA capture backend +│ │ └── dvp.h +│ ├── driver/ +│ │ └── ov2640/ +│ │ ├── ov2640.c # OV2640 RT-Thread device driver +│ │ ├── ov2640.h # Control-command macros and public API +│ │ ├── ov2640_regs.h # Register address definitions +│ │ └── ov2640_settings.h # Register initialization sequences +│ ├── handle/ +│ │ ├── camera_handle.c # High-level handle API implementation +│ │ └── camera_handle.h # Type definitions and API declarations +│ ├── camera_device_ops.h +│ └── camera_types.h +├── examples/ +│ ├── take_photo/ # RGB565 single-shot MSH command example +│ └── take_photo_to_sdcard/ # Capture and save to SD card example +├── Kconfig +├── SConscript +├── README.md +└── README_EN.md +``` + +## Architecture + +```text +┌────────────────────────────────────────────────────┐ +│ Application / Example Layer │ +│ rt_device_* APIs / camera_handle_* APIs / MSH │ +└───────────────────────┬────────────────────────────┘ + │ +┌───────────────────────▼────────────────────────────┐ +│ handle layer: camera_handle │ +│ single capture, stream capture, queue management │ +└───────────────────────┬────────────────────────────┘ + │ +┌───────────────────────▼────────────────────────────┐ +│ abstraction: camera_device_ops / types │ +│ command mapping, stream frame structs, adaptation │ +└───────────────────────┬────────────────────────────┘ + │ +┌───────────────────────▼────────────────────────────┐ +│ OV2640 driver layer: camera/driver │ +│ sensor register setup and RT-Thread binding │ +└───────────────────────┬────────────────────────────┘ + │ +┌───────────────────────▼────────────────────────────┐ +│ DVP data layer: camera/bus/data │ +│ GPIO + GPTIM + DMA, JPEG parsing, rearm for stream │ +└───────────────────────┬────────────────────────────┘ + │ + OV2640 hardware +``` + +## Hardware Connections + +| OV2640 Pin | MCU Pin | Function | Notes | +|------------|---------|----------|-------| +| D0 ~ D7 | Platform default data pin group | 8-bit parallel data | Applied through DVP default resource macros | +| PCLK | Default PA41 | Pixel clock | Default DVP mapping to GPTIM1_ETR | +| HSYNC/HREF | Default PA43 | Line sync | Default DVP mapping to GPTIM1_CH1 | +| VSYNC | Configured by Kconfig | Frame sync | Used as frame trigger interrupt | +| SDA | Default PA39 | SCCB data | Must be configured before open | +| SCL | Default PA40 | SCCB clock | Must be configured before open | +| XCLK | Default PA08 | Sensor clock input | Optional GPTIM2_CH1 output | + +Notes: + +- The default DVP resource description is consolidated in `dvp.h` through macros +- Pin muxing for SCCB (I2C), DVP (PCLK / HSYNC), and XCLK is handled internally by the OV2640 driver during initialization — the application does not need to call `HAL_PIN_Set()` + +## Kconfig Options + +All options are nested under the `ov2640 camera` menu and depend on `SENSOR_USING_OV2640`. + +### DVP + +| Option | Default | Range | Description | +|--------|---------|-------|-------------| +| `OV2640_DVP_PINGPONG_BUFFER_SIZE` | 8192 | 1024–65536 | DVP DMA ping-pong buffer size in bytes; minimum is row-width × 2 | +| `OV2640_DVP_VSYNC_PIN` | 42 | — | VSYNC GPIO interrupt pin index (PAx number) | + +> `OV2640_DVP_DATA_PIN_BASE` is reserved for internal use — do not modify. + +### SCCB + +| Option | Default | Range | Description | +|--------|---------|-------|-------------| +| `OV2640_SCCB_I2C_BUS_NAME` | `"i2c1"` | — | RT-Thread I2C bus device name | +| `OV2640_SCCB_TIMEOUT_MS` | 1000 | — | I2C transaction timeout (ms) | +| `OV2640_SCCB_MAX_HZ` | 100000 | 10000–400000 | I2C maximum frequency (Hz) | + +### Camera + +| Option | Default | Range | Description | +|--------|---------|-------|-------------| +| `OV2640_CAMERA_READ_TIMEOUT_MS` | 1000 | 100–10000 | Single-shot capture wait timeout (ms) | +| `OV2640_DVP_XCLK_PIN` | -1 | — | XCLK output pin (PAx); -1 disables | +| `OV2640_DVP_XCLK_FREQ` | 12 MHz | 6 / 12 MHz | XCLK frequency; derived from GPTIM2 running at 24 MHz | + +## Usage + +### Recommended: Handle-Layer API + +Drive the camera through `camera_handle.h` for a clean abstraction over the concrete sensor driver. The call order is always five steps: + +```c +#include "camera_handle.h" +#include "ov2640.h" + +/* 1. Initialize the handle instance */ +camera_handler_instance_t instance; +camera_handler_all_input_arg_t input_arg = { + .device_name = CAMERA_DEFAULT_DEVICE_NAME, /* "ov2640" */ + .device_ops = ov2640_get_device_ops(), +}; +camera_handler_instance_init(&instance, &input_arg); + +/* 2. Open the RT-Thread device */ +camera_init(&instance); + +/* 3. Push format + resolution (internally inserts ~500 ms AEC/AWB settle) */ +camera_capture_config_t cfg = { + .pixformat = PIXFORMAT_RGB565, + .framesize = FRAMESIZE_VGA, + .quality = 10, /* JPEG only; ignored for RGB565/RAW8/YUV422 */ +}; +camera_change_settings(&instance, &cfg); + +/* 4a. Blocking single-shot capture */ +camera_capture_request_t req = { + .buffer = buffer, + .buffer_size = buffer_size, + .frame_size = 0, +}; +camera_capture_single(&instance, &req); +/* req.frame_size now holds the actual captured byte count */ + +/* 4b. Continuous double-buffer streaming (mutually exclusive with 4a) */ +camera_stream_config_t stream_cfg = { + .buffers = {buffer0, buffer1}, + .buffer_size = frame_bytes, +}; +camera_start_stream(&instance, &stream_cfg); + +camera_stream_frame_t frame; +camera_get_stream_frame(&instance, &frame, 5 * RT_TICK_PER_SECOND); +/* frame.buffer / frame.frame_size / frame.sequence are valid until the next two frames arrive */ + +camera_stop_stream(&instance); + +/* 5. Close the device */ +camera_deinit(&instance); +``` + +### Low-Level RT-Thread Device Interface + +For direct register-level command access: + +```c +rt_device_t dev = rt_device_find("ov2640"); +rt_device_open(dev, RT_DEVICE_OFLAG_RDWR); + +rt_device_control(dev, OV2640_CMD_SET_PIXFORMAT, + (void *)(rt_ubase_t)PIXFORMAT_RGB565); +rt_device_control(dev, OV2640_CMD_SET_FRAMESIZE, + (void *)(rt_ubase_t)FRAMESIZE_VGA); +rt_device_control(dev, OV2640_CMD_SET_FRAME_BUFFER, buffer); +rt_device_control(dev, OV2640_CMD_SET_FRAME_BUFFER_SIZE, + (void *)(rt_ubase_t)buffer_size); + +rt_thread_mdelay(500); +rt_device_control(dev, OV2640_CMD_START_CAPTURE, NULL); + +uint32_t frame_size = 0; +rt_device_control(dev, OV2640_CMD_GET_FRAME_SIZE, &frame_size); + +rt_device_close(dev); +``` + +### PSRAM Allocation Note + +VGA-and-above RGB565 frames (e.g. VGA = 640×480×2 = 614 400 bytes) exceed internal SRAM capacity. The caller must allocate the frame buffer from PSRAM. RT-Thread's `rt_memheap` is the recommended approach; the example projects contain a ready-to-copy inline implementation (`examples/take_photo/src/main.c`). + +## Control Commands (`rt_device_control`) + +### Format and Resolution + +| Command | Value | Argument | Description | +|---------|-------|----------|-------------| +| `OV2640_CMD_SET_PIXFORMAT` | 0x01 | `pixformat_t` | Set pixel format | +| `OV2640_CMD_SET_FRAMESIZE` | 0x02 | `framesize_t` | Set frame size | + +### Image Quality + +| Command | Value | Argument | Description | +|---------|-------|----------|-------------| +| `OV2640_CMD_SET_BRIGHTNESS` | 0x03 | int −2…+2 | Brightness | +| `OV2640_CMD_SET_CONTRAST` | 0x04 | int −2…+2 | Contrast | +| `OV2640_CMD_SET_SATURATION` | 0x05 | int −2…+2 | Saturation | +| `OV2640_CMD_SET_QUALITY` | 0x06 | int 0…63 | JPEG quality (0 = best) | +| `OV2640_CMD_SET_GAINCEILING` | 0x1C | `gainceiling_t` | AGC gain ceiling | + +### Image Processing + +| Command | Value | Argument | Description | +|---------|-------|----------|-------------| +| `OV2640_CMD_SET_HMIRROR` | 0x07 | int 0/1 | Horizontal mirror | +| `OV2640_CMD_SET_VFLIP` | 0x08 | int 0/1 | Vertical flip | +| `OV2640_CMD_SET_COLORBAR` | 0x09 | int 0/1 | Color bar test pattern | +| `OV2640_CMD_SET_SPECIAL_EFFECT` | 0x14 | int 0…6 | Special effect | +| `OV2640_CMD_SET_DCW` | 0x17 | int 0/1 | Downsize control | +| `OV2640_CMD_SET_BPC` | 0x18 | int 0/1 | Bad pixel correction | +| `OV2640_CMD_SET_WPC` | 0x19 | int 0/1 | White pixel correction | +| `OV2640_CMD_SET_RAW_GMA` | 0x1A | int 0/1 | RAW gamma | +| `OV2640_CMD_SET_LENC` | 0x1B | int 0/1 | Lens correction | + +### White Balance + +| Command | Value | Argument | Description | +|---------|-------|----------|-------------| +| `OV2640_CMD_SET_WHITEBAL` | 0x0A | int 0/1 | AWB on/off | +| `OV2640_CMD_SET_AWB_GAIN` | 0x0E | int 0/1 | AWB gain on/off | +| `OV2640_CMD_SET_WB_MODE` | 0x15 | int 0…4 | WB mode (0 = auto) | + +### Exposure and Gain + +| Command | Value | Argument | Description | +|---------|-------|----------|-------------| +| `OV2640_CMD_SET_GAIN_CTRL` | 0x0B | int 0/1 | AGC on/off | +| `OV2640_CMD_SET_EXPOSURE_CTRL` | 0x0C | int 0/1 | AEC on/off | +| `OV2640_CMD_SET_AEC2` | 0x0D | int 0/1 | AEC DSP mode on/off | +| `OV2640_CMD_SET_AGC_GAIN` | 0x0F | int 0…30 | Manual AGC gain | +| `OV2640_CMD_SET_AEC_VALUE` | 0x13 | int 0…1200 | Manual AEC exposure | +| `OV2640_CMD_SET_AE_LEVEL` | 0x16 | int −2…+2 | AE target brightness offset | + +### Data Bus and Buffers + +| Command | Value | Argument | Description | +|---------|-------|----------|-------------| +| `OV2640_CMD_SET_FRAME_BUFFER` | 0x1F | `void *` | Set frame buffer pointer | +| `OV2640_CMD_SET_FRAME_BUFFER_SIZE` | 0x20 | `uint32_t` | Set frame buffer size | +| `OV2640_CMD_SET_PINGPONG_SIZE` | 0x21 | `uint32_t` | Set DVP DMA ping-pong size | + +### Capture Control + +| Command | Value | Argument | Description | +|---------|-------|----------|-------------| +| `OV2640_CMD_START_CAPTURE` | 0x10 | `void *` (NULL = keep current) | Start single-shot capture | +| `OV2640_CMD_STOP_CAPTURE` | 0x11 | — | Abort current single capture | +| `OV2640_CMD_GET_FRAME_SIZE` | 0x12 | `uint32_t *` | Get captured frame byte count | +| `OV2640_CMD_START_STREAM` | 0x22 | `camera_stream_start_args_t *` | Start continuous double-buffer stream | +| `OV2640_CMD_STOP_STREAM` | 0x23 | — | Stop continuous stream | + +### Supported Pixel Formats + +- PIXFORMAT_JPEG +- PIXFORMAT_RGB565 +- PIXFORMAT_YUV422 +- PIXFORMAT_RAW8 + +### Supported Frame Sizes + +- FRAMESIZE_96X96 +- FRAMESIZE_QQVGA +- FRAMESIZE_128X128 +- FRAMESIZE_QCIF +- FRAMESIZE_HQVGA +- FRAMESIZE_240X240 +- FRAMESIZE_QVGA +- FRAMESIZE_320X320 +- FRAMESIZE_CIF +- FRAMESIZE_HVGA +- FRAMESIZE_VGA +- FRAMESIZE_SVGA +- FRAMESIZE_XGA +- FRAMESIZE_HD +- FRAMESIZE_SXGA +- FRAMESIZE_UXGA + +## Example Commands + +## Examples + +| Directory | Description | +|-----------|-------------| +| `examples/take_photo/` | MSH command `take_photo`: RGB565 single-shot capture, prints frame buffer address | +| `examples/take_photo_to_sdcard/` | Capture and save image to SD card | + +### take_photo + +```text +msh> take_photo + +msh> take_photo QVGA 1 +msh> take_photo VGA 3 +``` + +After capture the frame buffer address is printed. View the image directly via J-Link (tools: `C:\WORK\SiFli-SDK\main\tools\bin2bmp\`): + +``` +python jlinkbin2bmp.py "-if SWD -speed 12000" rgb565 +``` + +Example (QVGA): + +``` +python jlinkbin2bmp.py "-if SWD -speed 12000" rgb565 320 240 0x20100000 +``` + +## Notes + +1. **Continuous capture** uses double buffering; the caller must supply two DMA-accessible frame buffers and process each frame before the next two arrive. +2. **Single-shot and streaming are mutually exclusive**: do not call `camera_capture_single` while a stream is active. +3. **JPEG buffer sizing**: output length is variable — allow at least 300 KB for VGA; UXGA may need 1–2 MB. +4. **Fixed-size modes** (RGB565 / YUV422 / RAW8): buffer must be exactly `width × height × bytes_per_pixel` (RGB565/YUV422 = 2, RAW8 = 1). +5. **After changing format or resolution**: `camera_change_settings` inserts ~500 ms internally; when using raw commands, call `rt_thread_mdelay(500)` manually. +6. **Ping-pong buffer size** (`OV2640_DVP_PINGPONG_BUFFER_SIZE`): for RAW mode set at least `row_width × 2` bytes; larger values reduce risk of DMA truncation in JPEG mode. +7. **Timeout or first-frame-only**: check VSYNC pin, PCLK / HSYNC / XCLK configuration, and DVP ping-pong buffer size first. + +## Dependencies + +- RT-Thread device framework +- RT-Thread I2C driver framework +- SiFli BSP HAL (GPIO, DMA, GPTIM) +- An I2C bus device available as `i2c1` (default) + +## Related Documents + +- Chinese version: [README.md](README.md) +- take_photo example: [examples/take_photo/README_EN.md](examples/take_photo/README_EN.md) diff --git a/camera_framework/SConscript b/camera_framework/SConscript new file mode 100644 index 0000000..1e2b994 --- /dev/null +++ b/camera_framework/SConscript @@ -0,0 +1,11 @@ +from building import * + +# keep current working dir +cwd = GetCurrentDir() +# collect sources under camera (support nested driver subdirs) +src = Glob('./camera/*.c') + Glob('./camera/*/*.c') + Glob('./camera/*/*/*.c') + Glob('./camera/*/*/*/*.c') +# include paths: camera root, camera/include, and specific ov2640 driver headers +include = ['./camera', './camera/bus/control', './camera/driver/ov2640', './camera/bus/data', './camera/handle'] +group = DefineGroup('Drivers', src, depend = ['SENSOR_USING_OV2640'], CPPPATH = include, LIBPATH = include) + +Return('group') diff --git a/camera_framework/camera/bus/control/sccb.c b/camera_framework/camera/bus/control/sccb.c new file mode 100644 index 0000000..6c21867 --- /dev/null +++ b/camera_framework/camera/bus/control/sccb.c @@ -0,0 +1,117 @@ +/** + * @file sccb.c + * @brief SCCB (Serial Camera Control Bus) communication layer + * + * This module implements the SCCB protocol over I2C for + * reading and writing OV2640 sensor registers. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2026 + */ + +#include "sccb.h" +#include + +#define DBG_TAG "sccb" +#define DBG_LVL DBG_LOG +#include + +static struct rt_i2c_bus_device *i2c_bus = RT_NULL; + +static rt_err_t sccb_configure_pins(const char *i2c_bus_name) +{ + if (i2c_bus_name == RT_NULL) + { + LOG_E("SCCB: I2C bus name is null"); + return -RT_EINVAL; + } + + if (strcmp(i2c_bus_name, "i2c1") == 0) + { + HAL_PIN_Set(SCCB_SCL_PIN, I2C1_SCL, PIN_PULLUP, 1); + HAL_PIN_Set(SCCB_SDA_PIN, I2C1_SDA, PIN_PULLUP, 1); + return RT_EOK; + } + + if (strcmp(i2c_bus_name, "i2c2") == 0) + { + HAL_PIN_Set(SCCB_SCL_PIN, I2C2_SCL, PIN_PULLUP, 1); + HAL_PIN_Set(SCCB_SDA_PIN, I2C2_SDA, PIN_PULLUP, 1); + return RT_EOK; + } + + LOG_E("SCCB: unsupported I2C bus name %s", i2c_bus_name); + return -RT_EINVAL; +} + +rt_err_t sccb_init(const char *i2c_bus_name) +{ + rt_err_t ret = sccb_configure_pins(i2c_bus_name); + if (ret != RT_EOK) + { + return ret; + } + + i2c_bus = rt_i2c_bus_device_find(i2c_bus_name); + if (i2c_bus == RT_NULL) + { + LOG_E("SCCB: I2C bus %s not found!", i2c_bus_name); + return -RT_ERROR; + } + ret = rt_device_open((rt_device_t)i2c_bus, RT_DEVICE_OFLAG_RDWR); + if (ret != RT_EOK) + { + LOG_E("Failed to open I2C bus: %d", ret); + return ret; + } + + struct rt_i2c_configuration configuration = + { + .mode = 0, + .addr = 0, + .timeout = SCCB_TIMEOUT_MS, //Waiting for timeout period (ms) + .max_hz = SCCB_MAX_HZ, //I2C rate (hz) + }; + // config I2C parameter + return rt_i2c_configure(i2c_bus, &configuration); +} + +void sccb_deinit(void) +{ + if (i2c_bus != RT_NULL) + { + rt_device_close((rt_device_t)i2c_bus); + i2c_bus = RT_NULL; + LOG_I("SCCB deinitialized"); + } +} + +int sccb_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t data) +{ + rt_size_t res = 0; + uint8_t buf[2]; + buf[0] = reg_addr; + buf[1] = data; + res = rt_i2c_master_send(i2c_bus, dev_addr, RT_I2C_WR , buf, 2); + if(res<2) return -RT_ERROR; + return RT_EOK; +} + +uint8_t sccb_read(uint8_t dev_addr, uint8_t reg_addr) +{ + rt_size_t res = 0; + uint8_t data = 0; + res = rt_i2c_master_send(i2c_bus, dev_addr, RT_I2C_WR , ®_addr, 1); + if(res<1) + { + LOG_W("SCCB: failed to send register 0x%02X to device 0x%02X", reg_addr, dev_addr); + return 0; + } + res = rt_i2c_master_recv(i2c_bus, dev_addr, RT_I2C_RD , &data, 1); + if(res<1) + { + LOG_W("SCCB: failed to read from device 0x%02X register 0x%02X", dev_addr, reg_addr); + return 0; + } + return data; +} \ No newline at end of file diff --git a/camera_framework/camera/bus/control/sccb.h b/camera_framework/camera/bus/control/sccb.h new file mode 100644 index 0000000..b05c3b9 --- /dev/null +++ b/camera_framework/camera/bus/control/sccb.h @@ -0,0 +1,59 @@ +/** + * @file sccb.h + * @brief SCCB (Serial Camera Control Bus) communication interface + * + * This header defines the SCCB initialization, read/write API + * and configurable parameters for I2C-based sensor register access. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2026 + */ + +#ifndef SCCB_H_ +#define SCCB_H_ + +#include +#include +#include "drivers/i2c.h" +#include "bf0_hal.h" + +#ifdef PKG_USING_OV2640 +#include +#endif + +#ifndef OV2640_SCCB_TIMEOUT_MS +#define SCCB_TIMEOUT_MS 1000 +#else +#define SCCB_TIMEOUT_MS OV2640_SCCB_TIMEOUT_MS +#endif + +#ifndef OV2640_SCCB_MAX_HZ +#define SCCB_MAX_HZ 100000 +#else +#define SCCB_MAX_HZ OV2640_SCCB_MAX_HZ +#endif + +#ifndef OV2640_SCCB_I2C_BUS_NAME +#define SCCB_USE_IIC "i2c1" +#else +#define SCCB_USE_IIC OV2640_SCCB_I2C_BUS_NAME +#endif + +#ifdef SF32LB52X +#define SCCB_SCL_PIN PAD_PA40 // PA40 +#define SCCB_SDA_PIN PAD_PA39 // PA39 + +#elif defined(SF32LB56X) +#define SCCB_SCL_PIN PAD_PA76 // PA76 +#define SCCB_SDA_PIN PAD_PA72 // PA72 + +#else +#error "SCCB pin definitions not set for this platform" +#endif + +rt_err_t sccb_init(const char *i2c_bus_name); +void sccb_deinit(void); +int sccb_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t data); +uint8_t sccb_read(uint8_t dev_addr, uint8_t reg_addr); + +#endif // SCCB_H_ \ No newline at end of file diff --git a/camera_framework/camera/bus/data/data_bus_adapter.c b/camera_framework/camera/bus/data/data_bus_adapter.c new file mode 100644 index 0000000..0696312 --- /dev/null +++ b/camera_framework/camera/bus/data/data_bus_adapter.c @@ -0,0 +1,249 @@ +/******************************************************************************** + * Copyright (C) 2026 SiFli, Inc.(Gmbh) or its affiliates. + * + * All Rights Reserved. + * + * @file data_bus_adapter.c + * + * @par dependencies + * - data_bus_adapter.h + * - string.h + * + * @author SiFli 思澈科技 + * + * @brief Data bus adapter registry and dispatch implementation. + * + * This module provides a lightweight static registry for `bus_adapter_t` + * instances and implements thin wrapper APIs that validate pointers then + * dispatch lifecycle/capture operations through `bus_adapter_ops_t`. + * + * Processing flow: + * - Backend registers itself once via `bus_adapter_register()`. + * - Upper layers locate backend by name with `bus_adapter_find()`. + * - Wrapper APIs (`bus_adapter_*`) check `self` and op pointers. + * - On valid dispatch, wrappers call into backend implementation directly. + * + * Notes: + * - Registry is static and has a fixed capacity (`BUS_ADAPTER_MAX`). + * - Duplicate registration by adapter name is treated as success. + * - Wrappers return unified error codes for invalid/unsupported operations. + * + * @version V1.0 2026-5-11 + * + * @note 1 tab == 4 spaces! + * + ******************************************************************************/ +#include "data_bus_adapter.h" +#include + +#define BUS_ADAPTER_MAX 8 +static bus_adapter_t *g_adapters[BUS_ADAPTER_MAX]; +static int g_adapter_count = 0; + +/** + * @brief Register a bus adapter into the static registry. + * + * Registration is idempotent by adapter name: if a same-name adapter already + * exists, this function returns BUS_OK and keeps the existing entry. + * + * @param adapter is a pointer to the adapter instance. + * + * @return Return BUS_OK on success. + * Return BUS_ERR_INVALID if adapter/name/ops is invalid. + * Return BUS_ERR_NO_SLOT when registry is full. + */ +int bus_adapter_register(bus_adapter_t *adapter) +{ + if (!adapter || !adapter->name || !adapter->ops) + return BUS_ERR_INVALID; + + if (g_adapter_count >= BUS_ADAPTER_MAX) + return BUS_ERR_NO_SLOT; + + for (int i = 0; i < g_adapter_count; i++) { + if (strcmp(g_adapters[i]->name, adapter->name) == 0) + return BUS_OK; + } + + g_adapters[g_adapter_count++] = adapter; + return BUS_OK; +} + +/** + * @brief Find a registered adapter by name. + * + * @param name is the adapter name string. + * + * @return Return adapter pointer when found; otherwise return NULL. + */ +bus_adapter_t *bus_adapter_find(const char *name) +{ + if (!name) + return NULL; + for (int i = 0; i < g_adapter_count; i++) { + if (strcmp(g_adapters[i]->name, name) == 0) + return g_adapters[i]; + } + return NULL; +} + +/* + * Common dispatch helper used by thin wrapper APIs. + * + * - Return BUS_ERR_INVALID if self or self->ops is NULL. + * - Return BUS_ERR_NOT_SUPPORTED if requested op is NULL. + * - Otherwise forward call to the concrete adapter op. + */ +#define BUS_OP_DISPATCH(self_, member_, ...) \ + do { \ + if (!(self_) || !(self_)->ops) \ + return BUS_ERR_INVALID; \ + if (!(self_)->ops->member_) \ + return BUS_ERR_NOT_SUPPORTED; \ + return (self_)->ops->member_(__VA_ARGS__); \ + } while (0) + +/** + * @brief Wrapper for adapter init op. + * @param self is the adapter instance. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_init(bus_adapter_t *self) +{ + BUS_OP_DISPATCH(self, init, self); +} + +/** + * @brief Wrapper for adapter deinit op. + * @param self is the adapter instance. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_deinit(bus_adapter_t *self) +{ + BUS_OP_DISPATCH(self, deinit, self); +} + +/** + * @brief Wrapper for adapter start op. + * @param self is the adapter instance. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_start(bus_adapter_t *self) +{ + BUS_OP_DISPATCH(self, start, self); +} + +/** + * @brief Wrapper for adapter stop op. + * @param self is the adapter instance. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_stop(bus_adapter_t *self) +{ + BUS_OP_DISPATCH(self, stop, self); +} + +/** + * @brief Wrapper for frame callback registration op. + * @param self is the adapter instance. + * @param callback is the callback function; NULL means unregister. + * @param user_data is the opaque pointer forwarded to `callback`. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_set_frame_callback(bus_adapter_t *self, + bus_frame_ready_callback_t callback, + void *user_data) +{ + BUS_OP_DISPATCH(self, set_frame_callback, self, callback, user_data); +} + +/** + * @brief Wrapper for start-capture op. + * @param self is the adapter instance. + * @param buffer is destination frame buffer pointer. + * @param size is destination frame buffer size in bytes. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_start_capture(bus_adapter_t *self, void *buffer, uint32_t size) +{ + BUS_OP_DISPATCH(self, start_capture, self, buffer, size); +} + +/** + * @brief Wrapper for rearm-capture op. + * @param self is the adapter instance. + * @param buffer is destination frame buffer pointer. + * @param size is destination frame buffer size in bytes. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_rearm_capture(bus_adapter_t *self, void *buffer, uint32_t size) +{ + BUS_OP_DISPATCH(self, rearm_capture, self, buffer, size); +} + +/** + * @brief Wrapper for abort-capture op. + * @param self is the adapter instance. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_abort_capture(bus_adapter_t *self) +{ + BUS_OP_DISPATCH(self, abort_capture, self); +} + +/** + * @brief Wrapper for update-buffer op. + * @param self is the adapter instance. + * @param buffer is destination frame buffer pointer. + * @param size is destination frame buffer size in bytes. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_update_buffer(bus_adapter_t *self, void *buffer, uint32_t size) +{ + BUS_OP_DISPATCH(self, update_buffer, self, buffer, size); +} + +/** + * @brief Wrapper for ping-pong-size op. + * @param self is the adapter instance. + * @param size is requested ping-pong size in bytes. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_set_pingpong_size(bus_adapter_t *self, uint32_t size) +{ + BUS_OP_DISPATCH(self, set_pingpong_size, self, size); +} + +/** + * @brief Wrapper for frame-size query op. + * @param self is the adapter instance. + * @return Return frame size when op is implemented; otherwise return 0. + */ +uint32_t bus_adapter_get_frame_size(bus_adapter_t *self) +{ + if (!self || !self->ops || !self->ops->get_frame_size) + return 0; + return self->ops->get_frame_size(self); +} + +/** + * @brief Wrapper for set-mode op. + * @param self is the adapter instance. + * @param mode is the requested generic capture mode. + * @return BUS_OK / BUS_ERR_INVALID / BUS_ERR_NOT_SUPPORTED or op return code. + */ +int bus_adapter_set_mode(bus_adapter_t *self, bus_capture_mode_t mode) +{ + BUS_OP_DISPATCH(self, set_mode, self, mode); +} + +/** + * @brief Invoke the adapter's optional dump_state op for hardware diagnostics. + * @param self is the adapter instance. + */ +void bus_adapter_dump_state(bus_adapter_t *self) +{ + if (!self || !self->ops || !self->ops->dump_state) + return; + self->ops->dump_state(self); +} diff --git a/camera_framework/camera/bus/data/data_bus_adapter.h b/camera_framework/camera/bus/data/data_bus_adapter.h new file mode 100644 index 0000000..bec47e9 --- /dev/null +++ b/camera_framework/camera/bus/data/data_bus_adapter.h @@ -0,0 +1,299 @@ +/******************************************************************************** + * Copyright (C) 2026 SiFli, Inc.(Gmbh) or its affiliates. + * + * All Rights Reserved. + * + * @file data_bus_adapter.h + * + * @par dependencies + * - stdint.h + * - stddef.h + * + * @author SiFli 思澈科技 + * + * @brief Data bus adapter interface for camera drivers. + * + * This header defines a generic data-bus abstraction used by camera sensor + * drivers (for example OV2640) to decouple upper-layer capture logic from + * concrete transport backends such as DVP, CSI, or SPI. + * + * Main design points: + * - A concrete backend registers one `bus_adapter_t` instance by name. + * - Backend private state is carried through `bus_adapter_t::priv`. + * - All operations are grouped in `bus_adapter_ops_t` and uniformly receive + * `bus_adapter_t *self` as the first argument. + * - Thin wrapper APIs validate pointers and dispatch calls to the backend ops. + * + * Notes: + * - `bus_adapter_set_frame_callback()` callback is typically invoked in bus ISR + * context; callback work should be short and non-blocking. + * - Wrapper APIs return unified `bus_status_t` style error codes when dispatch + * is invalid or unsupported. + * + * @version V1.0 2026-5-11 + * + * @note 1 tab == 4 spaces! + * + ******************************************************************************/ +#ifndef __DATA_BUS_ADAPTER_H__ +#define __DATA_BUS_ADAPTER_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +typedef enum { + BUS_TYPE_INVALID = 0, + BUS_TYPE_DVP, + BUS_TYPE_SPI, + BUS_TYPE_DCMI, + BUS_TYPE_MAX, +} bus_type_t; + +/* + * Generic capture pixel/stream mode advertised to bus adapters. + * + * Concrete adapters (e.g. DVP) may translate this to their native enum. + * Values are stable and shared between upper-layer drivers and bus adapters, + * so any new format must be appended at the tail to preserve ABI. + */ +typedef enum { + BUS_CAPTURE_MODE_JPEG = 0, /* JPEG stream (variable length, SOI/EOI framed) */ + BUS_CAPTURE_MODE_RAW = 1, /* RAW8 / fixed-size capture */ + BUS_CAPTURE_MODE_YUV422 = 2, /* YUV422 fixed-size capture */ + BUS_CAPTURE_MODE_RGB565 = 3, /* RGB565 fixed-size capture */ +} bus_capture_mode_t; + +/* Error codes returned by bus_adapter_* APIs. + * Negative values represent errors; BUS_OK (0) means success. */ +typedef enum { + BUS_OK = 0, + BUS_ERR_INVALID = -1, /* invalid argument (NULL self / bad cfg) */ + BUS_ERR_NOT_SUPPORTED = -2,/* op not implemented by this adapter */ + BUS_ERR_NO_SLOT = -3, /* registry full */ + BUS_ERR_HW = -4, /* underlying hardware reported failure */ +} bus_status_t; + +typedef struct bus_frame { + void *buffer; + uint32_t length; + uint32_t timestamp; + uint32_t sequence; +} bus_frame_t; + +struct bus_adapter; +typedef struct bus_adapter bus_adapter_t; + +/* Frame ready callback. Called from bus IRQ context; keep work minimal. */ +typedef void (*bus_frame_ready_callback_t)(bus_adapter_t *self, + const bus_frame_t *frame, + void *user_data); + +/* + * Bus adapter operations. + * + * - init / deinit / start / stop are lifecycle. + * - set_frame_callback registers a callback invoked when a complete frame is ready. + * - start_capture / abort_capture / rearm_capture drive a single-frame capture + * sequence that the bus implementation uses to build its data path. + * - update_buffer / set_pingpong_size / get_frame_size are runtime tunables + * common to DMA-driven bus implementations. + * + * Every op takes the owning adapter as `self`; implementations cast `self->priv` + * to access their private state. + * + * Any op may be NULL if the bus does not support that operation; callers must + * handle NULL gracefully (see bus_adapter_* helper wrappers below). + */ +typedef struct bus_adapter_ops { + int (*init)(bus_adapter_t *self); + int (*deinit)(bus_adapter_t *self); + int (*start)(bus_adapter_t *self); + int (*stop)(bus_adapter_t *self); + int (*set_frame_callback)(bus_adapter_t *self, + bus_frame_ready_callback_t callback, + void *user_data); + + int (*start_capture)(bus_adapter_t *self, void *buffer, uint32_t size); + int (*rearm_capture)(bus_adapter_t *self, void *buffer, uint32_t size); + int (*abort_capture)(bus_adapter_t *self); + int (*update_buffer)(bus_adapter_t *self, void *buffer, uint32_t size); + int (*set_pingpong_size)(bus_adapter_t *self, uint32_t size); + uint32_t (*get_frame_size)(bus_adapter_t *self); + /* + * Change the capture mode at runtime. + * + * Implementations are expected to (1) abort any in-flight transfer and + * stop the data path before mutating internal state, and (2) leave the + * adapter in the same lifecycle state as on entry (i.e. caller is still + * responsible for re-issuing bus_adapter_start when needed). Returns + * BUS_OK or a negative bus_status_t value. + */ + int (*set_mode)(bus_adapter_t *self, bus_capture_mode_t mode); + /* + * Optional diagnostics hook. Invoked when the upper layer needs to dump + * bus-specific hardware state (DMA counters, timer registers, etc.). + * NULL means this adapter provides no diagnostic output. + */ + void (*dump_state)(bus_adapter_t *self); +} bus_adapter_ops_t; + +struct bus_adapter { + const char *name; + bus_type_t type; + const bus_adapter_ops_t *ops; + void *priv; +}; + +/** + * @brief Register a bus adapter instance into the global registry. + * + * If another adapter with the same name already exists, this function returns + * BUS_OK and keeps the original entry. + * + * @param adapter is a pointer to the adapter instance to register. + * + * @return Return BUS_OK on success. + * Return BUS_ERR_INVALID if adapter/name/ops is invalid. + * Return BUS_ERR_NO_SLOT when registry is full. + */ +int bus_adapter_register(bus_adapter_t *adapter); + +/** + * @brief Find a registered bus adapter by name. + * + * @param name is the adapter name string. + * + * @return Return the matching adapter pointer when found; otherwise return NULL. + */ +bus_adapter_t *bus_adapter_find(const char *name); + +/* + * Thin wrappers that validate self/ops and dispatch to ops table. + * + * Return convention: + * - BUS_OK on success + * - BUS_ERR_INVALID when self/ops is invalid + * - BUS_ERR_NOT_SUPPORTED when the specific op is not implemented + * - or underlying adapter return code + */ + +/** + * @brief Initialize adapter runtime and hardware resources. + * @param self is the adapter instance. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_init(bus_adapter_t *self); + +/** + * @brief Deinitialize adapter runtime and hardware resources. + * @param self is the adapter instance. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_deinit(bus_adapter_t *self); + +/** + * @brief Start adapter hardware data path. + * @param self is the adapter instance. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_start(bus_adapter_t *self); + +/** + * @brief Stop adapter hardware data path. + * @param self is the adapter instance. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_stop(bus_adapter_t *self); + +/** + * @brief Register frame callback invoked when one frame is ready. + * @param self is the adapter instance. + * @param callback is the callback function; NULL means unregister. + * @param user_data is the opaque pointer forwarded to `callback`. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_set_frame_callback(bus_adapter_t *self, + bus_frame_ready_callback_t callback, + void *user_data); + +/** + * @brief Start one frame capture and optionally set destination buffer. + * @param self is the adapter instance. + * @param buffer is destination frame buffer pointer. + * @param size is destination frame buffer size in bytes. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_start_capture(bus_adapter_t *self, void *buffer, uint32_t size); + +/** + * @brief Rearm capture state for next frame without full stop/start. + * @param self is the adapter instance. + * @param buffer is destination frame buffer pointer. + * @param size is destination frame buffer size in bytes. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_rearm_capture(bus_adapter_t *self, void *buffer, uint32_t size); + +/** + * @brief Abort current capture sequence. + * @param self is the adapter instance. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_abort_capture(bus_adapter_t *self); + +/** + * @brief Update destination frame buffer pointer and size. + * @param self is the adapter instance. + * @param buffer is destination frame buffer pointer. + * @param size is destination frame buffer size in bytes. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_update_buffer(bus_adapter_t *self, void *buffer, uint32_t size); + +/** + * @brief Update ping-pong DMA buffer size. + * @param self is the adapter instance. + * @param size is requested ping-pong size in bytes. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_set_pingpong_size(bus_adapter_t *self, uint32_t size); + +/** + * @brief Query current captured frame size in bytes. + * @param self is the adapter instance. + * @return Return frame size when op is implemented; otherwise 0. + */ +uint32_t bus_adapter_get_frame_size(bus_adapter_t *self); + +/** + * @brief Change the bus capture mode at runtime. + * + * The adapter implementation must stop any in-flight capture before changing + * mode. Callers are still responsible for re-issuing @ref bus_adapter_start + * (or equivalent) when needed. + * + * @param self is the adapter instance. + * @param mode is the requested generic capture mode. + * @return See thin-wrapper return convention above. + */ +int bus_adapter_set_mode(bus_adapter_t *self, bus_capture_mode_t mode); + +/** + * @brief Dump bus adapter hardware diagnostic state to the log. + * + * Intended for debug/timeout diagnostics. No-op when the adapter does not + * implement the @c dump_state op. + * + * @param self is the adapter instance. + */ +void bus_adapter_dump_state(bus_adapter_t *self); + +#ifdef __cplusplus +} +#endif + +#endif /* __DATA_BUS_ADAPTER_H__ */ diff --git a/camera_framework/camera/bus/data/dvp.c b/camera_framework/camera/bus/data/dvp.c new file mode 100644 index 0000000..235b87d --- /dev/null +++ b/camera_framework/camera/bus/data/dvp.c @@ -0,0 +1,1698 @@ +/******************************************************************************** + * Copyright (C) 2026 SiFli, Inc.(Gmbh) or its affiliates. + * + * All Rights Reserved. + * + * @file dvp.c + * + * @par dependencies + * - rtthread.h + * - dvp.h + * - stdint.h + * - stdio.h + * - string.h + * - rthw.h + * - rtdevice.h + * + * @author SiFli 思澈科技 + * + * @brief DVP (Digital Video Port) software driver implementation. + * + * This module implements a software DVP interface used to capture camera + * parallel data (D0..D7) using a GPTIM as pixel/line clock and DMA with + * ping-pong buffering. It handles DMA callbacks, VSYNC interrupts and offers + * a small API to start/stop capture, configure ping-pong buffers and query + * capture status. + * + * Processing flow: + * - Initialize with `dvp_init()` which allocates buffers and configures + * pins, DMA and timer resources. + * - Hardware delivers pixels into a ping-pong DMA buffer. DMA callbacks + * call into this module to process halves or full transfers. + * - For JPEG mode the driver searches SOI/EOI markers to form frames; + * for raw/pixel modes it copies fixed-size chunks until the user buffer + * is filled. + * - The driver notifies the upper layer via the user callback registered with + * `bus_adapter_set_frame_callback()`. + * + * Notes: + * - This implementation binds to board-specific timer/DMA instances and + * pin mappings; platform abstraction is minimal for performance reasons. + * - Callers should treat these APIs as non-ISR (except callbacks) and + * avoid long-running work in callback contexts. + * + * @version V1.0 2026-4-3 + * + * @note 1 tab == 4 spaces! + * + ******************************************************************************/ +#include "dvp.h" +#include "drv_io.h" +#include "stdio.h" +#include "string.h" +#include "rtthread.h" +#include "rthw.h" +#include +#include + +#define DBG_TAG "dvp" +#define DBG_LVL DBG_LOG +#include + +#define DEBUG_DVP 0 + +/* When OV2640_DVP_PINGPONG_USE_SECTION is enabled in Kconfig the buffer is + * placed in the ".dvp_pingpong" section so the board link script can map it + * to a dedicated SRAM region (e.g. DVP_SRAM on SF32LB56X). When disabled + * the buffer falls into normal .bss, which is sufficient for SF32LB52X. + * + * The array is sized by OV2640_DVP_PINGPONG_POOL_SIZE — the compile-time + * ceiling. dvp_set_pingpong_size() can change the active size at runtime to + * any value in [1, POOL_SIZE] without recompiling. */ +#ifdef OV2640_DVP_PINGPONG_USE_SECTION +uint8_t dvp_pingpong_buffer[OV2640_DVP_PINGPONG_POOL_SIZE] + __attribute__((section(".dvp_pingpong"))); +#else +uint8_t dvp_pingpong_buffer[OV2640_DVP_PINGPONG_POOL_SIZE]; +#endif + +#if DEBUG_DVP +#define DVP_DEBUG_SNAPSHOT_SIZE 6000U +static uint8_t g_dvp_debug_snapshot[DVP_DEBUG_SNAPSHOT_SIZE]; +#endif + +/* + * DVP is a single hardware instance on this MCU. We expose the driver as a + * `bus_adapter_t` so upper layers (e.g. ov2640.c) drive it through the generic + * adapter interface; the public `dvp_*` symbols below are themselves the + * `bus_adapter_ops_t` entries (no extra wrapper layer). + */ +static dvp_handle_t s_dvp_handle; +static bus_frame_ready_callback_t s_user_frame_callback; +static void *s_user_frame_callback_data; +static bus_adapter_t s_dvp_bus_adapter; + +/* GPTIM2 used to generate XCLK (sensor master clock) via PWM */ +static GPT_HandleTypeDef s_xclk_gptim; +static rt_bool_t s_xclk_initialized = RT_FALSE; + +/* Build a bus_frame_t from the current DVP capture state and dispatch it + * directly to the user-registered bus callback. Called from the DMA ISR / + * timer-thread context whenever a complete frame is ready. */ +static void dvp_dispatch_frame(dvp_handle_t *handle) +{ + if (s_user_frame_callback == RT_NULL) + return; + + bus_frame_t f; + f.buffer = handle->config.frame_buffer; + f.length = handle->capture.current_size; + f.timestamp = rt_tick_get(); + f.sequence = 0; + s_user_frame_callback(&s_dvp_bus_adapter, &f, s_user_frame_callback_data); +} + +/* 为免搬运直接存放数据申请的高速SRAM空间:600KB */ +//uint8_t g_sram_debug_buffer[614400] __attribute__((section(".bss.sram"))); + +static dvp_handle_t *dvp_get_handle_from_dma(DMA_HandleTypeDef *hdma) +{ + if (hdma == RT_NULL) + { + return RT_NULL; + } + + return (dvp_handle_t *)((uint8_t *)hdma - offsetof(dvp_handle_t, dma)); +} + +static GPT_TypeDef *dvp_get_timer_instance(const dvp_handle_t *handle) +{ + return (GPT_TypeDef *)handle->config.resources.timer_instance; +} + +static GPIO_TypeDef *dvp_get_data_gpio_instance(const dvp_handle_t *handle) +{ + return (GPIO_TypeDef *)handle->config.resources.data_gpio_instance; +} + +/** + * @brief Search a memory buffer for the JPEG SOI marker. + * + * This function scans the specified buffer for the first occurrence of the + * JPEG Start Of Image marker sequence `0xFF 0xD8`. + * + * @param buffer is a pointer to the input buffer to search. + * + * @param length is the size of the input buffer in bytes. + * + * @return Return the zero-based offset of the SOI marker when found. + * If the marker is not found or the buffer is too short, `-1` is returned. + */ +static int search_for_SOI(uint8_t *buffer, size_t length) +{ + if (length < 2) return -1; + for (size_t i = 0; i < length - 1; i++) + { + if ((buffer[i] == (uint8_t)0xFF) && (buffer[i + 1] == (uint8_t)0xD8)) + { + return i; + } + } + return -1; +} + +/** + * @brief Search a memory buffer for the JPEG EOI marker. + * + * This function scans the specified buffer for the first occurrence of the + * JPEG End Of Image marker sequence `0xFF 0xD9`. + * + * @param buffer is a pointer to the input buffer to search. + * + * @param length is the size of the input buffer in bytes. + * + * @return Return the zero-based offset of the last byte of the EOI marker + * when found. If the marker is not found or the buffer is too short, + * `-1` is returned. + */ +static int search_for_EOI(uint8_t *buffer, size_t length) +{ + if (length < 2) return -1; + for (size_t i = 0; i < length - 1; i++) + { + if ((buffer[i] == (uint8_t)0xFF) && (buffer[i + 1] == (uint8_t)0xD9)) + { + return i + 1; + } + } + return -1; +} + +#if DEBUG_DVP +static void dump_buffer_hex(const uint8_t *buffer, + uint32_t length, + uint32_t pingpong_offset, + uint32_t frame_offset) +{ + uint32_t index = 0; + char line[80]; + + rt_kprintf("DVP raw chunk: pingpong_offset=%u frame_offset=%u bytes=%u\n", + (unsigned int)pingpong_offset, + (unsigned int)frame_offset, + (unsigned int)length); + while (index < length) + { + uint32_t line_bytes = ((length - index) > 16U) ? 16U : (length - index); + int pos = rt_snprintf(line, + sizeof(line), + "F%04u P%04u: ", + (unsigned int)(frame_offset + index), + (unsigned int)(pingpong_offset + index)); + + for (uint32_t i = 0; i < line_bytes; i++) + { + pos += rt_snprintf(&line[pos], sizeof(line) - pos, "%02X ", buffer[index + i]); + } + + rt_kprintf("%s\n", line); + index += line_bytes; + } +} + +static void snapshot_and_dump_buffer(const uint8_t *buffer, + uint32_t length, + uint32_t pingpong_offset, + uint32_t frame_offset) +{ + uint32_t snapshot_length = (length <= DVP_DEBUG_SNAPSHOT_SIZE) ? length : DVP_DEBUG_SNAPSHOT_SIZE; + + rt_memcpy(g_dvp_debug_snapshot, buffer, snapshot_length); + if (snapshot_length < length) + { + rt_kprintf("DVP raw chunk truncated: requested=%u snapshot=%u\n", + (unsigned int)length, + (unsigned int)snapshot_length); + } + + dump_buffer_hex(g_dvp_debug_snapshot, snapshot_length, pingpong_offset, frame_offset); +} +#endif /* DEBUG_DVP */ +static void dvp_reset_jpeg_boundary_state(dvp_handle_t *handle) +{ + handle->jpeg.prev_byte = 0; + handle->jpeg.prev_byte_valid = 0; +} + +static void dvp_update_jpeg_boundary_state(dvp_handle_t *handle, + const uint8_t *buffer, + uint32_t length) +{ + if (length == 0) + { + handle->jpeg.prev_byte_valid = 0; + return; + } + + handle->jpeg.prev_byte = buffer[length - 1]; + handle->jpeg.prev_byte_valid = 1; +} + +static void dvp_stop_jpeg_capture(dvp_handle_t *handle, uint8_t frame_ready) +{ + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.enable_capture = 0; + handle->capture.soi_found = 0; + handle->capture.frame_ready = frame_ready; + handle->capture.capture_started = 0; + if (!frame_ready) + { + handle->capture.current_size = 0; + } + dvp_reset_jpeg_boundary_state(handle); + rt_hw_interrupt_enable(level); +} + +static rt_err_t dvp_append_jpeg_bytes(dvp_handle_t *handle, + const uint8_t *source, + uint32_t copy_size) +{ + uint8_t *full_buffer = handle->config.frame_buffer; + uint32_t buffer_size = handle->config.buffer_size; + + if (handle->capture.current_size + copy_size > buffer_size) + { + LOG_E("DVP buffer overflow: current=%d, copy=%d, buffer=%d", + handle->capture.current_size, copy_size, buffer_size); + dvp_stop_jpeg_capture(handle, 0); + return -RT_ERROR; + } + + memcpy(&full_buffer[handle->capture.current_size], source, copy_size); + + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.current_size += copy_size; + rt_hw_interrupt_enable(level); + + return RT_EOK; +} + +/** + * @brief Process one RAW-format ping-pong buffer block. + * + * This function copies one half of the DMA ping-pong buffer into the user + * frame buffer for fixed-size capture modes such as RAW, RGB565 and YUV422. + * When the configured frame buffer becomes full, the function marks the frame + * as ready and notifies the registered callback if present. + * + * @param handle is a pointer to the DVP handle. + * + * @param buffer_offset is the offset of the active half-buffer inside the + * ping-pong buffer. Use `0` for the first half and `half_size` for the + * second half. + * + * @return This function does not return a value. + */ +static void process_raw_data(dvp_handle_t *handle, uint32_t buffer_offset) +{ +#if DEBUG_DVP + /* 调试模式下采用 DMA_NORMAL 且直接抛入 SRAM,因此不需要分段搬运,触发中断时即1帧结束 */ + // extern uint8_t g_sram_debug_buffer[614400]; + // rt_kprintf("\n[DEBUG] --- FULL FRAME DUMP FROM SRAM (First 64 bytes) ---\n"); + // dump_buffer_hex(g_sram_debug_buffer, 64, 0, 0); + + // /* 停止后续采集 */ + // __HAL_DMA_DISABLE(&handle->dma); + // handle->capture.frame_ready = 1; + // handle->capture.soi_found = 0; + // handle->capture.enable_capture = 0; + // handle->capture.capture_started = 0; +#endif + + uint8_t *full_buffer = handle->config.frame_buffer; + uint8_t *pingpong_buffer = handle->pingpong_buffer; + uint32_t half_size = handle->config.pingpong_buffer_size / 2; + uint8_t *source_ptr = &pingpong_buffer[buffer_offset]; + uint32_t buffer_size = handle->config.buffer_size; + + if (full_buffer == NULL || buffer_size == 0) + { + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.enable_capture = 0; + handle->capture.capture_started = 0; + handle->capture.frame_ready = 0; + rt_hw_interrupt_enable(level); + return; + } + + if (!handle->capture.soi_found) + return; /* Frame not started yet (waiting for VSYNC) */ + + uint32_t remaining = buffer_size - handle->capture.current_size; + if (remaining == 0) + return; + + uint32_t copy_size = (half_size <= remaining) ? half_size : remaining; + /* DEBUG: dump the last callback's pingpong buffer then halt to verify DMA data */ + // if (handle->capture.current_size >= 614400-5120 ) + // { + // __HAL_DMA_DISABLE(&handle->dma); + // rt_kprintf("\n[DEBUG] LAST pingpong dump (offset=%u, half=%u, frame_offset=%u):\n", + // (unsigned)buffer_offset, (unsigned)half_size, + // (unsigned)handle->capture.current_size); + // snapshot_and_dump_buffer(source_ptr, + // copy_size < DVP_DEBUG_SNAPSHOT_SIZE ? copy_size : DVP_DEBUG_SNAPSHOT_SIZE, + // buffer_offset, handle->capture.current_size); + // RT_ASSERT(0); + // } +#if DEBUG_DVP + /* 截取画面一半时的数据:总字节数614400,中间在 307200 左右 */ + //if (handle->capture.current_size >= (614400 / 8)*3) + if (handle->dma.Instance->CNDTR!=0x500&&handle->dma.Instance->CNDTR!=0xA00) + { + __HAL_DMA_DISABLE(&handle->dma); + rt_kprintf("\n[DEBUG] --- SOURCE DUMP (Pingpong Buffer) ---\n"); + snapshot_and_dump_buffer(source_ptr, 1400, buffer_offset, handle->capture.current_size); + RT_ASSERT(0); // 打印完毕后卡死并停留在当前状态 + } +#endif + /* Invalidate D-Cache for the pingpong half-buffer before CPU reads it. + * DMA writes directly to SRAM3 physical memory, bypassing the CPU cache. + * Without invalidation, the CPU memcpy would read stale cache lines and + * produce corrupted rows (visible as periodic snow bands) in the frame. */ + //mpu_dcache_invalidate((uint32_t *)source_ptr, copy_size); + memcpy(full_buffer + handle->capture.current_size, source_ptr, copy_size); + +#if DEBUG_DVP + if (handle->capture.current_size >= (614400 / 8)*3) + { + rt_kprintf("\n[DEBUG] --- DESTINATION DUMP (PSRAM FB) ---\n"); + snapshot_and_dump_buffer(full_buffer + handle->capture.current_size, copy_size, buffer_offset, handle->capture.current_size); + RT_ASSERT(0); // 打印完毕后卡死并停留在当前状态 + } +#endif + + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.current_size += copy_size; + + if (handle->capture.current_size >= buffer_size) + { + handle->capture.frame_ready = 1; + handle->capture.soi_found = 0; + handle->capture.enable_capture = 0; + handle->capture.capture_started = 0; + } + rt_hw_interrupt_enable(level); + + if (handle->capture.frame_ready) + { + /* Flush write-back DCache to PSRAM before display DMA reads the frame buffer */ + //mpu_dcache_clean(full_buffer, buffer_size); + dvp_dispatch_frame(handle); + } +} + +/** + * @brief Process one JPEG ping-pong buffer block. + * + * This function parses one half of the DMA ping-pong buffer in JPEG mode. + * It searches the captured stream for SOI and EOI markers, copies valid data + * into the user frame buffer and marks the frame ready after a complete JPEG + * image is assembled. + * + * @param handle is a pointer to the DVP handle. + * + * @param buffer_offset is the offset of the active half-buffer inside the + * ping-pong buffer. Use `0` for the first half and `half_size` for the + * second half. + * + * @return This function does not return a value. + */ +static void process_jpeg_data(dvp_handle_t *handle, uint32_t buffer_offset) +{ + uint8_t *full_buffer = handle->config.frame_buffer; + uint8_t *pingpong_buffer = handle->pingpong_buffer; + uint32_t half_size = handle->config.pingpong_buffer_size / 2; + uint8_t *source_ptr = &pingpong_buffer[buffer_offset]; + uint32_t buffer_size = handle->config.buffer_size; + int index = 0; + + if(full_buffer == NULL || buffer_size == 0) + { + dvp_stop_jpeg_capture(handle, 0); + return; + } + + if (half_size == 0) + { + dvp_reset_jpeg_boundary_state(handle); + return; + } + + if (!handle->capture.soi_found && + handle->jpeg.prev_byte_valid && + handle->jpeg.prev_byte == 0xFF && + source_ptr[0] == 0xD8) + { + uint8_t soi_marker[2] = {0xFF, 0xD8}; + if (dvp_append_jpeg_bytes(handle, soi_marker, sizeof(soi_marker)) != RT_EOK) + { + return; + } + + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.soi_found = 1; + rt_hw_interrupt_enable(level); + index = 1; + } + else if (handle->capture.soi_found && + handle->jpeg.prev_byte_valid && + handle->jpeg.prev_byte == 0xFF && + source_ptr[0] == 0xD9) + { + if (dvp_append_jpeg_bytes(handle, source_ptr, 1) != RT_EOK) + { + return; + } + + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.frame_ready = 1; + handle->capture.soi_found = 0; + handle->capture.enable_capture = 0; + handle->capture.capture_started = 0; + dvp_reset_jpeg_boundary_state(handle); + rt_hw_interrupt_enable(level); + index = 1; + } + + while(indexcapture.enable_capture) + { + if(!handle->capture.soi_found) + { + int soi_index = search_for_SOI(&source_ptr[index], half_size - index); + if (soi_index >= 0) + { + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.soi_found = 1; + rt_hw_interrupt_enable(level); + index += soi_index; + int eoi_index = search_for_EOI(&source_ptr[index], half_size - index); + if (eoi_index >= 0) + { + uint32_t copy_size = eoi_index + 1; + if (dvp_append_jpeg_bytes(handle, &source_ptr[index], copy_size) != RT_EOK) + { + break; + } + + level = rt_hw_interrupt_disable(); + handle->capture.frame_ready = 1; + handle->capture.soi_found = 0; + handle->capture.enable_capture = 0; + handle->capture.capture_started = 0; + dvp_reset_jpeg_boundary_state(handle); + rt_hw_interrupt_enable(level); + index += copy_size; + } + else + { + uint32_t copy_size = half_size - index; + if (dvp_append_jpeg_bytes(handle, &source_ptr[index], copy_size) != RT_EOK) + { + break; + } + dvp_update_jpeg_boundary_state(handle, source_ptr, half_size); + break; + } + + } + else + { + dvp_update_jpeg_boundary_state(handle, source_ptr, half_size); + break; + } + } + + if(handle->capture.soi_found) + { + int eoi_index = search_for_EOI(&source_ptr[index], half_size - index); + if (eoi_index >= 0) + { + uint32_t copy_size = eoi_index + 1; + if (dvp_append_jpeg_bytes(handle, &source_ptr[index], copy_size) != RT_EOK) + { + break; + } + + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.frame_ready = 1; + handle->capture.soi_found = 0; + handle->capture.enable_capture = 0; + handle->capture.capture_started = 0; + dvp_reset_jpeg_boundary_state(handle); + rt_hw_interrupt_enable(level); + index += copy_size; + } + else + { + uint32_t copy_size = half_size - index; + if (dvp_append_jpeg_bytes(handle, &source_ptr[index], copy_size) != RT_EOK) + { + break; + } + dvp_update_jpeg_boundary_state(handle, source_ptr, half_size); + break; + } + } + + if(handle->capture.frame_ready) + { + dvp_dispatch_frame(handle); + + /* + * Continue only when the callback explicitly rearms capture for the + * next frame by clearing frame_ready via dvp_start_capture() or an + * equivalent state reset. This avoids silently reusing the previous + * frame buffer during background capture. + */ + if(!handle->capture.enable_capture || handle->capture.frame_ready){ + break; + } + + full_buffer = handle->config.frame_buffer; + buffer_size = handle->config.buffer_size; + if (full_buffer == NULL || buffer_size == 0) + { + dvp_stop_jpeg_capture(handle, 0); + break; + } + } + } +} + + + +//******************************** Callbacks *********************************// +/** + * @brief Handle DMA transfer-complete events. + * + * This callback is invoked when DMA completes transfer of the second half of + * the ping-pong buffer. It dispatches the received data to the JPEG or fixed- + * size data processing path according to the current DVP mode. + * + * @param hdma is a pointer to the DMA handle that triggered the callback. + * + * @return This function does not return a value. + */ +void dvp_dma_xfer_cplt_callback(DMA_HandleTypeDef *hdma) +{ + + dvp_handle_t *handle = dvp_get_handle_from_dma(hdma); + + if (handle == RT_NULL || !handle->capture.enable_capture) + return; + + uint32_t half_size = handle->config.pingpong_buffer_size / 2; + + if (handle->config.mode == BUS_CAPTURE_MODE_JPEG) + { + process_jpeg_data(handle, half_size); + } + else + { + process_raw_data(handle, half_size); + //hdma->Instance->CNDTR=0x2800; + //__HAL_DMA_SET_COUNTER(hdma,10240); + } +} + +/** + * @brief Handle DMA half-transfer events. + * + * This callback is invoked when DMA completes transfer of the first half of + * the ping-pong buffer. It dispatches the received data to the JPEG or fixed- + * size data processing path according to the current DVP mode. + * + * @param hdma is a pointer to the DMA handle that triggered the callback. + * + * @return This function does not return a value. + */ +void dvp_dma_half_xfer_cplt_callback(DMA_HandleTypeDef *hdma) +{ + //__HAL_DMA_DISABLE(hdma); + // RT_ASSERT(0); + dvp_handle_t *handle = dvp_get_handle_from_dma(hdma); + + if (handle == RT_NULL || !handle->capture.enable_capture) + return; + + if (handle->config.mode == BUS_CAPTURE_MODE_JPEG) + { + process_jpeg_data(handle, 0); + } + else + { + process_raw_data(handle, 0); + //hdma->Instance->CNDTR=0x1400; + //__HAL_DMA_SET_COUNTER(hdma,5120); + } +} + +/** + * @brief Handle DMA error events. + * + * This callback is invoked when a DMA transfer error occurs while receiving + * image data from the camera interface. + * + * @param hdma is a pointer to the DMA handle that triggered the error. + * + * @return This function does not return a value. + */ +static void dvp_dma_error_callback(DMA_HandleTypeDef *hdma) +{ + (void)hdma; + LOG_E("DVP DMA Error occurred!"); +} + +/** + * @brief Handle VSYNC rising-edge interrupts. + * + * This interrupt handler is triggered on the camera VSYNC rising edge. For + * fixed-size capture modes it starts one capture cycle. For JPEG mode the + * driver relies on SOI and EOI markers, so VSYNC is only used as timing + * reference and does not directly restart capture. + * + * @param args is the user argument associated with the interrupt. It is not used. + * + * @return This function does not return a value. + */ +static void dvp_vsync_irq_handler(void *args) +{ + bus_adapter_t *self = (bus_adapter_t *)args; + if (self == RT_NULL) + return; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + if (handle == RT_NULL) + return; + + // JPEG mode runs as a continuous byte stream and is parsed by SOI/EOI, + // so do not restart DMA on each VSYNC. + if (handle->config.mode == BUS_CAPTURE_MODE_JPEG) + { + return; + } + + // Fixed-size modes use VSYNC to align the DMA start to a frame boundary. + if (!handle->capture.enable_capture) + { + return; + } + if (handle->capture.capture_started) + { + return; + } + if (handle->capture.frame_ready) + { + return; + } +#if DEBUG_DVP + GPIO_TypeDef *gpio = hwp_gpio1; + GPIO_InitTypeDef GPIO_InitStruct; + /* set GPIO1 pin10 to output mode */ + GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT; + GPIO_InitStruct.Pin = 74; + GPIO_InitStruct.Pull = GPIO_NOPULL; + HAL_GPIO_Init(gpio, &GPIO_InitStruct); + /* set pin to high */ + HAL_GPIO_WritePin(gpio, 74, GPIO_PIN_SET); +#endif + dvp_start(self); + // __HAL_DMA_DISABLE(&handle->dma); + // //rt_kprintf("\n[DEBUG] --- SOURCE DUMP (Pingpong Buffer) ---\n"); + // //snapshot_and_dump_buffer(source_ptr, 6000, buffer_offset, handle->capture.current_size); + // RT_ASSERT(0); // 打印完毕后卡死并停留在当前状态 +} +//******************************** Callbacks *********************************// + + + +//***************************** Initialization ******************************// +/** + * @brief Configure DVP data input pins. + * + * This function configures the camera parallel data pins D0 to D7 as GPIO + * inputs that match the DMA source register layout used by this driver. + * + * @param handle is a pointer to the DVP handle. + * + * @return Return `RT_EOK` on success. + */ +static int dvp_config_data_pins(dvp_handle_t *handle) +{ + GPIO_TypeDef *data_gpio = dvp_get_data_gpio_instance(handle); + + // Data pins must be mapped to GPIO1 bits 0-7 (because DMA reads low 8 bits of DIR register) + for (int i = 0; i < 8; i++) + { + HAL_PIN_Set(handle->config.resources.data_pin_pad_base + i, + handle->config.resources.data_pin_func_base + i, + PIN_PULLUP, + 1); + + GPIO_InitTypeDef GPIO_InitStruct; + GPIO_InitStruct.Pin = handle->config.resources.data_gpio_pin_base + i; + GPIO_InitStruct.Mode = GPIO_MODE_INPUT; + GPIO_InitStruct.Pull = GPIO_PULLUP; + HAL_GPIO_Init(data_gpio, &GPIO_InitStruct); + } + + return RT_EOK; +} + +/** + * @brief Configure DVP control pins and interrupts. + * + * This function attaches the VSYNC GPIO interrupt used to detect frame start. + * + * @param handle is a pointer to the DVP handle. + * + * @return Return `RT_EOK` on success. + */ +static int dvp_config_control_pins(dvp_handle_t *handle) +{ + // Configure VSYNC interrupt + rt_pin_attach_irq(handle->config.resources.vsync_pin, PIN_IRQ_MODE_RISING, dvp_vsync_irq_handler, &s_dvp_bus_adapter); + rt_pin_irq_enable(handle->config.resources.vsync_pin, PIN_IRQ_ENABLE); + + // rt_pin_attach_irq(73, PIN_IRQ_MODE_FALLING, dvp_href_irq_handler, handle); + // rt_pin_irq_enable(73, PIN_IRQ_ENABLE); + return RT_EOK; +} + +/** + * @brief Configure the DMA channel used by the DVP receiver. + * + * This function initializes the DMA instance, installs transfer callbacks and + * enables the corresponding interrupt. + * + * @param handle is a pointer to the DVP handle. + * + * @return Return `RT_EOK` on success. If DMA initialization fails, + * `-RT_ERROR` is returned. + */ +static int dvp_config_dma(dvp_handle_t *handle) +{ + handle->dma.Instance = handle->config.resources.dma_instance; + handle->dma.Init.Request = handle->config.resources.dma_request; + handle->dma.Init.Direction = DMA_PERIPH_TO_MEMORY; + handle->dma.Init.PeriphInc = DMA_PINC_DISABLE; + handle->dma.Init.MemInc = DMA_MINC_ENABLE; + handle->dma.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; + handle->dma.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; + handle->dma.Init.Mode = DMA_CIRCULAR; + handle->dma.Init.Priority = DMA_PRIORITY_VERY_HIGH; + handle->dma.Init.BurstSize = 0; + handle->dma.XferCpltCallback = dvp_dma_xfer_cplt_callback; + handle->dma.XferHalfCpltCallback = dvp_dma_half_xfer_cplt_callback; + handle->dma.XferErrorCallback = dvp_dma_error_callback; + + if (HAL_DMA_Init(&handle->dma) != HAL_OK) + { + LOG_E("DVP DMA init failed!"); + return -RT_ERROR; + } + + __HAL_LINKDMA(&handle->gptim, hdma[GPT_DMA_ID_UPDATE], handle->dma); + HAL_NVIC_SetPriority(handle->config.resources.dma_irqn, 1, 0); + HAL_NVIC_EnableIRQ(handle->config.resources.dma_irqn); + + return RT_EOK; +} +/** + * @brief Configure the GPT timer used as DVP clock and capture engine. + * + * This function configures the board-specific timer pins and initializes the + * GPT peripheral to use PCLK as external clock and HSYNC/HREF as the capture + * synchronization source for DMA-driven sampling. + * + * @param handle is a pointer to the DVP handle. + * + * @return Return `RT_EOK` on success. If timer initialization fails, + * `-RT_ERROR` is returned. + */ +static int dvp_config_timer(dvp_handle_t *handle) +{ + HAL_PIN_Set(handle->config.resources.pclk_pin_pad, + handle->config.resources.pclk_pin_func, + PIN_PULLUP, + 1); + HAL_PIN_Set(handle->config.resources.hsync_pin_pad, + handle->config.resources.hsync_pin_func, + PIN_PULLUP, + 1); + + HAL_RCC_EnableModule(handle->config.resources.timer_rcc_module); + + handle->gptim.Instance = handle->config.resources.timer_instance; + handle->gptim.Init.Prescaler = 0; + handle->gptim.Init.CounterMode = GPT_COUNTERMODE_DOWN; + handle->gptim.Init.Period = 0x0; + + if (HAL_GPT_Base_Init(&handle->gptim) != HAL_OK) + { + LOG_E("DVP timer init failed!"); + return -RT_ERROR; + } + + // Configure external clock source (PCLK via ETR) + GPT_ClockConfigTypeDef sClockSourceConfig = {0}; + sClockSourceConfig.ClockSource = GPT_CLOCKSOURCE_ETRMODE2; + sClockSourceConfig.ClockPolarity = GPT_TRIGGERPOLARITY_NONINVERTED; + sClockSourceConfig.ClockPrescaler = GPT_CLOCKPRESCALER_DIV1; + sClockSourceConfig.ClockFilter = 0; + + if (HAL_GPT_ConfigClockSource(&handle->gptim, &sClockSourceConfig) != HAL_OK) + { + LOG_E("DVP timer clock config failed!"); + return -RT_ERROR; + } + + GPT_SlaveConfigTypeDef sSlaveConfig = {0}; + sSlaveConfig.SlaveMode = GPT_SLAVEMODE_GATED; + sSlaveConfig.InputTrigger = GPT_TS_TI1FP1; + sSlaveConfig.TriggerPolarity = GPT_INPUTCHANNELPOLARITY_RISING; + sSlaveConfig.TriggerFilter = 0; + + if (HAL_GPT_SlaveConfigSynchronization(&handle->gptim, &sSlaveConfig) != HAL_OK) + { + LOG_E("DVP timer slave config failed!"); + return -RT_ERROR; + } + + // Configure input capture channel 1 (HSYNC) + GPT_IC_InitTypeDef sConfigIC = {0}; + sConfigIC.ICPolarity = GPT_INPUTCHANNELPOLARITY_RISING; + sConfigIC.ICSelection = GPT_ICSELECTION_DIRECTTI; + sConfigIC.ICPrescaler = GPT_ICPSC_DIV1; + sConfigIC.ICFilter = 0; + + if (HAL_GPT_IC_ConfigChannel(&handle->gptim, &sConfigIC, GPT_CHANNEL_1) != HAL_OK) + { + LOG_E("DVP timer IC config failed!"); + return -RT_ERROR; + } + + // Enable DMA request + __HAL_GPT_ENABLE_DMA(&handle->gptim, GPT_DMA_UPDATE); + + return RT_EOK; +} +/* + ******************************************************************************* + * XCLK (sensor master clock) generation via GPTIM2 + ******************************************************************************* + */ + +/** + * @brief Start XCLK output on a GPIO pin using GPTIM2 channel 1 PWM. + * + * Configures @p pin as GPTIM2_CH1 and generates a 50 % duty-cycle square + * wave at @p freq Hz. If XCLK is already running the timer is stopped and + * re-configured before restarting. A 10 ms stabilisation delay is inserted + * after the PWM starts so the sensor clock is stable before any SCCB access. + * + * @param pin is the PAx pin index (0-based; e.g. 5 for PA5). + * @param freq is the desired XCLK frequency in Hz. + */ +static void dvp_xclk_start(int pin, uint32_t freq) +{ + HAL_StatusTypeDef status; + uint32_t timer_clk; + uint32_t period; + GPT_OC_InitTypeDef sConfigOC = {0}; + + if (s_xclk_initialized) + { + HAL_GPT_PWM_Stop(&s_xclk_gptim, GPT_CHANNEL_1); + HAL_GPT_Base_DeInit(&s_xclk_gptim); + s_xclk_initialized = RT_FALSE; + } + + HAL_PIN_Set(PAD_PA00 + pin, GPTIM2_CH1, PIN_NOPULL, 1); + HAL_RCC_EnableModule(RCC_MOD_GPTIM2); + +#if defined(SOC_SF32LB52X) && SOC_SF32LB52X == 1 + timer_clk = 24000000; +#else + timer_clk = HAL_RCC_GetPCLKFreq(s_xclk_gptim.core, 1); +#endif + + period = (timer_clk / freq) - 1; + if (period < 1 || period > 0xFFFF) + { + LOG_E("XCLK: frequency %u Hz out of range (timer_clk=%u Hz)", + (unsigned)freq, (unsigned)timer_clk); + return; + } + + s_xclk_gptim.Instance = hwp_gptim2; + s_xclk_gptim.Init.Prescaler = 0; + s_xclk_gptim.Init.CounterMode = GPT_COUNTERMODE_UP; + s_xclk_gptim.Init.Period = period; + + status = HAL_GPT_Base_Init(&s_xclk_gptim); + if (status != HAL_OK) + { + LOG_E("XCLK: GPTIM2 base init failed (%d)", status); + return; + } + + sConfigOC.OCMode = GPT_OCMODE_PWM1; + sConfigOC.Pulse = period / 2 + 1; /* 50 % duty cycle */ + sConfigOC.OCPolarity = GPT_OCPOLARITY_HIGH; + sConfigOC.OCFastMode = GPT_OCFAST_DISABLE; + + status = HAL_GPT_PWM_ConfigChannel(&s_xclk_gptim, &sConfigOC, GPT_CHANNEL_1); + if (status != HAL_OK) + { + LOG_E("XCLK: GPTIM2 PWM config failed (%d)", status); + HAL_GPT_Base_DeInit(&s_xclk_gptim); + return; + } + + status = HAL_GPT_PWM_Start(&s_xclk_gptim, GPT_CHANNEL_1); + if (status != HAL_OK) + { + LOG_E("XCLK: GPTIM2 PWM start failed (%d)", status); + HAL_GPT_Base_DeInit(&s_xclk_gptim); + return; + } + + s_xclk_initialized = RT_TRUE; + rt_thread_mdelay(10); /* wait for clock to stabilise before SCCB access */ + LOG_I("XCLK: %u Hz on PA%d (period=%u)", (unsigned)freq, pin, (unsigned)period); +} + +/** + * @brief Stop XCLK PWM and restore the pin to GPIO input mode. + * + * @param pin is the PAx pin index that was used to output XCLK. + */ +static void dvp_xclk_stop(int pin) +{ + if (!s_xclk_initialized) + return; + + HAL_GPT_PWM_Stop(&s_xclk_gptim, GPT_CHANNEL_1); + HAL_GPT_Base_DeInit(&s_xclk_gptim); + s_xclk_initialized = RT_FALSE; + HAL_PIN_Set(PAD_PA00 + pin, GPIO_A0 + pin, PIN_NOPULL, 1); + LOG_I("XCLK: stopped (PA%d)", pin); +} + +/* Public function implementations */ + +/** + * @brief Initialize a DVP instance. + * + * This function stores the user configuration, wires the internal frame + * forwarder as the hardware callback, allocates the ping-pong DMA buffer, + * initializes runtime state and configures the required pins, DMA and timer + * resources. + * + * @param self is a pointer to the DVP bus adapter (use `dvp_get_bus_adapter()` + * to obtain the singleton). + * + * @param cfg is a pointer to a `dvp_config_t` describing hardware resources, + * capture mode and initial buffer settings. + * + * @return Return `RT_EOK` on success. Otherwise a negative RT-Thread error code + * is returned. + */ +int dvp_init(bus_adapter_t *self) +{ + if (self == NULL) + { + LOG_E("DVP init: invalid parameters!"); + return -RT_EINVAL; + } + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + /* Build configuration from Kconfig macros — all DVP hardware parameters + * belong here in the bus layer, not in the sensor driver above. */ + const dvp_config_t config = { + .mode = BUS_CAPTURE_MODE_JPEG, + .buffer_size = 0, + .frame_buffer = NULL, + .pingpong_buffer_size = OV2640_DVP_PINGPONG_BUFFER_SIZE, + .resources = DVP_DEFAULT_RESOURCE_CONFIG(OV2640_DVP_VSYNC_PIN), + .xclk_pin = OV2640_DVP_XCLK_PIN, + .xclk_freq = OV2640_DVP_XCLK_FREQ, + }; + + if (config.resources.timer_instance == RT_NULL || + config.resources.dma_instance == RT_NULL || + config.resources.data_gpio_instance == RT_NULL || + config.resources.data_pin_source_addr == 0) + { + LOG_E("DVP init: missing hardware resources!"); + return -RT_EINVAL; + } + + // Save configuration; the user bus callback is registered separately via dvp_set_frame_callback + memcpy(&handle->config, &config, sizeof(dvp_config_t)); + + // Allocate ping-pong buffer + if(handle->config.pingpong_buffer_size % 2 != 0) + { + LOG_W("DVP init: pingpong_buffer_size not even, rounding up"); + handle->config.pingpong_buffer_size += 1; + } + if (handle->config.pingpong_buffer_size > sizeof(dvp_pingpong_buffer)) + { + LOG_E("DVP init: pingpong_buffer_size (%d) exceeds static buffer size (%d)", + handle->config.pingpong_buffer_size, sizeof(dvp_pingpong_buffer)); + return -RT_EINVAL; + } + handle->pingpong_buffer = dvp_pingpong_buffer; + + // Initialize state + handle->capture.frame_ready = 0; + handle->capture.current_size = 0; + handle->capture.soi_found = 0; + handle->capture.enable_capture = 1; + handle->capture.capture_started = 0; + dvp_reset_jpeg_boundary_state(handle); + + // Configure hardware + if (dvp_config_data_pins(handle) != 0) + return -RT_ERROR; + if (dvp_config_control_pins(handle) != 0) + return -RT_ERROR; + if (dvp_config_dma(handle) != 0) + return -RT_ERROR; + if (dvp_config_timer(handle) != 0) + return -RT_ERROR; + + if (config.xclk_pin >= 0 && config.xclk_freq > 0) + dvp_xclk_start(config.xclk_pin, config.xclk_freq); + + LOG_I("DVP initialized successfully"); + LOG_I(" Mode: %s", + config.mode == BUS_CAPTURE_MODE_JPEG ? "JPEG" : + config.mode == BUS_CAPTURE_MODE_RAW ? "RAW" : + config.mode == BUS_CAPTURE_MODE_YUV422 ? "YUV422" : "RGB565"); + LOG_I(" Buffer size: %d bytes", config.buffer_size); + LOG_I(" Pingpong buffer: %d bytes", config.pingpong_buffer_size); + LOG_I(" VSYNC: PA%d (GPIO interrupt)", config.resources.vsync_pin); + + return RT_EOK; +} + +/** + * @brief Deinitialize a DVP instance. + * + * This function stops capture hardware, disables the VSYNC interrupt, + * deinitializes DMA and timer resources and clears driver state. The static + * ping-pong buffer is not freed. + * + * @param self is a pointer to the DVP bus adapter. + * + * @return Return `RT_EOK` on success. If `self` is `NULL`, `-RT_EINVAL` + * is returned. + */ +int dvp_deinit(bus_adapter_t *self) +{ + if (self == NULL) + return -RT_EINVAL; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + // Stop hardware + dvp_stop(self); + + if (handle->config.xclk_pin >= 0) + dvp_xclk_stop(handle->config.xclk_pin); + + // Deinitialize DMA + HAL_NVIC_DisableIRQ(handle->config.resources.dma_irqn); + HAL_DMA_DeInit(&handle->dma); + + // Deinitialize Timer + __HAL_GPT_DISABLE_DMA(&handle->gptim, GPT_DMA_UPDATE); + __HAL_GPT_DISABLE_IT(&handle->gptim, GPT_IT_UPDATE); + HAL_GPT_Base_DeInit(&handle->gptim); + + // Ping-pong buffer is at a fixed address, no release needed + handle->pingpong_buffer = NULL; + + // Disable VSYNC interrupt + rt_pin_irq_enable(handle->config.resources.vsync_pin, PIN_IRQ_DISABLE); + rt_pin_detach_irq(handle->config.resources.vsync_pin); + + // Reset state + handle->capture.frame_ready = 0; + handle->capture.current_size = 0; + handle->capture.soi_found = 0; + handle->capture.enable_capture = 0; + handle->capture.capture_started = 0; + dvp_reset_jpeg_boundary_state(handle); + + LOG_I("DVP deinitialized"); + return RT_EOK; +} + +//***************************** Control Functions ******************************// +/** + * @brief Start DVP hardware capture. + * + * This function aborts any active DMA, restarts the GPT timer and DMA engine, + * clears capture state and arms the ping-pong buffer for a new hardware + * capture cycle. + * + * @param self is a pointer to the DVP bus adapter. + * + * @return Return `RT_EOK` on success. Otherwise a negative RT-Thread error code + * is returned. + */ +int dvp_start(bus_adapter_t *self) +{ + if (self == NULL) + return -RT_EINVAL; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + GPT_TypeDef *timer_instance = dvp_get_timer_instance(handle); + + HAL_DMA_Abort(&handle->dma); + HAL_GPT_Base_Stop(&handle->gptim); + __HAL_GPT_SET_COUNTER(&handle->gptim, 0); + __HAL_GPT_CLEAR_FLAG(&handle->gptim, GPT_FLAG_UPDATE); + + handle->capture.soi_found = (handle->config.mode == BUS_CAPTURE_MODE_JPEG) ? 0 : 1; + handle->capture.frame_ready = 0; + handle->capture.current_size = 0; + dvp_reset_jpeg_boundary_state(handle); + + if (HAL_GPT_Base_Start(&handle->gptim) != HAL_OK) + { + LOG_E("DVP VSYNC: GPTIM restart failed"); + HAL_DMA_Abort(&handle->dma); + handle->capture.soi_found = 0; + handle->capture.enable_capture = 0; + return -RT_ERROR; + } + timer_instance->DIER &= ~GPT_DIER_UDE; + for (int i = 0; i < 100; i++) { __NOP(); } + timer_instance->DIER |= GPT_DIER_UDE; + + if (HAL_DMA_Start_IT(&handle->dma, + handle->config.resources.data_pin_source_addr, + (uint32_t)handle->pingpong_buffer, + (uint32_t)handle->config.pingpong_buffer_size) != HAL_OK) + { + LOG_E("DVP VSYNC: DMA restart failed"); + handle->capture.soi_found = 0; + handle->capture.enable_capture = 0; + handle->capture.capture_started = 0; + return -RT_ERROR; + } + handle->capture.capture_started = 1; + + return RT_EOK; +} + +/** + * @brief Stop DVP hardware capture. + * + * This function stops the GPT timer, aborts the active DMA transfer and + * clears the software capture-enable flag. Hardware pins and interrupt + * attachments are left in place; use `dvp_deinit` to tear those down. + * + * @param self is a pointer to the DVP bus adapter. + * + * @return Return `RT_EOK` on success. If `self` is `NULL`, `-RT_EINVAL` + * is returned. + */ +int dvp_stop(bus_adapter_t *self) +{ + if (self == NULL) + return -RT_EINVAL; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + // Stop timer and DMA + HAL_GPT_Base_Stop(&handle->gptim); + HAL_DMA_Abort(&handle->dma); + + // Clear capture flag + handle->capture.enable_capture = 0; + handle->capture.capture_started = 0; + + LOG_D("DVP hardware stopped"); + return RT_EOK; +} + +/** + * @brief Arm the driver for one frame capture. + * + * This function optionally updates the user frame buffer, resets capture state + * and enables the capture path. For JPEG mode it also calls `dvp_start()` to + * restart the DMA/timer pair so that SOI detection begins immediately. For + * fixed-size modes the hardware stays running; `dvp_vsync_irq_handler()` will + * call `dvp_start()` on the next VSYNC rising edge. + * + * @param self is a pointer to the DVP bus adapter. + * + * @param new_buffer is an optional pointer to a new frame buffer. Pass `NULL` + * to keep the previously configured buffer. + * + * @param buffer_size is the size of `new_buffer` in bytes. Must be non-zero + * when `new_buffer` is not `NULL`. + * + * @return Return `RT_EOK` on success. Otherwise a negative RT-Thread error code + * is returned. + */ +int dvp_start_capture(bus_adapter_t *self, void *new_buffer, uint32_t buffer_size) +{ + + if (self == NULL) + return -RT_EINVAL; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + // If new buffer specified, update configuration + if (new_buffer != NULL) + { + if (buffer_size == 0) + { + LOG_E("DVP error: buffer_size must be provided when new_buffer is not NULL"); + return -RT_EINVAL; + } + + handle->config.frame_buffer = (uint8_t *)new_buffer; + handle->config.buffer_size = buffer_size; + //LOG_D("DVP capture buffer updated to 0x%08X, size: %d bytes", + // (uint32_t)new_buffer, buffer_size); + } + + // Only set flags, don't operate hardware (hardware started by dvp_start) + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.frame_ready = 0; + handle->capture.current_size = 0; + handle->capture.soi_found = 0; + handle->capture.enable_capture = 1; + handle->capture.capture_started = 0; + dvp_reset_jpeg_boundary_state(handle); + rt_hw_interrupt_enable(level); + + if (handle->config.mode == BUS_CAPTURE_MODE_JPEG) + { + return dvp_start(self); + } + + //LOG_D("DVP capture enabled."); + return RT_EOK; +} + +/** + * @brief Re-arm capture state for the next frame without restarting the hardware stream. + * + * This function is designed to be called from inside the frame callback to + * queue the next capture without stopping the running DMA. It resets software + * capture counters and flags, optionally switching to a new user buffer. + * + * For JPEG mode it simply re-arms state flags (DMA is already circularly + * running). For RAW/RGB565/YUV422 modes it sets `enable_capture = 1` and + * `capture_started = 0` so that `dvp_vsync_irq_handler()` will call + * `dvp_start()` safely from its own interrupt context on the next VSYNC edge. + * + * @param self is a pointer to the DVP bus adapter. + * + * @param new_buffer is an optional pointer to a new frame buffer. Pass `NULL` + * to keep the previously configured buffer. + * + * @param buffer_size is the size of `new_buffer` in bytes. Must be non-zero + * when `new_buffer` is not `NULL`. + * + * @return Return `RT_EOK` on success. Otherwise a negative RT-Thread error code + * is returned. + */ +int dvp_rearm_capture(bus_adapter_t *self, void *new_buffer, uint32_t buffer_size) +{ + if (self == NULL) + return -RT_EINVAL; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + if (new_buffer != NULL) + { + if (buffer_size == 0) + { + LOG_E("DVP error: buffer_size must be provided when new_buffer is not NULL"); + return -RT_EINVAL; + } + + handle->config.frame_buffer = (uint8_t *)new_buffer; + handle->config.buffer_size = buffer_size; + } + + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.frame_ready = 0; + handle->capture.current_size = 0; + handle->capture.soi_found = 0; /* always 0: JPEG waits for SOI, raw waits for VSYNC */ + handle->capture.enable_capture = 1; + handle->capture.capture_started = (handle->config.mode == BUS_CAPTURE_MODE_JPEG) ? 1 : 0; + dvp_reset_jpeg_boundary_state(handle); + rt_hw_interrupt_enable(level); + + if (handle->config.mode == BUS_CAPTURE_MODE_JPEG) + { + return RT_EOK; + } + + /* + * Non-JPEG (RGB565 / YUV422 / RAW) stream mode: + * DO NOT call dvp_start() here — this function is invoked from within the + * DMA ISR (user bus callback chain), and dvp_start() calls HAL_DMA_Abort + + * HAL_DMA_Start_IT which are not safe to call while the DMA ISR is still + * executing. Instead, just set enable_capture = 1 and capture_started = 0 + * above. The dvp_vsync_irq_handler() will see these flags on the next + * VSYNC rising edge and call dvp_start() from its own (safe) interrupt + * context, ensuring frame-boundary alignment. + */ + return RT_EOK; +} + +/** + * @brief Abort the current frame capture. + * + * This function clears the software capture-enable and frame-ready flags + * without touching the hardware; the DMA and timer keep running, which + * allows the driver to resume cleanly on the next `dvp_start_capture` call. + * + * @param self is a pointer to the DVP bus adapter. + * + * @return Return `RT_EOK` on success. If `self` is `NULL`, `-RT_EINVAL` + * is returned. + */ +int dvp_abort_capture(bus_adapter_t *self) +{ + if (self == NULL) + return -RT_EINVAL; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + // Only clear flags, hardware continues running (continue detecting SOI) + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.enable_capture = 0; + handle->capture.frame_ready = 0; + handle->capture.capture_started = 0; + dvp_reset_jpeg_boundary_state(handle); + rt_hw_interrupt_enable(level); + + LOG_D("DVP capture aborted."); + return RT_EOK; +} + +/** + * @brief Get the size of the captured frame. + * + * This function returns the number of bytes written to the user frame buffer + * for the active or most recently completed capture. + * + * @param self is a pointer to the DVP bus adapter. + * + * @return Return the captured frame size in bytes. If `self` is `NULL`, `0` + * is returned. + */ +uint32_t dvp_get_frame_size(bus_adapter_t *self) +{ + if (self == NULL) + return 0; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + return handle->capture.current_size; +} + +/** + * @brief Reset software capture state (internal helper). + * + * Clears frame-ready related state and re-enables capture so the next + * hardware event can fill the configured frame buffer. Not exposed in the + * public header; external callers should use `dvp_start_capture` instead. + * + * @param handle is a pointer to the raw DVP handle. + * + * @return This function does not return a value. + */ +static void dvp_reset_capture(dvp_handle_t *handle) +{ + if (handle == NULL) + return; + + rt_base_t level = rt_hw_interrupt_disable(); + handle->capture.frame_ready = 0; + handle->capture.current_size = 0; + handle->capture.soi_found = 0; + handle->capture.enable_capture = 1; + handle->capture.capture_started = 0; + dvp_reset_jpeg_boundary_state(handle); + rt_hw_interrupt_enable(level); +} + +/** + * @brief Resize the DMA ping-pong buffer. + * + * This function stops the active DMA and timer pair, redirects the + * hardware to the new size within the statically allocated pool and + * updates the stored buffer-size field. DVP must be stopped before + * calling this function; it is not safe to call while capture is running. + * + * @param self is a pointer to the DVP bus adapter. + * + * @param new_size is the requested new ping-pong buffer size in bytes. + * The value will be rounded up to the nearest even number. It must + * not exceed the size of the internal static buffer. + * + * @return Return `RT_EOK` on success. Otherwise a negative RT-Thread error code + * is returned. + */ +int dvp_set_pingpong_size(bus_adapter_t *self, uint32_t new_size) +{ + if (self == NULL || new_size == 0) + return -RT_EINVAL; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + if (new_size % 2 != 0) + new_size += 1; + + if (new_size > sizeof(dvp_pingpong_buffer)) + { + LOG_E("dvp_set_pingpong_size: %d bytes exceeds static buffer size (%d)", + new_size, sizeof(dvp_pingpong_buffer)); + return -RT_EINVAL; + } + + /* Stop DMA and timer before switching buffer */ + HAL_DMA_Abort(&handle->dma); + HAL_GPT_Base_Stop(&handle->gptim); + + handle->pingpong_buffer = dvp_pingpong_buffer; + handle->config.pingpong_buffer_size = new_size; + LOG_I("Pingpong buffer resized to %d bytes", new_size); + LOG_I("BUFFER ADDR: 0x%08X", (uint32_t)handle->pingpong_buffer); + return RT_EOK; +} + +/** + * @brief Register a generic frame-ready callback. + * + * The callback is invoked directly from the DMA ISR / timer-thread context + * when a complete frame has been assembled in the user frame buffer. + * + * @param self is a pointer to the DVP bus adapter (unused, provided for + * interface symmetry). + * + * @param callback is the callback function to invoke when a frame is ready. + * Pass `NULL` to deregister an existing callback. + * + * @param user_data is an opaque pointer forwarded to `callback` as its third + * argument. + * + * @return Return `BUS_OK` on success. + */ +int dvp_set_frame_callback(bus_adapter_t *self, + bus_frame_ready_callback_t callback, + void *user_data) +{ + (void)self; + s_user_frame_callback = callback; + s_user_frame_callback_data = user_data; + return BUS_OK; +} + +/** + * @brief Replace the user frame buffer pointer and size. + * + * This function swaps the destination buffer for incoming frame data without + * resetting capture state or restarting the hardware. It is intended for + * buffer-rotation schemes where the caller provides a fresh buffer before the + * next `dvp_start_capture` or `dvp_rearm_capture` call. + * + * @param self is a pointer to the DVP bus adapter. + * + * @param buffer is the new frame buffer pointer. Must not be `NULL` (validated + * by the caller). + * + * @param size is the size of `buffer` in bytes. + * + * @return Return `BUS_OK` on success. If `self` is `NULL`, `-RT_EINVAL` + * is returned. + */ +int dvp_update_buffer(bus_adapter_t *self, void *buffer, uint32_t size) +{ + if (self == NULL) + return -RT_EINVAL; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + handle->config.frame_buffer = (uint8_t *)buffer; + handle->config.buffer_size = size; + return BUS_OK; +} + +/** + * @brief Change the DVP capture mode at runtime. + * + * Updates the cached handle configuration. The caller is expected to have + * already stopped the bus (`bus_adapter_stop`) before invoking this op; + * this function only mutates configuration state. + * + * @param self is a pointer to the DVP bus adapter. + * @param mode is the requested capture mode. + * + * @return Return `BUS_OK` on success. + * Return `BUS_ERR_INVALID` if `self` is NULL or `mode` is unknown. + */ +int dvp_set_mode(bus_adapter_t *self, bus_capture_mode_t mode) +{ + if (self == NULL) + return BUS_ERR_INVALID; + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + + if (mode != BUS_CAPTURE_MODE_JPEG && + mode != BUS_CAPTURE_MODE_RAW && + mode != BUS_CAPTURE_MODE_YUV422 && + mode != BUS_CAPTURE_MODE_RGB565) + return BUS_ERR_INVALID; + + handle->config.mode = mode; + return BUS_OK; +} +//***************************** Control Functions ******************************// + + +//***************************** Bus Adapter Registration **********************// + +/** + * @brief Dump DVP hardware diagnostic state to the log. + * + * Called via bus_adapter_dump_state() when the upper layer detects a capture + * timeout and needs bus-level diagnostics. Not safe to call from ISR context. + */ +void dvp_dump_state(bus_adapter_t *self) +{ + dvp_handle_t *handle = (dvp_handle_t *)self->priv; + uint32_t dma_counter = 0; + uint32_t gpt_cnt = 0; + uint32_t gpt_cr1 = 0; + uint32_t gpt_dier = 0; + GPT_TypeDef *gpt = (GPT_TypeDef *)handle->gptim.Instance; + int vsync_level = rt_pin_read(handle->config.resources.vsync_pin); + + if (gpt != RT_NULL) + { + gpt_cnt = gpt->CNT; + gpt_cr1 = gpt->CR1; + gpt_dier = gpt->DIER; + } + if (handle->dma.Instance != RT_NULL) + { + dma_counter = __HAL_DMA_GET_COUNTER(&handle->dma); + } + + LOG_W(" bus(dvp): mode=%d cfg_buf=%u", + (int)handle->config.mode, + (unsigned int)handle->config.buffer_size); + LOG_W(" capture: ready=%u enable=%u soi=%u current=%u", + (unsigned int)handle->capture.frame_ready, + (unsigned int)handle->capture.enable_capture, + (unsigned int)handle->capture.soi_found, + (unsigned int)handle->capture.current_size); + LOG_W(" mem: frame_buf=0x%08X pingpong=0x%08X pp_size=%u", + (unsigned int)(rt_ubase_t)handle->config.frame_buffer, + (unsigned int)(rt_ubase_t)handle->pingpong_buffer, + (unsigned int)handle->config.pingpong_buffer_size); + LOG_W(" dma: state=%d err=0x%08X req=%u counter=%u", + (int)HAL_DMA_GetState(&handle->dma), + (unsigned int)HAL_DMA_GetError(&handle->dma), + (unsigned int)handle->dma.Init.Request, + (unsigned int)dma_counter); + LOG_W(" gpt: state=%d cnt=%u cr1=0x%08X dier=0x%08X", + (int)handle->gptim.State, + (unsigned int)gpt_cnt, + (unsigned int)gpt_cr1, + (unsigned int)gpt_dier); + LOG_W(" vsync: pin=%u level=%d", + (unsigned int)handle->config.resources.vsync_pin, + vsync_level); + { + uint32_t dmac_isr = hwp_dmac1->ISR; + uint32_t dmac_ifcr = hwp_dmac1->IFCR; + uint32_t dmac_ccr = hwp_dmac1->CCR1; + uint32_t dmac_cndtr = hwp_dmac1->CNDTR1; + LOG_W(" dmac: ISR=0x%08X IFCR=0x%08X CCR=0x%08X CNDTR=%u", + (unsigned int)dmac_isr, + (unsigned int)dmac_ifcr, + (unsigned int)dmac_ccr, + (unsigned int)dmac_cndtr); + } +} + +/* + * The public `dvp_*` functions above already match `bus_adapter_ops_t` exactly, + * so the ops table just references them directly — no trampolines needed. + */ +static const bus_adapter_ops_t s_dvp_bus_ops = { + .init = dvp_init, + .deinit = dvp_deinit, + .start = dvp_start, + .stop = dvp_stop, + .set_frame_callback = dvp_set_frame_callback, + .start_capture = dvp_start_capture, + .rearm_capture = dvp_rearm_capture, + .abort_capture = dvp_abort_capture, + .update_buffer = dvp_update_buffer, + .set_pingpong_size = dvp_set_pingpong_size, + .get_frame_size = dvp_get_frame_size, + .set_mode = dvp_set_mode, + .dump_state = dvp_dump_state, +}; + +static bus_adapter_t s_dvp_bus_adapter = { + .name = DVP_BUS_ADAPTER_NAME, + .type = BUS_TYPE_DVP, + .ops = &s_dvp_bus_ops, + .priv = &s_dvp_handle, +}; + +bus_adapter_t *dvp_get_bus_adapter(void) +{ + return &s_dvp_bus_adapter; +} + +static int dvp_bus_adapter_register(void) +{ + int ret = bus_adapter_register(&s_dvp_bus_adapter); + if (ret != BUS_OK) + { + LOG_E("DVP bus adapter register failed: %d", ret); + } + return ret; +} +INIT_BOARD_EXPORT(dvp_bus_adapter_register); +//***************************** Bus Adapter Registration **********************// \ No newline at end of file diff --git a/camera_framework/camera/bus/data/dvp.h b/camera_framework/camera/bus/data/dvp.h new file mode 100644 index 0000000..1de731f --- /dev/null +++ b/camera_framework/camera/bus/data/dvp.h @@ -0,0 +1,245 @@ +/****************************************************************************** + * Copyright (C) 2026 SiFli, Inc.(Gmbh) or its affiliates. + * + * All Rights Reserved. + * + * @file dvp.h + * + * @par dependencies + * - rtthread.h + * - bf0_hal.h + * - stdint.h + * + * @author SiFli 思澈科技 + * + * @brief Provide the HAL APIs of camera handler + * and corresponding operations. + * + * Processing flow: + * + * Call directly. + * + * @version V1.0 2026-4-3 + * + * @note 1 tab == 4 spaces! + * + *****************************************************************************/ + +#ifndef __DVP_H__ +#define __DVP_H__ + +//******************************** Includes *********************************// +#include "rtthread.h" +#include "bf0_hal.h" +#include +//******************************** Includes *********************************// + +//******************************** Defines **********************************// +#ifdef __cplusplus +extern "C" { +#endif + +/* PCLK and HSYNC pin PAD numbers come from Kconfig. + * The alternate function is GPTIM1_ETR / GPTIM1_CH1 on all supported platforms. */ +#define DVP_PCLK_PIN_PAD (PAD_PA00 + OV2640_DVP_PCLK_PIN) +#define DVP_PCLK_PIN_FUNC GPTIM1_ETR +#define DVP_HSYNC_PIN_PAD (PAD_PA00 + OV2640_DVP_HSYNC_PIN) +#define DVP_HSYNC_PIN_FUNC GPTIM1_CH1 + +/* DATA bus pins and their GPIO/DMA source address are platform-specific. */ +#ifdef SF32LB52X +#define DVP_DATA_PIN_PAD_BASE PAD_PA00 +#define DVP_DATA_PIN_FUNC_BASE GPIO_A0 +#define DVP_DATA_GPIO_PIN_BASE 0 +#define DVP_DATA_PIN_SOURCE_ADDR ((uint32_t)&hwp_gpio1->DIR) + +#elif defined(SF32LB56X) +#define DVP_DATA_PIN_PAD_BASE (PAD_PA00 + 64) +#define DVP_DATA_PIN_FUNC_BASE (GPIO_A0 + 64) +#define DVP_DATA_GPIO_PIN_BASE 64 +#define DVP_DATA_PIN_SOURCE_ADDR ((uint32_t)&((GPIO1_TypeDef *)GPIO1_BASE)->DIR2) + +#else + #error "DVP default resource macros are not defined for this platform" +#endif + +#define DVP_DATA_GPIO_INSTANCE hwp_gpio1 +#define DVP_TIMER_INSTANCE hwp_gptim1 +#define DVP_TIMER_RCC_MODULE RCC_MOD_GPTIM1 +#define DVP_DMA_INSTANCE DMA1_Channel1 +#define DVP_DMA_REQUEST 8 +#define DVP_DMA_IRQn DMAC1_CH1_IRQn + +#define DVP_DEFAULT_RESOURCE_CONFIG(vsync_pin_) \ + { \ + .pclk_pin_pad = DVP_PCLK_PIN_PAD, \ + .pclk_pin_func = DVP_PCLK_PIN_FUNC, \ + .hsync_pin_pad = DVP_HSYNC_PIN_PAD, \ + .hsync_pin_func = DVP_HSYNC_PIN_FUNC, \ + .vsync_pin = (vsync_pin_), \ + .data_pin_pad_base = DVP_DATA_PIN_PAD_BASE, \ + .data_pin_func_base = DVP_DATA_PIN_FUNC_BASE, \ + .data_gpio_pin_base = DVP_DATA_GPIO_PIN_BASE, \ + .data_pin_source_addr = DVP_DATA_PIN_SOURCE_ADDR, \ + .data_gpio_instance = DVP_DATA_GPIO_INSTANCE, \ + .timer_instance = DVP_TIMER_INSTANCE, \ + .timer_rcc_module = DVP_TIMER_RCC_MODULE, \ + .dma_instance = DVP_DMA_INSTANCE, \ + .dma_request = DVP_DMA_REQUEST, \ + .dma_irqn = DVP_DMA_IRQn, \ + } + +//******************************** Defines **********************************// + +//******************************** Typedefs *********************************// +/* DVP capture mode is the same concept as bus_capture_mode_t — use it directly + * so the two layers share one enum and no conversion/assert is needed. */ +#include "data_bus_adapter.h" + +/* Forward declaration */ +typedef struct dvp_handle dvp_handle_t; + +/* Board/resource description for one DVP instance. */ +typedef struct { + uint32_t pclk_pin_pad; + uint32_t pclk_pin_func; + uint32_t hsync_pin_pad; + uint32_t hsync_pin_func; + uint8_t vsync_pin; + + uint32_t data_pin_pad_base; + uint32_t data_pin_func_base; + uint32_t data_gpio_pin_base; + uint32_t data_pin_source_addr; + void *data_gpio_instance; + + void *timer_instance; + uint32_t timer_rcc_module; + + void *dma_instance; + uint32_t dma_request; + int32_t dma_irqn; +} dvp_resource_config_t; + +/* DVP configuration structure */ +typedef struct { + bus_capture_mode_t mode; /* DVP capture mode */ + uint32_t buffer_size; /* Image buffer size in bytes */ + uint8_t *frame_buffer; /* Frame buffer pointer (user provided) */ + uint32_t pingpong_buffer_size; /* Ping-pong DMA buffer size in bytes */ + dvp_resource_config_t resources; + + /** PAx pin index for XCLK output via GPTIM2_CH1; -1 = XCLK disabled. */ + int xclk_pin; + /** XCLK output frequency in Hz; 0 = XCLK disabled. */ + uint32_t xclk_freq; +} dvp_config_t; + +typedef struct { + volatile uint8_t frame_ready; + volatile uint32_t current_size; + volatile uint8_t enable_capture; + volatile uint8_t capture_started; + volatile uint8_t soi_found; +} dvp_capture_state_t; + +typedef struct { + volatile uint8_t prev_byte; + volatile uint8_t prev_byte_valid; +} dvp_jpeg_parser_state_t; + +/* DVP handle structure */ +struct dvp_handle{ + dvp_config_t config; + GPT_HandleTypeDef gptim; + DMA_HandleTypeDef dma; + uint8_t *pingpong_buffer; + dvp_capture_state_t capture; + dvp_jpeg_parser_state_t jpeg; +}; +/* Function declarations */ + +#define DVP_BUS_ADAPTER_NAME "dvp" + +//******************************** Typedefs *********************************// + +//******************************** Function *********************************// +/* + * Public DVP API. The first argument matches `bus_adapter_t *self` so these + * functions are used directly as `bus_adapter_ops_t` entries; no trampoline + * layer exists. `self->priv` points at the DVP singleton handle. + */ + +/** + * @brief Initialize DVP interface. + * @param self DVP bus adapter (use dvp_get_bus_adapter() to obtain the singleton). + * @param cfg Pointer to a `dvp_config_t` describing hardware resources & mode. + * @return 0 on success, negative on failure. + */ +int dvp_init(bus_adapter_t *self); + +/** @brief Deinitialize DVP interface. */ +int dvp_deinit(bus_adapter_t *self); + +/** @brief Start DVP hardware (timer + DMA). */ +int dvp_start(bus_adapter_t *self); + +/** @brief Stop DVP hardware (timer + DMA). */ +int dvp_stop(bus_adapter_t *self); + +/** @brief Register a generic frame-ready callback. */ +int dvp_set_frame_callback(bus_adapter_t *self, + bus_frame_ready_callback_t callback, + void *user_data); + +/** @brief Arm the next frame capture, optionally swapping the user buffer. */ +int dvp_start_capture(bus_adapter_t *self, void *new_buffer, uint32_t buffer_size); + +/** @brief Re-arm capture from inside the frame callback (no HW restart). */ +int dvp_rearm_capture(bus_adapter_t *self, void *new_buffer, uint32_t buffer_size); + +/** @brief Abort current capture (HW keeps running). */ +int dvp_abort_capture(bus_adapter_t *self); + +/** @brief Replace user frame buffer pointer/size without touching state. */ +int dvp_update_buffer(bus_adapter_t *self, void *buffer, uint32_t size); + +/** @brief Get current captured frame size in bytes. */ +uint32_t dvp_get_frame_size(bus_adapter_t *self); + +/** + * @brief Resize the ping-pong buffer (must stop DVP first). + * @note For RAW mode, set to 2 * image_width for one-line ping-pong. + */ +int dvp_set_pingpong_size(bus_adapter_t *self, uint32_t new_size); + +/** + * @brief Switch the DVP capture mode (generic @ref bus_capture_mode_t). + * + * Caller is responsible for stopping the bus before invoking, and starting + * it again after. Returns BUS_OK on success or BUS_ERR_INVALID on bad input. + */ +int dvp_set_mode(bus_adapter_t *self, bus_capture_mode_t mode); + +/** + * @brief Dump DVP hardware diagnostic state (DMA, GPT, VSYNC, DMAC registers) to the log. + * + * Called via @ref bus_adapter_dump_state when the upper layer needs bus-level + * diagnostics (e.g. on capture timeout). Must not be called from ISR context. + */ +void dvp_dump_state(bus_adapter_t *self); + +/** + * @brief Get the DVP module's `bus_adapter_t` instance. + * + * The DVP driver itself implements `bus_adapter_ops_t`, so callers may use the + * generic `bus_adapter_*` interface to drive DVP. The adapter is also auto- + * registered in the global registry under `DVP_BUS_ADAPTER_NAME` at INIT_BOARD. + */ +bus_adapter_t *dvp_get_bus_adapter(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __DVP_H__ */ diff --git a/camera_framework/camera/driver/ov2640/ov2640.c b/camera_framework/camera/driver/ov2640/ov2640.c new file mode 100644 index 0000000..918d737 --- /dev/null +++ b/camera_framework/camera/driver/ov2640/ov2640.c @@ -0,0 +1,2277 @@ +/** + * @file ov2640.c + * @brief OV2640 camera sensor driver and RT-Thread device interface + * + * This module implements the OV2640 sensor initialization, register + * configuration, image parameter control, and RT-Thread standard + * device driver interface (open/read/close/control). + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2026 + */ + +#include "ov2640.h" +#include "ov2640_regs.h" +#include "ov2640_settings.h" +#include "rtthread.h" +#include "rthw.h" +#include "drivers/pin.h" + +#define DBG_TAG "ov2640" +#define DBG_LVL DBG_LOG +#include +#include + +/* + * Single-instance driver: the SCCB control bus (sccb.c) and DVP data bus + * (dvp.c) currently keep file-scope state, so only one ov2640_device_t may + * exist. Sensor-setter helpers below (ov2640_lock/unlock, ov2640_set_bank, + * …) take no parameters and reach the active instance through this + * singleton pointer, which is set in ov2640_device_register. A future + * multi-instance refactor only needs to thread an ov2640_device_t* into + * the setters; the per-instance fields already live on the struct. + */ +static ov2640_device_t *s_active_dev = RT_NULL; + +/* + ******************************************************************************* + * Module-level data + ******************************************************************************* + */ + +static const camera_device_ops_t g_ov2640_device_ops = { + .command_set = { + OV2640_CMD_SET_PIXFORMAT, + OV2640_CMD_SET_FRAMESIZE, + OV2640_CMD_SET_QUALITY, + OV2640_CMD_START_STREAM, + OV2640_CMD_STOP_STREAM, + } +}; + +/** + * @brief Reset streaming state to inactive / all-zeroes. + * + * @param stream is a pointer to the stream state to reset. + */ +static void ov2640_stream_reset(ov2640_stream_state_t *stream) +{ + if (stream == RT_NULL) + { + return; + } + + rt_memset(stream, 0, sizeof(*stream)); +} + +/** + * @brief Return the static ops table for the OV2640 camera device. + * + * @return Return a pointer to the static @c camera_device_ops_t. + */ +const camera_device_ops_t *ov2640_get_device_ops(void) +{ + return &g_ov2640_device_ops; +} + +/* + ******************************************************************************* + * Thread safety helpers + ******************************************************************************* + */ + +/** + * @brief Lazily initialize the per-instance SCCB mutex. + * + * @param dev is the device instance whose @c sccb_lock is initialized. + * If @c NULL, the call is a no-op. + */ +static void ov2640_mutex_init(ov2640_device_t *dev) +{ + if (dev == RT_NULL || dev->sccb_lock_initialized) + { + return; + } + rt_mutex_init(&dev->sccb_lock, "ov2640_lock", RT_IPC_FLAG_PRIO); + dev->sccb_lock_initialized = RT_TRUE; +} + +/** + * @brief Lock the active OV2640 instance's SCCB mutex. + * + * Uses @ref s_active_dev set by @ref ov2640_device_register. Returns + * @c RT_EOK silently when no active instance is bound — this matches the + * legacy behaviour of being safe to call before register/after unregister. + * + * @return Return RT_EOK on success or when there is no active instance. + */ +static rt_err_t ov2640_lock(void) +{ + ov2640_device_t *dev = s_active_dev; + if (dev == RT_NULL) + { + return RT_EOK; + } + if (!dev->sccb_lock_initialized) + { + ov2640_mutex_init(dev); + } + return rt_mutex_take(&dev->sccb_lock, RT_WAITING_FOREVER); +} + +/** + * @brief Unlock the active OV2640 instance's SCCB mutex. + */ +static void ov2640_unlock(void) +{ + ov2640_device_t *dev = s_active_dev; + if (dev != RT_NULL && dev->sccb_lock_initialized) + { + rt_mutex_release(&dev->sccb_lock); + } +} + +/* + ******************************************************************************* + * Internal resolution and aspect-ratio tables + ******************************************************************************* + */ + +typedef enum { + ASPECT_RATIO_4X3, + ASPECT_RATIO_3X2, + ASPECT_RATIO_16X10, + ASPECT_RATIO_5X3, + ASPECT_RATIO_16X9, + ASPECT_RATIO_21X9, + ASPECT_RATIO_5X4, + ASPECT_RATIO_1X1, + ASPECT_RATIO_9X16 +} aspect_ratio_t; + +typedef struct { + const uint16_t width; + const uint16_t height; + const aspect_ratio_t aspect_ratio; +} resolution_info_t; + +static const resolution_info_t resolution[FRAMESIZE_INVALID] = { + { 96, 96, ASPECT_RATIO_1X1 }, /* 96x96 */ + { 160, 120, ASPECT_RATIO_4X3 }, /* QQVGA */ + { 128, 128, ASPECT_RATIO_1X1 }, /* 128x128 */ + { 176, 144, ASPECT_RATIO_5X4 }, /* QCIF */ + { 240, 176, ASPECT_RATIO_4X3 }, /* HQVGA */ + { 240, 240, ASPECT_RATIO_1X1 }, /* 240x240 */ + { 320, 240, ASPECT_RATIO_4X3 }, /* QVGA */ + { 320, 320, ASPECT_RATIO_1X1 }, /* 320x320 */ + { 400, 296, ASPECT_RATIO_4X3 }, /* CIF */ + { 480, 320, ASPECT_RATIO_3X2 }, /* HVGA */ + { 640, 480, ASPECT_RATIO_4X3 }, /* VGA */ + { 800, 600, ASPECT_RATIO_4X3 }, /* SVGA */ + { 1024, 768, ASPECT_RATIO_4X3 }, /* XGA */ + { 1280, 720, ASPECT_RATIO_16X9 }, /* HD */ + { 1280, 1024, ASPECT_RATIO_5X4 }, /* SXGA */ + { 1600, 1200, ASPECT_RATIO_4X3 }, /* UXGA */ +}; + +/* + ******************************************************************************* + * Low-level SCCB register I/O + ******************************************************************************* + */ + +/* Bank cache lives in ov2640_device_t::current_bank; helpers below access it + * through s_active_dev so callers don't need to thread the device pointer. */ + +/** + * @brief Invalidate the cached bank so the next access forces a BANK_SEL write. + * + * Call on deinit to ensure the driver state is consistent after re-open. + */ +static void ov2640_reset_bank_state(void) +{ + if (s_active_dev != RT_NULL) + { + s_active_dev->current_bank = (rt_uint8_t)BANK_MAX; + } +} + +/** + * @brief Switch the active register bank if needed. + * + * Skips the SCCB write when @p bank already matches the cached value. + * + * @param bank is the target bank (BANK_SENSOR or BANK_DSP). + * + * @return Return 0 on success; negative SCCB error on failure. + */ +static int ov2640_set_bank(ov2640_bank_t bank) +{ + ov2640_device_t *dev = s_active_dev; + rt_uint8_t cached = (dev != RT_NULL) ? dev->current_bank : (rt_uint8_t)BANK_MAX; + if ((rt_uint8_t)bank == cached) return 0; + int res = sccb_write(OV2640_ADDR, BANK_SEL, (uint8_t)bank); + if(res) return res; + if (dev != RT_NULL) + { + dev->current_bank = (rt_uint8_t)bank; + } + return 0; +} + +/** + * @brief Write OV2640 single register + * @param bank: Bank ID + * @param reg: Register address + * @param data: Data to write + * @return 0 on success, negative error code on failure + */ +static int ov2640_write_reg(ov2640_bank_t bank, uint8_t reg, uint8_t data) +{ + int ret; + ret = ov2640_set_bank(bank); + if(ret) return ret; + ret = sccb_write(OV2640_ADDR, reg, data); + if(ret) return ret; + return 0; +} + +/** + * @brief Write OV2640 multiple registers + * @param regs: Pointer to register array, ending with {0, 0} + * @return RT_EOK on success, negative error code on failure + */ +static int ov2640_write_regs(const uint8_t (*regs)[2]) +{ + int ret; + const uint8_t (*reg_ptr)[2] = regs; + + while(((*reg_ptr)[0] != 0) || ((*reg_ptr)[1] != 0)) + { + ret = sccb_write(OV2640_ADDR, (*reg_ptr)[0], (*reg_ptr)[1]); + if(ret) return ret; + reg_ptr++; + } + return 0; +} + +/** + * @brief Read OV2640 single register + * @param data: Pointer to store read data + * @return register value, if read fails, return 0 + */ +static uint8_t ov2640_read_reg(uint8_t reg) +{ + return sccb_read(OV2640_ADDR, reg); +} + +/** + * @brief Get specific bits from OV2640 register, align to LSB by offset + * @param reg: Register address + * @param bank: Register bank + * @param mask: Bit mask + * @param offset: Bit offset + * @param value: Pointer to store the extracted bits + * @return aligned register bits, if read fails, return 0 + */ +static uint8_t ov2640_get_bits(uint8_t reg,ov2640_bank_t bank, uint8_t mask, uint32_t offset) +{ + int ret; + uint8_t reg_value; + ret = ov2640_set_bank(bank); + if(ret) return 0; + reg_value = ov2640_read_reg(reg); + return (reg_value >> offset) & mask; + +} + +/* + ******************************************************************************* + * Sensor vtable implementations + ******************************************************************************* + */ + +/* --- Lifecycle ------------------------------------------------------------ */ + +/** + * @brief Reset OV2640 sensor to default settings. + * + * Sends the software reset command then writes the base CIF initialisation + * register table. + * + * @param dev is a pointer to the sensor handle. + * + * @return Return 0 on success; negative error code on failure. + */ +static int ov2640_reset(ov2640_t *dev) +{ + int ret; + ov2640_lock(); + ov2640_write_reg(BANK_SENSOR, COM7, COM7_SRST); + rt_thread_mdelay(10); + ov2640_write_regs(ov2640_settings_cif); + ov2640_unlock(); + return 0; +} + +/** + * @brief Read all sensor registers and populate @p dev->status. + * + * Queries AEC, AGC, AWB, gain ceiling, gamma, lens correction and geometry + * settings over SCCB and mirrors them into the in-memory status struct so + * callers can inspect parameters without further I2C transactions. + * + * @param dev is a pointer to the sensor handle. + * + * @return Return 0 on success; -1 if a bank switch fails. + */ +static int ov2640_init_status(ov2640_t *dev) +{ + int ret; + uint8_t reg_value_h, reg_value_m, reg_value_l; + uint8_t temp_value; + + ov2640_lock(); + + dev->status.brightness = 0; + dev->status.contrast = 0; + dev->status.saturation = 0; + dev->status.ae_level = 0; + dev->status.special_effect = 0; + dev->status.wb_mode = 0; + + // Read AEC value (16-bit value split across 3 registers) + // AEC[15:10] from REG45, AEC[9:2] from AEC, AEC[1:0] from REG04 + reg_value_h = ov2640_get_bits(REG45, BANK_SENSOR, REG45_AEC_MASK, REG45_AEC_OFFSET); + ret = ov2640_set_bank(BANK_SENSOR); + if(ret != 0) return -1; + reg_value_m = ov2640_read_reg(AEC); + reg_value_l = ov2640_get_bits(REG04, BANK_SENSOR, REG04_AEC_MASK, REG04_AEC_OFFSET); + dev->status.aec_value = ((uint16_t)reg_value_h << 10) | ((uint16_t)reg_value_m << 2) | reg_value_l; // 0 - 1200 + + // Read quality + ret = ov2640_set_bank(BANK_DSP); + if(ret != 0) return -1; + temp_value = ov2640_read_reg(QS); + dev->status.quality = temp_value; + + // Read gain ceiling + dev->status.gainceiling = ov2640_get_bits(COM9, BANK_SENSOR, COM9_GAINCEILING_MASK, COM9_GAINCEILING_OFFSET); + + // Read AWB + dev->status.awb = ov2640_get_bits(CTRL1, BANK_DSP, CTRL1_AWB_MASK, CTRL1_AWB_OFFSET); + + // Read AWB gain + dev->status.awb_gain = ov2640_get_bits(CTRL1, BANK_DSP, CTRL1_AWB_GAIN_MASK, CTRL1_AWB_GAIN_OFFSET); + + // Read AEC + dev->status.aec = ov2640_get_bits(COM8, BANK_SENSOR, COM8_AEC_MASK, COM8_AEC_OFFSET); + + // Read AEC2 + dev->status.aec2 = ov2640_get_bits(CTRL0, BANK_DSP, CTRL0_AEC2_MASK, CTRL0_AEC2_OFFSET); + + // Read AGC + dev->status.agc = ov2640_get_bits(COM8, BANK_SENSOR, COM8_AGC_MASK, COM8_AGC_OFFSET); + + // Read BPC + dev->status.bpc = ov2640_get_bits(CTRL3, BANK_DSP, CTRL3_BPC_MASK, CTRL3_BPC_OFFSET); + + // Read WPC + dev->status.wpc = ov2640_get_bits(CTRL3, BANK_DSP, CTRL3_WPC_MASK, CTRL3_WPC_OFFSET); + + // Read RAW GMA + dev->status.raw_gma = ov2640_get_bits(CTRL1, BANK_DSP, CTRL1_RAW_GMA_MASK, CTRL1_RAW_GMA_OFFSET); + + // Read LENC + dev->status.lenc = ov2640_get_bits(CTRL1, BANK_DSP, CTRL1_LENC_MASK, CTRL1_LENC_OFFSET); + + // Read horizontal mirror + dev->status.hmirror = ov2640_get_bits(REG04, BANK_SENSOR, REG04_HMIRROR_MASK, REG04_HMIRROR_OFFSET); + + // Read vertical flip + dev->status.vflip = ov2640_get_bits(REG04, BANK_SENSOR, REG04_VFLIP_MASK, REG04_VFLIP_OFFSET); + + // Read DCW + dev->status.dcw = ov2640_get_bits(CTRL2, BANK_DSP, CTRL2_DCW_MASK, CTRL2_DCW_OFFSET); + + // Read color bar + dev->status.colorbar = ov2640_get_bits(COM7, BANK_SENSOR, COM7_COLORBAR_MASK, COM7_COLORBAR_OFFSET); + + // Sharpness and denoise are not supported + dev->status.sharpness = 0; + dev->status.denoise = 0; + + ov2640_unlock(); + return 0; +} + +/* --- Image format & resolution -------------------------------------------- */ + +/** + * @brief Set pixel output format (RGB565, YUV422, JPEG, RAW8). + * + * @param dev is a pointer to the sensor handle. + * @param pixformat is the desired output format. + * + * @return Return 0 on success; -1 for an unsupported format. + */ +static int ov2640_set_pixformat(ov2640_t *dev, pixformat_t pixformat) +{ + int ret = 0; + + ov2640_lock(); + dev->pixformat = pixformat; + + switch(pixformat) + { + case PIXFORMAT_RGB565: + ov2640_write_regs(ov2640_settings_rgb565); + break; + case PIXFORMAT_YUV422: + ov2640_write_regs(ov2640_settings_yuv422); + break; + case PIXFORMAT_JPEG: + ov2640_write_regs(ov2640_settings_jpeg3); + break; + case PIXFORMAT_RAW8: + ov2640_write_regs(ov2640_settings_raw8); + break; + default: + ret = -1; + } + ov2640_unlock(); + + return ret; +} + +/** + * @brief Configure sensor window (crop and scale) settings + * @param dev Pointer to ov2640_t structure + * @param mode Sensor mode (CIF, SVGA, UXGA) + * @param offset_x Horizontal offset + * @param offset_y Vertical offset + * @param max_x Maximum horizontal size + * @param max_y Maximum vertical size + * @param w Output width + * @param h Output height + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_window(ov2640_t *dev, ov2640_sensor_mode_t mode,int offset_x, int offset_y, int max_x, int max_y, int w, int h) +{ + int ret; + const uint8_t (*regs)[2]; + ov2640_clk_t c; + c.reserved = 0; + + ov2640_lock(); + + max_x /= 4; + max_y /= 4; + w /= 4; + h /= 4; + uint8_t win_regs[][2] = { + {BANK_SEL, BANK_DSP}, + {HSIZE, max_x & 0xFF}, + {VSIZE, max_y & 0xFF}, + {XOFFL, offset_x & 0xFF}, + {YOFFL, offset_y & 0xFF}, + {VHYX, ((max_y >> 1) & 0X80) | ((offset_y >> 4) & 0X70) | ((max_x >> 5) & 0X08) | ((offset_x >> 8) & 0X07)}, + {TEST, (max_x >> 2) & 0X80}, + {ZMOW, (w)&0xFF}, + {ZMOH, (h)&0xFF}, + {ZMHH, ((h>>6)&0x04)|((w>>8)&0x03)}, + {0, 0} + }; + + if (dev->pixformat == PIXFORMAT_JPEG) { + c.clk_2x = 1; + c.clk_div = 0; + c.pclk_auto = 0; + c.pclk_div =6; + if(mode == OV2640_MODE_UXGA) { + c.pclk_div = 24; + } + } else { + c.clk_2x = 1; + c.clk_div =3; + c.pclk_auto = 1; + c.pclk_div = 4; + if (mode == OV2640_MODE_CIF) { + c.clk_div = 3; + } else if(mode == OV2640_MODE_UXGA) { + c.pclk_div = 12; + } + } + + if (mode == OV2640_MODE_CIF) { + regs = ov2640_settings_to_cif; + } else if (mode == OV2640_MODE_SVGA) { + regs = ov2640_settings_to_svga; + } else { + regs = ov2640_settings_to_uxga; + } + + ov2640_set_bank(BANK_DSP); + ov2640_write_reg(BANK_DSP, R_BYPASS, R_BYPASS_DSP_BYPAS); + ov2640_write_regs(regs); + ov2640_write_regs(win_regs); + ov2640_set_bank(BANK_SENSOR); + ov2640_write_reg(BANK_SENSOR, CLKRC, c.clk); + ov2640_set_bank(BANK_DSP); + ov2640_write_reg(BANK_DSP, R_DVP_SP, c.pclk); + ov2640_set_bank(BANK_DSP); + ov2640_write_reg(BANK_DSP, R_BYPASS, R_BYPASS_DSP_EN); + + rt_thread_mdelay(10); + //required when changing resolution + /* Note: ov2640_set_pixformat will acquire its own lock, so we need to release first */ + ov2640_unlock(); + ov2640_set_pixformat(dev, dev->pixformat); + + return 0; +} + +/** + * @brief Set frame size/resolution + * @param dev Pointer to ov2640_t structure + * @param framesize Desired frame size (QVGA, VGA, SVGA, UXGA, etc.) + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_framesize(ov2640_t *dev, framesize_t framesize) +{ + if(framesize >= FRAMESIZE_INVALID) { + return -1; + } + + int ret = 0; + uint16_t w = resolution[framesize].width; + uint16_t h = resolution[framesize].height; + aspect_ratio_t ratio = resolution[framesize].aspect_ratio; + uint16_t max_x = ratio_table[ratio].max_x; + uint16_t max_y = ratio_table[ratio].max_y; + uint16_t offset_x = ratio_table[ratio].offset_x; + uint16_t offset_y = ratio_table[ratio].offset_y; + ov2640_sensor_mode_t mode = OV2640_MODE_UXGA; + + dev->status.framesize = framesize; + + if (framesize <= FRAMESIZE_CIF) { + mode = OV2640_MODE_CIF; + max_x /= 4; + max_y /= 4; + offset_x /= 4; + offset_y /= 4; + if(max_y > 296){ + max_y = 296; + } + } else if (framesize <= FRAMESIZE_SVGA) { + mode = OV2640_MODE_SVGA; + max_x /= 2; + max_y /= 2; + offset_x /= 2; + offset_y /= 2; + } + + ret = ov2640_set_window(dev, mode, offset_x, offset_y, max_x, max_y, w, h); + return ret; +} + +/* --- Image quality -------------------------------------------------------- */ + +/** + * @brief Set image contrast level. + * + * @param dev is a pointer to the sensor handle. + * @param level is the contrast level: -2 (low) ... +2 (high), 0 = default. + * + * @return Return 0 on success; negative SCCB error on failure. + */ +static int ov2640_set_contrast(ov2640_t *dev, int level) +{ + int ret = 0; + + ov2640_lock(); + level += 2; // Map -2~2 to 0~4 + ret = ov2640_write_reg(BANK_SENSOR, 0x81, (uint8_t)(0x14 + level * 4)); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.contrast = level - 2; + ov2640_unlock(); + return 0; +} + +/** + * @brief Set image brightness level + * @param dev Pointer to ov2640_t structure + * @param level Brightness level (-2 to +2, 0 is default) + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_brightness(ov2640_t *dev, int level) +{ + int ret = 0; + + ov2640_lock(); + if(level < -2) level = -2; + if(level > 2) level = 2; + level += 2; // Map -2~2 to 0~4 + ret = ov2640_write_reg(BANK_SENSOR, 0x9B, (uint8_t)(level * 0x20)); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.brightness = level - 2; + ov2640_unlock(); + return 0; +} + +/** + * @brief Set image color saturation level + * @param dev Pointer to ov2640_t structure + * @param level Saturation level (-2 to +2, 0 is default) + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_saturation(ov2640_t *dev, int level) +{ + int ret = 0; + + ov2640_lock(); + if(level < -2) level = -2; + if(level > 2) level = 2; + level += 2; // Map -2~2 to 0~4 + ret = ov2640_write_reg(BANK_DSP, 0xD9, (uint8_t)(0x40 + level * 0x10)); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.saturation = level - 2; + ov2640_unlock(); + return 0; +} + +/** + * @brief Set image sharpness (not supported by OV2640) + * @param dev Pointer to ov2640_t structure + * @param level Sharpness level (ignored) + * @return 0 (always success, feature not supported) + */ +static int ov2640_set_sharpness(ov2640_t *dev, int level) +{ + // OV2640 doesn't support sharpness adjustment + dev->status.sharpness = 0; + return 0; +} + +/** + * @brief Set denoise level (not supported by OV2640) + * @param dev Pointer to ov2640_t structure + * @param level Denoise level (ignored) + * @return 0 (always success, feature not supported) + */ +static int ov2640_set_denoise(ov2640_t *dev, int level) +{ + // OV2640 doesn't support direct denoise level adjustment + dev->status.denoise = 0; + return 0; +} + +/** + * @brief Set AGC (Automatic Gain Control) gain ceiling + * @param dev Pointer to ov2640_t structure + * @param gainceiling Gain ceiling value (2x, 4x, 8x, 16x, 32x, 64x, 128x) + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_gainceiling(ov2640_t *dev, gainceiling_t gainceiling) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(COM9); + reg_value = (reg_value & ~COM9_GAINCEILING_MASK) | ((gainceiling << COM9_GAINCEILING_OFFSET) & COM9_GAINCEILING_MASK); + ret = ov2640_write_reg(BANK_SENSOR, COM9, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.gainceiling = gainceiling; + ov2640_unlock(); + return 0; +} + +/** + * @brief Set JPEG compression quality + * @param dev Pointer to ov2640_t structure + * @param quality Quality scale (0-63, lower = better quality, higher compression) + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_quality(ov2640_t *dev, int quality) +{ + int ret = 0; + + ov2640_lock(); + // Quality scale: 0-63 (lower = better quality, higher compression) + if(quality < 0) quality = 0; + if(quality > 63) quality = 63; + ret = ov2640_write_reg(BANK_DSP, QS, (uint8_t)quality); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.quality = quality; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable color bar test pattern + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_colorbar(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(COM7); + if(enable) { + reg_value |= COM7_COLORBAR_MASK; + } else { + reg_value &= ~COM7_COLORBAR_MASK; + } + ret = ov2640_write_reg(BANK_SENSOR, COM7, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.colorbar = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/* --- White balance, exposure & gain --------------------------------------- */ + +/** + * @brief Enable or disable automatic white balance (AWB). + * + * @param dev is a pointer to the sensor handle. + * @param enable is 1 to enable AWB, 0 to disable. + * + * @return Return 0 on success; negative SCCB error on failure. + */ +static int ov2640_set_whitebal(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(CTRL1); + if(enable) { + reg_value |= CTRL1_AWB_MASK; + } else { + reg_value &= ~CTRL1_AWB_MASK; + } + ret = ov2640_set_bank(BANK_DSP); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, CTRL1, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.awb = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable automatic gain control (AGC) + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable AGC, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_gain_ctrl(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(COM8); + if(enable) { + reg_value |= COM8_AGC_MASK; + } else { + reg_value &= ~COM8_AGC_MASK; + } + ret = ov2640_write_reg(BANK_SENSOR, COM8, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.agc = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable automatic exposure control (AEC) + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable AEC, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_exposure_ctrl(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(COM8); + if(enable) { + reg_value |= COM8_AEC_MASK; + } else { + reg_value &= ~COM8_AEC_MASK; + } + ret = ov2640_write_reg(BANK_SENSOR, COM8, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.aec = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable horizontal mirror (flip image horizontally) + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable horizontal mirror, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_hmirror(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(REG04); + if(enable) { + reg_value |= REG04_HMIRROR_MASK; + } else { + reg_value &= ~REG04_HMIRROR_MASK; + } + ret = ov2640_write_reg(BANK_SENSOR, REG04, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.hmirror = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable vertical flip (flip image vertically) + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable vertical flip, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_vflip(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(REG04); + if(enable) { + reg_value |= REG04_VFLIP_MASK; + } else { + reg_value &= ~REG04_VFLIP_MASK; + } + ret = ov2640_write_reg(BANK_SENSOR, REG04, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.vflip = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable AEC2 (enhanced automatic exposure control) + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable AEC2, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_aec2(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(CTRL0); + if(enable) { + reg_value |= CTRL0_AEC2_MASK; + } else { + reg_value &= ~CTRL0_AEC2_MASK; + } + ret = ov2640_set_bank(BANK_DSP); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, CTRL0, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.aec2 = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable AWB gain control + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable AWB gain, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_awb_gain(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(CTRL1); + if(enable) { + reg_value |= CTRL1_AWB_GAIN_MASK; + } else { + reg_value &= ~CTRL1_AWB_GAIN_MASK; + } + ret = ov2640_set_bank(BANK_DSP); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, CTRL1, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.awb_gain = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Set manual AGC gain value + * @param dev Pointer to ov2640_t structure + * @param gain Gain value (0-30) + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_agc_gain(ov2640_t *dev, int gain) +{ + int ret = 0; + + ov2640_lock(); + // AGC gain: 0-30 maps to register value + if(gain < 0) gain = 0; + if(gain > 30) gain = 30; + ret = ov2640_write_reg(BANK_SENSOR, GAIN, (uint8_t)gain); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.agc_gain = gain; + ov2640_unlock(); + return 0; +} + +/** + * @brief Set manual AEC exposure value + * @param dev Pointer to ov2640_t structure + * @param value Exposure value (0-1200) + * @return 0 on success, negative error code on failure + * @note Value is split across 3 registers: REG45[5:0], AEC[7:0], REG04[1:0] + */ +static int ov2640_set_aec_value(ov2640_t *dev, int value) +{ + int ret = 0; + // AEC value: 0-1200, split across 3 registers + // AEC[15:10] -> REG45[5:0] + // AEC[9:2] -> AEC[7:0] + // AEC[1:0] -> REG04[1:0] + if(value < 0) value = 0; + if(value > 1200) value = 1200; + + ov2640_lock(); + + uint8_t reg45_val = ov2640_read_reg(REG45); + reg45_val = (reg45_val & ~REG45_AEC_MASK) | ((value >> 10) & REG45_AEC_MASK); + ret = ov2640_write_reg(BANK_SENSOR, REG45, reg45_val); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + + ret = ov2640_write_reg(BANK_SENSOR, AEC, (uint8_t)((value >> 2) & 0xFF)); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + + uint8_t reg04_val = ov2640_read_reg(REG04); + reg04_val = (reg04_val & ~REG04_AEC_MASK) | (value & REG04_AEC_MASK); + ret = ov2640_write_reg(BANK_SENSOR, REG04, reg04_val); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + + dev->status.aec_value = value; + ov2640_unlock(); + return 0; +} + +/** + * @brief Set special image effect + * @param dev Pointer to ov2640_t structure + * @param effect Effect mode: + * - 0: Normal + * - 1: Negative + * - 2: Black & White + * - 3: Reddish + * - 4: Greenish + * - 5: Bluish + * - 6: Sepia + * @return 0 on success, -1 if effect is out of range + */ +static int ov2640_set_special_effect(ov2640_t *dev, int effect) +{ + int ret = 0; + // Special effects: 0=Normal, 1=Negative, 2=B&W, 3=Reddish, 4=Greenish, 5=Bluish, 6=Sepia + const uint8_t effects[][3] = { + {0x00, 0x80, 0x80}, // Normal + {0x40, 0x80, 0x80}, // Negative + {0x18, 0x80, 0x80}, // B&W + {0x18, 0x40, 0xC0}, // Reddish + {0x18, 0x40, 0x40}, // Greenish + {0x18, 0xA0, 0x40}, // Bluish + {0x18, 0x40, 0xA0}, // Sepia + }; + + if(effect < 0 || effect > 6) return -1; + + ov2640_lock(); + + ret = ov2640_write_reg(BANK_DSP, 0xD7, effects[effect][0]); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, 0xD8, effects[effect][1]); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, 0xD9, effects[effect][2]); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + + dev->status.special_effect = effect; + ov2640_unlock(); + return 0; +} + +/** + * @brief Set white balance mode + * @param dev Pointer to ov2640_t structure + * @param mode White balance mode: + * - 0: Auto (AWB enabled) + * - 1: Sunny + * - 2: Cloudy + * - 3: Office + * - 4: Home + * @return 0 on success, -1 if mode is out of range + */ +static int ov2640_set_wb_mode(ov2640_t *dev, int mode) +{ + int ret = 0; + // WB modes: 0=Auto, 1=Sunny, 2=Cloudy, 3=Office, 4=Home + const uint8_t wb_modes[][3] = { + {0x00, 0x00, 0x00}, // Auto (AWB enabled) + {0x52, 0x41, 0x00}, // Sunny + {0x65, 0x41, 0x00}, // Cloudy + {0x45, 0x51, 0x00}, // Office + {0x42, 0x51, 0x00}, // Home + }; + + if(mode < 0 || mode > 4) return -1; + + if(mode == 0) { + // Enable AWB (set_whitebal will acquire its own lock) + ret = ov2640_set_whitebal(dev, 1); + } else { + // Disable AWB and set manual WB + ret = ov2640_set_whitebal(dev, 0); + if(ret != 0) return ret; + + ov2640_lock(); + ret = ov2640_write_reg(BANK_DSP, 0xCC, wb_modes[mode][0]); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, 0xCD, wb_modes[mode][1]); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, 0xCE, wb_modes[mode][2]); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ov2640_unlock(); + } + + dev->status.wb_mode = mode; + return 0; +} + +/** + * @brief Set automatic exposure level + * @param dev Pointer to ov2640_t structure + * @param level AE level (-2 to +2, 0 is default) + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_ae_level(ov2640_t *dev, int level) +{ + int ret = 0; + // AE level: -2 to +2 + level += 2; // Map to 0~4 + if(level < 0) level = 0; + if(level > 4) level = 4; + + const uint8_t ae_levels[5] = {0x40, 0x30, 0x24, 0x18, 0x10}; + + ov2640_lock(); + ret = ov2640_write_reg(BANK_SENSOR, AEW, ae_levels[level]); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_SENSOR, AEB, ae_levels[level]); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + + dev->status.ae_level = level - 2; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable DCW (Downsize Clock) + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable DCW, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_dcw(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(CTRL2); + if(enable) { + reg_value |= CTRL2_DCW_MASK; + } else { + reg_value &= ~CTRL2_DCW_MASK; + } + ret = ov2640_set_bank(BANK_DSP); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, CTRL2, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.dcw = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable BPC (Black Pixel Correction) + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable BPC, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_bpc(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(CTRL3); + if(enable) { + reg_value |= CTRL3_BPC_MASK; + } else { + reg_value &= ~CTRL3_BPC_MASK; + } + ret = ov2640_set_bank(BANK_DSP); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, CTRL3, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.bpc = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable WPC (White Pixel Correction) + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable WPC, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_wpc(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(CTRL3); + if(enable) { + reg_value |= CTRL3_WPC_MASK; + } else { + reg_value &= ~CTRL3_WPC_MASK; + } + ret = ov2640_set_bank(BANK_DSP); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, CTRL3, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.wpc = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable RAW Gamma correction + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable gamma correction, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_raw_gma(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(CTRL1); + if(enable) { + reg_value |= CTRL1_RAW_GMA_MASK; + } else { + reg_value &= ~CTRL1_RAW_GMA_MASK; + } + ret = ov2640_set_bank(BANK_DSP); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, CTRL1, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.raw_gma = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/** + * @brief Enable or disable LENC (Lens Correction) + * @param dev Pointer to ov2640_t structure + * @param enable 1 to enable lens correction, 0 to disable + * @return 0 on success, negative error code on failure + */ +static int ov2640_set_lenc(ov2640_t *dev, int enable) +{ + int ret = 0; + uint8_t reg_value; + + ov2640_lock(); + reg_value = ov2640_read_reg(CTRL1); + if(enable) { + reg_value |= CTRL1_LENC_MASK; + } else { + reg_value &= ~CTRL1_LENC_MASK; + } + ret = ov2640_set_bank(BANK_DSP); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + ret = ov2640_write_reg(BANK_DSP, CTRL1, reg_value); + if(ret != 0) { + ov2640_unlock(); + return ret; + } + dev->status.lenc = enable ? 1 : 0; + ov2640_unlock(); + return 0; +} + +/* + * NOTE: Raw register access wrappers (ov2640_get_reg / ov2640_set_reg) and + * the placeholder vtable hooks (ov2640_set_res_raw / ov2640_set_pll / + * ov2640_set_xclk) have been removed. XCLK is now owned by the DVP bus + * layer (see dvp_config_t::xclk_pin / xclk_freq). Add real implementations + * here if direct register tuning or PLL configuration is ever required. + */ + +/* + ******************************************************************************* + * Sensor public API + ******************************************************************************* + */ + +/** + * @brief Reset OV2640 and load the initial register state into the handle. + * + * Issues a software reset (CIF init table) and reads the configuration + * registers into @p dev->status. The sensor is left in JPEG/UXGA defaults. + * + * @param dev is a pointer to the sensor handle to initialise. + * + * @return Return 0 on success; negative error code on failure. + */ +int ov2640_init(ov2640_t *dev) +{ + int ret; + + ret = ov2640_reset(dev); + if (ret != 0) + { + return ret; + } + + ret = ov2640_init_status(dev); + if (ret != 0) + { + return ret; + } + + return 0; +} + +/* + ******************************************************************************* + * RT-Thread device interface + ******************************************************************************* + * + * Control command definitions are in ov2640.h. + */ + +/** + * @brief Frame-ready callback registered with the bus adapter. + * + * For streaming mode, rotates ping-pong buffers and invokes the user + * frame callback. For single-shot mode, releases @c frame_sem and halts + * the bus. Always called from ISR / timer-thread context. + * + * @param self is the bus adapter that fired the callback. + * @param bus_frame describes the captured frame (buffer, length, sequence). + * @param user is the @c rt_device_t handle cast to @c void*. + */ +static void ov2640_frame_ready_callback(bus_adapter_t *self, const bus_frame_t *bus_frame, void *user) +{ + rt_device_t dev = (rt_device_t)user; + ov2640_device_t *cam_dev = (ov2640_device_t *)dev->user_data; + ov2640_stream_state_t *stream = &cam_dev->stream; + rt_size_t frame_size = bus_frame ? bus_frame->length : 0; + + /* + * Snapshot the streaming callback under an interrupt lock. The callback + * pointer being non-NULL is the single source of truth for "streaming + * is armed". STOP_STREAM clears it inside the same critical section so + * we cannot observe a half-torn-down stream here. + */ + rt_base_t level = rt_hw_interrupt_disable(); + camera_stream_frame_callback_t frame_callback = stream->frame_callback; + void *callback_context = stream->callback_context; + rt_hw_interrupt_enable(level); + + if (frame_callback != RT_NULL) + { + rt_uint8_t completed_index = stream->active_buffer_index; + camera_stream_frame_t frame; + + frame.buffer = stream->buffers[completed_index]; + frame.buffer_size = stream->buffer_size; + frame.frame_size = frame_size; + frame.sequence = ++stream->sequence; + frame.buffer_index = completed_index; + + stream->active_buffer_index ^= 1U; + + if (bus_adapter_rearm_capture(self, + stream->buffers[stream->active_buffer_index], + stream->buffer_size) != RT_EOK) + { + /* Re-arm failed: tear down stream state and abort the bus so + * subsequent ISRs (if any) fall through the single-shot path. */ + rt_base_t lock = rt_hw_interrupt_disable(); + ov2640_stream_reset(stream); + rt_hw_interrupt_enable(lock); + bus_adapter_abort_capture(self); + } + + frame_callback(callback_context, &frame); + + if (dev && dev->rx_indicate) + { + dev->rx_indicate(dev, frame_size); + } + return; + } + + /* Notify blocking OV2640 read path via semaphore */ + rt_sem_release(&cam_dev->frame_sem); + bus_adapter_stop(self); + bus_adapter_abort_capture(self); + if (dev && dev->rx_indicate) + { + dev->rx_indicate(dev, frame_size); + } +} + +/** + * @brief RT-Thread device `init` hook (no-op; real initialisation is in `open`). + * + * @param dev is the RT-Thread device handle. + * + * @return Return RT_EOK. + */ +static rt_err_t ov2640_dev_init(rt_device_t dev) +{ + /* Device init is now handled in ov2640_open for proper re-initialization */ + return RT_EOK; +} + +/** + * @brief RT-Thread device `open` — power up sensor and initialise data bus. + * + * Starts XCLK, initialises the SCCB interface, resets the OV2640 and + * binds the DVP bus adapter. The data bus is left in a ready-but-idle + * state; capture is started by @c rt_device_read or @c OV2640_CMD_START_STREAM. + * + * @param dev is the RT-Thread device handle. + * @param oflag is unused (camera is always opened read-write). + * + * @return Return RT_EOK on success; @c -RT_ERROR on hardware failure. + */ +static rt_err_t ov2640_open(rt_device_t dev, rt_uint16_t oflag) +{ + ov2640_device_t *cam_dev = (ov2640_device_t *)dev->user_data; + int ret; + + ov2640_stream_reset(&cam_dev->stream); + + /* Initialize frame ready semaphore */ + rt_sem_init(&cam_dev->frame_sem, "cam_frm", 0, RT_IPC_FLAG_FIFO); + + ret = sccb_init(SCCB_USE_IIC); + if (ret != RT_EOK) + { + LOG_E("SCCB init failed: %d", ret); + return -RT_ERROR; + } + + /* Initialize OV2640 sensor */ + ret = ov2640_init(&cam_dev->sensor); + if (ret != 0) + { + LOG_E("OV2640 init failed: %d", ret); + sccb_deinit(); + return -RT_ERROR; + } + + /* Default pixel format / framesize / quality are applied later via + * OV2640_CMD_SET_* by the camera handle / application. */ + + /* Locate the data bus adapter and prepare its configuration */ + cam_dev->data_bus = bus_adapter_find("dvp"); + if (cam_dev->data_bus == RT_NULL) + { + LOG_E("Data bus adapter 'dvp' not registered"); + sccb_deinit(); + return -RT_ERROR; + } + + /* Mirror bus-agnostic fields; dvp_init() will self-configure hardware from Kconfig. */ + cam_dev->bus_snapshot.mode = BUS_CAPTURE_MODE_JPEG; + cam_dev->bus_snapshot.frame_buffer = NULL; + cam_dev->bus_snapshot.buffer_size = 0; + + /* Register high-level frame callback on the bus adapter */ + bus_adapter_set_frame_callback(cam_dev->data_bus, ov2640_frame_ready_callback, dev); + + ret = bus_adapter_init(cam_dev->data_bus); + if (ret != 0) + { + LOG_E("Bus adapter init failed: %d", ret); + sccb_deinit(); + return -RT_ERROR; + } + + return RT_EOK; +} + +/** + * @brief RT-Thread device `close` — abort capture and power down. + * + * Stops any ongoing DMA transfer, deinitialises the data bus (which also + * stops XCLK via `dvp_deinit`) and SCCB, then destroys the frame semaphore. + * + * @param dev is the RT-Thread device handle. + * + * @return Return RT_EOK. + */ +static rt_err_t ov2640_close(rt_device_t dev) +{ + ov2640_device_t *cam_dev = (ov2640_device_t *)dev->user_data; + + ov2640_stream_reset(&cam_dev->stream); + + /* Stop any ongoing capture */ + bus_adapter_abort_capture(cam_dev->data_bus); + + /* Stop DVP hardware */ + bus_adapter_stop(cam_dev->data_bus); + + /* Deinitialize data bus */ + bus_adapter_deinit(cam_dev->data_bus); + + /* Deinitialize SCCB/I2C interface */ + sccb_deinit(); + + /* Reset sensor bank state */ + ov2640_reset_bank_state(); + + /* Destroy frame ready semaphore */ + rt_sem_detach(&cam_dev->frame_sem); + + LOG_I("Camera device closed"); + return RT_EOK; +} + +#ifndef OV2640_CAMERA_READ_TIMEOUT_MS +#define OV2640_READ_TIMEOUT_MS 1000 /* 1 second timeout */ +#else +#define OV2640_READ_TIMEOUT_MS OV2640_CAMERA_READ_TIMEOUT_MS +#endif +#define OV2640_READ_TIMEOUT_TICKS (OV2640_READ_TIMEOUT_MS * RT_TICK_PER_SECOND / 1000) + +#ifndef OV2640_ENABLE_CAPTURE_TIMEOUT +#define OV2640_ENABLE_CAPTURE_TIMEOUT 1 +#endif + +#if OV2640_ENABLE_CAPTURE_TIMEOUT +static void ov2640_dump_timeout_state(ov2640_device_t *cam_dev, + rt_size_t request_size, + rt_err_t wait_result, + rt_tick_t start_tick) +{ + rt_tick_t elapsed_ticks = rt_tick_get() - start_tick; + unsigned long elapsed_ms = (unsigned long)elapsed_ticks * 1000UL / RT_TICK_PER_SECOND; + + LOG_W("Camera read timeout"); + LOG_W(" wait: result=%d elapsed=%lu ms req=%u", + wait_result, elapsed_ms, (unsigned int)request_size); + LOG_W(" sensor: pix=%d frame=%d quality=%u", + (int)cam_dev->sensor.pixformat, + (int)cam_dev->sensor.status.framesize, + (unsigned int)cam_dev->sensor.status.quality); + /* Delegate hardware-level diagnostics to the bus adapter. */ + bus_adapter_dump_state(cam_dev->data_bus); +} +#endif /* OV2640_ENABLE_CAPTURE_TIMEOUT */ + +/** + * @brief RT-Thread device `read` — trigger a single-shot capture and block. + * + * Starts a DVP DMA transfer into @p buffer then waits on @c frame_sem. + * If @c OV2640_ENABLE_CAPTURE_TIMEOUT is set the wait is bounded by + * @c OV2640_READ_TIMEOUT_MS; a timeout dumps diagnostic state and returns 0. + * + * @param dev is the RT-Thread device handle. + * @param pos is unused. + * @param buffer is the destination frame buffer (must be DMA-accessible). + * @param size is the buffer size in bytes. + * + * @return Return the number of bytes captured; 0 on timeout or error. + */ +static rt_size_t ov2640_read(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size) +{ + ov2640_device_t *cam_dev = (ov2640_device_t *)dev->user_data; + rt_size_t frame_size = 0; + rt_err_t result; +#if OV2640_ENABLE_CAPTURE_TIMEOUT + rt_tick_t start_tick; +#endif + + /* Drain any stale semaphore tokens before starting capture */ + while (rt_sem_trytake(&cam_dev->frame_sem) == RT_EOK); + +#if OV2640_ENABLE_CAPTURE_TIMEOUT + start_tick = rt_tick_get(); +#endif + result = (rt_err_t)bus_adapter_start_capture(cam_dev->data_bus, buffer, size); + if (result != RT_EOK) + { + LOG_E("Failed to start capture: %d", result); + return 0; + } + +#if OV2640_ENABLE_CAPTURE_TIMEOUT + /* Block until frame ready or timeout */ + result = rt_sem_take(&cam_dev->frame_sem, OV2640_READ_TIMEOUT_TICKS); + + if (result != RT_EOK) + { + ov2640_dump_timeout_state(cam_dev, size, result, start_tick); + bus_adapter_abort_capture(cam_dev->data_bus); + return 0; + } +#else + /* Block until frame ready indefinitely */ + result = rt_sem_take(&cam_dev->frame_sem, RT_WAITING_FOREVER); +#endif + + + // // // Step 2: Stop GPTIM1 and reset its counter to 0 + // // HAL_GPT_Base_Stop(&handle->gptim); + // // __HAL_GPT_SET_COUNTER(&handle->gptim, 0); + + /* Note: pingpong buffer auto-clear on next DMA write — no explicit memset needed */ + frame_size = bus_adapter_get_frame_size(cam_dev->data_bus); + + return frame_size; +} + +/** + * @brief RT-Thread device `write` — not supported by camera devices. + * + * @param dev is the RT-Thread device handle. + * @param pos is unused. + * @param buffer is unused. + * @param size is unused. + * + * @return Return 0 always. + */ +static rt_size_t ov2640_write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size) +{ + (void)dev; (void)pos; (void)buffer; (void)size; + return 0; +} + +/** + * @brief Map a sensor-level @c pixformat_t to a generic @c bus_capture_mode_t. + * + * Centralizes the pixel-format → bus-capture-mode translation so that the + * mapping lives in exactly one place. Returns @c RT_TRUE on success and + * writes the resolved mode through @p out_mode; returns @c RT_FALSE for + * formats that have no bus equivalent. + */ +static rt_bool_t ov2640_pixformat_to_bus_mode(pixformat_t format, bus_capture_mode_t *out_mode) +{ + switch (format) + { + case PIXFORMAT_JPEG: *out_mode = BUS_CAPTURE_MODE_JPEG; return RT_TRUE; + case PIXFORMAT_RGB565: *out_mode = BUS_CAPTURE_MODE_RGB565; return RT_TRUE; + case PIXFORMAT_YUV422: *out_mode = BUS_CAPTURE_MODE_YUV422; return RT_TRUE; + case PIXFORMAT_RAW8: *out_mode = BUS_CAPTURE_MODE_RAW; return RT_TRUE; + default: return RT_FALSE; + } +} + +/** + * + * Routes all @c OV2640_CMD_* values to the appropriate vtable function or + * bus adapter op. Unknown commands return @c -RT_EINVAL. + * + * @param dev is the RT-Thread device handle. + * @param cmd is one of the @c OV2640_CMD_* constants (see ov2640.h). + * @param args is the command argument; type varies per command. + * + * @return Return RT_EOK on success; @c -RT_EINVAL for unknown commands; + * @c -RT_ERROR if the underlying operation fails. + */ +static rt_err_t ov2640_control(rt_device_t dev, int cmd, void *args) +{ + ov2640_device_t *cam_dev = (ov2640_device_t *)dev->user_data; + int ret; + + if (cam_dev == RT_NULL) + { + return -RT_ERROR; + } + + switch (cmd) + { + case OV2640_CMD_SET_PIXFORMAT: + { + pixformat_t format = (pixformat_t)(rt_ubase_t)args; + ret = ov2640_set_pixformat(&cam_dev->sensor, format); + if (ret != 0) + { + return -RT_ERROR; + } + + /* Update bus capture mode accordingly and restart hardware */ + bus_capture_mode_t new_mode; + if (!ov2640_pixformat_to_bus_mode(format, &new_mode)) + { + return -RT_EINVAL; + } + + /* If mode changed, restart the bus through the generic adapter + * interface — no direct access to DVP-private state. */ + if (cam_dev->bus_snapshot.mode != new_mode) + { + bus_adapter_stop(cam_dev->data_bus); + if (bus_adapter_set_mode(cam_dev->data_bus, new_mode) != BUS_OK) + { + LOG_E("Failed to set bus mode"); + return -RT_ERROR; + } + cam_dev->bus_snapshot.mode = new_mode; + ret = bus_adapter_start(cam_dev->data_bus); + if (ret != 0) + { + LOG_E("Failed to restart bus after mode change"); + return -RT_ERROR; + } + } + + return RT_EOK; + } + + case OV2640_CMD_SET_FRAMESIZE: + { + if((framesize_t)(rt_ubase_t)args >= FRAMESIZE_INVALID) + { + return -RT_EINVAL; + } + framesize_t framesize = (framesize_t)(rt_ubase_t)args; + ret = ov2640_set_framesize(&cam_dev->sensor, framesize); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_BRIGHTNESS: + { + int level = (int)(rt_base_t)args; + ret = ov2640_set_brightness(&cam_dev->sensor, level); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_CONTRAST: + { + int level = (int)(rt_base_t)args; + ret = ov2640_set_contrast(&cam_dev->sensor, level); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_SATURATION: + { + int level = (int)(rt_base_t)args; + ret = ov2640_set_saturation(&cam_dev->sensor, level); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_QUALITY: + { + int quality = (int)(rt_base_t)args; + ret = ov2640_set_quality(&cam_dev->sensor, quality); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_HMIRROR: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_hmirror(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_VFLIP: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_vflip(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_COLORBAR: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_colorbar(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_WHITEBAL: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_whitebal(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_GAIN_CTRL: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_gain_ctrl(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_EXPOSURE_CTRL: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_exposure_ctrl(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_AEC2: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_aec2(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_AWB_GAIN: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_awb_gain(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_AGC_GAIN: + { + int gain = (int)(rt_base_t)args; + ret = ov2640_set_agc_gain(&cam_dev->sensor, gain); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_AEC_VALUE: + { + int value = (int)(rt_base_t)args; + ret = ov2640_set_aec_value(&cam_dev->sensor, value); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_SPECIAL_EFFECT: + { + int effect = (int)(rt_base_t)args; + ret = ov2640_set_special_effect(&cam_dev->sensor, effect); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_WB_MODE: + { + int mode = (int)(rt_base_t)args; + ret = ov2640_set_wb_mode(&cam_dev->sensor, mode); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_AE_LEVEL: + { + int level = (int)(rt_base_t)args; + ret = ov2640_set_ae_level(&cam_dev->sensor, level); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_DCW: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_dcw(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_BPC: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_bpc(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_WPC: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_wpc(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_RAW_GMA: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_raw_gma(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_LENC: + { + int enable = (int)(rt_base_t)args; + ret = ov2640_set_lenc(&cam_dev->sensor, enable); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_GAINCEILING: + { + gainceiling_t gainceiling = (gainceiling_t)(rt_ubase_t)args; + ret = ov2640_set_gainceiling(&cam_dev->sensor, gainceiling); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_SHARPNESS: + { + int level = (int)(rt_base_t)args; + ret = ov2640_set_sharpness(&cam_dev->sensor, level); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_SET_DENOISE: + { + int level = (int)(rt_base_t)args; + ret = ov2640_set_denoise(&cam_dev->sensor, level); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + + case OV2640_CMD_START_CAPTURE: + { + (void)args; + ov2640_stream_reset(&cam_dev->stream); + bus_adapter_start_capture(cam_dev->data_bus, RT_NULL, 0); + return RT_EOK; + } + + case OV2640_CMD_STOP_CAPTURE: + { + (void)args; + ov2640_stream_reset(&cam_dev->stream); + bus_adapter_abort_capture(cam_dev->data_bus); + return RT_EOK; + } + + case OV2640_CMD_GET_FRAME_SIZE: + { + uint32_t *size_ptr = (uint32_t *)args; + if (size_ptr != RT_NULL) + { + *size_ptr = bus_adapter_get_frame_size(cam_dev->data_bus); + return RT_EOK; + } + return -RT_EINVAL; + } + + case OV2640_CMD_SET_FRAME_BUFFER: + { + uint8_t *buffer = (uint8_t *)args; + if (buffer != RT_NULL) + { + cam_dev->bus_snapshot.frame_buffer = buffer; + bus_adapter_update_buffer(cam_dev->data_bus, buffer, cam_dev->bus_snapshot.buffer_size); + LOG_I("Bus frame buffer set to 0x%08X", (uint32_t)buffer); + return RT_EOK; + } + return -RT_EINVAL; + } + + case OV2640_CMD_SET_FRAME_BUFFER_SIZE: + { + uint32_t size = (uint32_t)(rt_ubase_t)args; + if (size > 0) + { + cam_dev->bus_snapshot.buffer_size = size; + bus_adapter_update_buffer(cam_dev->data_bus, cam_dev->bus_snapshot.frame_buffer, size); + LOG_I("Bus frame buffer size set to %d bytes", size); + return RT_EOK; + } + return -RT_EINVAL; + } + + case OV2640_CMD_SET_PINGPONG_SIZE: + { + uint32_t size = (uint32_t)(rt_ubase_t)args; + if (size > 0) + { + ret = bus_adapter_set_pingpong_size(cam_dev->data_bus, size); + return (ret == 0) ? RT_EOK : -RT_ERROR; + } + return -RT_EINVAL; + } + + case OV2640_CMD_START_STREAM: + { + camera_stream_start_args_t *stream_args = (camera_stream_start_args_t *)args; + + if (stream_args == RT_NULL || + stream_args->buffers[0] == RT_NULL || + stream_args->buffers[1] == RT_NULL || + stream_args->buffer_size == 0) + { + return -RT_EINVAL; + } + + /* + * Populate buffers and indices first; only publish frame_callback + * (the "streaming armed" flag) after everything else is in place, + * so an early ISR can never see a torn state. The callback store + * is wrapped in a critical section to pair with the ISR's read + * and STOP_STREAM's clear. + */ + cam_dev->stream.buffers[0] = stream_args->buffers[0]; + cam_dev->stream.buffers[1] = stream_args->buffers[1]; + cam_dev->stream.buffer_size = stream_args->buffer_size; + cam_dev->stream.active_buffer_index = 0; + cam_dev->stream.sequence = 0; + + { + rt_base_t level = rt_hw_interrupt_disable(); + cam_dev->stream.callback_context = stream_args->callback_context; + cam_dev->stream.frame_callback = stream_args->frame_callback; + rt_hw_interrupt_enable(level); + } + + ret = bus_adapter_start_capture(cam_dev->data_bus, + cam_dev->stream.buffers[cam_dev->stream.active_buffer_index], + cam_dev->stream.buffer_size); + if (ret != RT_EOK) + { + rt_base_t level = rt_hw_interrupt_disable(); + ov2640_stream_reset(&cam_dev->stream); + rt_hw_interrupt_enable(level); + return -RT_ERROR; + } + + return RT_EOK; + } + + case OV2640_CMD_STOP_STREAM: + { + /* + * Order matters: + * 1) Abort the bus first so the DMA stops generating new + * frame-ready events. + * 2) Clear stream state (including frame_callback) inside an + * interrupt lock. Any ISR already in flight observes either + * the old callback (and dispatches one final frame) or NULL + * (and falls through to the single-shot path which is a + * no-op when no thread is waiting on frame_sem). + */ + bus_adapter_abort_capture(cam_dev->data_bus); + { + rt_base_t level = rt_hw_interrupt_disable(); + ov2640_stream_reset(&cam_dev->stream); + rt_hw_interrupt_enable(level); + } + return RT_EOK; + } + + default: + return -RT_EINVAL; + } +} + +#ifdef RT_USING_DEVICE_OPS +static const struct rt_device_ops ov2640_ops = +{ + ov2640_dev_init, + ov2640_open, + ov2640_close, + ov2640_read, + ov2640_write, + ov2640_control +}; +#endif + +/* + ******************************************************************************* + * Device registration + ******************************************************************************* + */ + +/** + * @brief Register the OV2640 as an RT-Thread device named "ov2640". + * + * Allocates @c rt_device_t and @c ov2640_device_t on the heap, hooks the + * RT-Thread device ops and calls @c rt_device_register. Automatically + * invoked by @c INIT_DEVICE_EXPORT. + * + * @return Return RT_EOK on success; @c -RT_ENOMEM or device error on failure. + */ +/** + * @brief Register the OV2640 as an RT-Thread device under @p name. + * + * Allocates @c rt_device_t and @c ov2640_device_t on the heap, hooks the + * RT-Thread device ops and calls @c rt_device_register. The driver is + * single-instance — see @ref s_active_dev for details — and calling this + * more than once will overwrite the singleton pointer. + * + * @param name is the RT-Thread device name to register under; if @c NULL + * the default @ref OV2640_DEVICE_NAME is used. + * + * @return Return RT_EOK on success; @c -RT_ENOMEM or device error on failure. + */ +int ov2640_device_register(const char *name) +{ + rt_device_t dev; + int ret; + if (name == RT_NULL) + { + name = OV2640_DEVICE_NAME; + } + + /* Allocate OV2640 device structure */ + dev = rt_malloc(sizeof(struct rt_device)); + if (dev == RT_NULL) + { + LOG_E("Failed to allocate ov2640 device memory"); + return -RT_ENOMEM; + } + + rt_memset(dev, 0, sizeof(struct rt_device)); + + /* Initialize device structure */ + dev->type = RT_Device_Class_Miscellaneous; + +#ifdef RT_USING_DEVICE_OPS + dev->ops = &ov2640_ops; +#else + dev->init = ov2640_dev_init; + dev->open = ov2640_open; + dev->close = ov2640_close; + dev->read = ov2640_read; + dev->write = ov2640_write; + dev->control = ov2640_control; +#endif + dev->user_data = rt_malloc(sizeof(ov2640_device_t)); + if (dev->user_data == RT_NULL) + { + LOG_E("Failed to allocate ov2640 device user data memory"); + rt_free(dev); + return -RT_ENOMEM; + } + rt_memset(dev->user_data, 0, sizeof(ov2640_device_t)); + + /* Initialize per-instance state moved out of file scope (#6/#16). */ + ov2640_device_t *cam_dev = (ov2640_device_t *)dev->user_data; + cam_dev->current_bank = (rt_uint8_t)BANK_MAX; + ov2640_mutex_init(cam_dev); + + /* Publish the singleton pointer used by SCCB setter helpers. */ + s_active_dev = cam_dev; + + /* Register device */ + ret = rt_device_register(dev, name, RT_DEVICE_FLAG_RDWR); + if (ret != RT_EOK) + { + LOG_E("Failed to register ov2640 device: %d", ret); + s_active_dev = RT_NULL; + rt_free(dev->user_data); + rt_free(dev); + return ret; + } + + LOG_I("ov2640 device '%s' registered successfully", name); + return RT_EOK; +} + +/** + * @brief Register the OV2640 device using @ref OV2640_DEVICE_NAME. + * + * Wrapper used by @c INIT_DEVICE_EXPORT, which only accepts a parameter-less + * function. Equivalent to @c ov2640_device_register(NULL). + */ +int ov2640_device_register_default(void) +{ + return ov2640_device_register(RT_NULL); +} + +/** + * @brief Unregister and free the OV2640 RT-Thread device. + * + * Finds the "ov2640" device, closes it if still open, unregisters it + * and frees all heap allocations. + * + * @return Return RT_EOK on success; @c -RT_ENOSYS if device not found. + */ +int ov2640_device_unregister(void) +{ + rt_device_t dev; + const char *name = OV2640_DEVICE_NAME; + + dev = rt_device_find(name); + if (dev == RT_NULL) + { + LOG_W("ov2640 device '%s' not found", name); + return -RT_ENOSYS; + } + + /* Close device if still open */ + if (dev->ref_count > 0) + { + rt_device_close(dev); + } + + /* Unregister device */ + rt_err_t ret = rt_device_unregister(dev); + if (ret != RT_EOK) + { + LOG_E("Failed to unregister ov2640 device: %d", ret); + return ret; + } + + /* Free user data */ + if (dev->user_data != RT_NULL) + { + ov2640_device_t *cam_dev = (ov2640_device_t *)dev->user_data; + if (cam_dev == s_active_dev) + { + s_active_dev = RT_NULL; + } + if (cam_dev->sccb_lock_initialized) + { + rt_mutex_detach(&cam_dev->sccb_lock); + cam_dev->sccb_lock_initialized = RT_FALSE; + } + rt_free(dev->user_data); + dev->user_data = RT_NULL; + } + + /* Free device structure */ + rt_free(dev); + + LOG_I("ov2640 device '%s' unregistered successfully", name); + return RT_EOK; +} + +INIT_DEVICE_EXPORT(ov2640_device_register_default); \ No newline at end of file diff --git a/camera_framework/camera/driver/ov2640/ov2640.h b/camera_framework/camera/driver/ov2640/ov2640.h new file mode 100644 index 0000000..e29059f --- /dev/null +++ b/camera_framework/camera/driver/ov2640/ov2640.h @@ -0,0 +1,330 @@ +/** + * @file ov2640.h + * @brief OV2640 camera sensor RT-Thread device driver interface + * + * This header provides the OV2640 sensor data structures, camera + * status definitions, RT-Thread device interface, and control + * command definitions. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2026 + */ + +#ifndef OV2640_H_ +#define OV2640_H_ + +#include +#include +#include "sccb.h" +#include "rtthread.h" +#include "rtconfig.h" +#include "../../handle/camera_handle.h" + +#define OV2640_ADDR 0x30 /* 7-bit I2C/SCCB slave address */ + +/** RT-Thread device name used to register & locate the OV2640 instance. + * Defined to equal CAMERA_DEFAULT_DEVICE_NAME (from camera_handle.h, already + * included above) so the two stay in sync automatically. */ +#define OV2640_DEVICE_NAME CAMERA_DEFAULT_DEVICE_NAME + +/* + ******************************************************************************* + * Data types + ******************************************************************************* + */ + +/** + * @brief Snapshot of all configurable camera sensor parameters. + * + * Updated by the driver each time a parameter is successfully written to the + * sensor; callers can read this struct to query the current sensor state + * without issuing I2C transactions. + */ +typedef struct { + /* Resolution & format */ + framesize_t framesize; /* 0 – FRAMESIZE_INVALID-1 */ + bool scale; + bool binning; + + /* Image quality */ + uint8_t quality; /* JPEG quality scale 0–63 (lower = better) */ + int8_t brightness; /* -2 – +2, 0 = default */ + int8_t contrast; /* -2 – +2, 0 = default */ + int8_t saturation; /* -2 – +2, 0 = default */ + int8_t sharpness; /* -2 – +2 (not supported on OV2640, always 0) */ + uint8_t denoise; /* denoise level (not supported on OV2640, always 0) */ + uint8_t special_effect; /* 0 = Normal, 1–6 = various effects */ + + /* White balance */ + uint8_t wb_mode; /* 0–4: auto / sunny / cloudy / office / home */ + uint8_t awb; /* 1 = AWB enabled */ + uint8_t awb_gain; /* 1 = AWB gain enabled */ + + /* Exposure & gain */ + uint8_t aec; /* 1 = AEC enabled */ + uint8_t aec2; /* 1 = AEC2 (enhanced) enabled */ + int8_t ae_level; /* -2 – +2 AE compensation */ + uint16_t aec_value; /* manual AEC value 0–1200 */ + uint8_t agc; /* 1 = AGC enabled */ + uint8_t agc_gain; /* manual AGC gain 0–30 */ + uint8_t gainceiling; /* AGC gain ceiling 0–6 (2x–128x) */ + + /* Image processing */ + uint8_t bpc; /* 1 = bad pixel correction enabled */ + uint8_t wpc; /* 1 = white pixel correction enabled */ + uint8_t raw_gma; /* 1 = RAW gamma enabled */ + uint8_t lenc; /* 1 = lens correction enabled */ + uint8_t hmirror; /* 1 = horizontal mirror enabled */ + uint8_t vflip; /* 1 = vertical flip enabled */ + uint8_t dcw; /* 1 = downsize crop & window enabled */ + uint8_t colorbar; /* 1 = color bar test pattern enabled */ +} camera_status_t; + +/** + * @brief AGC gain ceiling values. + * + * Passed to `set_gainceiling` to cap the automatic gain loop. + */ +typedef enum { + GAINCEILING_2X, + GAINCEILING_4X, + GAINCEILING_8X, + GAINCEILING_16X, + GAINCEILING_32X, + GAINCEILING_64X, + GAINCEILING_128X, +} gainceiling_t; + +/** + * @brief OV2640 sensor handle. + * + * Holds sensor identity (from ID registers) and a mirror of the last-applied + * configuration. Sensor operations are exposed as plain functions in + * `ov2640.c` (see `ov2640_set_pixformat`, `ov2640_set_framesize`, …) — there + * is no runtime vtable because the RT-Thread device layer is the only caller + * and the sensor model is fixed at build time. + */ +typedef struct ov2640 +{ + /* Sensor identification registers (read from HW during init) */ + uint8_t MIDH; /* Manufacturer ID high byte */ + uint8_t MIDL; /* Manufacturer ID low byte */ + uint16_t PID; /* Product ID */ + uint8_t VER; /* Product version */ + + /* Current format and status mirror */ + pixformat_t pixformat; + camera_status_t status; +} ov2640_t; + +/** + * @brief Initialize an OV2640 sensor handle. + * + * Populates all vtable function pointers and reads identification + * registers from the sensor via SCCB. Must be called once before any + * vtable operation. + * + * @param dev is a pointer to the sensor handle to initialize. + * + * @return Return 0 on success. Otherwise a negative error code is returned. + */ +int ov2640_init(ov2640_t *dev); + +/* + ******************************************************************************* + * RT-Thread device layer + ******************************************************************************* + */ + +#include "data_bus_adapter.h" + +/** + * @brief Bus-agnostic runtime state mirrored from the active configuration. + * + * Replaces the former dvp_config_t snapshot so the sensor driver no longer + * depends on a specific bus type. Only the fields actually queried at runtime + * by the device-control handlers are kept here; all hardware-specific + * parameters are passed to bus_adapter_init() as a local config struct and + * are not retained in this instance. + */ +typedef struct { + bus_capture_mode_t mode; /**< current capture mode (bus-agnostic) */ + uint8_t *frame_buffer; /**< current user frame buffer, may be NULL */ + uint32_t buffer_size; /**< size of frame_buffer in bytes */ +} ov2640_bus_snapshot_t; + +/** + * @brief Streaming state for continuous multi-buffer capture. + * + * Maintained by the RT-Thread device layer; not accessed directly by sensor + * logic. Buffers are ping-pong rotated on each frame-ready callback. + * + * The presence of a non-NULL @c frame_callback is the single authoritative + * indicator that streaming is armed: the frame-ready ISR dispatches to the + * streaming branch iff @c frame_callback is set, otherwise it treats the + * frame as a single-shot completion. STOP_STREAM clears the callback under + * an interrupt lock (after aborting the bus) so no in-flight ISR can see + * a stale callback pointer. + */ +typedef struct +{ + uint8_t *buffers[2]; /* ping-pong buffer pair */ + rt_size_t buffer_size; /* size of each buffer in bytes */ + rt_uint8_t active_buffer_index; /* index of the buffer currently being filled */ + rt_uint32_t sequence; /* monotonically increasing frame counter */ + camera_stream_frame_callback_t frame_callback; /* user callback per frame; NULL = single-shot mode */ + void *callback_context; /* opaque pointer forwarded to callback */ +} ov2640_stream_state_t; + +/** + * @brief OV2640 RT-Thread device instance. + * + * One global instance is registered as RT-Thread device "ov2640" (see + * @ref OV2640_DEVICE_NAME). Upper layers access it through the standard + * @c rt_device_* API. + * + * @note This driver is currently single-instance: the SCCB control bus + * (sccb.c) and DVP data bus (dvp.c) both keep file-scope state, so + * only one @ref ov2640_device_t may exist at a time. The mutex and + * register-bank cache below live in the instance rather than file + * scope to make a future multi-instance refactor easier — the + * sensor setters reach the active instance through a singleton + * pointer cached at @ref ov2640_device_register time. + */ +typedef struct { + ov2640_t sensor; /* sensor vtable and status */ + bus_adapter_t *data_bus; /* data bus backend (DVP by default) */ + ov2640_bus_snapshot_t bus_snapshot; /* bus-agnostic runtime state (mode, buffers) */ + struct rt_semaphore frame_sem; /* posted by frame callback, consumed by rt_device_read */ + ov2640_stream_state_t stream; /* streaming state (inactive for single-shot capture) */ + struct rt_mutex sccb_lock; /* serializes SCCB transactions for sensor setters */ + rt_bool_t sccb_lock_initialized; /* RT_TRUE once @ref sccb_lock has been initialized */ + rt_uint8_t current_bank; /* cached BANK_SEL value; ov2640_bank_t cast at use site */ +} ov2640_device_t; + +/* + ******************************************************************************* + * Control commands (passed as `cmd` to rt_device_control) + ******************************************************************************* + * + * Usage: + * rt_device_control(camera, OV2640_CMD_*, (void *)(uintptr_t)value); + */ + +/* --- Image format & resolution ------------------------------------------ */ +#define OV2640_CMD_SET_PIXFORMAT 0x01 /* pixformat_t */ +#define OV2640_CMD_SET_FRAMESIZE 0x02 /* framesize_t */ + +/* --- Image quality ------------------------------------------------------- */ +#define OV2640_CMD_SET_BRIGHTNESS 0x03 /* int -2…+2 */ +#define OV2640_CMD_SET_CONTRAST 0x04 /* int -2…+2 */ +#define OV2640_CMD_SET_SATURATION 0x05 /* int -2…+2 */ +#define OV2640_CMD_SET_QUALITY 0x06 /* int 0…63 */ +#define OV2640_CMD_SET_SHARPNESS 0x1D /* int -2…+2 (no-op on OV2640) */ +#define OV2640_CMD_SET_DENOISE 0x1E /* int level (no-op on OV2640) */ +#define OV2640_CMD_SET_GAINCEILING 0x1C /* gainceiling_t */ + +/* --- White balance ------------------------------------------------------- */ +#define OV2640_CMD_SET_WHITEBAL 0x0A /* int 0=off 1=on */ +#define OV2640_CMD_SET_AWB_GAIN 0x0E /* int 0=off 1=on */ +#define OV2640_CMD_SET_WB_MODE 0x15 /* int 0=auto … 4=home */ + +/* --- Exposure & gain ----------------------------------------------------- */ +#define OV2640_CMD_SET_GAIN_CTRL 0x0B /* int 0=off 1=on */ +#define OV2640_CMD_SET_EXPOSURE_CTRL 0x0C /* int 0=off 1=on */ +#define OV2640_CMD_SET_AEC2 0x0D /* int 0=off 1=on */ +#define OV2640_CMD_SET_AGC_GAIN 0x0F /* int 0…30 */ +#define OV2640_CMD_SET_AEC_VALUE 0x13 /* int 0…1200 */ +#define OV2640_CMD_SET_AE_LEVEL 0x16 /* int -2…+2 */ + +/* --- Image processing ---------------------------------------------------- */ +#define OV2640_CMD_SET_HMIRROR 0x07 /* int 0=off 1=on */ +#define OV2640_CMD_SET_VFLIP 0x08 /* int 0=off 1=on */ +#define OV2640_CMD_SET_COLORBAR 0x09 /* int 0=off 1=on */ +#define OV2640_CMD_SET_SPECIAL_EFFECT 0x14 /* int 0=normal … 6 */ +#define OV2640_CMD_SET_DCW 0x17 /* int 0=off 1=on */ +#define OV2640_CMD_SET_BPC 0x18 /* int 0=off 1=on */ +#define OV2640_CMD_SET_WPC 0x19 /* int 0=off 1=on */ +#define OV2640_CMD_SET_RAW_GMA 0x1A /* int 0=off 1=on */ +#define OV2640_CMD_SET_LENC 0x1B /* int 0=off 1=on */ + +/* --- Data bus / DMA buffer ----------------------------------------------- */ +#define OV2640_CMD_SET_FRAME_BUFFER 0x1F /* void* frame buffer pointer */ +#define OV2640_CMD_SET_FRAME_BUFFER_SIZE 0x20 /* uint32_t buffer size bytes */ +#define OV2640_CMD_SET_PINGPONG_SIZE 0x21 /* uint32_t ping-pong size bytes */ + +/* --- Capture control ----------------------------------------------------- */ +#define OV2640_CMD_START_CAPTURE 0x10 /* void* buffer (NULL = keep current) */ +#define OV2640_CMD_STOP_CAPTURE 0x11 /* no argument */ +#define OV2640_CMD_GET_FRAME_SIZE 0x12 /* uint32_t* out: captured bytes */ +#define OV2640_CMD_START_STREAM 0x22 /* no argument */ +#define OV2640_CMD_STOP_STREAM 0x23 /* no argument */ + +/* + ******************************************************************************* + * Public API + ******************************************************************************* + */ + +/** + * @brief Register the OV2640 RT-Thread device under @p name. + * + * Creates and registers a device named @p name (or @ref OV2640_DEVICE_NAME + * if @p name is @c NULL). The default-name variant + * @ref ov2640_device_register_default is automatically invoked by + * @c INIT_DEVICE_EXPORT. + * + * @param name is the device name to register under; @c NULL to use the + * default @ref OV2640_DEVICE_NAME. + * + * @return Return RT_EOK on success; negative error code on failure. + * + * @note The driver is single-instance — calling this more than once + * (even with different @p name values) will overwrite the + * singleton pointer used by the SCCB setters. + */ +int ov2640_device_register(const char *name); + +/** + * @brief Register the OV2640 device using @ref OV2640_DEVICE_NAME. + * + * Convenience wrapper installed by @c INIT_DEVICE_EXPORT; equivalent to + * @c ov2640_device_register(NULL). + */ +int ov2640_device_register_default(void); + +/** + * @brief Unregister the OV2640 RT-Thread device. + * + * Closes the data bus, stops hardware and removes the device from + * RT-Thread's device tree. + * + * @return Return RT_EOK on success; negative error code on failure. + */ +int ov2640_device_unregister(void); + +/** + * @brief Set the receive-indication callback for asynchronous frame notification. + * + * The callback is invoked each time a frame is ready, from ISR context. + * It should only perform minimal work such as posting a semaphore. + * + * @param dev is the RT-Thread device handle ("ov2640"). + * @param rx_ind is the callback function; pass NULL to clear. + * + * @return Return RT_EOK on success. + */ +rt_err_t ov2640_set_rx_indicate(rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev, rt_size_t size)); + +/** + * @brief Get the OV2640 RT-Thread device ops table. + * + * Returns the static `camera_device_ops_t` that wraps the OV2640 device + * interface. Used by `camera_handle` to reach the device without coupling + * to RT-Thread device symbols directly. + * + * @return Return a pointer to the static ops table. + */ +const camera_device_ops_t *ov2640_get_device_ops(void); + +#endif /* OV2640_H_ */ \ No newline at end of file diff --git a/camera_framework/camera/driver/ov2640/ov2640_regs.h b/camera_framework/camera/driver/ov2640/ov2640_regs.h new file mode 100644 index 0000000..4ed3a3c --- /dev/null +++ b/camera_framework/camera/driver/ov2640/ov2640_regs.h @@ -0,0 +1,267 @@ +/* + * This file is part of the OpenMV project. + * Copyright (c) 2013/2014 Ibrahim Abdelkader + * This work is licensed under the MIT license, see the file LICENSE for details. + * + * OV2640 register definitions. + */ +#ifndef __REG_REGS_H__ +#define __REG_REGS_H__ +/* DSP register bank FF=0x00*/ +#define R_BYPASS 0x05 +#define QS 0x44 +#define CTRLI 0x50 +#define HSIZE 0x51 +#define VSIZE 0x52 +#define XOFFL 0x53 +#define YOFFL 0x54 +#define VHYX 0x55 +#define DPRP 0x56 +#define TEST 0x57 +#define ZMOW 0x5A +#define ZMOH 0x5B +#define ZMHH 0x5C +#define BPADDR 0x7C +#define BPDATA 0x7D +#define CTRL2 0x86 +#define CTRL3 0x87 +#define SIZEL 0x8C +#define HSIZE8 0xC0 +#define VSIZE8 0xC1 +#define CTRL0 0xC2 +#define CTRL1 0xC3 +#define R_DVP_SP 0xD3 +#define IMAGE_MODE 0xDA +#define OV2640_REG_RESET 0xE0 +#define MS_SP 0xF0 +#define SS_ID 0xF7 +#define SS_CTRL 0xF7 +#define MC_BIST 0xF9 +#define MC_AL 0xFA +#define MC_AH 0xFB +#define MC_D 0xFC +#define P_CMD 0xFD +#define P_STATUS 0xFE +#define BANK_SEL 0xFF + +#define CTRLI_LP_DP 0x80 +#define CTRLI_ROUND 0x40 + +#define CTRL0_AEC_EN 0x80 +#define CTRL0_AEC_SEL 0x40 +#define CTRL0_STAT_SEL 0x20 +#define CTRL0_VFIRST 0x10 +#define CTRL0_YUV422 0x08 +#define CTRL0_YUV_EN 0x04 +#define CTRL0_RGB_EN 0x02 +#define CTRL0_RAW_EN 0x01 + +#define CTRL2_DCW_EN 0x20 +#define CTRL2_SDE_EN 0x10 +#define CTRL2_UV_ADJ_EN 0x08 +#define CTRL2_UV_AVG_EN 0x04 +#define CTRL2_CMX_EN 0x01 + +#define CTRL3_BPC_EN 0x80 +#define CTRL3_WPC_EN 0x40 + +#define R_DVP_SP_AUTO_MODE 0x80 + +#define R_BYPASS_DSP_EN 0x00 +#define R_BYPASS_DSP_BYPAS 0x01 + +#define IMAGE_MODE_Y8_DVP_EN 0x40 +#define IMAGE_MODE_JPEG_EN 0x10 +#define IMAGE_MODE_YUV422 0x00 +#define IMAGE_MODE_RAW10 0x04 +#define IMAGE_MODE_RGB565 0x08 +#define IMAGE_MODE_HREF_VSYNC 0x02 +#define IMAGE_MODE_LBYTE_FIRST 0x01 + +#define RESET_MICROC 0x40 +#define RESET_SCCB 0x20 +#define RESET_JPEG 0x10 +#define RESET_DVP 0x04 +#define RESET_IPU 0x02 +#define RESET_CIF 0x01 + +#define MC_BIST_RESET 0x80 +#define MC_BIST_BOOT_ROM_SEL 0x40 +#define MC_BIST_12KB_SEL 0x20 +#define MC_BIST_12KB_MASK 0x30 +#define MC_BIST_512KB_SEL 0x08 +#define MC_BIST_512KB_MASK 0x0C +#define MC_BIST_BUSY_BIT_R 0x02 +#define MC_BIST_MC_RES_ONE_SH_W 0x02 +#define MC_BIST_LAUNCH 0x01 + + +typedef enum { + BANK_DSP, BANK_SENSOR, BANK_MAX +} ov2640_bank_t; + +/* Sensor register bank FF=0x01*/ +#define GAIN 0x00 +#define COM1 0x03 +#define REG04 0x04 +#define REG08 0x08 +#define COM2 0x09 +#define REG_PID 0x0A +#define REG_VER 0x0B +#define COM3 0x0C +#define COM4 0x0D +#define AEC 0x10 +#define CLKRC 0x11 +#define COM7 0x12 +#define COM8 0x13 +#define COM9 0x14 /* AGC gain ceiling */ +#define COM10 0x15 +#define HSTART 0x17 +#define HSTOP 0x18 +#define VSTART 0x19 +#define VSTOP 0x1A +#define REG_MIDH 0x1C +#define REG_MIDL 0x1D +#define AEW 0x24 +#define AEB 0x25 +#define VV 0x26 +#define REG2A 0x2A +#define FRARL 0x2B +#define ADDVSL 0x2D +#define ADDVSH 0x2E +#define YAVG 0x2F +#define HSDY 0x30 +#define HEDY 0x31 +#define OV2640_REG32 0x32 +#define ARCOM2 0x34 +#define REG45 0x45 +#define FLL 0x46 +#define FLH 0x47 +#define COM19 0x48 +#define ZOOMS 0x49 +#define COM22 0x4B +#define COM25 0x4E +#define BD50 0x4F +#define BD60 0x50 +#define REG5D 0x5D +#define REG5E 0x5E +#define REG5F 0x5F +#define REG60 0x60 +#define HISTO_LOW 0x61 +#define HISTO_HIGH 0x62 + +#define REG04_DEFAULT 0x28 +#define REG04_HFLIP_IMG 0x80 +#define REG04_VFLIP_IMG 0x40 +#define REG04_VREF_EN 0x10 +#define REG04_HREF_EN 0x08 +#define REG04_SET(x) (REG04_DEFAULT|x) + +#define COM2_STDBY 0x10 +#define COM2_OUT_DRIVE_1x 0x00 +#define COM2_OUT_DRIVE_2x 0x01 +#define COM2_OUT_DRIVE_3x 0x02 +#define COM2_OUT_DRIVE_4x 0x03 + +#define COM3_DEFAULT 0x38 +#define COM3_BAND_50Hz 0x04 +#define COM3_BAND_60Hz 0x00 +#define COM3_BAND_AUTO 0x02 +#define COM3_BAND_SET(x) (COM3_DEFAULT|x) + +#define COM7_SRST 0x80 +#define COM7_RES_UXGA 0x00 /* UXGA */ +#define COM7_RES_SVGA 0x40 /* SVGA */ +#define COM7_RES_CIF 0x20 /* CIF */ +#define COM7_ZOOM_EN 0x04 /* Enable Zoom */ +#define COM7_COLOR_BAR 0x02 /* Enable Color Bar Test */ + +#define COM8_DEFAULT 0xC0 +#define COM8_BNDF_EN 0x20 /* Enable Banding filter */ +#define COM8_AGC_EN 0x04 /* AGC Auto/Manual control selection */ +#define COM8_AEC_EN 0x01 /* Auto/Manual Exposure control */ +#define COM8_SET(x) (COM8_DEFAULT|x) + +#define COM9_DEFAULT 0x08 +#define COM9_AGC_GAIN_2x 0x00 /* AGC: 2x */ +#define COM9_AGC_GAIN_4x 0x01 /* AGC: 4x */ +#define COM9_AGC_GAIN_8x 0x02 /* AGC: 8x */ +#define COM9_AGC_GAIN_16x 0x03 /* AGC: 16x */ +#define COM9_AGC_GAIN_32x 0x04 /* AGC: 32x */ +#define COM9_AGC_GAIN_64x 0x05 /* AGC: 64x */ +#define COM9_AGC_GAIN_128x 0x06 /* AGC: 128x */ +#define COM9_AGC_SET(x) (COM9_DEFAULT|(x<<5)) + +#define COM10_HREF_EN 0x80 /* HSYNC changes to HREF */ +#define COM10_HSYNC_EN 0x40 /* HREF changes to HSYNC */ +#define COM10_PCLK_FREE 0x20 /* PCLK output option: free running PCLK */ +#define COM10_PCLK_EDGE 0x10 /* Data is updated at the rising edge of PCLK */ +#define COM10_HREF_NEG 0x08 /* HREF negative */ +#define COM10_VSYNC_NEG 0x02 /* VSYNC negative */ +#define COM10_HSYNC_NEG 0x01 /* HSYNC negative */ + +#define CTRL1_AWB 0x08 /* Enable AWB */ + +#define VV_AGC_TH_SET(h,l) ((h<<4)|(l&0x0F)) + +#define REG32_UXGA 0x36 +#define REG32_SVGA 0x09 +#define REG32_CIF 0x89 + +#define CLKRC_2X 0x80 +#define CLKRC_2X_UXGA (0x01 | CLKRC_2X) +#define CLKRC_2X_SVGA CLKRC_2X +#define CLKRC_2X_CIF CLKRC_2X + +/* Register bit masks and offsets for status reading */ +/* REG45 - AEC[15:10] */ +#define REG45_AEC_MASK 0x3F +#define REG45_AEC_OFFSET 0 + +/* REG04 - AEC[1:0] */ +#define REG04_AEC_MASK 0x03 +#define REG04_AEC_OFFSET 0 +#define REG04_HMIRROR_MASK 0x80 +#define REG04_HMIRROR_OFFSET 7 +#define REG04_VFLIP_MASK 0x40 +#define REG04_VFLIP_OFFSET 6 + +/* COM9 - Gain ceiling */ +#define COM9_GAINCEILING_MASK 0xE0 +#define COM9_GAINCEILING_OFFSET 5 + +/* COM8 - Auto/Manual control */ +#define COM8_AEC_MASK 0x01 +#define COM8_AEC_OFFSET 0 +#define COM8_AGC_MASK 0x04 +#define COM8_AGC_OFFSET 2 + +/* CTRL1 - AWB and other controls */ +#define CTRL1_AWB_MASK 0x08 +#define CTRL1_AWB_OFFSET 3 +#define CTRL1_AWB_GAIN_MASK 0x04 +#define CTRL1_AWB_GAIN_OFFSET 2 +#define CTRL1_RAW_GMA_MASK 0x20 +#define CTRL1_RAW_GMA_OFFSET 5 +#define CTRL1_LENC_MASK 0x02 +#define CTRL1_LENC_OFFSET 1 + +/* CTRL0 - AEC2 */ +#define CTRL0_AEC2_MASK 0x40 +#define CTRL0_AEC2_OFFSET 6 + +/* CTRL3 - BPC and WPC */ +#define CTRL3_BPC_MASK 0x80 +#define CTRL3_BPC_OFFSET 7 +#define CTRL3_WPC_MASK 0x40 +#define CTRL3_WPC_OFFSET 6 + +/* CTRL2 - DCW */ +#define CTRL2_DCW_MASK 0x20 +#define CTRL2_DCW_OFFSET 5 + +/* COM7 - Color bar test */ +#define COM7_COLORBAR_MASK 0x02 +#define COM7_COLORBAR_OFFSET 1 + +#endif //__REG_REGS_H__ diff --git a/camera_framework/camera/driver/ov2640/ov2640_settings.h b/camera_framework/camera/driver/ov2640/ov2640_settings.h new file mode 100644 index 0000000..e9016f4 --- /dev/null +++ b/camera_framework/camera/driver/ov2640/ov2640_settings.h @@ -0,0 +1,441 @@ +// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#ifndef _OV2640_SETTINGS_H_ +#define _OV2640_SETTINGS_H_ + +#include +#include "ov2640_regs.h" + +typedef enum +{ + OV2640_MODE_UXGA, + OV2640_MODE_SVGA, + OV2640_MODE_CIF, + OV2640_MODE_MAX +} ov2640_sensor_mode_t; +typedef struct +{ + union + { + struct + { + uint8_t pclk_div : 7; + uint8_t pclk_auto : 1; + }; + uint8_t pclk; + }; + union + { + struct + { + uint8_t clk_div : 6; + uint8_t reserved : 1; + uint8_t clk_2x : 1; + }; + uint8_t clk; + }; +} ov2640_clk_t; + +typedef struct +{ + uint16_t offset_x; + uint16_t offset_y; + uint16_t max_x; + uint16_t max_y; +} ov2640_ratio_settings_t; +static const ov2640_ratio_settings_t ratio_table[] = { + {0, 0, 1600, 1200}, + {8, 72, 1584, 1056}, + {0, 100, 1600, 1000}, + {0, 120, 1600, 960}, + {0, 150, 1600, 900}, + {2, 258, 1596, 684}, + {50, 0, 1500, 1200}, + {200, 0, 1200, 1200}, + {462, 0, 676, 1200}}; +static const uint8_t ov2640_settings_cif[][2] = { + {0xFF, BANK_DSP}, + {0x2c, 0xff}, + {0x2e, 0xdf}, + {0xFF, BANK_SENSOR}, + {0x3c, 0x32}, + {0x11, 0x01}, + {0x09, 0x00}, + {0x04, 0x28}, + {0x13, 0xC0 | 0x20 | 0x04 | 0x01}, + {0x14, (0x08 | (0x02 << 5))}, + {0x2c, 0x0c}, + {0x33, 0x78}, + {0x3a, 0x33}, + {0x3b, 0xfB}, + {0x3e, 0x00}, + {0x43, 0x11}, + {0x16, 0x10}, + {0x39, 0x92}, + {0x35, 0xda}, + {0x22, 0x1a}, + {0x37, 0xc3}, + {0x23, 0x00}, + {0x34, 0xc0}, + {0x06, 0x88}, + {0x07, 0xc0}, + {0x0D, 0x87}, + {0x0e, 0x41}, + {0x4c, 0x00}, + {0x4a, 0x81}, + {0x21, 0x99}, + {0x24, 0x40}, + {0x25, 0x38}, + {0x26, ((8 << 4) | (2 & 0x0F))}, + {0x5c, 0x00}, + {0x63, 0x00}, + {0x61, 0x70}, + {0x62, 0x80}, + {0x7c, 0x05}, + {0x20, 0x80}, + {0x28, 0x30}, + {0x6c, 0x00}, + {0x6d, 0x80}, + {0x6e, 0x00}, + {0x70, 0x02}, + {0x71, 0x94}, + {0x73, 0xc1}, + {0x3d, 0x34}, + {0x5a, 0x57}, + {0x4F, 0xbb}, + {0x50, 0x9c}, + {0x12, 0x20}, + {0x17, 0x11}, + {0x18, 0x43}, + {0x19, 0x00}, + {0x1A, 0x25}, + {0x32, 0x89}, + {0x37, 0xc0}, + {0x4F, 0xca}, + {0x50, 0xa8}, + {0x6d, 0x00}, + {0x3d, 0x38}, + {0xFF, BANK_DSP}, + {0xe5, 0x7f}, + {0xF9, 0x80 | 0x40}, + {0x41, 0x24}, + {0xE0, 0x10 | 0x04}, + {0x76, 0xff}, + {0x33, 0xa0}, + {0x42, 0x20}, + {0x43, 0x18}, + {0x4c, 0x00}, + {0x87, 0x40 | 0x10}, + {0x88, 0x3f}, + {0xd7, 0x03}, + {0xd9, 0x10}, + {0xD3, 0x80 | 0x02}, + {0xc8, 0x08}, + {0xc9, 0x80}, + {0x7C, 0x00}, + {0x7D, 0x00}, + {0x7C, 0x03}, + {0x7D, 0x48}, + {0x7D, 0x48}, + {0x7C, 0x08}, + {0x7D, 0x20}, + {0x7D, 0x10}, + {0x7D, 0x0e}, + {0x90, 0x00}, + {0x91, 0x0e}, + {0x91, 0x1a}, + {0x91, 0x31}, + {0x91, 0x5a}, + {0x91, 0x69}, + {0x91, 0x75}, + {0x91, 0x7e}, + {0x91, 0x88}, + {0x91, 0x8f}, + {0x91, 0x96}, + {0x91, 0xa3}, + {0x91, 0xaf}, + {0x91, 0xc4}, + {0x91, 0xd7}, + {0x91, 0xe8}, + {0x91, 0x20}, + {0x92, 0x00}, + {0x93, 0x06}, + {0x93, 0xe3}, + {0x93, 0x05}, + {0x93, 0x05}, + {0x93, 0x00}, + {0x93, 0x04}, + {0x93, 0x00}, + {0x93, 0x00}, + {0x93, 0x00}, + {0x93, 0x00}, + {0x93, 0x00}, + {0x93, 0x00}, + {0x93, 0x00}, + {0x96, 0x00}, + {0x97, 0x08}, + {0x97, 0x19}, + {0x97, 0x02}, + {0x97, 0x0c}, + {0x97, 0x24}, + {0x97, 0x30}, + {0x97, 0x28}, + {0x97, 0x26}, + {0x97, 0x02}, + {0x97, 0x98}, + {0x97, 0x80}, + {0x97, 0x00}, + {0x97, 0x00}, + {0xa4, 0x00}, + {0xa8, 0x00}, + {0xc5, 0x11}, + {0xc6, 0x51}, + {0xbf, 0x80}, + {0xc7, 0x10}, + {0xb6, 0x66}, + {0xb8, 0xA5}, + {0xb7, 0x64}, + {0xb9, 0x7C}, + {0xb3, 0xaf}, + {0xb4, 0x97}, + {0xb5, 0xFF}, + {0xb0, 0xC5}, + {0xb1, 0x94}, + {0xb2, 0x0f}, + {0xc4, 0x5c}, + {0xC3, 0xfd}, + {0x7f, 0x00}, + {0xe5, 0x1f}, + {0xe1, 0x67}, + {0xdd, 0x7f}, + {0xDA, 0x00}, + {0xE0, 0x00}, + {0x05, 0x00}, + {0, 0}}; +static const uint8_t ov2640_settings_to_cif[][2] = { + {0xFF, BANK_SENSOR}, + {0x12, 0x20}, + {0x03, 0x0A}, + {0x32, 0x89}, + {0x17, 0x11}, + {0x18, 0x43}, + {0x19, 0x00}, + {0x1A, 0x25}, + {0x4F, 0xca}, + {0x50, 0xa8}, + {0x5a, 0x23}, + {0x6d, 0x00}, + {0x3d, 0x38}, + {0x39, 0x92}, + {0x35, 0xda}, + {0x22, 0x1a}, + {0x37, 0xc3}, + {0x23, 0x00}, + {0x34, 0xc0}, + {0x06, 0x88}, + {0x07, 0xc0}, + {0x0D, 0x87}, + {0x0e, 0x41}, + {0x4c, 0x00}, + {0xFF, BANK_DSP}, + {0xE0, 0x04}, + {0xC0, 0x32}, + {0xC1, 0x25}, + {0x8C, 0x00}, + {0x51, 0x64}, + {0x52, 0x4a}, + {0x53, 0x00}, + {0x54, 0x00}, + {0x55, 0x00}, + {0x57, 0x00}, + {0x86, 0x20 | 0x1D}, + {0x50, 0x80 | 0x00}, + {0, 0}}; +static const uint8_t ov2640_settings_to_svga[][2] = { + {0xFF, BANK_SENSOR}, + {0x12, 0x40}, + {0x03, 0x0A}, + {0x32, 0x09}, + {0x17, 0x11}, + {0x18, 0x43}, + {0x19, 0x00}, + {0x1A, 0x4b}, + {0x37, 0xc0}, + {0x4F, 0xca}, + {0x50, 0xa8}, + {0x5a, 0x23}, + {0x6d, 0x00}, + {0x3d, 0x38}, + {0x39, 0x92}, + {0x35, 0xda}, + {0x22, 0x1a}, + {0x37, 0xc3}, + {0x23, 0x00}, + {0x34, 0xc0}, + {0x06, 0x88}, + {0x07, 0xc0}, + {0x0D, 0x87}, + {0x0e, 0x41}, + {0x42, 0x03}, + {0x4c, 0x00}, + {0xFF, BANK_DSP}, + {0xE0, 0x04}, + {0xC0, 0x64}, + {0xC1, 0x4B}, + {0x8C, 0x00}, + {0x51, 0xC8}, + {0x52, 0x96}, + {0x53, 0x00}, + {0x54, 0x00}, + {0x55, 0x00}, + {0x57, 0x00}, + {0x86, 0x20 | 0x1D}, + {0x50, 0x80 | 0x00}, + {0, 0}}; +static const uint8_t ov2640_settings_to_uxga[][2] = { + {0xFF, BANK_SENSOR}, + {0x12, 0x00}, + {0x03, 0x0F}, + {0x32, 0x36}, + {0x17, 0x11}, + {0x18, 0x75}, + {0x19, 0x01}, + {0x1A, 0x97}, + {0x3d, 0x34}, + {0x4F, 0xbb}, + {0x50, 0x9c}, + {0x5a, 0x57}, + {0x6d, 0x80}, + {0x39, 0x82}, + {0x23, 0x00}, + {0x07, 0xc0}, + {0x4c, 0x00}, + {0x35, 0x88}, + {0x22, 0x0a}, + {0x37, 0x40}, + {0x34, 0xa0}, + {0x06, 0x02}, + {0x0D, 0xb7}, + {0x0e, 0x01}, + {0x42, 0x83}, + {0xFF, BANK_DSP}, + {0xE0, 0x04}, + {0xC0, 0xc8}, + {0xC1, 0x96}, + {0x8C, 0x00}, + {0x51, 0x90}, + {0x52, 0x2c}, + {0x53, 0x00}, + {0x54, 0x00}, + {0x55, 0x88}, + {0x57, 0x00}, + {0x86, 0x20 | 0x1d}, + {0x50, 0x00}, + {0, 0}}; +static const uint8_t ov2640_settings_jpeg3[][2] = { + {0xFF, BANK_DSP}, + {0xE0, 0x10 | 0x04}, + {0xDA, 0x10 | 0x02}, + {0xD7, 0x03}, + {0xE1, 0x77}, + {0xE5, 0x1F}, + {0xD9, 0x10}, + {0xDF, 0x80}, + {0x33, 0x80}, + {0x3C, 0x10}, + {0xEB, 0x30}, + {0xDD, 0x7F}, + {0xE0, 0x00}, + {0, 0}}; +static const uint8_t ov2640_settings_yuv422[][2] = { + {0xFF, BANK_DSP}, + {0xE0, 0x04}, + {0xDA, 0x00}, + {0xD7, 0x01}, + {0xE1, 0x67}, + {0xE0, 0x00}, + {0, 0}, +}; +static const uint8_t ov2640_settings_rgb565[][2] = { + {0xFF, BANK_DSP}, + {0xE0, 0x04}, + {0xDA, 0x09}, + {0xD7, 0x03}, + {0xE1, 0x77}, + {0xE0, 0x00}, + {0, 0}, +}; +static const uint8_t ov2640_settings_raw8[][2] = { + {0xFF, BANK_DSP}, + {0xE0, 0x04}, /* RESET - hold DVP */ + {0xDA, 0x04}, /* IMAGE_MODE = RAW10 (upper 8 bits via 8-bit DVP) */ + {0xD7, 0x01}, /* R_DVP_SP */ + {0xE1, 0x67}, + {0xE0, 0x00}, /* RESET - latch settings */ + {0, 0}, +}; +static const uint8_t brightness_regs[(5) + 1][5] = { + {0x7C, 0x7D, 0x7C, 0x7D, 0x7D}, + {0x00, 0x04, 0x09, 0x00, 0x00}, + {0x00, 0x04, 0x09, 0x10, 0x00}, + {0x00, 0x04, 0x09, 0x20, 0x00}, + {0x00, 0x04, 0x09, 0x30, 0x00}, + {0x00, 0x04, 0x09, 0x40, 0x00}, +}; +static const uint8_t contrast_regs[(5) + 1][7] = { + {0x7C, 0x7D, 0x7C, 0x7D, 0x7D, 0x7D, 0x7D}, + {0x00, 0x04, 0x07, 0x20, 0x18, 0x34, 0x06}, + {0x00, 0x04, 0x07, 0x20, 0x1c, 0x2a, 0x06}, + {0x00, 0x04, 0x07, 0x20, 0x20, 0x20, 0x06}, + {0x00, 0x04, 0x07, 0x20, 0x24, 0x16, 0x06}, + {0x00, 0x04, 0x07, 0x20, 0x28, 0x0c, 0x06}, +}; +static const uint8_t saturation_regs[(5) + 1][5] = { + {0x7C, 0x7D, 0x7C, 0x7D, 0x7D}, + {0x00, 0x02, 0x03, 0x28, 0x28}, + {0x00, 0x02, 0x03, 0x38, 0x38}, + {0x00, 0x02, 0x03, 0x48, 0x48}, + {0x00, 0x02, 0x03, 0x58, 0x58}, + {0x00, 0x02, 0x03, 0x68, 0x68}, +}; +static const uint8_t special_effects_regs[(7) + 1][5] = { + {0x7C, 0x7D, 0x7C, 0x7D, 0x7D}, + {0x00, 0X00, 0x05, 0X80, 0X80}, + {0x00, 0X40, 0x05, 0X80, 0X80}, + {0x00, 0X18, 0x05, 0X80, 0X80}, + {0x00, 0X18, 0x05, 0X40, 0XC0}, + {0x00, 0X18, 0x05, 0X40, 0X40}, + {0x00, 0X18, 0x05, 0XA0, 0X40}, + {0x00, 0X18, 0x05, 0X40, 0XA6}, +}; +static const uint8_t wb_modes_regs[(4) + 1][3] = { + {0XCC, 0XCD, 0XCE}, + {0x5E, 0X41, 0x54}, + {0x65, 0X41, 0x4F}, + {0x52, 0X41, 0x66}, + {0x42, 0X3F, 0x71}, +}; +static const uint8_t ae_levels_regs[(5) + 1][3] = { + {0x24, 0x25, 0x26}, + {0x20, 0X18, 0x60}, + {0x34, 0X1C, 0x00}, + {0x3E, 0X38, 0x81}, + {0x48, 0X40, 0x81}, + {0x58, 0X50, 0x92}, +}; +static const uint8_t agc_gain_tbl[31] = { + 0x00, 0x10, 0x18, 0x30, 0x34, 0x38, 0x3C, 0x70, 0x72, 0x74, 0x76, 0x78, 0x7A, 0x7C, 0x7E, 0xF0, + 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF}; + +#endif /* _OV2640_SETTINGS_H_ */ diff --git a/camera_framework/camera/handle/camera_handle.c b/camera_framework/camera/handle/camera_handle.c new file mode 100644 index 0000000..f50bff4 --- /dev/null +++ b/camera_framework/camera/handle/camera_handle.c @@ -0,0 +1,597 @@ +/****************************************************************************** + * Copyright (C) 2026 SiFli, Inc.(Gmbh) or its affiliates. + * + * All Rights Reserved. + * + * @file camera_handle.c + * + * @par dependencies + * - camera_handle.h : public API and all type definitions + * - rthw.h : rt_hw_interrupt_disable/enable for ISR-safe queue ops + * - rtdbg.h : LOG_W / LOG_E macros (pulled in via DBG_TAG block) + * + * @author SiFli 思澈科技 + * + * @brief Camera handle layer — sits between application code and the + * underlying RT-Thread sensor device driver (e.g. ov2640). + * + * This module provides two capture modes: + * + * - **Single-shot** (camera_capture_single): + * The caller supplies a destination buffer; the function blocks via + * rt_device_read until one frame is available or the driver's internal + * timeout fires, then returns the actual byte count in request->frame_size. + * + * - **Streaming** (camera_start_stream / camera_get_stream_frame / + * camera_stop_stream): + * Double-buffered DMA streaming with a two-slot FIFO ready queue. + * Frames are pushed into the queue by camera_stream_frame_ready_callback, + * which runs in ISR context (via the bus-adapter callback chain) each time + * a DMA transfer completes. The application thread dequeues frames one at + * a time via camera_get_stream_frame, which blocks on a semaphore until a + * frame is ready or the given timeout expires. When the consumer is too + * slow and the two-slot queue is full, the oldest frame is silently + * overwritten and dropped_count is incremented. + * + * @par Processing flow (typical single-sensor use) + * + * 1. camera_handler_instance_init() — zero-initialise instance + set + * JPEG/VGA/quality-10 defaults. + * 2. camera_init() — rt_device_find + rt_device_open. + * 3. camera_change_settings() — push pixformat / framesize / quality + * then wait 500 ms for AEC/AWB settle. + * 4a. camera_capture_single() — single blocking frame grab, OR + * 4b. camera_start_stream() — arm DMA streaming, + * camera_get_stream_frame() — dequeue frames (blocking), + * camera_stop_stream() — stop DMA + drain queue. + * 5. camera_deinit() — close RT-Thread device + detach + * frame semaphore. + * + * @version V1.0 2026-4-3 + * + * @note 1 tab == 4 spaces! + * + *****************************************************************************/ +#include "camera_handle.h" +#include "rthw.h" + +#define DBG_TAG "camera.handle" +#define DBG_LVL DBG_INFO +#include + +/** + * @brief Map an RT-Thread error code to the equivalent @c camera_handle_status_t. + * + * Only the subset of RT-Thread codes that can realistically be returned by the + * APIs used in this file are mapped explicitly; every other code falls back to + * the generic @c CAMERA_ERROR. + */ +static camera_handle_status_t camera_status_from_rt_err(rt_err_t err) +{ + switch (err) + { + case RT_EOK: + return CAMERA_OK; + case -RT_ETIMEOUT: + return CAMERA_ERRORTIMEOUT; + case -RT_ENOMEM: + return CAMERA_ERRORNOMEMORY; + case -RT_EINVAL: + return CAMERA_ERRORPARAMETER; + default: + return CAMERA_ERROR; + } +} + +/** + * @brief Reset the stream frame queue to an empty, zero-drop state. + * + * Clears @c head, @c tail, @c count, and @c dropped_count under an interrupt + * lock so the operation is safe whether called from thread or ISR context. + * Must be called before re-arming the stream (e.g. inside camera_start_stream) + * and after stopping it. + */ +static void camera_stream_reset_queue(camera_handler_instance_t *instance) +{ + rt_base_t level = rt_hw_interrupt_disable(); + instance->stream.head = 0; + instance->stream.tail = 0; + instance->stream.count = 0; + instance->stream.dropped_count = 0; + rt_hw_interrupt_enable(level); +} + +/** + * @brief ISR-context frame-ready callback for streaming mode. + * + * Registered with the driver via @c camera_stream_start_args_t::frame_callback + * and called (through the bus-adapter callback chain) each time a DMA transfer + * completes. The function inserts the new frame into the two-slot ready + * queue under interrupt lock: + * + * - If the queue has room (@c count < 2) the frame is enqueued and the + * semaphore is released so a waiting @ref camera_get_stream_frame returns. + * - If the queue is full the oldest slot is overwritten (tail advanced), + * @c stream.dropped_count is incremented, and **no** extra semaphore token + * is released (the existing token already covers the new frame). A + * rate-limited @c LOG_W is emitted on the first drop and every 32nd + * drop thereafter to surface chronic consumer-side slowness. + */ +static void camera_stream_frame_ready_callback(void *context, + const camera_stream_frame_t *frame) +{ + camera_handler_instance_t *instance = (camera_handler_instance_t *)context; + rt_base_t level; + + if (instance == RT_NULL || frame == RT_NULL || !instance->stream.is_active) + { + return; + } + + level = rt_hw_interrupt_disable(); + rt_bool_t dropped = RT_FALSE; + if (instance->stream.count == 2) + { + instance->stream.tail = (instance->stream.tail + 1) % 2; + instance->stream.count--; + instance->stream.dropped_count++; + dropped = RT_TRUE; + } + rt_uint32_t dropped_total = instance->stream.dropped_count; + + instance->stream.ready_frames[instance->stream.head] = *frame; + instance->stream.head = (instance->stream.head + 1) % 2; + instance->stream.count++; + rt_hw_interrupt_enable(level); + + /* Rate-limited warning: log on first drop and then every 32 drops to + * avoid flooding while still surfacing chronic consumer slowness. */ + if (dropped && ((dropped_total == 1) || ((dropped_total & 0x1FU) == 0U))) + { + LOG_W("camera: stream queue full, dropped frames=%u", (unsigned int)dropped_total); + } + + /* + * Only release the semaphore when no frame was dropped. + * When a drop occurs the oldest frame's semaphore token is re-used for + * the new frame; releasing again would make sem_val exceed count and + * cause camera_get_stream_frame() to return CAMERA_ERRORRESOURCE after + * a successful rt_sem_take(). + */ + if (instance->stream.sem_initialized && !dropped) + { + rt_sem_release(&instance->stream.frame_sem); + } +} + +/** + * @brief Initialize a camera handler instance with optional caller-supplied config. + * + * Zeroes the entire instance, then copies the device name and ops pointer from + * @p input_arg (if provided). If @p input_arg is @c NULL or its + * @c device_name field is @c NULL the device name falls back to + * @ref CAMERA_DEFAULT_DEVICE_NAME so the caller can skip the argument when + * using a single-sensor board. The active config is pre-loaded with + * JPEG/VGA/quality-10 defaults; the stream queue is reset to empty. + * + * @note This function does NOT open the RT-Thread device; call + * @ref camera_init afterwards. + */ +camera_handle_status_t camera_handler_instance_init(camera_handler_instance_t *instance, + camera_handler_all_input_arg_t *input_arg) +{ + const camera_device_ops_t *device_ops = RT_NULL; + + if (instance == NULL) + return CAMERA_ERRORPARAMETER; + + rt_memset(instance, 0, sizeof(*instance)); + + if (input_arg != RT_NULL) + { + if (input_arg->device_name != RT_NULL) + { + instance->device_name = input_arg->device_name; + } + if (input_arg->device_ops != RT_NULL) + { + device_ops = input_arg->device_ops; + } + } + + if (instance->device_name == RT_NULL) + { + instance->device_name = CAMERA_DEFAULT_DEVICE_NAME; + } + + instance->device_ops = device_ops; + + instance->active_config.pixformat = PIXFORMAT_JPEG; + instance->active_config.framesize = FRAMESIZE_VGA; + instance->active_config.quality = 10; + instance->stream.sem_initialized = RT_FALSE; + instance->stream.is_active = RT_FALSE; + camera_stream_reset_queue(instance); + + return CAMERA_OK; +} + +/** + * @brief Open the RT-Thread camera device and mark the instance as ready. + * + * Locates the device by name via @c rt_device_find, opens it with + * @c RT_DEVICE_OFLAG_RDWR, and sets @c instance->is_open. Idempotent: if + * the device is already open the function returns @c CAMERA_OK immediately. + * The @c device_ops pointer must be set before calling this function. + */ +camera_handle_status_t camera_init(camera_handler_instance_t *instance) +{ + rt_err_t result; + + if (instance == RT_NULL) + { + return CAMERA_ERRORPARAMETER; + } + + if (instance->is_open) + { + return CAMERA_OK; + } + + instance->camera_device = rt_device_find(instance->device_name); + if (instance->camera_device == RT_NULL) + { + return CAMERA_ERRORRESOURCE; + } + + if (instance->device_ops == RT_NULL) + { + return CAMERA_ERRORRESOURCE; + } + + result = rt_device_open(instance->camera_device, RT_DEVICE_OFLAG_RDWR); + if (result != RT_EOK) + { + instance->camera_device = RT_NULL; + return camera_status_from_rt_err(result); + } + + instance->is_open = RT_TRUE; + return CAMERA_OK; +} + +/** + * @brief Close the camera device and release all associated resources. + * + * If streaming is still active, @ref camera_stop_stream is called first. + * Then the RT-Thread device is closed and the frame semaphore is detached + * (if it was initialised). After this call the instance can be re-used by + * calling @ref camera_handler_instance_init followed by @ref camera_init. + */ +camera_handle_status_t camera_deinit(camera_handler_instance_t *instance) +{ + if (instance == RT_NULL) + { + return CAMERA_ERRORPARAMETER; + } + + if (instance->stream.is_active) + { + camera_stop_stream(instance); + } + + if (instance->is_open && instance->camera_device != RT_NULL) + { + rt_device_close(instance->camera_device); + } + + instance->camera_device = RT_NULL; + instance->is_open = RT_FALSE; + + if (instance->stream.sem_initialized) + { + rt_sem_detach(&instance->stream.frame_sem); + instance->stream.sem_initialized = RT_FALSE; + } + + return CAMERA_OK; +} + +/** + * @brief Push a new capture configuration to the sensor driver. + * + * Issues three @c rt_device_control calls in sequence — set_pixformat, + * set_framesize, set_quality — using the integer command IDs stored in + * @c instance->device_ops->command_set. On full success the accepted + * configuration is saved to @c instance->active_config and a 500 ms + * blocking delay is inserted to let the sensor AEC / AWB loops converge. + * + * @note May not be called while streaming is active (@c stream.is_active); + * returns @c CAMERA_ERRORRESOURCE in that case. + */ +camera_handle_status_t camera_change_settings(camera_handler_instance_t *instance, + const camera_capture_config_t *config) +{ + rt_err_t result; + const camera_device_ops_t *device_ops; + + if (instance == RT_NULL || config == RT_NULL) + { + return CAMERA_ERRORPARAMETER; + } + + if (!instance->is_open || instance->camera_device == RT_NULL) + { + return CAMERA_ERRORRESOURCE; + } + + if (instance->stream.is_active) + { + return CAMERA_ERRORRESOURCE; + } + + device_ops = instance->device_ops; + if (device_ops == RT_NULL) + { + return CAMERA_ERRORRESOURCE; + } + + result = rt_device_control(instance->camera_device, + device_ops->command_set.set_pixformat, + (void *)(rt_ubase_t)config->pixformat); + if (result != RT_EOK) + { + return camera_status_from_rt_err(result); + } + + result = rt_device_control(instance->camera_device, + device_ops->command_set.set_framesize, + (void *)(rt_ubase_t)config->framesize); + if (result != RT_EOK) + { + return camera_status_from_rt_err(result); + } + + result = rt_device_control(instance->camera_device, + device_ops->command_set.set_quality, + (void *)(rt_ubase_t)config->quality); + if (result != RT_EOK) + { + return camera_status_from_rt_err(result); + } + + instance->active_config = *config; + /* + * Wait for sensor AEC / AWB loops to converge after a format / framesize + * change. 500 ms is empirically enough for the OV2640 to produce stable + * frames; lower values give visibly under-exposed first frames. + */ + rt_thread_mdelay(500); + return CAMERA_OK; +} + +/** + * @brief Perform a single blocking frame capture via @c rt_device_read. + * + * Delegates to @c rt_device_read with the caller-provided buffer; the driver + * blocks internally until one frame is available or its internal timeout + * fires. On success @p request->frame_size is set to the byte count returned + * by the driver (e.g. compressed JPEG payload length). + * + * @note Not usable while streaming is active (@c stream.is_active). + */ +camera_handle_status_t camera_capture_single(camera_handler_instance_t *instance, + camera_capture_request_t *request) +{ + rt_size_t read_size; + + if (instance == RT_NULL || request == RT_NULL) + { + return CAMERA_ERRORPARAMETER; + } + + if (!instance->is_open || instance->camera_device == RT_NULL) + { + return CAMERA_ERRORRESOURCE; + } + + if (request->buffer == RT_NULL || request->buffer_size == 0) + { + return CAMERA_ERRORPARAMETER; + } + + if (instance->stream.is_active) + { + return CAMERA_ERRORRESOURCE; + } + + if (instance->device_ops == RT_NULL) + { + return CAMERA_ERRORRESOURCE; + } + + read_size = rt_device_read(instance->camera_device, 0, + request->buffer, + request->buffer_size); + if (read_size == 0) + { + request->frame_size = 0; + return CAMERA_ERRORTIMEOUT; + } + + request->frame_size = read_size; + return CAMERA_OK; +} + +/** + * @brief Arm the driver for continuous double-buffered DMA streaming. + * + * Lazily initialises the frame semaphore on the first call, drains any stale + * tokens, resets the ready queue, and fills a @c camera_stream_start_args_t + * (wiring in @ref camera_stream_frame_ready_callback as the internal frame + * sink) before forwarding it to the driver via @c rt_device_control. On + * driver error the stream is rolled back to inactive and the queue is reset. + * + * @note The two buffers in @p config must remain valid (DMA-accessible) + * until @ref camera_stop_stream returns. + */ +camera_handle_status_t camera_start_stream(camera_handler_instance_t *instance, + const camera_stream_config_t *config) +{ + camera_stream_start_args_t args; + rt_err_t result; + + if (instance == RT_NULL || config == RT_NULL) + { + return CAMERA_ERRORPARAMETER; + } + + if (!instance->is_open || instance->camera_device == RT_NULL) + { + return CAMERA_ERRORRESOURCE; + } + + if (config->buffers[0] == RT_NULL || + config->buffers[1] == RT_NULL || + config->buffer_size == 0) + { + return CAMERA_ERRORPARAMETER; + } + + if (instance->device_ops == RT_NULL || + instance->device_ops->command_set.start_stream == 0) + { + return CAMERA_ERRORRESOURCE; + } + + if (instance->stream.is_active) + { + return CAMERA_ERRORRESOURCE; + } + + if (!instance->stream.sem_initialized) + { + result = rt_sem_init(&instance->stream.frame_sem, "cam_strm", 0, RT_IPC_FLAG_FIFO); + if (result != RT_EOK) + { + return camera_status_from_rt_err(result); + } + instance->stream.sem_initialized = RT_TRUE; + } + + while (rt_sem_trytake(&instance->stream.frame_sem) == RT_EOK) + { + } + + camera_stream_reset_queue(instance); + instance->stream.is_active = RT_TRUE; + + args.buffers[0] = config->buffers[0]; + args.buffers[1] = config->buffers[1]; + args.buffer_size = config->buffer_size; + args.frame_callback = camera_stream_frame_ready_callback; + args.callback_context = instance; + + result = rt_device_control(instance->camera_device, + instance->device_ops->command_set.start_stream, + &args); + if (result != RT_EOK) + { + instance->stream.is_active = RT_FALSE; + camera_stream_reset_queue(instance); + return camera_status_from_rt_err(result); + } + + return CAMERA_OK; +} + +/** + * @brief Dequeue one ready frame from the streaming ring buffer. + * + * Takes the frame semaphore with the given @p timeout. On success the tail + * entry is copied to @p frame under interrupt lock and the queue count is + * decremented. The spurious-empty guard (@c count == 0 after a successful + * semaphore take) returns @c CAMERA_ERRORRESOURCE — this should not happen + * in normal operation but defends against queue corruption. + */ +camera_handle_status_t camera_get_stream_frame(camera_handler_instance_t *instance, + camera_stream_frame_t *frame, + rt_int32_t timeout) +{ + rt_base_t level; + rt_err_t result; + + if (instance == RT_NULL || frame == RT_NULL) + { + return CAMERA_ERRORPARAMETER; + } + + if (!instance->stream.is_active || !instance->stream.sem_initialized) + { + return CAMERA_ERRORRESOURCE; + } + + result = rt_sem_take(&instance->stream.frame_sem, timeout); + if (result != RT_EOK) + { + return camera_status_from_rt_err(result); + } + + level = rt_hw_interrupt_disable(); + if (instance->stream.count == 0) + { + rt_hw_interrupt_enable(level); + return CAMERA_ERRORRESOURCE; + } + + *frame = instance->stream.ready_frames[instance->stream.tail]; + instance->stream.tail = (instance->stream.tail + 1) % 2; + instance->stream.count--; + rt_hw_interrupt_enable(level); + + return CAMERA_OK; +} + +/** + * @brief Command the driver to stop DMA streaming and clean up queue state. + * + * Issues the stop_stream control command, marks @c stream.is_active as + * @c RT_FALSE, and resets the ready queue (including @c dropped_count). + * The driver error code is checked after the bookkeeping so that local state + * is always cleaned up even if the driver returns an error. Idempotent: + * returns @c CAMERA_OK immediately if the stream is already inactive. + */ +camera_handle_status_t camera_stop_stream(camera_handler_instance_t *instance) +{ + rt_err_t result; + + if (instance == RT_NULL) + { + return CAMERA_ERRORPARAMETER; + } + + if (!instance->stream.is_active) + { + return CAMERA_OK; + } + + if (instance->device_ops == RT_NULL || + instance->device_ops->command_set.stop_stream == 0) + { + return CAMERA_ERRORRESOURCE; + } + + result = rt_device_control(instance->camera_device, + instance->device_ops->command_set.stop_stream, + RT_NULL); + instance->stream.is_active = RT_FALSE; + camera_stream_reset_queue(instance); + + if (result != RT_EOK) + { + return camera_status_from_rt_err(result); + } + + return CAMERA_OK; +} + diff --git a/camera_framework/camera/handle/camera_handle.h b/camera_framework/camera/handle/camera_handle.h new file mode 100644 index 0000000..7f13d47 --- /dev/null +++ b/camera_framework/camera/handle/camera_handle.h @@ -0,0 +1,349 @@ +/****************************************************************************** + * Copyright (C) 2026 SiFli, Inc.(Gmbh) or its affiliates. + * + * All Rights Reserved. + * + * @file camera_handle.h + * + * @par dependencies + * - .h + * - stdbool.h + * - stdint.h + * + * @author SiFli 思澈科技 + * + * @brief Provide the HAL APIs of camera handler + * and corresponding operations. + * + * Processing flow: + * + * Call directly. + * + * @version V1.0 2026-4-3 + * + * @note 1 tab == 4 spaces! + * + *****************************************************************************/ + +#ifndef __CAMERA_HANDLE_H +#define __CAMERA_HANDLE_H + +//******************************** Includes *********************************// +#include +#include +//******************************** Includes *********************************// + +/** + * Default RT-Thread device name used when the caller does not provide one via + * `camera_handler_input_arg_t::device_name`. Keep in sync with the name passed + * to `rt_device_register()` in the underlying sensor driver. + */ +#define CAMERA_DEFAULT_DEVICE_NAME "ov2640" + +//******************************** Defines **********************************// +#ifdef __cplusplus +extern "C" { +#endif +//******************************** Defines **********************************// + +//******************************** Typedefs *********************************// + +typedef enum +{ + PIXFORMAT_RGB565, + PIXFORMAT_YUV422, + PIXFORMAT_JPEG, + PIXFORMAT_RAW8, + PIXFORMAT_INVALID +} pixformat_t; + +typedef enum +{ + FRAMESIZE_96X96, + FRAMESIZE_QQVGA, + FRAMESIZE_128X128, + FRAMESIZE_QCIF, + FRAMESIZE_HQVGA, + FRAMESIZE_240X240, + FRAMESIZE_QVGA, + FRAMESIZE_320X320, + FRAMESIZE_CIF, + FRAMESIZE_HVGA, + FRAMESIZE_VGA, + FRAMESIZE_SVGA, + FRAMESIZE_XGA, + FRAMESIZE_HD, + FRAMESIZE_SXGA, + FRAMESIZE_UXGA, + FRAMESIZE_INVALID +} framesize_t; + +typedef struct +{ + void *buffer; + rt_size_t buffer_size; + rt_size_t frame_size; + rt_uint32_t sequence; + rt_uint8_t buffer_index; +} camera_stream_frame_t; + +typedef void (*camera_stream_frame_callback_t)(void *context, + const camera_stream_frame_t *frame); + +/* + * Argument struct for the sensor's start-stream control command. This is the + * inter-layer contract between camera_handle.c and the underlying RT-Thread + * camera device driver (e.g. ov2640) — the handle layer fills it in and the + * driver consumes it inside its OV2640_CMD_START_STREAM handler. + */ +typedef struct +{ + void *buffers[2]; + rt_size_t buffer_size; + camera_stream_frame_callback_t frame_callback; + void *callback_context; +} camera_stream_start_args_t; + +typedef struct +{ + int set_pixformat; + int set_framesize; + int set_quality; + int start_stream; + int stop_stream; +} camera_device_command_set_t; + +/* + * Camera device ops structure. + * + * The handle layer talks to RT-Thread device API directly via + * rt_device_open/close/read/control; only the command_set indirection is + * needed so that the same handle code can drive sensors that use different + * integer command IDs. + */ +typedef struct +{ + camera_device_command_set_t command_set; +} camera_device_ops_t; + +/* Emulation of return case */ +typedef enum +{ + CAMERA_OK = 0, /* Operation completed successfully. */ + CAMERA_ERROR = 1, /* Run-time error without case matched*/ + CAMERA_ERRORTIMEOUT = 2, /* Operation failed with timeout */ + CAMERA_ERRORRESOURCE = 3, /* Resource not available. */ + CAMERA_ERRORPARAMETER = 4, /* Parameter error. */ + CAMERA_ERRORNOMEMORY = 5, /* Out of memory. */ + CAMERA_ERRORISR = 6, /* Not allowed in ISR context */ + CAMERA_RESERVED = 0x7FFFFFFF /* Reserved May check the caller */ +} camera_handle_status_t; + +typedef struct +{ + pixformat_t pixformat; + framesize_t framesize; + uint8_t quality; +} camera_capture_config_t; + +typedef struct +{ + void *buffer; + rt_size_t buffer_size; + rt_size_t frame_size; +} camera_capture_request_t; + +typedef struct +{ + void *buffers[2]; + rt_size_t buffer_size; +} camera_stream_config_t; + +typedef struct +{ + struct rt_semaphore frame_sem; + rt_bool_t sem_initialized; + rt_bool_t is_active; + camera_stream_frame_t ready_frames[2]; + rt_uint8_t head; + rt_uint8_t tail; + rt_uint8_t count; + /* Total frames overwritten because the ready queue was full when a new + * frame arrived. Useful for diagnosing "stuttering" in the application + * consumer; reset to 0 on each camera_start_stream(). */ + rt_uint32_t dropped_count; +} camera_stream_runtime_t; +//******************************** Typedefs *********************************// + + +//**************************** Interface Structs ****************************// + +typedef struct +{ + const char *device_name; + const camera_device_ops_t *device_ops; +} camera_handler_all_input_arg_t; + +typedef struct +{ + rt_device_t camera_device; + camera_capture_config_t active_config; + camera_stream_runtime_t stream; + const char *device_name; + const camera_device_ops_t *device_ops; + rt_bool_t is_open; +} camera_handler_instance_t; + +//**************************** Interface Structs ****************************// + + +//******************************** APIs *************************************// + +// /** +// * @brief Camera handler thread that processes camera events and frames. +// * +// * This thread processes camera-related events (capture requests, config +// * changes, notifications). It polls or waits on the internal event queue, +// * dispatches work to the underlying camera driver, and forwards completed +// * frames or status updates to the registered callbacks or higher layers. +// */ +// void camera_handler_thread(void *argument); + +/** + * @brief Initialize a camera handler instance. + * + * Prepare a camera handler instance for operation. This sets up the required + * interfaces (driver bindings, event queue, resources) and records initial + * state needed by the handler. + * + * @param handler_instance A pointer to the camera handler instance to be initialized. + * @param input_arg A pointer to the input arguments containing driver and + * platform-specific interfaces required by the handler. + * + * @return `CAMERA_OK` on success, otherwise an error code from `camera_handle_status_t`. + */ +camera_handle_status_t camera_handler_instance_init( + camera_handler_instance_t *instance, + camera_handler_all_input_arg_t *input_arg); + + +/** + * @brief Capture a single frame using the previously configured settings. + * + * Blocking single-shot capture: the caller provides the destination buffer + * via @p request, and on success @p request->frame_size is filled in with + * the byte count of the captured image (e.g. JPEG payload length). + * + * @param[in] instance The camera handler instance. + * @param[in,out] request Destination buffer + size; @c frame_size is filled + * in on success. + * + * @return `CAMERA_OK` on success, otherwise a `camera_handle_status_t` error. + */ +camera_handle_status_t camera_capture_single(camera_handler_instance_t *instance, + camera_capture_request_t *request); + +/** + * @brief Start continuous double-buffered frame streaming. + * + * Allocates the internal frame semaphore (first call only), resets the ready + * queue, and commands the driver to begin DMA streaming using the two buffers + * supplied in @p config. Frames are delivered asynchronously to the internal + * ready queue; call @ref camera_get_stream_frame to consume them. + * + * @param[in] instance The camera handler instance (must be open). + * @param[in] config Two DMA-accessible frame buffers and their common size. + * + * @return @c CAMERA_OK on success; @c CAMERA_ERRORRESOURCE if the stream is + * already active or required ops are missing; otherwise another + * @c camera_handle_status_t error. + */ +camera_handle_status_t camera_start_stream(camera_handler_instance_t *instance, + const camera_stream_config_t *config); + +/** + * @brief Retrieve the next available frame from the streaming queue. + * + * Blocks until a frame is ready or @p timeout elapses. The returned @p frame + * is a shallow copy of the queue entry; the embedded buffer pointer is valid + * until the driver reuses that DMA buffer for the next capture, so the caller + * should process or copy the data before the next two frames arrive. + * + * @param[in] instance The camera handler instance (stream must be active). + * @param[out] frame Filled with buffer pointer, sizes, and sequence number + * on success. + * @param[in] timeout Maximum wait in RT-Thread ticks; use + * @c RT_WAITING_FOREVER to block indefinitely. + * + * @return @c CAMERA_OK on success; @c CAMERA_ERRORTIMEOUT if no frame arrived + * within @p timeout; @c CAMERA_ERRORRESOURCE if the stream is not + * active or the queue is unexpectedly empty. + */ +camera_handle_status_t camera_get_stream_frame(camera_handler_instance_t *instance, + camera_stream_frame_t *frame, + rt_int32_t timeout); + +/** + * @brief Stop continuous frame streaming. + * + * Commands the driver to cease DMA captures, marks the stream as inactive, + * and resets the internal ready queue (including the @c dropped_count + * diagnostic counter). Safe to call when streaming is not active — returns + * @c CAMERA_OK immediately in that case. + * + * @param[in] instance The camera handler instance. + * + * @return @c CAMERA_OK on success; @c CAMERA_ERRORRESOURCE if the required + * stop-stream op is not registered. + */ +camera_handle_status_t camera_stop_stream(camera_handler_instance_t *instance); + +/** + * @brief Apply camera configuration changes. + * + * Push the requested pixel format / frame size / quality down to the sensor + * driver, then wait for the auto-exposure / auto-white-balance loops to + * converge before returning. The successfully-applied configuration is + * stored in @p instance->active_config. + * + * @param[in] instance The camera handler instance that owns the device session. + * @param[in] config New capture configuration (pixformat / framesize / quality). + * + * @return `CAMERA_OK` if settings applied successfully, otherwise an error code + * from `camera_handle_status_t`. + */ +camera_handle_status_t camera_change_settings(camera_handler_instance_t *instance, + const camera_capture_config_t *config); + +/** + * @brief Deinitializes the camera. + * + * This function deinitializes the camera, releasing any resources + * that were allocated during initialization. + * + * @return An 8-bit signed integer representing the status of the deinitialization. + * - 0: Deinitialization successful. + * - Non-zero: Deinitialization failed. + */ +camera_handle_status_t camera_deinit(camera_handler_instance_t *instance); + +/** + * @brief Initializes the camera. + * + * This function initializes the camera, setting up any necessary + * resources and configurations. + * + * @return An 8-bit signed integer representing the status of the initialization. + * - 0: Initialization successful. + * - Non-zero: Initialization failed. + */ +camera_handle_status_t camera_init(camera_handler_instance_t *instance); + +//******************************** APIs *************************************// + +#ifdef __cplusplus +} +#endif + + +#endif // __CAMERA_HANDLE_H \ No newline at end of file diff --git a/camera_framework/conanfile.py b/camera_framework/conanfile.py new file mode 100644 index 0000000..dde08f0 --- /dev/null +++ b/camera_framework/conanfile.py @@ -0,0 +1,26 @@ +from conan import ConanFile + +class Ov2640Recipe(ConanFile): + name = "ov2640" + version = "0.0.2" + + license = "Apache-2.0" + user = "sifli" + author = "sifli" + url = "https://github.com/OpenSiFli/SiFli-SDK-Peripherals" + homepage = "https://packages.sifli.com/zh/packages/sifli" + description = "SiFli SDK Peripherals - OV2640 Camera Module" + topics = ("camera", "ov2640", "peripherals") + + support_sdk_version = "2.4" + + # Sources are located in the same place as this recipe, copy them to the recipe + exports_sources = "*" + + python_requires = "sf-pkg-base/[^1.0]@sifli" + python_requires_extend = "sf-pkg-base.SourceOnlyBase" + + def requirements(self): + # add your package dependencies here, for example: + # self.requires("fmt/8.1.1") + pass diff --git a/camera_framework/conaninfo.txt b/camera_framework/conaninfo.txt new file mode 100644 index 0000000..8317876 --- /dev/null +++ b/camera_framework/conaninfo.txt @@ -0,0 +1,2 @@ +[python_requires] +sf-pkg-base/1.0.Z@sifli diff --git a/camera_framework/conanmanifest.txt b/camera_framework/conanmanifest.txt new file mode 100644 index 0000000..4749205 --- /dev/null +++ b/camera_framework/conanmanifest.txt @@ -0,0 +1,40 @@ +1770801959 +Kconfig: 6c40905a027ea4921a847ec521e25f06 +README.md: a7e04c48546457d8fa4b6fc25a07da09 +README_EN.md: e61aacd991cb802de69250462aa77b4a +SConscript: 4178ab065f73337788b0d6c0698ad09b +conanfile.py: dd33b318aa82be4f1862802ab9b0d4e3 +conaninfo.txt: 80cae1e2744f91cad799860f75dee70a +examples/take_photo/README.md: 53954aeede71b02d43051753ccf15b6b +examples/take_photo/README_EN.md: 7b60c6244e00a378536f0836c6943105 +examples/take_photo/project/Kconfig: 85c3e73e3f8aa703903e04b5e857f734 +examples/take_photo/project/Kconfig.proj: c605b7b381d7fd8b57ff1bf3851786af +examples/take_photo/project/Kconfig.tmp: f7b27bcb89a4d295d7772ef7cd056593 +examples/take_photo/project/SConscript: 67dd6a193c0b2e9955c0e66e4bf80cd6 +examples/take_photo/project/SConstruct: 0415cfd087f0dc8f0bfa39cdf4c08aa5 +examples/take_photo/project/proj.conf: a602623d072bb73e4786f8867a499614 +examples/take_photo/project/rtconfig.py: 4420ba1de7de45a8ce8f97b8dbfe5295 +examples/take_photo/project/rtconfig_project.h: 3e767b2ec2b8803fc950a7d26f2a0e32 +examples/take_photo/src/SConscript: eec4aac1113c72c80be7f58c8bfebb0d +examples/take_photo/src/main.c: b9502dc34d8901d2f993aa175984e5a1 +examples/take_photo_to_sdcard/README.md: f938fe76c01389ee5594982edf0fd0cf +examples/take_photo_to_sdcard/README_EN.md: 33a347ede12940ff7175c9cdcf1dc5d9 +examples/take_photo_to_sdcard/project/Kconfig: 85c3e73e3f8aa703903e04b5e857f734 +examples/take_photo_to_sdcard/project/Kconfig.proj: c605b7b381d7fd8b57ff1bf3851786af +examples/take_photo_to_sdcard/project/Kconfig.tmp: f7b27bcb89a4d295d7772ef7cd056593 +examples/take_photo_to_sdcard/project/SConscript: 67dd6a193c0b2e9955c0e66e4bf80cd6 +examples/take_photo_to_sdcard/project/SConstruct: 0415cfd087f0dc8f0bfa39cdf4c08aa5 +examples/take_photo_to_sdcard/project/proj.conf: a602623d072bb73e4786f8867a499614 +examples/take_photo_to_sdcard/project/rtconfig.py: 4420ba1de7de45a8ce8f97b8dbfe5295 +examples/take_photo_to_sdcard/project/rtconfig_project.h: 3e767b2ec2b8803fc950a7d26f2a0e32 +examples/take_photo_to_sdcard/src/SConscript: eec4aac1113c72c80be7f58c8bfebb0d +examples/take_photo_to_sdcard/src/main.c: 5bfe95b6d78ca2b9c9f82c7eee7625fa +include/dvp.h: 3be5e2c46b18fccb0965cecb5b9ab7a4 +include/ov2640.h: 4fa93eea906b5c190dba264c3148fb17 +include/ov2640_regs.h: fd662669d2a0e21f008d9bb1f498c331 +include/ov2640_settings.h: 68206a35c5342e516aaef66e2e160188 +include/sccb.h: 628d1afe00b04b8928f0817f53d985f0 +script/hex_to_jpg.py: 6e4ce699277f69d0171e7e8f4785c223 +src/dvp.c: e4b786fcd3a4158344c5ffe1cb69d1a2 +src/ov2640.c: 5a48700b8da33789eca9d23f685a5e1e +src/sccb.c: 4384033656d4ba27fb88a9f86fe2df89 diff --git a/camera_framework/examples/take_photo/README.md b/camera_framework/examples/take_photo/README.md new file mode 100644 index 0000000..baad1e7 --- /dev/null +++ b/camera_framework/examples/take_photo/README.md @@ -0,0 +1,60 @@ +# take_photo 示例说明 + +[中文](README.md) | [English](README_EN.md) + +## 示例简介 + +本示例演示如何通过 `ov2640` 设备采集 RGB565 原始图像帧,并将帧缓冲区地址打印到控制台,方便通过 SDK 工具将 PSRAM 中的数据导出到主机后查看。 + +该示例位于 [examples/take_photo](examples/take_photo)。 + +## 使用方法 + +在 MSH 控制台执行: + +``` +msh> take_photo +``` + +参数说明: +- **framesize**:分辨率,可选值:QQVGA / QCIF / QVGA / CIF / VGA / SVGA / XGA / HD / SXGA / UXGA +- **count**:采集帧数(>= 1) + +示例: + +``` +msh> take_photo QVGA 1 +``` + +## 输出示例 + +``` +RGB565 capture: 320x240, 153600 bytes/frame, buffer @ 0x20100000 +Frame 1 captured: 153600 bytes @ 0x20100000 (RGB565 320x240) +Export the buffer with the SDK script, e.g.: + sftool ... read_mem 0x20100000 153600 rgb565.bin +``` + +## 导出与查看 + +采集完成后,`take_photo` 会打印 PSRAM 帧缓冲区的起始地址(如 `0x20100000`)和字节数。 + +使用 `jlinkbin2bmp.py` 直接从目标板读取帧缓冲区并生成 BMP,无需先导出 bin 文件: + +工具目录:`C:\WORK\SiFli-SDK\main\tools\bin2bmp\` + +``` +python jlinkbin2bmp.py "" rgb565 +``` + +示例(SWD 12 MHz,QVGA 缓冲区地址 `0x20100000`): + +``` +python jlinkbin2bmp.py "-if SWD -speed 12000" rgb565 320 240 0x20100000 +``` + +## 注意事项 + +- 帧缓冲区分配在 PSRAM(通过 `psram_heap_malloc`),内部 SRAM 无法容纳大于 QVGA 的 RGB565 帧。 +- 多帧拍摄时复用同一缓冲区,仅最后一帧保留在 PSRAM 中。 +- 引脚复用(SCCB / DVP / XCLK)由 OV2640 驱动内部完成,无需在应用层调用 `HAL_PIN_Set()`。 diff --git a/camera_framework/examples/take_photo/README_EN.md b/camera_framework/examples/take_photo/README_EN.md new file mode 100644 index 0000000..cbbd229 --- /dev/null +++ b/camera_framework/examples/take_photo/README_EN.md @@ -0,0 +1,60 @@ +# take_photo Example + +[中文](README.md) | [English](README_EN.md) + +## Overview + +This example captures raw RGB565 frames from the `ov2640` device and prints the frame buffer address to the console. The captured pixels can then be exported from PSRAM using SDK tools and viewed as an image on the host. + +This example is located in [examples/take_photo](examples/take_photo). + +## Usage + +Run in MSH: + +``` +msh> take_photo +``` + +Parameters: +- **framesize**: resolution — one of QQVGA / QCIF / QVGA / CIF / VGA / SVGA / XGA / HD / SXGA / UXGA +- **count**: number of frames to capture (>= 1) + +Example: + +``` +msh> take_photo QVGA 1 +``` + +## Sample Output + +``` +RGB565 capture: 320x240, 153600 bytes/frame, buffer @ 0x20100000 +Frame 1 captured: 153600 bytes @ 0x20100000 (RGB565 320x240) +Export the buffer with the SDK script, e.g.: + sftool ... read_mem 0x20100000 153600 rgb565.bin +``` + +## Exporting and Viewing + +After capture, `take_photo` prints the PSRAM frame buffer start address (e.g. `0x20100000`) and byte count. + +Use `jlinkbin2bmp.py` to read the frame buffer directly from the live target and generate a BMP in one step — no intermediate file needed. + +Tools are located in: `C:\WORK\SiFli-SDK\main\tools\bin2bmp\` + +``` +python jlinkbin2bmp.py "" rgb565 +``` + +Example (SWD at 12 MHz, QVGA buffer at `0x20100000`): + +``` +python jlinkbin2bmp.py "-if SWD -speed 12000" rgb565 320 240 0x20100000 +``` + +## Notes + +- The frame buffer is allocated in PSRAM via `psram_heap_malloc`. Internal SRAM is not large enough to hold frames bigger than QVGA in RGB565. +- When capturing multiple frames, the same buffer is reused; only the last frame remains in PSRAM after the command finishes. +- Pin muxing for SCCB / DVP / XCLK is handled internally by the OV2640 driver — the application does not need to call `HAL_PIN_Set()`. diff --git a/camera_framework/examples/take_photo/project/Kconfig b/camera_framework/examples/take_photo/project/Kconfig new file mode 100644 index 0000000..6445854 --- /dev/null +++ b/camera_framework/examples/take_photo/project/Kconfig @@ -0,0 +1,3 @@ +#Kconfig root for APP. +source "$SIFLI_SDK/Kconfig.v2 +rsource "Kconfig.proj" diff --git a/camera_framework/examples/take_photo/project/Kconfig.proj b/camera_framework/examples/take_photo/project/Kconfig.proj new file mode 100644 index 0000000..067927d --- /dev/null +++ b/camera_framework/examples/take_photo/project/Kconfig.proj @@ -0,0 +1,7 @@ +#APP specific configuration. +config CUSTOM_MEM_MAP + bool + select custom_mem_map + default y if !SOC_SIMULATOR + +orsource "../ov2640/Kconfig" \ No newline at end of file diff --git a/camera_framework/examples/take_photo/project/SConscript b/camera_framework/examples/take_photo/project/SConscript new file mode 100644 index 0000000..2d0d0f4 --- /dev/null +++ b/camera_framework/examples/take_photo/project/SConscript @@ -0,0 +1,16 @@ +import os +from building import * + +cwd = GetCurrentDir() +objs = [] +list = os.listdir(cwd) + +# Add SDK +Import('SIFLI_SDK') +objs.extend(SConscript(os.path.join(SIFLI_SDK, 'SConscript'), variant_dir="sifli_sdk", duplicate=0)) + +# Add application source code +objs.extend(SConscript(cwd+'/../src/SConscript', variant_dir="src", duplicate=0)) +objs.extend(SConscript(cwd+'/../ov2640/SConscript', variant_dir="ov2640", duplicate=0)) + +Return('objs') diff --git a/camera_framework/examples/take_photo/project/SConstruct b/camera_framework/examples/take_photo/project/SConstruct new file mode 100644 index 0000000..60b518f --- /dev/null +++ b/camera_framework/examples/take_photo/project/SConstruct @@ -0,0 +1,38 @@ +import os +import rtconfig + +# Check SDK +SIFLI_SDK = os.getenv('SIFLI_SDK') +if not SIFLI_SDK: + print("Please run set_env.bat in root folder of SIFLI SDK to set environment.") + exit() +from building import * + +# Prepare environment. +PrepareEnv() + +################################## change rtconfig.xxx to customize build ######################################## + +# Add bootloader project +AddBootLoader(SIFLI_SDK,rtconfig.CHIP) + +# Set default compile options +SifliEnv() + +TARGET = rtconfig.OUTPUT_DIR + rtconfig.TARGET_NAME + '.' + rtconfig.TARGET_EXT +# Prepare building environment +objs = PrepareBuilding(None) +env = GetCurrentEnv() +# env.Append(CCFLAGS=['-Og']) + +# Add linker flags to display memory usage +env.Append(LINKFLAGS=['-Wl,--print-memory-usage']) + +# Build application. +DoBuilding(TARGET, objs) + +# Add flash table buld. +AddFTAB(SIFLI_SDK,rtconfig.CHIP) + +# Generate download .bat script +GenDownloadScript(env) diff --git a/camera_framework/examples/take_photo/project/proj.conf b/camera_framework/examples/take_photo/project/proj.conf new file mode 100644 index 0000000..176cbe8 --- /dev/null +++ b/camera_framework/examples/take_photo/project/proj.conf @@ -0,0 +1,8 @@ +# CONFIG_BSP_SPI1_TX_USING_DMA is not set +# CONFIG_BSP_SPI1_RX_USING_DMA is not set +CONFIG_PSRAM_CACHE_WB=y +CONFIG_RT_MAIN_THREAD_STACK_SIZE=4096 +CONFIG_RT_USING_DFS_ELMFAT=y +CONFIG_RT_USING_SPI_MSD=y +CONFIG_RT_USING_MEMHEAP=y +CONFIG_BSP_USING_FULL_ASSERT=y diff --git a/camera_framework/examples/take_photo/project/rtconfig.py b/camera_framework/examples/take_photo/project/rtconfig.py new file mode 100644 index 0000000..72efdf0 --- /dev/null +++ b/camera_framework/examples/take_photo/project/rtconfig.py @@ -0,0 +1,6 @@ + + + + + + diff --git a/camera_framework/examples/take_photo/project/rtconfig_project.h b/camera_framework/examples/take_photo/project/rtconfig_project.h new file mode 100644 index 0000000..d444515 --- /dev/null +++ b/camera_framework/examples/take_photo/project/rtconfig_project.h @@ -0,0 +1,21 @@ +#ifndef RTCONFIG_PROJECT_H__ +#define RTCONFIG_PROJECT_H__ + +#if defined(_MSC_VER) + #define RT_HEAP_SIZE (680000) + #define NORESOURCE //RT_VESRION in winuser.h + #define _CRT_ERRNO_DEFINED //errno macro redefinition + #define _INC_WTIME_INL//dfs_elm.c time.h conflicts with wtime.inl + #define _INC_TIME_INL //dfs_elm.c time.h conflicts with wtime.inl + + /* disable some warning in MSC */ + #pragma warning(disable:4273) /* to ignore: warning C4273: inconsistent dll linkage */ + #pragma warning(disable:4312) /* to ignore: warning C4312: 'type cast' : conversion from 'rt_uint32_t' to 'rt_uint32_t *' */ + #pragma warning(disable:4311) /* to ignore: warning C4311: 'type cast' : pointer truncation from 'short *__w64 ' to 'long' */ + #pragma warning(disable:4996) /* to ignore: warning C4996: The POSIX name for this item is deprecated. */ + #pragma warning(disable:4267) /* to ignore: warning C4267: conversion from 'size_t' to 'rt_size_t', possible loss of data */ + #pragma warning(disable:4244) /* to ignore: warning C4244: '=' : conversion from '__w64 int' to 'rt_size_t', possible loss of data */ + +#endif /* end of _MSC_VER */ + +#endif diff --git a/camera_framework/examples/take_photo/src/SConscript b/camera_framework/examples/take_photo/src/SConscript new file mode 100644 index 0000000..0e84297 --- /dev/null +++ b/camera_framework/examples/take_photo/src/SConscript @@ -0,0 +1,8 @@ +import os +from building import * + +# Add source code +src = Glob('*.c') +group = DefineGroup('Applications', src, depend = ['']) + +Return('group') diff --git a/camera_framework/examples/take_photo/src/main.c b/camera_framework/examples/take_photo/src/main.c new file mode 100644 index 0000000..bbf839b --- /dev/null +++ b/camera_framework/examples/take_photo/src/main.c @@ -0,0 +1,324 @@ +/****************************************************************************** + * @file main.c + * @brief OV2640 take_photo example - capture RGB565 frames via the camera + * handle high-level API. + * + * The example uses the camera_handle.h API to grab one or more raw RGB565 + * frames into a PSRAM buffer. It then prints the buffer address and size so + * the captured pixels can be exported from RAM with the SDK helper scripts + * (e.g. format_sram.py) and viewed as an image on the host. + * + * Call sequence: + * camera_handler_instance_init() - bind device name + device_ops + * camera_init() - open the RT-Thread device + * camera_change_settings() - configure RGB565 + framesize + * camera_capture_single() - blocking single-frame grab (looped) + * camera_deinit() - close the device + * + * Note: low-level pin muxing for SCCB / DVP / XCLK is already performed by + * the OV2640 driver itself, so this example does not call HAL_PIN_Set(). + *****************************************************************************/ + +#include "rtthread.h" +#include "bf0_hal.h" +#include "stdio.h" +#include "string.h" +#include +#include +#include "mem_section.h" +#include "ov2640.h" +#include "camera_handle.h" + +/* ------------------------------------------------------------------ * + * PSRAM heap - holds the RGB565 frame buffer (internal SRAM is too + * small for anything beyond QVGA). + * ------------------------------------------------------------------ */ +static uint8_t psram_heap_pool[4096 * 1024] L2_RET_BSS_SECT(psram_heap_pool); +static struct rt_memheap psram_memheap; + +/** + * @brief Initialize the PSRAM heap pool. + * + * @return Return 0 on success (fixed value). + */ +int psram_heap_init(void) +{ + rt_memheap_init(&psram_memheap, "psram_heap", (void *)psram_heap_pool, + sizeof(psram_heap_pool)); + return 0; +} + +/** + * @brief Allocate memory from the PSRAM heap. + * + * @param size is the number of bytes to allocate. + * + * @return Return a pointer on success, RT_NULL on failure. + */ +void *psram_heap_malloc(uint32_t size) +{ + return rt_memheap_alloc(&psram_memheap, size); +} + +/** + * @brief Release memory previously returned by psram_heap_malloc(). + * + * @param p is the pointer to free. + */ +void psram_heap_free(void *p) +{ + rt_memheap_free(p); +} + +/* ------------------------------------------------------------------ * + * Frame-size parsing and buffer-size computation + * ------------------------------------------------------------------ */ + +/** + * @brief Convert a frame-size name (e.g. "VGA") into the framesize_t enum. + * + * @param str is the frame-size string to look up. + * + * @return Return the matching framesize_t, or FRAMESIZE_INVALID if unknown. + */ +static framesize_t format_string_to_framesize(const char *str) +{ + if (strcmp(str, "QQVGA") == 0) return FRAMESIZE_QQVGA; + else if (strcmp(str, "QCIF") == 0) return FRAMESIZE_QCIF; + else if (strcmp(str, "QVGA") == 0) return FRAMESIZE_QVGA; + else if (strcmp(str, "CIF") == 0) return FRAMESIZE_CIF; + else if (strcmp(str, "VGA") == 0) return FRAMESIZE_VGA; + else if (strcmp(str, "SVGA") == 0) return FRAMESIZE_SVGA; + else if (strcmp(str, "XGA") == 0) return FRAMESIZE_XGA; + else if (strcmp(str, "HD") == 0) return FRAMESIZE_HD; + else if (strcmp(str, "SXGA") == 0) return FRAMESIZE_SXGA; + else if (strcmp(str, "UXGA") == 0) return FRAMESIZE_UXGA; + else return FRAMESIZE_INVALID; +} + +/** + * @brief Resolve a framesize_t into pixel width and height. + * + * @param size is the input framesize_t. + * @param width is the output pointer that receives the width in pixels. + * @param height is the output pointer that receives the height in pixels. + * + * @return Return RT_EOK on success, or -RT_EINVAL when @p size is unknown + * or one of the output pointers is RT_NULL. + */ +static int framesize_to_resolution(framesize_t size, uint16_t *width, uint16_t *height) +{ + if (width == RT_NULL || height == RT_NULL) + { + return -RT_EINVAL; + } + + switch (size) + { + case FRAMESIZE_QQVGA: *width = 160; *height = 120; break; + case FRAMESIZE_QCIF: *width = 176; *height = 144; break; + case FRAMESIZE_QVGA: *width = 320; *height = 240; break; + case FRAMESIZE_CIF: *width = 400; *height = 296; break; + case FRAMESIZE_VGA: *width = 640; *height = 480; break; + case FRAMESIZE_SVGA: *width = 800; *height = 600; break; + case FRAMESIZE_XGA: *width = 1024; *height = 768; break; + case FRAMESIZE_HD: *width = 1280; *height = 720; break; + case FRAMESIZE_SXGA: *width = 1280; *height = 1024; break; + case FRAMESIZE_UXGA: *width = 1600; *height = 1200; break; + default: + return -RT_EINVAL; + } + + return RT_EOK; +} + +/** + * @brief Compute the exact RGB565 frame size in bytes for a given resolution. + * + * RGB565 is two bytes per pixel, so the buffer size is simply + * width * height * 2. + * + * @param size is the target framesize_t. + * @param width is the output pointer that receives the resolved width. + * @param height is the output pointer that receives the resolved height. + * + * @return Return the buffer size in bytes, or 0 if @p size is unsupported. + */ +static rt_size_t calc_rgb565_buffer_size(framesize_t size, + uint16_t *width, uint16_t *height) +{ + if (framesize_to_resolution(size, width, height) != RT_EOK) + { + return 0; + } + return (rt_size_t)(*width) * (rt_size_t)(*height) * 2u; +} + +/* ------------------------------------------------------------------ * + * MSH command: take_photo + * ------------------------------------------------------------------ */ + +/** + * @brief MSH command: capture one or more RGB565 frames and print their + * PSRAM addresses so they can be exported with the SDK helper scripts. + * + * Usage: + * take_photo + * + * framesize : QQVGA / QCIF / QVGA / CIF / VGA / SVGA / XGA / HD / SXGA / UXGA + * count : number of frames to capture (>= 1) + * + * Example: + * take_photo QVGA 1 + * + * After the capture finishes, the command prints the buffer base address, + * the resolved width / height, and the byte count. The host can then dump + * that memory region (e.g. via sftool / J-Link savebin) and feed it into + * the SDK conversion script to render the raw RGB565 buffer as an image. + * + * @param argc is the argument count (3 including the command name). + * @param argv is the argument vector. + */ +void take_photo(int argc, char **argv) +{ + camera_handler_instance_t camera_instance; + camera_handler_all_input_arg_t input_arg; + camera_capture_config_t cfg; + camera_capture_request_t req; + camera_handle_status_t status; + uint8_t *buffer = RT_NULL; + uint16_t width = 0; + uint16_t height = 0; + rt_size_t buffer_size; + + if (argc != 3) + { + rt_kprintf("Usage: take_photo \n"); + rt_kprintf("Framesize options: QQVGA, QCIF, QVGA, CIF, VGA, SVGA, XGA, HD, SXGA, UXGA\n"); + rt_kprintf("count: number of RGB565 frames to capture (>=1)\n"); + rt_kprintf("Example: take_photo QVGA 1\n"); + return; + } + + framesize_t framesize = format_string_to_framesize(argv[1]); + if (framesize == FRAMESIZE_INVALID) + { + rt_kprintf("Unsupported framesize: %s\n", argv[1]); + return; + } + + int count = atoi(argv[2]); + if (count <= 0) + { + rt_kprintf("Count must be >= 1\n"); + return; + } + + buffer_size = calc_rgb565_buffer_size(framesize, &width, &height); + if (buffer_size == 0) + { + rt_kprintf("Failed to resolve framesize\n"); + return; + } + + /* 1) Prepare the camera handler instance. */ + input_arg.device_name = CAMERA_DEFAULT_DEVICE_NAME; /* "ov2640" */ + input_arg.device_ops = ov2640_get_device_ops(); + status = camera_handler_instance_init(&camera_instance, &input_arg); + if (status != CAMERA_OK) + { + rt_kprintf("Failed to initialize camera handler (%d)\n", status); + return; + } + + /* 2) Open the underlying RT-Thread camera device. */ + status = camera_init(&camera_instance); + if (status != CAMERA_OK) + { + rt_kprintf("Failed to open camera device (%d)\n", status); + return; + } + + /* 3) Push RGB565 + framesize configuration. The handle layer inserts + * a 500 ms AEC/AWB settle delay internally. The quality field is + * unused for RGB565 but must still hold a valid JPEG-range value; + * 10 is used as a harmless placeholder. */ + cfg.pixformat = PIXFORMAT_RGB565; + cfg.framesize = framesize; + cfg.quality = 10; + status = camera_change_settings(&camera_instance, &cfg); + if (status != CAMERA_OK) + { + rt_kprintf("Failed to configure camera (%d)\n", status); + goto close_camera; + } + + /* 4) Allocate one RGB565 frame buffer in PSRAM. */ + buffer = psram_heap_malloc(buffer_size); + if (buffer == RT_NULL) + { + rt_kprintf("Failed to allocate %u bytes for RGB565 capture!\n", + (unsigned int)buffer_size); + goto close_camera; + } + + rt_kprintf("RGB565 capture: %ux%u, %u bytes/frame, buffer @ %p\n", + (unsigned int)width, (unsigned int)height, + (unsigned int)buffer_size, buffer); + + /* 5) Grab `count` frames sequentially; the same buffer is reused each + * iteration so only the last frame survives in PSRAM. */ + for (int photo_idx = 0; photo_idx < count; photo_idx++) + { + req.buffer = buffer; + req.buffer_size = buffer_size; + req.frame_size = 0; + + status = camera_capture_single(&camera_instance, &req); + if (status != CAMERA_OK) + { + rt_kprintf("Capture failed or timed out (index=%d, status=%d)\n", + photo_idx, status); + continue; + } + + rt_kprintf("Frame %d captured: %u bytes @ %p (RGB565 %ux%u)\n", + photo_idx + 1, (unsigned int)req.frame_size, + buffer, (unsigned int)width, (unsigned int)height); + } + + rt_kprintf("Export the buffer with the SDK script, e.g.:\n" + " sftool ... read_mem %p %u rgb565.bin\n", + buffer, (unsigned int)buffer_size); + + psram_heap_free(buffer); + buffer = RT_NULL; + +close_camera: + camera_deinit(&camera_instance); + if (buffer != RT_NULL) + { + psram_heap_free(buffer); + } +} +MSH_CMD_EXPORT(take_photo, Capture RGB565 frame(s) using ov2640 camera); + +/** + * @brief Program entry point: initialize the PSRAM heap and idle, waiting + * for MSH commands. + * + * Capture is triggered through the `take_photo` command on the serial + * console. + * + * @return Return 0 (never actually returns; the main loop spins forever). + */ +int main(void) +{ + rt_kprintf("OV2640 Camera Take Photo Example (RGB565)\n"); + psram_heap_init(); + + while (1) + { + rt_thread_mdelay(1000); + } +} diff --git a/camera_framework/examples/take_photo_to_sdcard/README.md b/camera_framework/examples/take_photo_to_sdcard/README.md new file mode 100644 index 0000000..7aab6b3 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/README.md @@ -0,0 +1,80 @@ +# take_photo_to_sdcard 示例 + +[中文](README.md) | [English](README_EN.md) + +## 示例简介 + +本示例演示如何使用 OV2640 摄像头组件的 **handle 层 API**(`camera_handle.h`)采集 JPEG 图像,并将其写入挂载在 `/` 下的 SD 卡文件系统,默认保存到 `/photo` 目录。 + +调用流程与 [`take_photo`](../take_photo/README.md) 示例完全一致: + +1. `camera_handler_instance_init` +2. `camera_init` +3. `camera_change_settings`(`PIXFORMAT_JPEG` + framesize + quality) +4. 循环 `camera_capture_single`,每次成功后写入一个 `.jpg` 文件 +5. `camera_deinit` + +> SCCB / DVP / XCLK 等引脚的复用由 OV2640 驱动在初始化时内部完成,**应用层不需要也不应该调用 `HAL_PIN_Set()`**。 + +## 硬件要求 + +- OV2640 摄像头模组,按照 OV2640 组件 README 中的默认引脚连接 +- SPI / SDIO 接口的 SD 卡,并在 RT-Thread 中注册为 `sd0` 设备 +- 至少 4 MB PSRAM(示例从 PSRAM heap 中分配 JPEG 缓冲) + +## 使用方法 + +启动后会自动初始化 PSRAM heap 并挂载 SD 卡: + +``` +OV2640 Camera Take Photo to SD Card Example +mount fs on tf card to / success +``` + +挂载成功后在 MSH 控制台执行: + +``` +msh> take_photo +``` + +| 参数 | 取值 | 说明 | +|------|------|------| +| `framesize` | QQVGA / QCIF / QVGA / CIF / VGA / SVGA / XGA / HD / SXGA / UXGA | 分辨率 | +| `quality` | 0 ~ 63 | JPEG 质量,0 = 最佳,63 = 最大压缩 | +| `count` | ≥ 1 | 拍照张数 | + +示例: + +``` +msh> take_photo VGA 10 3 +JPEG capture: framesize=VGA, quality=10, buffer=307200 bytes @ 0x6XXXXXXX +Saved /photo/photo_001.jpg (28456 bytes) +Saved /photo/photo_002.jpg (28612 bytes) +Saved /photo/photo_003.jpg (28391 bytes) +``` + +## 输出文件 + +每次拍摄完成后会生成: + +``` +/photo/photo_001.jpg +/photo/photo_002.jpg +/photo/photo_003.jpg +... +``` + +把 SD 卡接到 PC 上即可直接打开 `.jpg` 文件验证。 + +## 说明事项 + +- JPEG 输出长度不固定,示例按 `width × height` 估算缓冲大小并限制在 `[64 KB, 2 MB]` 之间;UXGA 等高分辨率请预留充足的 PSRAM。 +- `quality` 越小图像越清晰,但文件越大;越大压缩越强,文件越小。 +- 第一次切换分辨率/质量后,`camera_change_settings` 内部已经插入约 500 ms 的 AEC/AWB 稳定延时,无需再手动等待。 +- 若提示 `sd card not found` 或 `mount fs ... fail`,请确认:SD 卡已格式化为 FAT、`sd0` 设备已注册、SPI / 时钟引脚连接正确。 + +## 相关文档 + +- 英文版:[README_EN.md](README_EN.md) +- 组件主文档:[../../README.md](../../README.md) +- 同款 RGB565 单帧示例:[`take_photo`](../take_photo/README.md) diff --git a/camera_framework/examples/take_photo_to_sdcard/README_EN.md b/camera_framework/examples/take_photo_to_sdcard/README_EN.md new file mode 100644 index 0000000..a469349 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/README_EN.md @@ -0,0 +1,80 @@ +# take_photo_to_sdcard Example + +[中文](README.md) | [English](README_EN.md) + +## Overview + +This example demonstrates how to use the OV2640 component's **handle-layer API** (`camera_handle.h`) to capture JPEG frames and write them to a SD card filesystem mounted at `/` (default output directory: `/photo`). + +The call sequence is identical to the [`take_photo`](../take_photo/README_EN.md) example: + +1. `camera_handler_instance_init` +2. `camera_init` +3. `camera_change_settings` (`PIXFORMAT_JPEG` + framesize + quality) +4. Loop `camera_capture_single`; on success, write one `.jpg` file +5. `camera_deinit` + +> Pin muxing for SCCB / DVP / XCLK is performed by the OV2640 driver during initialization. The application **does not** (and must not) call `HAL_PIN_Set()`. + +## Hardware Requirements + +- OV2640 camera module wired according to the default pins documented in the OV2640 component README +- A SPI / SDIO SD card registered as the `sd0` device in RT-Thread +- At least 4 MB PSRAM available (the example allocates the JPEG buffer from a PSRAM heap) + +## Usage + +After boot the example initializes the PSRAM heap and mounts the SD card automatically: + +``` +OV2640 Camera Take Photo to SD Card Example +mount fs on tf card to / success +``` + +Once mounted, run from the MSH console: + +``` +msh> take_photo +``` + +| Parameter | Value | Description | +|-----------|-------|-------------| +| `framesize` | QQVGA / QCIF / QVGA / CIF / VGA / SVGA / XGA / HD / SXGA / UXGA | Resolution | +| `quality` | 0 ~ 63 | JPEG quality (0 = best, 63 = most compressed) | +| `count` | ≥ 1 | Number of frames to capture | + +Example: + +``` +msh> take_photo VGA 10 3 +JPEG capture: framesize=VGA, quality=10, buffer=307200 bytes @ 0x6XXXXXXX +Saved /photo/photo_001.jpg (28456 bytes) +Saved /photo/photo_002.jpg (28612 bytes) +Saved /photo/photo_003.jpg (28391 bytes) +``` + +## Output Files + +Captured photos are saved as: + +``` +/photo/photo_001.jpg +/photo/photo_002.jpg +/photo/photo_003.jpg +... +``` + +Plug the SD card into a host PC and open the `.jpg` files directly to verify the result. + +## Notes + +- JPEG output size is variable; the example estimates the buffer as `width × height` and clamps it to `[64 KB, 2 MB]`. Allocate enough PSRAM for high-resolution modes such as UXGA. +- Lower `quality` produces a sharper image but a larger file; higher `quality` increases compression and shrinks the file. +- After switching format or resolution, `camera_change_settings` already inserts roughly 500 ms of AEC/AWB settle time internally - no extra delay is required from the caller. +- If you see `sd card not found` or `mount fs ... fail`: make sure the SD card is FAT-formatted, the `sd0` device is registered, and the SPI / clock pins are wired correctly. + +## Related Documents + +- Chinese version: [README.md](README.md) +- Component main documentation: [../../README_EN.md](../../README_EN.md) +- Sibling RGB565 single-shot example: [`take_photo`](../take_photo/README_EN.md) diff --git a/camera_framework/examples/take_photo_to_sdcard/project/Kconfig b/camera_framework/examples/take_photo_to_sdcard/project/Kconfig new file mode 100644 index 0000000..6445854 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/project/Kconfig @@ -0,0 +1,3 @@ +#Kconfig root for APP. +source "$SIFLI_SDK/Kconfig.v2 +rsource "Kconfig.proj" diff --git a/camera_framework/examples/take_photo_to_sdcard/project/Kconfig.proj b/camera_framework/examples/take_photo_to_sdcard/project/Kconfig.proj new file mode 100644 index 0000000..067927d --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/project/Kconfig.proj @@ -0,0 +1,7 @@ +#APP specific configuration. +config CUSTOM_MEM_MAP + bool + select custom_mem_map + default y if !SOC_SIMULATOR + +orsource "../ov2640/Kconfig" \ No newline at end of file diff --git a/camera_framework/examples/take_photo_to_sdcard/project/SConscript b/camera_framework/examples/take_photo_to_sdcard/project/SConscript new file mode 100644 index 0000000..2d0d0f4 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/project/SConscript @@ -0,0 +1,16 @@ +import os +from building import * + +cwd = GetCurrentDir() +objs = [] +list = os.listdir(cwd) + +# Add SDK +Import('SIFLI_SDK') +objs.extend(SConscript(os.path.join(SIFLI_SDK, 'SConscript'), variant_dir="sifli_sdk", duplicate=0)) + +# Add application source code +objs.extend(SConscript(cwd+'/../src/SConscript', variant_dir="src", duplicate=0)) +objs.extend(SConscript(cwd+'/../ov2640/SConscript', variant_dir="ov2640", duplicate=0)) + +Return('objs') diff --git a/camera_framework/examples/take_photo_to_sdcard/project/SConstruct b/camera_framework/examples/take_photo_to_sdcard/project/SConstruct new file mode 100644 index 0000000..60b518f --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/project/SConstruct @@ -0,0 +1,38 @@ +import os +import rtconfig + +# Check SDK +SIFLI_SDK = os.getenv('SIFLI_SDK') +if not SIFLI_SDK: + print("Please run set_env.bat in root folder of SIFLI SDK to set environment.") + exit() +from building import * + +# Prepare environment. +PrepareEnv() + +################################## change rtconfig.xxx to customize build ######################################## + +# Add bootloader project +AddBootLoader(SIFLI_SDK,rtconfig.CHIP) + +# Set default compile options +SifliEnv() + +TARGET = rtconfig.OUTPUT_DIR + rtconfig.TARGET_NAME + '.' + rtconfig.TARGET_EXT +# Prepare building environment +objs = PrepareBuilding(None) +env = GetCurrentEnv() +# env.Append(CCFLAGS=['-Og']) + +# Add linker flags to display memory usage +env.Append(LINKFLAGS=['-Wl,--print-memory-usage']) + +# Build application. +DoBuilding(TARGET, objs) + +# Add flash table buld. +AddFTAB(SIFLI_SDK,rtconfig.CHIP) + +# Generate download .bat script +GenDownloadScript(env) diff --git a/camera_framework/examples/take_photo_to_sdcard/project/proj.conf b/camera_framework/examples/take_photo_to_sdcard/project/proj.conf new file mode 100644 index 0000000..176cbe8 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/project/proj.conf @@ -0,0 +1,8 @@ +# CONFIG_BSP_SPI1_TX_USING_DMA is not set +# CONFIG_BSP_SPI1_RX_USING_DMA is not set +CONFIG_PSRAM_CACHE_WB=y +CONFIG_RT_MAIN_THREAD_STACK_SIZE=4096 +CONFIG_RT_USING_DFS_ELMFAT=y +CONFIG_RT_USING_SPI_MSD=y +CONFIG_RT_USING_MEMHEAP=y +CONFIG_BSP_USING_FULL_ASSERT=y diff --git a/camera_framework/examples/take_photo_to_sdcard/project/rtconfig.py b/camera_framework/examples/take_photo_to_sdcard/project/rtconfig.py new file mode 100644 index 0000000..72efdf0 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/project/rtconfig.py @@ -0,0 +1,6 @@ + + + + + + diff --git a/camera_framework/examples/take_photo_to_sdcard/project/rtconfig_project.h b/camera_framework/examples/take_photo_to_sdcard/project/rtconfig_project.h new file mode 100644 index 0000000..d444515 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/project/rtconfig_project.h @@ -0,0 +1,21 @@ +#ifndef RTCONFIG_PROJECT_H__ +#define RTCONFIG_PROJECT_H__ + +#if defined(_MSC_VER) + #define RT_HEAP_SIZE (680000) + #define NORESOURCE //RT_VESRION in winuser.h + #define _CRT_ERRNO_DEFINED //errno macro redefinition + #define _INC_WTIME_INL//dfs_elm.c time.h conflicts with wtime.inl + #define _INC_TIME_INL //dfs_elm.c time.h conflicts with wtime.inl + + /* disable some warning in MSC */ + #pragma warning(disable:4273) /* to ignore: warning C4273: inconsistent dll linkage */ + #pragma warning(disable:4312) /* to ignore: warning C4312: 'type cast' : conversion from 'rt_uint32_t' to 'rt_uint32_t *' */ + #pragma warning(disable:4311) /* to ignore: warning C4311: 'type cast' : pointer truncation from 'short *__w64 ' to 'long' */ + #pragma warning(disable:4996) /* to ignore: warning C4996: The POSIX name for this item is deprecated. */ + #pragma warning(disable:4267) /* to ignore: warning C4267: conversion from 'size_t' to 'rt_size_t', possible loss of data */ + #pragma warning(disable:4244) /* to ignore: warning C4244: '=' : conversion from '__w64 int' to 'rt_size_t', possible loss of data */ + +#endif /* end of _MSC_VER */ + +#endif diff --git a/camera_framework/examples/take_photo_to_sdcard/src/SConscript b/camera_framework/examples/take_photo_to_sdcard/src/SConscript new file mode 100644 index 0000000..0e84297 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/src/SConscript @@ -0,0 +1,8 @@ +import os +from building import * + +# Add source code +src = Glob('*.c') +group = DefineGroup('Applications', src, depend = ['']) + +Return('group') diff --git a/camera_framework/examples/take_photo_to_sdcard/src/main.c b/camera_framework/examples/take_photo_to_sdcard/src/main.c new file mode 100644 index 0000000..7ed45ef --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard/src/main.c @@ -0,0 +1,406 @@ +/****************************************************************************** + * @file main.c + * @brief OV2640 take_photo_to_sdcard example - capture JPEG frames via the + * camera handle high-level API and save them to the SD card. + * + * The example uses the camera_handle.h API to grab one or more JPEG frames + * into a PSRAM buffer and then writes each frame as a numbered .jpg file + * under /photo on the mounted SD card filesystem. + * + * Call sequence (per `take_photo` invocation): + * camera_handler_instance_init() - bind device name + device_ops + * camera_init() - open the RT-Thread device + * camera_change_settings() - configure JPEG + framesize + quality + * camera_capture_single() (loop) - blocking single-frame grab + * write to /photo/photo_NNN.jpg - save each captured frame + * camera_deinit() - close the device + * + * Note: low-level pin muxing for SCCB / DVP / XCLK is performed by the + * OV2640 driver itself, so this example does not call HAL_PIN_Set(). + *****************************************************************************/ + +#include "rtthread.h" +#include "bf0_hal.h" +#include "stdio.h" +#include "string.h" +#include +#include +#include +#include +#include "mem_section.h" +#include "dfs_file.h" +#include "dfs_posix.h" +#include "spi_msd.h" +#include "ov2640.h" +#include "camera_handle.h" + +/* ------------------------------------------------------------------ * + * PSRAM heap - holds the JPEG frame buffer (internal SRAM is too + * small for VGA-and-above JPEG output). + * ------------------------------------------------------------------ */ +static uint8_t psram_heap_pool[4096 * 1024] L2_RET_BSS_SECT(psram_heap_pool); +static struct rt_memheap psram_memheap; + +#define JPEG_MIN_BUFFER_SIZE (64 * 1024) +#define JPEG_MAX_BUFFER_SIZE (2 * 1024 * 1024) +#define PHOTO_DIR "/photo" + +/** + * @brief Initialize the PSRAM heap pool. + * + * @return Return 0 on success (fixed value). + */ +int psram_heap_init(void) +{ + rt_memheap_init(&psram_memheap, "psram_heap", (void *)psram_heap_pool, + sizeof(psram_heap_pool)); + return 0; +} + +/** + * @brief Allocate memory from the PSRAM heap. + * + * @param size is the number of bytes to allocate. + * + * @return Return a pointer on success, RT_NULL on failure. + */ +void *psram_heap_malloc(uint32_t size) +{ + return rt_memheap_alloc(&psram_memheap, size); +} + +/** + * @brief Release memory previously returned by psram_heap_malloc(). + * + * @param p is the pointer to free. + */ +void psram_heap_free(void *p) +{ + rt_memheap_free(p); +} + +/* ------------------------------------------------------------------ * + * SD card mount + * ------------------------------------------------------------------ */ + +/** + * @brief Locate the SD card device and mount it as the root FAT volume. + */ +void sdcard_init(void) +{ + rt_device_t msd = rt_device_find("sd0"); + if (msd == RT_NULL) + { + rt_kprintf("sd card not found\n"); + return; + } + + if (dfs_mount("sd0", "/", "elm", 0, 0) != 0) + { + rt_kprintf("mount fs on tf card to / fail\n"); + rt_kprintf("sd card might not be formatted or is corrupted.\n"); + return; + } + + rt_kprintf("mount fs on tf card to / success\n"); +} + +/* ------------------------------------------------------------------ * + * Frame-size parsing and buffer-size computation + * ------------------------------------------------------------------ */ + +/** + * @brief Convert a frame-size name (e.g. "VGA") into the framesize_t enum. + * + * @param str is the frame-size string to look up. + * + * @return Return the matching framesize_t, or FRAMESIZE_INVALID if unknown. + */ +static framesize_t format_string_to_framesize(const char *str) +{ + if (strcmp(str, "QQVGA") == 0) return FRAMESIZE_QQVGA; + else if (strcmp(str, "QCIF") == 0) return FRAMESIZE_QCIF; + else if (strcmp(str, "QVGA") == 0) return FRAMESIZE_QVGA; + else if (strcmp(str, "CIF") == 0) return FRAMESIZE_CIF; + else if (strcmp(str, "VGA") == 0) return FRAMESIZE_VGA; + else if (strcmp(str, "SVGA") == 0) return FRAMESIZE_SVGA; + else if (strcmp(str, "XGA") == 0) return FRAMESIZE_XGA; + else if (strcmp(str, "HD") == 0) return FRAMESIZE_HD; + else if (strcmp(str, "SXGA") == 0) return FRAMESIZE_SXGA; + else if (strcmp(str, "UXGA") == 0) return FRAMESIZE_UXGA; + else return FRAMESIZE_INVALID; +} + +/** + * @brief Resolve a framesize_t into pixel width and height. + * + * @param size is the input framesize_t. + * @param width is the output pointer that receives the width in pixels. + * @param height is the output pointer that receives the height in pixels. + * + * @return Return RT_EOK on success, or -RT_EINVAL when @p size is unknown. + */ +static int framesize_to_resolution(framesize_t size, uint16_t *width, uint16_t *height) +{ + if (width == RT_NULL || height == RT_NULL) + { + return -RT_EINVAL; + } + + switch (size) + { + case FRAMESIZE_QQVGA: *width = 160; *height = 120; break; + case FRAMESIZE_QCIF: *width = 176; *height = 144; break; + case FRAMESIZE_QVGA: *width = 320; *height = 240; break; + case FRAMESIZE_CIF: *width = 400; *height = 296; break; + case FRAMESIZE_VGA: *width = 640; *height = 480; break; + case FRAMESIZE_SVGA: *width = 800; *height = 600; break; + case FRAMESIZE_XGA: *width = 1024; *height = 768; break; + case FRAMESIZE_HD: *width = 1280; *height = 720; break; + case FRAMESIZE_SXGA: *width = 1280; *height = 1024; break; + case FRAMESIZE_UXGA: *width = 1600; *height = 1200; break; + default: + return -RT_EINVAL; + } + + return RT_EOK; +} + +/** + * @brief Estimate a reasonable JPEG output buffer size for a given resolution. + * + * JPEG output length is variable; we use width * height as a rough upper + * bound (about 1 byte per pixel for typical photographic content) and clamp + * it to the [JPEG_MIN_BUFFER_SIZE, JPEG_MAX_BUFFER_SIZE] range. + * + * @param size is the target framesize_t. + * + * @return Return the buffer size in bytes (never zero). + */ +static rt_size_t calc_jpeg_buffer_size(framesize_t size) +{ + uint16_t width = 0; + uint16_t height = 0; + if (framesize_to_resolution(size, &width, &height) != RT_EOK) + { + return JPEG_MAX_BUFFER_SIZE; + } + + rt_size_t buf_size = (rt_size_t)width * (rt_size_t)height; + if (buf_size < JPEG_MIN_BUFFER_SIZE) + { + buf_size = JPEG_MIN_BUFFER_SIZE; + } + if (buf_size > JPEG_MAX_BUFFER_SIZE) + { + buf_size = JPEG_MAX_BUFFER_SIZE; + } + return buf_size; +} + +/* ------------------------------------------------------------------ * + * SD-card output path helpers + * ------------------------------------------------------------------ */ + +/** + * @brief Create the photo directory if it does not exist. + * + * @return Return RT_EOK on success or when the directory already exists. + */ +static int ensure_photo_dir(void) +{ + int ret = mkdir(PHOTO_DIR, 0); + if (ret == 0) + { + return RT_EOK; + } + + int err = rt_get_errno(); + if (ret < 0 && (err == EEXIST || err == -EEXIST)) + { + return RT_EOK; + } + + return -RT_ERROR; +} + +/* ------------------------------------------------------------------ * + * MSH command: take_photo + * ------------------------------------------------------------------ */ + +/** + * @brief MSH command: capture one or more JPEG frames and save each as + * /photo/photo_NNN.jpg on the SD card. + * + * Usage: + * take_photo + * + * framesize : QQVGA / QCIF / QVGA / CIF / VGA / SVGA / XGA / HD / SXGA / UXGA + * quality : JPEG quality (0 = best, 63 = most compressed) + * count : number of frames to capture (>= 1) + * + * Example: + * take_photo VGA 10 3 + * + * @param argc is the argument count (4 including the command name). + * @param argv is the argument vector. + */ +void take_photo(int argc, char **argv) +{ + camera_handler_instance_t camera_instance; + camera_handler_all_input_arg_t input_arg; + camera_capture_config_t cfg; + camera_capture_request_t req; + camera_handle_status_t status; + uint8_t *buffer = RT_NULL; + rt_size_t buffer_size; + int quality; + int count; + + if (argc != 4) + { + rt_kprintf("Usage: take_photo \n"); + rt_kprintf("Framesize options: QQVGA, QCIF, QVGA, CIF, VGA, SVGA, XGA, HD, SXGA, UXGA\n"); + rt_kprintf("quality: 0 (highest) to 63 (lowest)\n"); + rt_kprintf("count: number of photos to capture (>=1)\n"); + rt_kprintf("Example: take_photo VGA 10 3\n"); + return; + } + + framesize_t framesize = format_string_to_framesize(argv[1]); + if (framesize == FRAMESIZE_INVALID) + { + rt_kprintf("Unsupported framesize: %s\n", argv[1]); + return; + } + + quality = atoi(argv[2]); + if (quality < 0 || quality > 63) + { + rt_kprintf("Quality must be between 0 and 63\n"); + return; + } + + count = atoi(argv[3]); + if (count <= 0) + { + rt_kprintf("Count must be >= 1\n"); + return; + } + + if (ensure_photo_dir() != RT_EOK) + { + rt_kprintf("Failed to create or access %s\n", PHOTO_DIR); + return; + } + + /* 1) Prepare the camera handler instance. */ + input_arg.device_name = CAMERA_DEFAULT_DEVICE_NAME; /* "ov2640" */ + input_arg.device_ops = ov2640_get_device_ops(); + status = camera_handler_instance_init(&camera_instance, &input_arg); + if (status != CAMERA_OK) + { + rt_kprintf("Failed to initialize camera handler (%d)\n", status); + return; + } + + /* 2) Open the underlying RT-Thread camera device. */ + status = camera_init(&camera_instance); + if (status != CAMERA_OK) + { + rt_kprintf("Failed to open camera device (%d)\n", status); + return; + } + + /* 3) Push JPEG + framesize + quality configuration. The handle layer + * inserts a 500 ms AEC/AWB settle delay internally. */ + cfg.pixformat = PIXFORMAT_JPEG; + cfg.framesize = framesize; + cfg.quality = quality; + status = camera_change_settings(&camera_instance, &cfg); + if (status != CAMERA_OK) + { + rt_kprintf("Failed to configure camera (%d)\n", status); + goto close_camera; + } + + /* 4) Allocate the JPEG frame buffer in PSRAM. */ + buffer_size = calc_jpeg_buffer_size(framesize); + buffer = psram_heap_malloc(buffer_size); + if (buffer == RT_NULL) + { + rt_kprintf("Failed to allocate %u bytes for JPEG capture!\n", + (unsigned int)buffer_size); + goto close_camera; + } + + rt_kprintf("JPEG capture: framesize=%s, quality=%d, buffer=%u bytes @ %p\n", + argv[1], quality, (unsigned int)buffer_size, buffer); + + /* 5) Grab `count` frames and write each one as a numbered .jpg file. */ + for (int photo_idx = 0; photo_idx < count; photo_idx++) + { + req.buffer = buffer; + req.buffer_size = buffer_size; + req.frame_size = 0; + + status = camera_capture_single(&camera_instance, &req); + if (status != CAMERA_OK || req.frame_size == 0) + { + rt_kprintf("Capture failed or timed out (index=%d, status=%d)\n", + photo_idx, status); + continue; + } + + char file_path[64]; + rt_snprintf(file_path, sizeof(file_path), + "%s/photo_%03d.jpg", PHOTO_DIR, photo_idx + 1); + int fd = open(file_path, O_WRONLY | O_CREAT | O_TRUNC, 0); + if (fd < 0) + { + rt_kprintf("Failed to open %s for writing\n", file_path); + continue; + } + + int written = write(fd, buffer, req.frame_size); + close(fd); + if (written != (int)req.frame_size) + { + rt_kprintf("Write failed for %s (%d/%u bytes)\n", + file_path, written, (unsigned int)req.frame_size); + continue; + } + + rt_kprintf("Saved %s (%u bytes)\n", file_path, + (unsigned int)req.frame_size); + } + + psram_heap_free(buffer); + buffer = RT_NULL; + +close_camera: + camera_deinit(&camera_instance); + if (buffer != RT_NULL) + { + psram_heap_free(buffer); + } +} +MSH_CMD_EXPORT(take_photo, Capture JPEG photo(s) using ov2640 and save to SD card); + +/** + * @brief Program entry point: initialize the PSRAM heap, mount the SD card + * and idle, waiting for MSH commands. + * + * @return Return 0 (never actually returns; the main loop spins forever). + */ +int main(void) +{ + rt_kprintf("OV2640 Camera Take Photo to SD Card Example\n"); + psram_heap_init(); + sdcard_init(); + + while (1) + { + rt_thread_mdelay(1000); + } +} diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/README.md b/camera_framework/examples/take_photo_to_sdcard_streaming/README.md new file mode 100644 index 0000000..a0c47df --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/README.md @@ -0,0 +1,108 @@ +# take_photo_to_sdcard(流式采集)示例 + +[中文](README.md) | [English](README_EN.md) + +## 示例简介 + +本示例演示如何使用 OV2640 摄像头组件的 **handle 层流式 API**(`camera_handle.h`)以双缓冲连续采集(streaming)方式获取 JPEG 帧,并在取到每一帧后**立即写入 SD 卡**,实现"边采集边存"。 + +与单帧 `camera_capture_single` 方式不同,流式 API 在底层持续进行 DMA 乒乓传输,应用层只需轮询取帧,延迟更低,更适合连续拍摄场景。 + +调用流程: + +1. `camera_handler_instance_init` +2. `camera_init` +3. `camera_change_settings`(`PIXFORMAT_JPEG` + framesize + quality) +4. 分配两块 PSRAM 帧缓冲,调用 `camera_start_stream` +5. 循环 `camera_get_stream_frame`,每取到一帧立即写入 `/photo/photo_NNN.jpg` +6. `camera_stop_stream` +7. `camera_deinit` + +> SCCB / DVP / XCLK 等引脚的复用由 OV2640 驱动在初始化时内部完成,**应用层不需要也不应该调用 `HAL_PIN_Set()`**。 + +## 硬件要求 + +- OV2640 摄像头模组,按照 OV2640 组件 README 中的默认引脚连接 +- SPI / SDIO 接口的 SD 卡,并在 RT-Thread 中注册为 `sd0` 设备 +- 至少 4 MB PSRAM(示例从 PSRAM heap 中分配两块 JPEG 帧缓冲) + +## 使用方法 + +启动后会自动初始化 PSRAM heap 并挂载 SD 卡: + +``` +OV2640 Camera Take Photo to SD Card Example +mount fs on tf card to / success +``` + +挂载成功后在 MSH 控制台执行: + +``` +msh> take_photo +``` + +| 参数 | 取值 | 说明 | +|------|------|------| +| `framesize` | QQVGA / QCIF / QVGA / CIF / VGA / SVGA / XGA / HD / SXGA / UXGA | 分辨率 | +| `quality` | 0 ~ 63 | JPEG 质量,0 = 最佳,63 = 最大压缩 | +| `count` | ≥ 1 | 采集帧数 | + +示例: + +``` +msh /> +take_photo VGA 10 10 +[I/dvp] DVP initialized successfully +[I/dvp] Mode: JPEG +[I/dvp] Buffer size: 0 bytes +[I/dvp] Pingpong buffer: 1024 bytes +[I/dvp] VSYNC: PA42 (GPIO interrupt) +Stream start: framesize=VGA, quality=10, buffer=307200 bytes x2 @ 6000001c / 6004b038 +Frame[0] seq=1 buf=0 size=15252 wait=56 ms +Saved /photo/photo_001.jpg (15252 bytes) +Frame[1] seq=2 buf=1 size=15475 wait=4 ms +Saved /photo/photo_002.jpg (15475 bytes) +Frame[2] seq=3 buf=0 size=15460 wait=3 ms +Saved /photo/photo_003.jpg (15460 bytes) +Frame[3] seq=4 buf=1 size=15684 wait=6 ms +Saved /photo/photo_004.jpg (15684 bytes) +Frame[4] seq=5 buf=0 size=15578 wait=6 ms +Saved /photo/photo_005.jpg (15578 bytes) +Frame[5] seq=6 buf=1 size=15556 wait=7 ms +Saved /photo/photo_006.jpg (15556 bytes) +Frame[6] seq=7 buf=0 size=15497 wait=5 ms +Saved /photo/photo_007.jpg (15497 bytes) +Frame[7] seq=8 buf=1 size=15729 wait=5 ms +Saved /photo/photo_008.jpg (15729 bytes) +Frame[8] seq=9 buf=0 size=15637 wait=4 ms +Saved /photo/photo_009.jpg (15637 bytes) +Frame[9] seq=10 buf=1 size=15913 wait=5 ms +Saved /photo/photo_010.jpg (15913 bytes) +``` + +## 输出文件 + +每帧采集后立即生成: + +``` +/photo/photo_001.jpg +/photo/photo_002.jpg +/photo/photo_003.jpg +... +``` + +把 SD 卡接到 PC 上即可直接打开 `.jpg` 文件验证。 + +## 说明事项 + +- 本示例**仅支持 JPEG 像素格式**,RGB565/YUV422/RAW8 等格式请使用 `cam_capture.c` 中对应命令。 +- 流式采集会分配 **两块** PSRAM 帧缓冲(双缓冲 DMA),每块大小约为 `width × height`,最大 2 MB。 +- 若写 SD 卡速度跟不上出帧速率,驱动会覆盖最旧未消费帧;命令结束后会打印 `dropped N frame(s)` 供诊断。 +- `quality` 越小图像越清晰,但文件越大;越大压缩越强,文件越小。 +- `camera_change_settings` 内部已插入约 500 ms AEC/AWB 稳定延时,无需再手动等待。 +- 若提示 `sd card not found` 或 `mount fs ... fail`,请确认:SD 卡已格式化为 FAT、`sd0` 设备已注册、SPI / 时钟引脚连接正确。 + +## 相关文档 + +- 英文版:[README_EN.md](README_EN.md) +- 组件主文档:[../../README.md](../../README.md) diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/README_EN.md b/camera_framework/examples/take_photo_to_sdcard_streaming/README_EN.md new file mode 100644 index 0000000..c3174c6 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/README_EN.md @@ -0,0 +1,88 @@ +# take_photo_to_sdcard (Streaming) Example + +[中文](README.md) | [English](README_EN.md) + +## Overview + +This example demonstrates how to use the OV2640 component's **handle-layer streaming API** (`camera_handle.h`) to capture JPEG frames using double-buffered continuous DMA and **write each frame to the SD card immediately** as it arrives — a true "capture-while-saving" pipeline. + +Unlike `camera_capture_single`, the streaming API keeps DMA ping-pong transfers running in the background. The application simply polls for ready frames, resulting in lower per-frame latency and better throughput for burst captures. + +Call sequence: + +1. `camera_handler_instance_init` +2. `camera_init` +3. `camera_change_settings` (`PIXFORMAT_JPEG` + framesize + quality) +4. Allocate two PSRAM frame buffers, call `camera_start_stream` +5. Loop `camera_get_stream_frame`; write each frame to `/photo/photo_NNN.jpg` immediately +6. `camera_stop_stream` +7. `camera_deinit` + +> Pin muxing for SCCB / DVP / XCLK is performed by the OV2640 driver during initialization. The application **does not** (and must not) call `HAL_PIN_Set()`. + +## Hardware Requirements + +- OV2640 camera module wired according to the default pins documented in the OV2640 component README +- A SPI / SDIO SD card registered as the `sd0` device in RT-Thread +- At least 4 MB PSRAM (the example allocates two JPEG frame buffers from a PSRAM heap) + +## Usage + +After boot the example initializes the PSRAM heap and mounts the SD card automatically: + +``` +OV2640 Camera Take Photo to SD Card Example +mount fs on tf card to / success +``` + +Once mounted, run from the MSH console: + +``` +msh> take_photo +``` + +| Parameter | Value | Description | +|-----------|-------|-------------| +| `framesize` | QQVGA / QCIF / QVGA / CIF / VGA / SVGA / XGA / HD / SXGA / UXGA | Resolution | +| `quality` | 0 ~ 63 | JPEG quality (0 = best, 63 = most compressed) | +| `count` | ≥ 1 | Number of frames to capture | + +Example: + +``` +msh> take_photo VGA 10 3 +Stream start: framesize=VGA, quality=10, buffer=307200 bytes x2 @ 0x6XXXXXXX / 0x6XXXXXXX +Frame[0] seq=1 buf=0 size=28456 wait=42 ms +Saved /photo/photo_001.jpg (28456 bytes) +Frame[1] seq=2 buf=1 size=28612 wait=41 ms +Saved /photo/photo_002.jpg (28612 bytes) +Frame[2] seq=3 buf=0 size=28391 wait=43 ms +Saved /photo/photo_003.jpg (28391 bytes) +``` + +## Output Files + +Each captured frame is saved immediately as: + +``` +/photo/photo_001.jpg +/photo/photo_002.jpg +/photo/photo_003.jpg +... +``` + +Plug the SD card into a host PC and open the `.jpg` files directly to verify the result. + +## Notes + +- This example supports **JPEG pixel format only**. For RGB565 / YUV422 / RAW8 capture, use the corresponding commands in `cam_capture.c`. +- Streaming allocates **two** PSRAM frame buffers (ping-pong DMA). Each buffer is sized as `width × height`, capped at 2 MB. +- If the SD card write speed cannot keep up with the frame rate, the driver overwrites the oldest unconsumed frame. A `dropped N frame(s)` message is printed after the command finishes for diagnostics. +- Lower `quality` produces sharper images but larger files; higher `quality` increases compression and shrinks file size. +- `camera_change_settings` already inserts roughly 500 ms of AEC/AWB settle time internally — no extra delay is required from the caller. +- If you see `sd card not found` or `mount fs ... fail`: make sure the SD card is FAT-formatted, the `sd0` device is registered, and the SPI / clock pins are wired correctly. + +## Related Documents + +- Chinese version: [README.md](README.md) +- Component main documentation: [../../README_EN.md](../../README_EN.md) diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/project/Kconfig b/camera_framework/examples/take_photo_to_sdcard_streaming/project/Kconfig new file mode 100644 index 0000000..6445854 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/project/Kconfig @@ -0,0 +1,3 @@ +#Kconfig root for APP. +source "$SIFLI_SDK/Kconfig.v2 +rsource "Kconfig.proj" diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/project/Kconfig.proj b/camera_framework/examples/take_photo_to_sdcard_streaming/project/Kconfig.proj new file mode 100644 index 0000000..b28681f --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/project/Kconfig.proj @@ -0,0 +1,7 @@ +#APP specific configuration. +config CUSTOM_MEM_MAP + bool + select custom_mem_map + default y if !SOC_SIMULATOR + +orsource "../../../../Kconfig" \ No newline at end of file diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/project/SConscript b/camera_framework/examples/take_photo_to_sdcard_streaming/project/SConscript new file mode 100644 index 0000000..2ae4c5f --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/project/SConscript @@ -0,0 +1,31 @@ +import os +from building import * + +cwd = GetCurrentDir() +objs = [] +list = os.listdir(cwd) + +# Add SDK +Import('SIFLI_SDK') +objs.extend(SConscript(os.path.join(SIFLI_SDK, 'SConscript'), variant_dir="sifli_sdk", duplicate=0)) + +# Add application source code +objs.extend(SConscript(cwd+'/../src/SConscript', variant_dir="src", duplicate=0)) + +# Add ov2640 camera package sources directly (avoids variant_dir+absolute-path issues) +ov2640_root = os.path.normpath(os.path.join(cwd, '../../../../')) +ov2640_src = (Glob(ov2640_root + '/camera/*.c') + + Glob(ov2640_root + '/camera/*/*.c') + + Glob(ov2640_root + '/camera/*/*/*.c') + + Glob(ov2640_root + '/camera/*/*/*/*.c')) +ov2640_inc = [ + ov2640_root + '/camera', + ov2640_root + '/camera/bus/control', + ov2640_root + '/camera/driver/ov2640', + ov2640_root + '/camera/bus/data', + ov2640_root + '/camera/handle', +] +objs.extend(DefineGroup('OV2640', ov2640_src, depend=['SENSOR_USING_OV2640'], + CPPPATH=ov2640_inc, LIBPATH=ov2640_inc)) + +Return('objs') diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/project/SConstruct b/camera_framework/examples/take_photo_to_sdcard_streaming/project/SConstruct new file mode 100644 index 0000000..60b518f --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/project/SConstruct @@ -0,0 +1,38 @@ +import os +import rtconfig + +# Check SDK +SIFLI_SDK = os.getenv('SIFLI_SDK') +if not SIFLI_SDK: + print("Please run set_env.bat in root folder of SIFLI SDK to set environment.") + exit() +from building import * + +# Prepare environment. +PrepareEnv() + +################################## change rtconfig.xxx to customize build ######################################## + +# Add bootloader project +AddBootLoader(SIFLI_SDK,rtconfig.CHIP) + +# Set default compile options +SifliEnv() + +TARGET = rtconfig.OUTPUT_DIR + rtconfig.TARGET_NAME + '.' + rtconfig.TARGET_EXT +# Prepare building environment +objs = PrepareBuilding(None) +env = GetCurrentEnv() +# env.Append(CCFLAGS=['-Og']) + +# Add linker flags to display memory usage +env.Append(LINKFLAGS=['-Wl,--print-memory-usage']) + +# Build application. +DoBuilding(TARGET, objs) + +# Add flash table buld. +AddFTAB(SIFLI_SDK,rtconfig.CHIP) + +# Generate download .bat script +GenDownloadScript(env) diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/project/proj.conf b/camera_framework/examples/take_photo_to_sdcard_streaming/project/proj.conf new file mode 100644 index 0000000..c23f469 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/project/proj.conf @@ -0,0 +1,21 @@ +# CONFIG_BSP_UART1_RX_USING_DMA is not set +# CONFIG_BSP_SPI1_TX_USING_DMA is not set +# CONFIG_BSP_SPI1_RX_USING_DMA is not set +CONFIG_BSP_USING_GPTIM1=y + +CONFIG_BSP_USING_SDMMC1=y +# CONFIG_BSP_ENABLE_AUD_PRC is not set +# CONFIG_BSP_ENABLE_AUD_CODEC is not set +# CONFIG_BSP_USING_TOUCHD is not set +# CONFIG_BSP_USING_KEY1 is not set +# CONFIG_BSP_USING_LED1 is not set +CONFIG_RT_USING_DFS_ELMFAT=y +CONFIG_RT_USING_SPI_MSD=y +CONFIG_RT_USING_MEMHEAP=y +CONFIG_BSP_USING_FULL_ASSERT=y +CONFIG_OV2640_DVP_PINGPONG_BUFFER_SIZE=1024 +CONFIG_OV2640_DVP_DATA_PIN_BASE=-1 +CONFIG_OV2640_SCCB_I2C_BUS_NAME="i2c2" +CONFIG_OV2640_SCCB_MAX_HZ=200000 +CONFIG_OV2640_CAMERA_READ_TIMEOUT_MS=10000 + diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/project/rtconfig.py b/camera_framework/examples/take_photo_to_sdcard_streaming/project/rtconfig.py new file mode 100644 index 0000000..72efdf0 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/project/rtconfig.py @@ -0,0 +1,6 @@ + + + + + + diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/project/rtconfig_project.h b/camera_framework/examples/take_photo_to_sdcard_streaming/project/rtconfig_project.h new file mode 100644 index 0000000..d444515 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/project/rtconfig_project.h @@ -0,0 +1,21 @@ +#ifndef RTCONFIG_PROJECT_H__ +#define RTCONFIG_PROJECT_H__ + +#if defined(_MSC_VER) + #define RT_HEAP_SIZE (680000) + #define NORESOURCE //RT_VESRION in winuser.h + #define _CRT_ERRNO_DEFINED //errno macro redefinition + #define _INC_WTIME_INL//dfs_elm.c time.h conflicts with wtime.inl + #define _INC_TIME_INL //dfs_elm.c time.h conflicts with wtime.inl + + /* disable some warning in MSC */ + #pragma warning(disable:4273) /* to ignore: warning C4273: inconsistent dll linkage */ + #pragma warning(disable:4312) /* to ignore: warning C4312: 'type cast' : conversion from 'rt_uint32_t' to 'rt_uint32_t *' */ + #pragma warning(disable:4311) /* to ignore: warning C4311: 'type cast' : pointer truncation from 'short *__w64 ' to 'long' */ + #pragma warning(disable:4996) /* to ignore: warning C4996: The POSIX name for this item is deprecated. */ + #pragma warning(disable:4267) /* to ignore: warning C4267: conversion from 'size_t' to 'rt_size_t', possible loss of data */ + #pragma warning(disable:4244) /* to ignore: warning C4244: '=' : conversion from '__w64 int' to 'rt_size_t', possible loss of data */ + +#endif /* end of _MSC_VER */ + +#endif diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/src/SConscript b/camera_framework/examples/take_photo_to_sdcard_streaming/src/SConscript new file mode 100644 index 0000000..137b931 --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/src/SConscript @@ -0,0 +1,18 @@ +import os +from building import * + +cwd = GetCurrentDir() +ov2640_root = os.path.normpath(os.path.join(cwd, '../../../')) + +# Add source code +src = Glob('*.c') +inc = [ + ov2640_root + '/camera', + ov2640_root + '/camera/bus/control', + ov2640_root + '/camera/driver/ov2640', + ov2640_root + '/camera/bus/data', + ov2640_root + '/camera/handle', +] +group = DefineGroup('Applications', src, depend=[''], CPPPATH=inc) + +Return('group') diff --git a/camera_framework/examples/take_photo_to_sdcard_streaming/src/main.c b/camera_framework/examples/take_photo_to_sdcard_streaming/src/main.c new file mode 100644 index 0000000..5900aca --- /dev/null +++ b/camera_framework/examples/take_photo_to_sdcard_streaming/src/main.c @@ -0,0 +1,441 @@ +/****************************************************************************** + * @file main.c + * @brief OV2640 take_photo_to_sdcard example - 流式(streaming)JPEG 采集, + * 边采集边把每一帧写入 SD 卡。 + * + * 仅支持 JPEG 像素格式,使用 camera_handle.h 的双缓冲流式 API: + * camera_handler_instance_init() - 绑定设备名 + device_ops + * camera_init() - 打开 RT-Thread 设备 + * camera_change_settings() - 配置 JPEG + framesize + quality + * camera_start_stream() - 启动双缓冲 DMA 连续采集 + * camera_get_stream_frame() loop - 取一帧 -> 立刻写 SD 卡 /photo/photo_NNN.jpg + * camera_stop_stream() - 停止流式采集 + * camera_deinit() - 关闭设备 + * + * Note: low-level pin muxing for SCCB / DVP / XCLK is performed by the + * OV2640 driver itself, so this example does not call HAL_PIN_Set(). + *****************************************************************************/ + +#include "rtthread.h" +#include "bf0_hal.h" +#include "stdio.h" +#include "string.h" +#include +#include +#include +#include +#include "mem_section.h" +#include "dfs_file.h" +#include "dfs_posix.h" +#include "spi_msd.h" +#include "ov2640.h" +#include "camera_handle.h" + +/* ------------------------------------------------------------------ * + * PSRAM heap - holds the JPEG frame buffer (internal SRAM is too + * small for VGA-and-above JPEG output). + * ------------------------------------------------------------------ */ +static uint8_t psram_heap_pool[4096 * 1024] L2_RET_BSS_SECT(psram_heap_pool); +static struct rt_memheap psram_memheap; + +#define JPEG_MIN_BUFFER_SIZE (64 * 1024) +#define JPEG_MAX_BUFFER_SIZE (2 * 1024 * 1024) +#define PHOTO_DIR "/photo" + +/** + * @brief Initialize the PSRAM heap pool. + * + * @return Return 0 on success (fixed value). + */ +int psram_heap_init(void) +{ + rt_memheap_init(&psram_memheap, "psram_heap", (void *)psram_heap_pool, + sizeof(psram_heap_pool)); + return 0; +} + +/** + * @brief Allocate memory from the PSRAM heap. + * + * @param size is the number of bytes to allocate. + * + * @return Return a pointer on success, RT_NULL on failure. + */ +void *psram_heap_malloc(uint32_t size) +{ + return rt_memheap_alloc(&psram_memheap, size); +} + +/** + * @brief Release memory previously returned by psram_heap_malloc(). + * + * @param p is the pointer to free. + */ +void psram_heap_free(void *p) +{ + rt_memheap_free(p); +} + +/* ------------------------------------------------------------------ * + * SD card mount + * ------------------------------------------------------------------ */ + +/** + * @brief Locate the SD card device and mount it as the root FAT volume. + */ +void sdcard_init(void) +{ + rt_device_t msd = rt_device_find("sd0"); + if (msd == RT_NULL) + { + rt_kprintf("sd card not found\n"); + return; + } + + if (dfs_mount("sd0", "/", "elm", 0, 0) != 0) + { + rt_kprintf("mount fs on tf card to / fail\n"); + rt_kprintf("sd card might not be formatted or is corrupted.\n"); + return; + } + + rt_kprintf("mount fs on tf card to / success\n"); +} + +/* ------------------------------------------------------------------ * + * Frame-size parsing and buffer-size computation + * ------------------------------------------------------------------ */ + +/** + * @brief Convert a frame-size name (e.g. "VGA") into the framesize_t enum. + * + * @param str is the frame-size string to look up. + * + * @return Return the matching framesize_t, or FRAMESIZE_INVALID if unknown. + */ +static framesize_t format_string_to_framesize(const char *str) +{ + if (strcmp(str, "QQVGA") == 0) return FRAMESIZE_QQVGA; + else if (strcmp(str, "QCIF") == 0) return FRAMESIZE_QCIF; + else if (strcmp(str, "QVGA") == 0) return FRAMESIZE_QVGA; + else if (strcmp(str, "CIF") == 0) return FRAMESIZE_CIF; + else if (strcmp(str, "VGA") == 0) return FRAMESIZE_VGA; + else if (strcmp(str, "SVGA") == 0) return FRAMESIZE_SVGA; + else if (strcmp(str, "XGA") == 0) return FRAMESIZE_XGA; + else if (strcmp(str, "HD") == 0) return FRAMESIZE_HD; + else if (strcmp(str, "SXGA") == 0) return FRAMESIZE_SXGA; + else if (strcmp(str, "UXGA") == 0) return FRAMESIZE_UXGA; + else return FRAMESIZE_INVALID; +} + +/** + * @brief Resolve a framesize_t into pixel width and height. + * + * @param size is the input framesize_t. + * @param width is the output pointer that receives the width in pixels. + * @param height is the output pointer that receives the height in pixels. + * + * @return Return RT_EOK on success, or -RT_EINVAL when @p size is unknown. + */ +static int framesize_to_resolution(framesize_t size, uint16_t *width, uint16_t *height) +{ + if (width == RT_NULL || height == RT_NULL) + { + return -RT_EINVAL; + } + + switch (size) + { + case FRAMESIZE_QQVGA: *width = 160; *height = 120; break; + case FRAMESIZE_QCIF: *width = 176; *height = 144; break; + case FRAMESIZE_QVGA: *width = 320; *height = 240; break; + case FRAMESIZE_CIF: *width = 400; *height = 296; break; + case FRAMESIZE_VGA: *width = 640; *height = 480; break; + case FRAMESIZE_SVGA: *width = 800; *height = 600; break; + case FRAMESIZE_XGA: *width = 1024; *height = 768; break; + case FRAMESIZE_HD: *width = 1280; *height = 720; break; + case FRAMESIZE_SXGA: *width = 1280; *height = 1024; break; + case FRAMESIZE_UXGA: *width = 1600; *height = 1200; break; + default: + return -RT_EINVAL; + } + + return RT_EOK; +} + +/** + * @brief Estimate a reasonable JPEG output buffer size for a given resolution. + * + * JPEG output length is variable; we use width * height as a rough upper + * bound (about 1 byte per pixel for typical photographic content) and clamp + * it to the [JPEG_MIN_BUFFER_SIZE, JPEG_MAX_BUFFER_SIZE] range. + * + * @param size is the target framesize_t. + * + * @return Return the buffer size in bytes (never zero). + */ +static rt_size_t calc_jpeg_buffer_size(framesize_t size) +{ + uint16_t width = 0; + uint16_t height = 0; + if (framesize_to_resolution(size, &width, &height) != RT_EOK) + { + return JPEG_MAX_BUFFER_SIZE; + } + + rt_size_t buf_size = (rt_size_t)width * (rt_size_t)height; + if (buf_size < JPEG_MIN_BUFFER_SIZE) + { + buf_size = JPEG_MIN_BUFFER_SIZE; + } + if (buf_size > JPEG_MAX_BUFFER_SIZE) + { + buf_size = JPEG_MAX_BUFFER_SIZE; + } + return buf_size; +} + +/* ------------------------------------------------------------------ * + * SD-card output path helpers + * ------------------------------------------------------------------ */ + +/** + * @brief Create the photo directory if it does not exist. + * + * @return Return RT_EOK on success or when the directory already exists. + */ +static int ensure_photo_dir(void) +{ + int ret = mkdir(PHOTO_DIR, 0); + if (ret == 0) + { + return RT_EOK; + } + + int err = rt_get_errno(); + if (ret < 0 && (err == EEXIST || err == -EEXIST)) + { + return RT_EOK; + } + + return -RT_ERROR; +} + +/* ------------------------------------------------------------------ * + * MSH command: take_photo + * ------------------------------------------------------------------ */ + +/** + * @brief MSH command: 流式采集 JPEG 帧,每取到一帧立即写入 SD 卡 + * /photo/photo_NNN.jpg。 + * + * Usage: + * take_photo + * + * framesize : QQVGA / QCIF / QVGA / CIF / VGA / SVGA / XGA / HD / SXGA / UXGA + * quality : JPEG quality (0 = best, 63 = most compressed) + * count : number of frames to capture (>= 1) + * + * Example: + * take_photo VGA 10 3 + * + * @param argc is the argument count (4 including the command name). + * @param argv is the argument vector. + */ +void take_photo(int argc, char **argv) +{ + camera_handler_instance_t camera_instance; + camera_handler_all_input_arg_t input_arg; + camera_capture_config_t cfg; + camera_stream_config_t stream_cfg; + camera_stream_frame_t frame; + camera_handle_status_t status; + uint8_t *buffers[2] = { RT_NULL, RT_NULL }; + rt_bool_t stream_started = RT_FALSE; + rt_size_t buffer_size; + int quality; + int count; + + if (argc != 4) + { + rt_kprintf("Usage: take_photo \n"); + rt_kprintf("Framesize options: QQVGA, QCIF, QVGA, CIF, VGA, SVGA, XGA, HD, SXGA, UXGA\n"); + rt_kprintf("quality: 0 (highest) to 63 (lowest)\n"); + rt_kprintf("count: number of photos to capture (>=1)\n"); + rt_kprintf("Example: take_photo VGA 10 3\n"); + return; + } + + framesize_t framesize = format_string_to_framesize(argv[1]); + if (framesize == FRAMESIZE_INVALID) + { + rt_kprintf("Unsupported framesize: %s\n", argv[1]); + return; + } + + quality = atoi(argv[2]); + if (quality < 0 || quality > 63) + { + rt_kprintf("Quality must be between 0 and 63\n"); + return; + } + + count = atoi(argv[3]); + if (count <= 0) + { + rt_kprintf("Count must be >= 1\n"); + return; + } + + if (ensure_photo_dir() != RT_EOK) + { + rt_kprintf("Failed to create or access %s\n", PHOTO_DIR); + return; + } + + /* 1) Prepare the camera handler instance. */ + input_arg.device_name = CAMERA_DEFAULT_DEVICE_NAME; /* "ov2640" */ + input_arg.device_ops = ov2640_get_device_ops(); + status = camera_handler_instance_init(&camera_instance, &input_arg); + if (status != CAMERA_OK) + { + rt_kprintf("Failed to initialize camera handler (%d)\n", status); + return; + } + + /* 2) Open the underlying RT-Thread camera device. */ + status = camera_init(&camera_instance); + if (status != CAMERA_OK) + { + rt_kprintf("Failed to open camera device (%d)\n", status); + return; + } + + /* 3) Configure JPEG + framesize + quality. The handle layer inserts + * a 500 ms AEC/AWB settle delay internally. */ + cfg.pixformat = PIXFORMAT_JPEG; + cfg.framesize = framesize; + cfg.quality = (uint8_t)quality; + status = camera_change_settings(&camera_instance, &cfg); + if (status != CAMERA_OK) + { + rt_kprintf("Failed to configure camera (%d)\n", status); + goto close_camera; + } + + /* 4) Allocate two PSRAM frame buffers for ping-pong streaming. */ + buffer_size = calc_jpeg_buffer_size(framesize); + buffers[0] = psram_heap_malloc(buffer_size); + buffers[1] = psram_heap_malloc(buffer_size); + if (buffers[0] == RT_NULL || buffers[1] == RT_NULL) + { + rt_kprintf("Failed to allocate stream buffers (%u bytes each)\n", + (unsigned int)buffer_size); + goto free_buffers; + } + + rt_kprintf("Stream start: framesize=%s, quality=%d, buffer=%u bytes x2 @ %p / %p\n", + argv[1], quality, (unsigned int)buffer_size, buffers[0], buffers[1]); + + /* 5) Start continuous double-buffered streaming. */ + stream_cfg.buffers[0] = buffers[0]; + stream_cfg.buffers[1] = buffers[1]; + stream_cfg.buffer_size = buffer_size; + status = camera_start_stream(&camera_instance, &stream_cfg); + if (status != CAMERA_OK) + { + rt_kprintf("Failed to start stream (%d)\n", status); + goto free_buffers; + } + stream_started = RT_TRUE; + + /* 6) Pull `count` frames from the ready queue and write each one + * directly to the SD card. */ + for (int photo_idx = 0; photo_idx < count; photo_idx++) + { + rt_tick_t start_tick = rt_tick_get(); + status = camera_get_stream_frame(&camera_instance, &frame, + 5 * RT_TICK_PER_SECOND); + if (status != CAMERA_OK) + { + rt_kprintf("Stream frame timeout/error: index=%d status=%d\n", + photo_idx, status); + break; + } + + if (frame.frame_size == 0 || frame.buffer == RT_NULL) + { + rt_kprintf("Empty stream frame at index=%d, skip\n", photo_idx); + continue; + } + + long wait_ms = (long)(rt_tick_get() - start_tick) * 1000L / RT_TICK_PER_SECOND; + rt_kprintf("Frame[%d] seq=%lu buf=%u size=%u wait=%ld ms\n", + photo_idx, + (unsigned long)frame.sequence, + (unsigned int)frame.buffer_index, + (unsigned int)frame.frame_size, + wait_ms); + + char file_path[64]; + rt_snprintf(file_path, sizeof(file_path), + "%s/photo_%03d.jpg", PHOTO_DIR, photo_idx + 1); + int fd = open(file_path, O_WRONLY | O_CREAT | O_TRUNC, 0); + if (fd < 0) + { + rt_kprintf("Failed to open %s for writing\n", file_path); + continue; + } + + int written = write(fd, frame.buffer, frame.frame_size); + close(fd); + if (written != (int)frame.frame_size) + { + rt_kprintf("Write failed for %s (%d/%u bytes)\n", + file_path, written, (unsigned int)frame.frame_size); + continue; + } + + rt_kprintf("Saved %s (%u bytes)\n", file_path, + (unsigned int)frame.frame_size); + } + + if (camera_instance.stream.dropped_count != 0) + { + rt_kprintf("Stream dropped %lu frame(s) due to slow consumer\n", + (unsigned long)camera_instance.stream.dropped_count); + } + + camera_stop_stream(&camera_instance); + stream_started = RT_FALSE; + +free_buffers: + if (stream_started) + { + camera_stop_stream(&camera_instance); + } + if (buffers[0] != RT_NULL) psram_heap_free(buffers[0]); + if (buffers[1] != RT_NULL) psram_heap_free(buffers[1]); + +close_camera: + camera_deinit(&camera_instance); +} +MSH_CMD_EXPORT(take_photo, Stream JPEG photo(s) using ov2640 and save to SD card); + +/** + * @brief Program entry point: initialize the PSRAM heap, mount the SD card + * and idle, waiting for MSH commands. + * + * @return Return 0 (never actually returns; the main loop spins forever). + */ +int main(void) +{ + rt_kprintf("OV2640 Camera Take Photo to SD Card Example\n"); + psram_heap_init(); + sdcard_init(); + + while (1) + { + rt_thread_mdelay(1000); + } +} diff --git a/camera_framework/script/hex_to_jpg.py b/camera_framework/script/hex_to_jpg.py new file mode 100644 index 0000000..80b43e1 --- /dev/null +++ b/camera_framework/script/hex_to_jpg.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +""" +将包含十六进制数据的文本文件转换为JPG图片文件 +支持解析多张JPG图片(通过检测 FFD8 起始标记和 FFD9 结束标记) +输入文件格式: 0xff,0xd8,0xff,0xe0, ... +""" + +import re +import sys +import os + + +def find_jpg_images(byte_data): + """ + 在字节数据中查找所有JPG图片 + JPG图片以 FFD8 开始,以 FFD9 结束 + + Args: + byte_data: 字节数据 + + Returns: + list: 包含所有找到的JPG图片字节数据的列表 + """ + images = [] + start_marker = b'\xff\xd8' + end_marker = b'\xff\xd9' + + pos = 0 + while pos < len(byte_data): + # 查找JPG起始标记 + start_pos = byte_data.find(start_marker, pos) + if start_pos == -1: + break + + # 查找JPG结束标记 + end_pos = byte_data.find(end_marker, start_pos + 2) + if end_pos == -1: + # 没有找到结束标记,提取到末尾 + print(f"警告: 第 {len(images) + 1} 张图片没有找到结束标记 (FFD9)") + images.append(byte_data[start_pos:]) + break + + # 提取完整的JPG数据(包含结束标记的两个字节) + jpg_data = byte_data[start_pos:end_pos + 2] + images.append(jpg_data) + + # 继续搜索下一张图片 + pos = end_pos + 2 + + return images + + +def hex_to_jpg(input_file, output_file=None): + """ + 读取包含十六进制数据的文件并转换为JPG + 支持自动检测并分离多张JPG图片 + + Args: + input_file: 输入文件路径 + output_file: 输出JPG文件路径(可选,默认为输入文件名.jpg 或 输入文件名_1.jpg 等) + """ + # 读取输入文件 + try: + with open(input_file, 'r', encoding='utf-8') as f: + content = f.read() + except FileNotFoundError: + print(f"错误: 找不到文件 '{input_file}'") + return False + except Exception as e: + print(f"错误: 读取文件时出错 - {e}") + return False + + # 使用正则表达式提取所有十六进制数据 + # 匹配 0x 开头的十六进制数字 + hex_pattern = r'0x([0-9a-fA-F]{1,2})' + hex_values = re.findall(hex_pattern, content) + + if not hex_values: + print("错误: 文件中没有找到有效的十六进制数据") + return False + + # 转换为字节数据 + try: + byte_data = bytes([int(h, 16) for h in hex_values]) + except ValueError as e: + print(f"错误: 十六进制数据转换失败 - {e}") + return False + + print(f"读取到 {len(byte_data)} 字节数据") + + # 查找所有JPG图片 + images = find_jpg_images(byte_data) + + if not images: + print("错误: 没有找到有效的JPG图片数据(缺少 FFD8 起始标记)") + return False + + print(f"找到 {len(images)} 张JPG图片") + + # 生成基础文件名 + base_name = os.path.splitext(input_file)[0] + if output_file: + base_name = os.path.splitext(output_file)[0] + + # 写入JPG文件 + success_count = 0 + for i, img_data in enumerate(images): + # 生成输出文件名 + if len(images) == 1: + out_file = f"{base_name}.jpg" + else: + out_file = f"{base_name}_{i + 1}.jpg" + + try: + with open(out_file, 'wb') as f: + f.write(img_data) + print(f"成功: 第 {i + 1} 张图片,{len(img_data)} 字节 -> '{out_file}'") + success_count += 1 + except Exception as e: + print(f"错误: 写入第 {i + 1} 张图片时出错 - {e}") + + print(f"\n完成: 成功保存 {success_count}/{len(images)} 张图片") + return success_count > 0 + + +def main(): + if len(sys.argv) < 2: + print("用法: python hex_to_jpg.py <输入文件> [输出文件前缀]") + print("示例: python hex_to_jpg.py input.txt output") + print(" python hex_to_jpg.py input.txt (自动生成 input.jpg 或 input_1.jpg, input_2.jpg ...)") + print("\n说明: 支持自动检测并分离多张JPG图片") + sys.exit(1) + + input_file = sys.argv[1] + output_file = sys.argv[2] if len(sys.argv) > 2 else None + + success = hex_to_jpg(input_file, output_file) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/develop.md b/develop.md new file mode 100644 index 0000000..fd226fd --- /dev/null +++ b/develop.md @@ -0,0 +1,1370 @@ +# Camera 框架使用与开发手册 + +> 适用范围:`camera/`(`project/sf-pkgs/full_deploy/host/ov2640/0.0.2/camera/`) +> 目标平台:SF32LB52X / SF32LB56X,RT-Thread 操作系统 + +--- + +## 目录 + +1. [架构概述](#1-架构概述) +2. [目录结构](#2-目录结构) +3. [快速上手(用户指南)](#3-快速上手用户指南) + - 3.1 单帧拍照 + - 3.2 连续流式采集 + - 3.3 修改图像参数 +4. [配置参考](#4-配置参考) + - 4.1 Kconfig 选项 + - 4.2 关键宏与常量 +5. [内存管理](#5-内存管理) +6. [API 参考](#6-api-参考) + - 6.1 实例生命周期与状态机 + - 6.2 Handle 层 API + - 6.3 错误码 + - 6.4 OV2640 扩展控制命令表 +7. [开发指南——接入新摄像头传感器](#7-开发指南接入新摄像头传感器) +8. [开发指南——接入新数据总线](#8-开发指南接入新数据总线) +9. [分层交互时序](#9-分层交互时序) +10. [常见问题](#10-常见问题) + +--- + +## 1. 架构概述 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 应用层代码 │ +│ cam_capture.c / lcd_preview.c / … │ +└───────────────────────┬─────────────────────────────────┘ + │ camera_handle.h API +┌───────────────────────▼─────────────────────────────────┐ +│ Handle 层 camera_handle.c │ +│ · 统一错误码 · 流队列(双槽) · 信号量同步 │ +│ · 单帧 / 流 两种采集模式 │ +└───────────┬───────────────────────────┬─────────────────┘ + │ rt_device_find/open/ │ psram_heap.h + │ read/control │ (PSRAM 帧缓冲) +┌───────────▼───────────────────────────────────────────┐ +│ RT-Thread 设备层 "ov2640" │ +│ driver/ov2640/ov2640.c │ +│ · 传感器寄存器配置 · 模式切换 · 帧分发 │ +└──────────┬────────────────────────────────────────────┘ + │ bus_adapter_t * (data_bus_adapter.h) + ┌────────▼──────────┐ ┌──────────────────┐ + │ DVP 数据总线 │ │ 控制总线 SCCB │ + │ bus/data/dvp.c │ │ bus/control/ │ + │ GPTIM + DMA │ │ sccb.c I²C │ + └───────────────────┘ └──────────────────┘ +``` + +**关键设计原则** + +| 原则 | 体现 | +|------|------| +| 传感器与总线解耦 | `ov2640_device_t` 仅持有 `bus_adapter_t *`,不直接包含 `dvp_handle_t` | +| 总线可替换 | 任何实现了 `bus_adapter_ops_t` 的模块都可注册并被传感器驱动使用 | +| ISR 安全的帧通知 | DMA ISR → `dvp_dispatch_frame()` → 用户回调,链路仅 3 层,无额外锁 | +| 丢帧可观测 | Handle 层维护 `dropped_count`,首次及每 32 次丢帧通过 `LOG_W` 告警 | + +--- + +## 2. 目录结构 + +``` +camera/ +├── README.md ← 本文件 +├── handle/ +│ ├── camera_handle.h ← 公共 API 与类型定义(用户只需包含此文件) +│ └── camera_handle.c ← Handle 层实现 +├── mem/ +│ ├── psram_heap.h ← PSRAM 帧缓冲分配器接口 +│ └── psram_heap.c ← PSRAM memheap 实现 +├── bus/ +│ ├── control/ +│ │ ├── sccb.h ← SCCB/I²C 控制总线接口 +│ │ └── sccb.c +│ └── data/ +│ ├── data_bus_adapter.h ← 数据总线抽象层接口(注册/查找/分发) +│ ├── data_bus_adapter.c +│ ├── dvp.h ← DVP 具体实现接口 +│ └── dvp.c +└── driver/ + └── ov2640/ + ├── ov2640.h ← OV2640 RT-Thread 设备驱动接口 + 控制命令表 + ├── ov2640.c + ├── ov2640_regs.h ← 寄存器定义 + └── ov2640_settings.h ← 分辨率/格式预设表 +``` + +--- + +## 3. 快速上手(用户指南) + +应用层**只需**包含一个头文件: + +```c +#include "camera_handle.h" +``` + +### 3.1 单帧拍照 + +```c +/* 1. 声明实例(可放全局或局部) */ +static camera_handler_instance_t g_cam; + +/* 2. ops 表:告知 Handle 层传感器命令 ID */ +static const camera_device_ops_t g_cam_ops = { + .command_set = { + .set_pixformat = OV2640_CMD_SET_PIXFORMAT, + .set_framesize = OV2640_CMD_SET_FRAMESIZE, + .set_quality = OV2640_CMD_SET_QUALITY, + .start_stream = OV2640_CMD_START_STREAM, + .stop_stream = OV2640_CMD_STOP_STREAM, + }, +}; + +void app_camera_init(void) +{ + camera_handler_all_input_arg_t arg = { + .device_name = NULL, /* NULL → 使用默认值 "ov2640" */ + .device_ops = &g_cam_ops, + }; + + /* 初始化实例(不打开设备) */ + camera_handler_instance_init(&g_cam, &arg); + + /* 打开 RT-Thread 设备 */ + if (camera_init(&g_cam) != CAMERA_OK) + { + LOG_E("camera_init failed"); + return; + } + + /* 可选:修改默认分辨率/格式(初始默认为 JPEG VGA quality=10) */ + camera_capture_config_t cfg = { + .pixformat = PIXFORMAT_JPEG, + .framesize = FRAMESIZE_SVGA, + .quality = 12, + }; + camera_change_settings(&g_cam, &cfg); +} + +/* 帧缓冲建议放 PSRAM */ +static uint8_t *g_frame_buf; + +void app_capture_one_frame(void) +{ + if (g_frame_buf == NULL) + g_frame_buf = psram_heap_malloc(200 * 1024); /* 200 KB for JPEG */ + + camera_capture_request_t req = { + .buffer = g_frame_buf, + .buffer_size = 200 * 1024, + }; + + camera_handle_status_t ret = camera_capture_single(&g_cam, &req); + if (ret == CAMERA_OK) + { + /* req.frame_size 为本帧实际字节数 */ + do_something_with_jpeg(g_frame_buf, req.frame_size); + } +} +``` + +### 3.2 连续流式采集 + +```c +/* 双缓冲:建议放 PSRAM */ +static uint8_t *g_stream_buf[2]; + +void app_start_stream(void) +{ + psram_heap_init(); + for (int i = 0; i < 2; i++) + g_stream_buf[i] = psram_heap_malloc(200 * 1024); + + camera_stream_config_t cfg = { + .buffers[0] = g_stream_buf[0], + .buffers[1] = g_stream_buf[1], + .buffer_size = 200 * 1024, + }; + + if (camera_start_stream(&g_cam, &cfg) != CAMERA_OK) + { + LOG_E("start_stream failed"); + return; + } +} + +/* 消费线程 */ +void stream_consumer_thread(void *param) +{ + camera_stream_frame_t frame; + while (1) + { + /* 阻塞等待下一帧,超时 500 ms */ + camera_handle_status_t ret = + camera_get_stream_frame(&g_cam, &frame, rt_tick_from_millisecond(500)); + if (ret != CAMERA_OK) + continue; + + /* frame.buffer → 帧数据指针(DMA 缓冲,处理须在下两帧到来前完成) + * frame.frame_size → 本帧字节数 + * frame.sequence → 单调递增帧序号 */ + display_frame(frame.buffer, frame.frame_size); + } +} + +void app_stop_stream(void) +{ + camera_stop_stream(&g_cam); +} +``` + +> **注意**:`camera_get_stream_frame()` 返回的 `frame.buffer` 是 DMA 缓冲区的指针,**不复制数据**。调用方须在下一轮双缓冲(约 2 帧内)完成处理或另行拷贝,否则数据会被覆盖。 + +### 3.3 修改图像参数 + +`camera_change_settings()` 不可在流运行时调用;需先 `camera_stop_stream()`,修改完再 `camera_start_stream()`。 + +```c +camera_capture_config_t cfg = { + .pixformat = PIXFORMAT_RGB565, + .framesize = FRAMESIZE_QVGA, + .quality = 10, +}; +camera_change_settings(&g_cam, &cfg); +/* 函数内部包含 500 ms AEC/AWB 收敛等待,属正常行为 */ +``` + +若需修改亮度、对比度等细粒度参数,直接操作 RT-Thread 设备: + +```c +rt_device_control(g_cam.camera_device, + OV2640_CMD_SET_BRIGHTNESS, + (void *)(rt_ubase_t)1); /* +1 档亮度 */ +``` + +完整命令 ID 见 `driver/ov2640/ov2640.h` 中的 `OV2640_CMD_*` 宏。 + +--- + +## 4. 配置参考 + +### 4.1 Kconfig 选项 + +通过 `menuconfig → ov2640 camera` 进入配置界面。 + +**DVP 配置** + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| `OV2640_DVP_PINGPONG_BUFFER_SIZE` | 8192 | DVP DMA 乒乓缓冲区大小(字节),范围 1024–65536。该缓冲区是 DMA 中转暂存区,与帧缓冲无关;对于宽度 > 640 的分辨率或非 JPEG 模式需适当调大。 | +| `OV2640_DVP_DATA_PIN_BASE` | 0 | DVP D0–D7 数据引脚起始编号,**请勿修改**,仅供内部使用。 | +| `OV2640_DVP_VSYNC_PIN` | 42 | VSYNC 信号连接的 GPIO 引脚编号(PAD_PA*x*)。用于帧同步中断,须与硬件原理图一致。 | + +**SCCB(I²C 控制总线)配置** + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| `OV2640_SCCB_I2C_BUS_NAME` | `"i2c1"` | RT-Thread I²C 总线设备名,须与 `rt_device_register` 时一致。 | +| `OV2640_SCCB_TIMEOUT_MS` | 1000 | SCCB 单次 I²C 操作超时(毫秒)。总线频繁超时时可适当增大,或检查总线负载。 | +| `OV2640_SCCB_MAX_HZ` | 100000 | SCCB 时钟频率上限(Hz),范围 10000–400000。OV2640 标准为 100 kHz,部分板卡可提升至 400 kHz(Fast-mode)。 | + +**摄像头通用配置** + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| `OV2640_CAMERA_READ_TIMEOUT_MS` | 1000 | 单帧同步读取超时(毫秒),范围 100–10000。低帧率或长曝光场景下出现 `CAMERA_ERRORTIMEOUT` 时可增大。 | +| `OV2640_DVP_XCLK_PIN` | -1 | XCLK 输出引脚编号(PAD_PA*x* 索引)。`-1` 表示不由驱动生成 XCLK(使用外部时钟源)。启用时需在 `main.c` 中手动配置引脚复用,详见下方说明。 | +| `OV2640_DVP_XCLK_FREQ` | 12000000 | XCLK 频率(Hz),可选 6 MHz(`OV2640_DVP_XCLK_FREQ_6M`)或 12 MHz(`OV2640_DVP_XCLK_FREQ_12M`)。GPTIM2 运行于 24 MHz,故仅支持这两档。 | + +> **XCLK 引脚配置**:启用 XCLK 输出时,须在打开摄像头设备前手动配置引脚复用,例如: +> ```c +> HAL_PIN_Set(PAD_PA00 + OV2640_DVP_XCLK_PIN, GPTIM2_CH1, PIN_NOPULL, 1); +> ``` +> 具体 `PAD_PA*` 偏移量取决于 XCLK 连接的引脚,参见硬件原理图。 + +### 4.2 关键宏与常量 + +| 宏 | 定义位置 | 说明 | +|----|---------|------| +| `OV2640_DEVICE_NAME` | `ov2640.h` | 驱动注册/查找用的 RT-Thread 设备名 | +| `CAMERA_DEFAULT_DEVICE_NAME` | `camera_handle.h` | Handle 层在未指定名称时的回退值,与上行保持一致 | +| `DVP_BUS_ADAPTER_NAME` | `dvp.h` | DVP 适配器在注册表中的名字(`"dvp"`) | +| `BUS_ADAPTER_MAX` | `data_bus_adapter.c` | 总线适配器注册表容量(默认 8) | +| `DEBUG_DVP` | `dvp.c` | 置 1 开启 DVP 调试快照及 hex dump(默认 0) | + +--- + +## 5. 内存管理 + +帧缓冲推荐放置于 PSRAM,以避免挤占有限的内部 SRAM: + +```c +psram_heap_init(); /* 初始化 PSRAM 堆(幂等) */ +void *buf = psram_heap_malloc(200 * 1024); /* 分配帧缓冲 */ +psram_heap_free(buf); /* 释放 */ +``` + +`psram_heap_init()` 使用 `INIT_BOARD_EXPORT` 在系统启动时自动调用,通常无需手动初始化。 + +**DMA 可达性要求**:DVP DMA 引擎对 PSRAM 地址可达,但若平台有 DMA 内存限制,请确认目标地址落在 DMA 能访问的地址窗口内。 + +**DVP DMA 乒乓缓冲区的放置约束**:乒乓缓冲区(`dvp_pingpong_buffer`)由链接脚本静态分配在 `.dvp_pingpong` 段,其起始地址 `DVP_PINGPONG_START_ADDR` 刻意**避开** `0x2000_0000–0x2001_FFFF`(128 KB zero-wait-cycle SRAM,与 Cortex-M33 D-TCM 共享)。原因:该区域的 DMA 访问存在 **3 个周期**的仲裁延迟,当 CPU 与 DMA 同时访问时,DMA 会发生采集丢数,造成画面花屏或帧数据损坏。若自行分配缓冲区(例如调试场景),请确保目标地址不落在 `0x2000_0000–0x2001_FFFF` 范围内。 + +**DCache 一致性**:若平台开启了 D-Cache,DMA 写入完成后 CPU 直接读取帧缓冲区可能命中脏缓存行而得到旧数据。必须在使用帧数据前执行 Cache Invalidate: + +```c +/* DMA 完成后,CPU 读取前 */ +mpu_dcache_invalidate((uint32_t *)frame.buffer, (uint32_t)frame.frame_size); +``` + +若帧数据需要由显示 DMA 读取(写回型 Cache),则在派发前执行 Clean: + +```c +mpu_dcache_clean(frame.buffer, frame.frame_size); +``` + +DVP DMA 乒乓缓冲区本身建议通过 MPU 配置为 Non-Cacheable,以彻底消除一致性问题。帧目标缓冲区(PSRAM)同理,驱动层在派发帧回调前不会自动执行 Invalidate,由应用层负责。 + +--- + +## 6. API 参考 + +### 6.1 实例生命周期与状态机 + +``` + camera_handler_instance_init() + │ + ┌───────▼──────────┐ + │ UNINITIALIZED │ + └───────┬──────────┘ + camera_init()│ + ┌───────▼──────────┐ camera_change_settings() + │ IDLE │◄─────────────────────────────┐ + └──────┬─────┬─────┘ (500 ms AEC 等待) │ + camera_start_stream() │ camera_capture_single() │ + │ │ (阻塞直到帧就绪或超时) │ + ┌────────▼────────┐ └────────────────────────────────────┘ + │ STREAMING │ + │ │ camera_get_stream_frame()(消费帧) + └────────┬────────┘ + │ camera_stop_stream() + ▼ + IDLE ──── camera_deinit() ──► RELEASED +``` + +| 状态 | 含义 | 可调 API | +|------|------|----------| +| UNINITIALIZED | 实例内存未初始化 | `camera_handler_instance_init` | +| IDLE | 设备已打开,未在采集 | `camera_change_settings`, `camera_capture_single`, `camera_start_stream`, `camera_deinit` | +| STREAMING | DMA 流运行中 | `camera_get_stream_frame`, `camera_stop_stream`, `camera_deinit` | +| RELEASED | `camera_deinit` 后 | — | + +> `camera_deinit` 在 STREAMING 状态下会自动先调用 `camera_stop_stream`,再关闭设备和销毁信号量。 + +--- + +### 6.2 Handle 层 API + +> **线程安全总则**:所有 Handle 层 API 均**不可从中断上下文调用**(内部使用信号量和 `rt_device_control`)。帧回调由框架内部注入,应用代码无需直接调用。 + +--- + +#### `camera_handler_instance_init` + +```c +camera_handle_status_t camera_handler_instance_init( + camera_handler_instance_t *instance, + camera_handler_all_input_arg_t *input_arg); +``` + +将 `*instance` 清零并填入设备名和 ops 指针。**不打开 RT-Thread 设备,也不分配任何内核对象**。 + +| 参数 | 方向 | 说明 | +|------|------|------| +| `instance` | in | 要初始化的实例,不可为 NULL | +| `input_arg` | in/可 NULL | 若为 NULL 或 `device_name` 为 NULL,设备名回退为 `CAMERA_DEFAULT_DEVICE_NAME`(`"ov2640"`) | + +默认预设:`pixformat = PIXFORMAT_JPEG`,`framesize = FRAMESIZE_VGA`,`quality = 10`。 + +--- + +#### `camera_init` + +```c +camera_handle_status_t camera_init(camera_handler_instance_t *instance); +``` + +调用 `rt_device_find` + `rt_device_open`,标记 `instance->is_open = RT_TRUE`。**幂等**:已打开时立即返回 `CAMERA_OK`。 + +前置条件:`instance->device_ops` 必须已设置(否则返回 `CAMERA_ERRORRESOURCE`)。 + +--- + +#### `camera_deinit` + +```c +camera_handle_status_t camera_deinit(camera_handler_instance_t *instance); +``` + +若当前处于 STREAMING 状态,内部先调用 `camera_stop_stream()`;再关闭 RT-Thread 设备,最后销毁信号量(若已创建)。实例可在 `camera_handler_instance_init` 后重新使用。 + +--- + +#### `camera_change_settings` + +```c +camera_handle_status_t camera_change_settings( + camera_handler_instance_t *instance, + const camera_capture_config_t *config); +``` + +依次下发 `set_pixformat` → `set_framesize` → `set_quality` 三条控制命令,成功后保存至 `instance->active_config`,并**阻塞 500 ms** 等待 AEC/AWB 收敛。 + +| 约束 | 说明 | +|------|------| +| 不可在 STREAMING 期间调用 | 返回 `CAMERA_ERRORRESOURCE` | +| 设备必须已打开 | 否则返回 `CAMERA_ERRORRESOURCE` | + +--- + +#### `camera_capture_single` + +```c +camera_handle_status_t camera_capture_single( + camera_handler_instance_t *instance, + camera_capture_request_t *request); +``` + +启动一次 DMA 采集,在驱动信号量上阻塞,帧就绪后填写 `request->frame_size` 并返回。超时由 `OV2640_CAMERA_READ_TIMEOUT_MS` 控制。 + +| 参数字段 | 说明 | +|----------|------| +| `request->buffer` | 目标缓冲区,须 DMA 可达(推荐 PSRAM) | +| `request->buffer_size` | 缓冲区容量(字节),须足以容纳一帧 | +| `request->frame_size` | **输出**:本帧实际字节数(成功时填写) | + +> **注意**:若函数超时返回,DVP DMA 可能仍在运行。建议超时后调用 `camera_deinit` 再重新 `camera_init` 彻底重置状态。 + +--- + +#### `camera_start_stream` + +```c +camera_handle_status_t camera_start_stream( + camera_handler_instance_t *instance, + const camera_stream_config_t *config); +``` + +懒初始化帧信号量(仅第一次),排空遗留令牌,重置队列,向驱动发送 `START_STREAM` 命令(内部通过 `camera_stream_start_args_t` 注入帧回调)。 + +| 参数字段 | 说明 | +|----------|------| +| `config->buffers[0/1]` | 两块 DMA 可达的帧缓冲区,须在 `camera_stop_stream` 返回前保持有效 | +| `config->buffer_size` | 每块缓冲区大小(字节) | + +--- + +#### `camera_get_stream_frame` + +```c +camera_handle_status_t camera_get_stream_frame( + camera_handler_instance_t *instance, + camera_stream_frame_t *frame, + rt_int32_t timeout); +``` + +在信号量上阻塞,成功时从队列尾部取出一帧浅拷贝到 `frame`。 + +| 参数 | 说明 | +|------|------| +| `timeout` | RT-Thread tick 数;`RT_WAITING_FOREVER` 永久等待;`0` 非阻塞轮询 | + +`frame.buffer` 指向 DMA 缓冲区,**不复制数据**。必须在下两帧到来前完成处理或另行拷贝,否则数据被覆盖。 + +`frame.sequence` 是驱动层单调递增帧计数器,可用于检测丢帧:连续两次获取的 sequence 差值 > 1 说明 Handle 层队列满时发生了覆盖丢帧。 + +--- + +#### `camera_stop_stream` + +```c +camera_handle_status_t camera_stop_stream(camera_handler_instance_t *instance); +``` + +向驱动发送 `STOP_STREAM` 命令(内部:先 `bus_adapter_abort_capture` 停 DMA,再在临界区内清空流状态),然后将队列与 `dropped_count` 归零。**幂等**:流未激活时返回 `CAMERA_OK`。 + +--- + +### 6.3 错误码 + +| 枚举值 | 数值 | 含义 | 典型触发场景 | +|--------|------|------|-------------| +| `CAMERA_OK` | 0 | 成功 | — | +| `CAMERA_ERROR` | 1 | 通用错误 | 驱动返回了未映射的 RT-Thread 错误码 | +| `CAMERA_ERRORTIMEOUT` | 2 | 操作超时 | `camera_capture_single` 超时;`camera_get_stream_frame` 等待超时 | +| `CAMERA_ERRORRESOURCE` | 3 | 资源不可用 | 设备未打开;ops 未注册;流已激活时调用 `camera_change_settings` | +| `CAMERA_ERRORPARAMETER` | 4 | 参数非法 | `instance` 或 `config` 为 NULL;`buffer_size == 0` | +| `CAMERA_ERRORNOMEMORY` | 5 | 内存不足 | 信号量创建失败(内核对象池耗尽) | +| `CAMERA_ERRORISR` | 6 | ISR 上下文不允许 | 预留,当前版本未使用 | + +--- + +### 6.4 OV2640 扩展控制命令表 + +通过 `rt_device_control(instance->camera_device, OV2640_CMD_*, arg)` 直接访问传感器细粒度参数,绕过 Handle 层封装。整数参数须转型为 `(void *)(rt_ubase_t)value`。 + +**图像格式与分辨率** + +| 命令 | 值 | 参数类型 | 说明 | +|------|----|----------|------| +| `OV2640_CMD_SET_PIXFORMAT` | 0x01 | `pixformat_t` | 像素格式(JPEG / RGB565 / YUV422 / RAW8) | +| `OV2640_CMD_SET_FRAMESIZE` | 0x02 | `framesize_t` | 帧分辨率(96×96 → UXGA 1600×1200) | + +**图像质量** + +| 命令 | 值 | 参数 | 说明 | +|------|----|------|------| +| `OV2640_CMD_SET_BRIGHTNESS` | 0x03 | int −2…+2 | 亮度 | +| `OV2640_CMD_SET_CONTRAST` | 0x04 | int −2…+2 | 对比度 | +| `OV2640_CMD_SET_SATURATION` | 0x05 | int −2…+2 | 饱和度 | +| `OV2640_CMD_SET_QUALITY` | 0x06 | int 0…63 | JPEG 压缩质量(值越小文件越小,画质越低) | +| `OV2640_CMD_SET_SHARPNESS` | 0x1D | int −2…+2 | 锐度(OV2640 硬件无此功能,写入无效) | +| `OV2640_CMD_SET_DENOISE` | 0x1E | int level | 降噪级别(OV2640 硬件无此功能,写入无效) | +| `OV2640_CMD_SET_GAINCEILING` | 0x1C | `gainceiling_t` | AGC 增益上限 | + +**白平衡** + +| 命令 | 值 | 参数 | 说明 | +|------|----|------|------| +| `OV2640_CMD_SET_WHITEBAL` | 0x0A | int 0/1 | 开关硬件白平衡 | +| `OV2640_CMD_SET_AWB_GAIN` | 0x0E | int 0/1 | 开关 AWB 增益 | +| `OV2640_CMD_SET_WB_MODE` | 0x15 | int 0–4 | 0=自动, 1=晴天, 2=阴天, 3=办公室, 4=家庭 | + +**曝光与增益** + +| 命令 | 值 | 参数 | 说明 | +|------|----|------|------| +| `OV2640_CMD_SET_GAIN_CTRL` | 0x0B | int 0/1 | 开关 AGC(自动增益控制) | +| `OV2640_CMD_SET_EXPOSURE_CTRL` | 0x0C | int 0/1 | 开关 AEC(自动曝光控制) | +| `OV2640_CMD_SET_AEC2` | 0x0D | int 0/1 | 开关 AEC2(带符号曝光补偿) | +| `OV2640_CMD_SET_AGC_GAIN` | 0x0F | int 0–30 | AGC 手动增益值 | +| `OV2640_CMD_SET_AEC_VALUE` | 0x13 | int 0–1200 | AEC 手动曝光值 | +| `OV2640_CMD_SET_AE_LEVEL` | 0x16 | int −2…+2 | 自动曝光目标亮度偏置 | + +**图像处理** + +| 命令 | 值 | 参数 | 说明 | +|------|----|------|------| +| `OV2640_CMD_SET_HMIRROR` | 0x07 | int 0/1 | 水平镜像 | +| `OV2640_CMD_SET_VFLIP` | 0x08 | int 0/1 | 垂直翻转 | +| `OV2640_CMD_SET_COLORBAR` | 0x09 | int 0/1 | 开启彩条测试图(用于验证数据通路) | +| `OV2640_CMD_SET_SPECIAL_EFFECT` | 0x14 | int 0–6 | 特效:0=正常, 1=负片, 2=黑白, 3=红调, 4=绿调, 5=蓝调, 6=复古 | +| `OV2640_CMD_SET_DCW` | 0x17 | int 0/1 | 数字裁剪缩放 | +| `OV2640_CMD_SET_BPC` | 0x18 | int 0/1 | 坏点校正 | +| `OV2640_CMD_SET_WPC` | 0x19 | int 0/1 | 白点校正 | +| `OV2640_CMD_SET_RAW_GMA` | 0x1A | int 0/1 | RAW gamma 开关 | +| `OV2640_CMD_SET_LENC` | 0x1B | int 0/1 | 镜头阴影校正 | + +**数据总线与缓冲区** + +| 命令 | 值 | 参数 | 说明 | +|------|----|------|------| +| `OV2640_CMD_SET_FRAME_BUFFER` | 0x1F | `void *` | 更新帧缓冲区指针(不重启 DMA) | +| `OV2640_CMD_SET_FRAME_BUFFER_SIZE` | 0x20 | `uint32_t` | 更新帧缓冲区大小(字节) | +| `OV2640_CMD_SET_PINGPONG_SIZE` | 0x21 | `uint32_t` | 运行时调整 DVP DMA 乒乓缓冲大小 | + +--- + +## 7. 开发指南——接入新摄像头传感器 + +以下以接入虚构的 `OV5640` 为例,给出**可直接参考的最小完整实现**。各步骤严格对应框架的分层约定,如无特殊说明,不需要修改 Handle 层任何代码。 + +### 步骤一:创建驱动目录 + +``` +camera/driver/ov5640/ + ov5640.h ← 设备实例类型、命令 ID、注册声明 + ov5640.c ← RT-Thread 设备 ops 实现 + ov5640_regs.h ← 寄存器地址常量 +``` + +> **为什么不放 `camera/` 根目录?** 框架把传感器驱动和总线适配器各自隔离,未来可以无摩擦地换传感器或换总线。 + +### 步骤二:定义流状态结构体与头文件 + +```c +/* ov5640.h */ +#pragma once + +#include "camera_handle.h" /* camera_handle_status_t, camera_stream_frame_t … */ +#include "data_bus_adapter.h" /* bus_adapter_t */ +#include "rtthread.h" +#include "rthw.h" /* rt_hw_interrupt_disable / enable */ + +#define OV5640_DEVICE_NAME "ov5640" +#define OV5640_I2C_ADDR 0x3C /* 7-bit */ + +/* ─── 控制命令 ID(与 Handle 层约定一一对应) ─── */ +#define OV5640_CMD_SET_PIXFORMAT 0x01 +#define OV5640_CMD_SET_FRAMESIZE 0x02 +#define OV5640_CMD_SET_QUALITY 0x06 +#define OV5640_CMD_START_STREAM 0x22 +#define OV5640_CMD_STOP_STREAM 0x23 +/* … 其余 SET_BRIGHTNESS / SET_HMIRROR 等按需添加 … */ + +/* ─── 流状态(与 ov2640_stream_state_t 结构相同) ─── */ +typedef struct { + void *buffers[2]; /* DMA 乒乓缓冲区指针 */ + uint32_t buffer_size; /* 每块缓冲区大小(字节) */ + uint8_t active_buffer_index; /* 当前 DMA 写入的缓冲区下标(0/1) */ + uint32_t sequence; /* 单调递增帧序号 */ + /* + * frame_callback != RT_NULL ⟺ 流处于"已激活"状态。 + * ISR 和 START/STOP 命令均以临界区保护此字段的读写, + * 严禁在临界区外直接写入。 + */ + camera_frame_callback_t frame_callback; + void *callback_context; +} ov5640_stream_state_t; + +/* ─── 传感器设备私有数据 ─── */ +typedef struct { + bus_adapter_t *data_bus; /* DVP / CSI 适配器(通过名字查找) */ + ov5640_stream_state_t stream; + rt_sem_t frame_sem; /* 单帧拍照信号量 */ + void *snap_buffer; /* 单帧拍照目标缓冲区 */ + uint32_t snap_size; /* 本次单帧拍照实际字节数 */ +} ov5640_device_t; + +int ov5640_device_register(void); +``` + +### 步骤三:实现帧回调(ISR 上下文) + +帧回调在 DVP 中断 / DMA 完成中断中被调用。必须: + +- 在临界区内**原子地读取** `frame_callback` 和 `callback_context`,防止与 START/STOP 竞争; +- 回调结束后立即重装 DMA(`rearm_capture`),若重装失败则中止采集并清除流状态。 + +```c +static void ov5640_frame_ready_callback(bus_adapter_t *self, + const bus_frame_t *bus_frame, + void *user_data) +{ + ov5640_device_t *cam = (ov5640_device_t *)user_data; + rt_base_t level; + + /* ① 在临界区内原子地取出回调指针快照 */ + level = rt_hw_interrupt_disable(); + camera_frame_callback_t frame_callback = cam->stream.frame_callback; + void *callback_context = cam->stream.callback_context; + rt_hw_interrupt_enable(level); + + /* ② frame_callback == RT_NULL 表示流未激活,直接返回 */ + if (frame_callback == RT_NULL) + return; + + /* ③ 构造应用侧帧描述符(浅拷贝,不复制像素数据) */ + camera_stream_frame_t frame = { + .buffer = bus_frame->buffer, + .buffer_size = cam->stream.buffer_size, + .frame_size = bus_frame->length, + .sequence = cam->stream.sequence++, + .buffer_index = cam->stream.active_buffer_index, + }; + + /* ④ 切换乒乓缓冲区并重装 DMA */ + cam->stream.active_buffer_index ^= 1; + void *next_buf = cam->stream.buffers[cam->stream.active_buffer_index]; + + if (bus_adapter_rearm_capture(cam->data_bus, next_buf, + cam->stream.buffer_size) != BUS_OK) + { + /* 重装失败:原子清除流状态,中止 DMA */ + level = rt_hw_interrupt_disable(); + rt_memset(&cam->stream, 0, sizeof(cam->stream)); /* frame_callback → NULL */ + rt_hw_interrupt_enable(level); + bus_adapter_abort_capture(cam->data_bus); + return; + } + + /* ⑤ 通知 Handle 层 */ + frame_callback(callback_context, &frame); +} +``` + +### 步骤四:实现 RT-Thread 设备 ops + +#### `ov5640_open` + +```c +static rt_err_t ov5640_open(rt_device_t dev, rt_uint16_t oflag) +{ + ov5640_device_t *cam = (ov5640_device_t *)dev->user_data; + + /* 初始化控制总线(SCCB over I²C),传入传感器 7-bit 地址 */ + if (sccb_init(SCCB_USE_IIC) != 0) + { + LOG_E("ov5640: sccb init failed"); + return -RT_ERROR; + } + + /* 查找数据总线适配器(确保 INIT_BOARD_EXPORT 已执行) */ + cam->data_bus = bus_adapter_find(DVP_BUS_ADAPTER_NAME); + if (cam->data_bus == RT_NULL) + { + LOG_E("ov5640: data bus '%s' not found", DVP_BUS_ADAPTER_NAME); + return -RT_ERROR; + } + + /* 硬件上电 + 写入默认寄存器表 */ + ov5640_hw_reset(); + if (ov5640_write_default_regs() != 0) + return -RT_ERROR; + + /* 初始化 DVP 总线(引脚、时钟、中断、乒乓 DMA) */ + dvp_config_t dvp_cfg = { + .mode = DVP_MODE_JPEG, + .vsync_pin = CONFIG_OV2640_DVP_VSYNC_PIN, + .data_pin_base = CONFIG_OV2640_DVP_DATA_PIN_BASE, + .xclk_pin = CONFIG_OV2640_DVP_XCLK_PIN, + .xclk_freq = CONFIG_OV2640_DVP_XCLK_FREQ, + .pingpong_size = CONFIG_OV2640_DVP_PINGPONG_BUFFER_SIZE, + }; + if (bus_adapter_init(cam->data_bus, &dvp_cfg) != BUS_OK) + return -RT_ERROR; + + rt_memset(&cam->stream, 0, sizeof(cam->stream)); + cam->frame_sem = RT_NULL; /* 懒创建,在 read 时按需分配 */ + + LOG_I("ov5640: opened"); + return RT_EOK; +} +``` + +#### `ov5640_close` + +```c +static rt_err_t ov5640_close(rt_device_t dev) +{ + ov5640_device_t *cam = (ov5640_device_t *)dev->user_data; + + /* 若流仍在运行,强制停止 */ + rt_base_t level = rt_hw_interrupt_disable(); + int streaming = (cam->stream.frame_callback != RT_NULL); + rt_hw_interrupt_enable(level); + if (streaming) + bus_adapter_abort_capture(cam->data_bus); + + bus_adapter_deinit(cam->data_bus); + sccb_deinit(); + + if (cam->frame_sem != RT_NULL) + { + rt_sem_delete(cam->frame_sem); + cam->frame_sem = RT_NULL; + } + + rt_memset(&cam->stream, 0, sizeof(cam->stream)); + LOG_I("ov5640: closed"); + return RT_EOK; +} +``` + +#### `ov5640_read`(单帧拍照模式) + +`rt_device_read` 被 `camera_capture_single` 调用。参数 `size` 为缓冲区大小,返回实际字节数。 + +```c +static rt_size_t ov5640_read(rt_device_t dev, rt_off_t pos, + void *buffer, rt_size_t size) +{ + ov5640_device_t *cam = (ov5640_device_t *)dev->user_data; + + /* 懒创建信号量(只创建一次) */ + if (cam->frame_sem == RT_NULL) + { + cam->frame_sem = rt_sem_create("ov5640_snap", 0, RT_IPC_FLAG_FIFO); + if (cam->frame_sem == RT_NULL) + return 0; + } + + cam->snap_buffer = buffer; + cam->snap_size = 0; + + /* 启动单帧 DMA,帧中断中调用 ov5640_snap_isr_callback */ + if (bus_adapter_start_capture(cam->data_bus, buffer, size, + ov5640_snap_isr_callback, cam) != BUS_OK) + return 0; + + /* 阻塞等待,超时由 Kconfig 控制 */ + rt_err_t ret = rt_sem_take(cam->frame_sem, + rt_tick_from_millisecond(OV5640_READ_TIMEOUT_MS)); + if (ret != RT_EOK) + { + bus_adapter_abort_capture(cam->data_bus); + return 0; + } + + return (rt_size_t)cam->snap_size; +} + +/* 单帧拍照回调(ISR 上下文)*/ +static void ov5640_snap_isr_callback(bus_adapter_t *self, + const bus_frame_t *bus_frame, + void *user_data) +{ + ov5640_device_t *cam = (ov5640_device_t *)user_data; + cam->snap_size = bus_frame->length; + rt_sem_release(cam->frame_sem); +} +``` + +#### `ov5640_control`(START/STOP 流 + 图像参数) + +**关键约定**: +- `OV5640_CMD_START_STREAM`:先填充缓冲区信息,**最后**在临界区发布 `frame_callback`——这样 ISR 不会在缓冲区初始化完成前看到非 NULL 的回调指针。 +- `OV5640_CMD_STOP_STREAM`:**先** `abort_capture` 停止 DMA,**再**在临界区清零流状态——防止 ISR 在清理期间读到半初始化状态。 + +```c +static rt_err_t ov5640_control(rt_device_t dev, int cmd, void *args) +{ + ov5640_device_t *cam = (ov5640_device_t *)dev->user_data; + rt_base_t level; + + switch (cmd) + { + /* ── 格式 / 分辨率 / 质量 ── */ + case OV5640_CMD_SET_PIXFORMAT: + return ov5640_set_pixformat(cam, (pixformat_t)(rt_ubase_t)args); + + case OV5640_CMD_SET_FRAMESIZE: + return ov5640_set_framesize(cam, (framesize_t)(rt_ubase_t)args); + + case OV5640_CMD_SET_QUALITY: + return ov5640_set_quality(cam, (int)(rt_ubase_t)args); + + /* ── 连续流启动 ── */ + case OV5640_CMD_START_STREAM: + { + camera_stream_start_args_t *sa = (camera_stream_start_args_t *)args; + if (sa == RT_NULL || sa->frame_callback == RT_NULL) + return -RT_EINVAL; + + /* ① 先在临界区外填充缓冲区信息(无竞态) */ + cam->stream.buffers[0] = sa->buffers[0]; + cam->stream.buffers[1] = sa->buffers[1]; + cam->stream.buffer_size = sa->buffer_size; + cam->stream.active_buffer_index = 0; + cam->stream.sequence = 0; + + /* ② 注册 DVP 帧回调并启动 DMA(此时 frame_callback 仍为 NULL) */ + bus_adapter_set_frame_callback(cam->data_bus, + ov5640_frame_ready_callback, cam); + if (bus_adapter_start(cam->data_bus) != BUS_OK) + { + level = rt_hw_interrupt_disable(); + rt_memset(&cam->stream, 0, sizeof(cam->stream)); + rt_hw_interrupt_enable(level); + return -RT_ERROR; + } + + /* ③ DMA 已运行,最后原子发布回调指针 ── ISR 从此刻起才"看到"流 */ + level = rt_hw_interrupt_disable(); + cam->stream.callback_context = sa->callback_context; + cam->stream.frame_callback = sa->frame_callback; + rt_hw_interrupt_enable(level); + + return RT_EOK; + } + + /* ── 连续流停止 ── */ + case OV5640_CMD_STOP_STREAM: + { + /* ① 先停 DMA,确保 ISR 不再触发 */ + bus_adapter_abort_capture(cam->data_bus); + + /* ② 再清除流状态 */ + level = rt_hw_interrupt_disable(); + rt_memset(&cam->stream, 0, sizeof(cam->stream)); + rt_hw_interrupt_enable(level); + + return RT_EOK; + } + + default: + return -RT_EINVAL; + } +} +``` + +### 步骤五:注册 RT-Thread 设备 + +```c +/* ov5640.c 底部 */ +static ov5640_device_t s_ov5640_dev; +static struct rt_device s_ov5640_rtdev; + +static const struct rt_device_ops s_ov5640_ops = { + .open = ov5640_open, + .close = ov5640_close, + .read = ov5640_read, + .write = RT_NULL, + .control = ov5640_control, +}; + +int ov5640_device_register(void) +{ + rt_memset(&s_ov5640_dev, 0, sizeof(s_ov5640_dev)); + s_ov5640_rtdev.ops = &s_ov5640_ops; + s_ov5640_rtdev.user_data = &s_ov5640_dev; + return rt_device_register(&s_ov5640_rtdev, OV5640_DEVICE_NAME, + RT_DEVICE_FLAG_RDWR); +} +INIT_DEVICE_EXPORT(ov5640_device_register); +``` + +### 步骤六:更新应用侧 ops 表(Handle 层零改动) + +```c +/* main.c 或 cam_capture.c */ +static const camera_device_ops_t g_ov5640_ops = { + .command_set = { + .set_pixformat = OV5640_CMD_SET_PIXFORMAT, + .set_framesize = OV5640_CMD_SET_FRAMESIZE, + .set_quality = OV5640_CMD_SET_QUALITY, + .start_stream = OV5640_CMD_START_STREAM, + .stop_stream = OV5640_CMD_STOP_STREAM, + }, +}; + +camera_handler_all_input_arg_t arg = { + .device_name = OV5640_DEVICE_NAME, + .device_ops = &g_ov5640_ops, +}; +camera_handler_instance_t g_cam; +camera_handler_instance_init(&g_cam, &arg); +camera_init(&g_cam); +``` + +### 常见陷阱 + +| 陷阱 | 现象 | 正确做法 | +|------|------|----------| +| START_STREAM 时先发布 `frame_callback` 再填充 buffers | ISR 在 buffers 有效前进入,写入野指针 | 先填 buffers,最后发布 `frame_callback` | +| STOP_STREAM 时先清 stream 再 `abort_capture` | ISR 可能在清零后重新写入 `sequence` 或 `active_buffer_index` | 先 `abort_capture`,再清 stream | +| 在回调中直接写 `cam->stream.frame_callback` | 无临界区保护,与 STOP 竞争 | 回调内仅读快照,写操作统一在临界区内 | +| `bus_adapter_find` 在 board init 前调用 | 返回 NULL | 确保在 RT-Thread 初始化链(`INIT_BOARD_EXPORT`)之后打开设备 | +| `ov5640_read` 超时后不调用 `abort_capture` | DMA 仍运行,下次 `read` 时信号量立即触发,得到脏数据 | 超时后调用 `abort_capture`,或 `camera_deinit` + 重新 `camera_init` | + +--- + +## 8. 开发指南——接入新数据总线 + +以下以接入虚构的 `CSI`(MIPI Camera Serial Interface)为例,给出**所有 ops 函数的完整合约说明**。 + +> **命名约定**:适配器函数名前缀应与总线名保持一致(本例为 `csi_`),全局适配器实例命名为 `g__adapter`,私有状态命名为 `s__priv`。 + +### 步骤一:创建适配器模块 + +``` +camera/bus/data/csi/ + csi.h ← 适配器名字宏、配置结构体 + csi.c ← bus_adapter_ops_t 实现 + 注册 +``` + +### 步骤二:定义私有状态结构体 + +```c +/* csi.c */ +#include "data_bus_adapter.h" +#include "rtthread.h" +#include "rthw.h" + +#define CSI_BUS_ADAPTER_NAME "csi" + +typedef struct { + /* 硬件相关 */ + volatile uint32_t *reg_base; /* CSI 控制器寄存器基址 */ + void *dma_handle; /* 平台 DMA 句柄 */ + uint8_t lane_count; + uint32_t bit_rate_mbps; + + /* 帧状态(在帧中断与 start_capture / abort_capture 之间共享) */ + void *active_buffer; /* 当前 DMA 写入目标 */ + uint32_t active_buffer_size; + uint32_t last_frame_size; /* 最近一帧实际字节数 */ + uint32_t frame_sequence; + + /* 上层注册的回调(连续流模式)*/ + bus_frame_ready_callback_t user_callback; + void *user_data; + + /* 单帧捕获信号量(单帧拍照模式)*/ + rt_sem_t snap_sem; +} csi_priv_t; + +static csi_priv_t s_csi_priv; +static bus_adapter_t g_csi_adapter; /* 前向声明,供 ISR 使用 */ +``` + +### 步骤三:实现各 ops 函数 + +#### `init` ── 硬件初始化 + +**合约**:上电、配置引脚 / 时钟、分配 DMA 资源,**不启动传输**。成功返回 `BUS_OK`(0),失败返回负数错误码。 + +```c +static int csi_init(bus_adapter_t *self, const void *cfg) +{ + csi_priv_t *priv = (csi_priv_t *)self->priv; + const csi_config_t *c = (const csi_config_t *)cfg; + + priv->lane_count = c->lane_count; + priv->bit_rate_mbps = c->bit_rate_mbps; + priv->reg_base = (volatile uint32_t *)CSI_BASE_ADDR; + + /* 1. 配置引脚复用 */ + hal_pin_set_mux(c->d0_pin, PIN_MUX_CSI_D0); + /* … 其余数据线 / 时钟 / 同步信号引脚 … */ + + /* 2. 使能模块时钟 */ + HAL_RCC_EnableModule(RCC_MOD_CSI); + + /* 3. 分配 DMA 通道 */ + priv->dma_handle = platform_dma_alloc(DMA_CHANNEL_CSI); + if (priv->dma_handle == NULL) + return -BUS_ERR_NORESOURCE; + + /* 4. 分配单帧拍照信号量 */ + priv->snap_sem = rt_sem_create("csi_snap", 0, RT_IPC_FLAG_FIFO); + if (priv->snap_sem == RT_NULL) + return -BUS_ERR_NORESOURCE; + + return BUS_OK; +} +``` + +#### `deinit` ── 释放硬件资源 + +**合约**:在 `abort_capture` / `stop` 之后调用,释放 `init` 中分配的一切资源。 + +```c +static int csi_deinit(bus_adapter_t *self) +{ + csi_priv_t *priv = (csi_priv_t *)self->priv; + HAL_RCC_DisableModule(RCC_MOD_CSI); + platform_dma_free(priv->dma_handle); + priv->dma_handle = NULL; + if (priv->snap_sem != RT_NULL) + { + rt_sem_delete(priv->snap_sem); + priv->snap_sem = RT_NULL; + } + return BUS_OK; +} +``` + +#### `start` / `stop` ── 连续流控制 + +**合约**:`start` 使能硬件时钟 / DMA 流水线,使其持续接收帧;`stop` 停止流但**不释放资源**(资源由 `deinit` 释放)。 + +```c +static int csi_start(bus_adapter_t *self) +{ + csi_priv_t *priv = (csi_priv_t *)self->priv; + /* 使能帧中断 */ + platform_irq_enable(IRQ_CSI_FRAME_END, csi_frame_isr, self); + /* 打开 CSI 数据流 */ + REG_WRITE(priv->reg_base, CSI_CTRL_EN, 1); + return BUS_OK; +} + +static int csi_stop(bus_adapter_t *self) +{ + csi_priv_t *priv = (csi_priv_t *)self->priv; + REG_WRITE(priv->reg_base, CSI_CTRL_EN, 0); + platform_irq_disable(IRQ_CSI_FRAME_END); + return BUS_OK; +} +``` + +#### `start_capture` ── 单帧捕获 + +**合约**:配置 DMA 目标缓冲区并触发单次传输,**立即返回**(不阻塞)。传输完成由 ISR 通知(调用 `callback` 或释放 `snap_sem`)。若 `callback` 为 NULL,由适配器自行维护信号量(供 `ov_read` 等待)。 + +```c +static int csi_start_capture(bus_adapter_t *self, + void *buffer, uint32_t size, + bus_frame_ready_callback_t callback, + void *user_data) +{ + csi_priv_t *priv = (csi_priv_t *)self->priv; + priv->active_buffer = buffer; + priv->active_buffer_size = size; + priv->user_callback = callback; /* 可以为 NULL(单帧模式) */ + priv->user_data = user_data; + + /* 配置 DMA,传输完成后触发 csi_frame_isr */ + platform_dma_setup(priv->dma_handle, buffer, size, csi_frame_isr, self); + platform_dma_start(priv->dma_handle); + return BUS_OK; +} +``` + +#### `rearm_capture` ── ISR 内重装 DMA(连续流核心) + +**合约**: +- **只能在帧回调(ISR 上下文)内调用**——即在上层 `user_callback` 执行期间,或由传感器帧回调调用。 +- 更换 DMA 目标缓冲区并重新触发传输,**不停止时钟 / 流水线**,以实现零抖动乒乓切换。 +- 返回 `BUS_OK` 才能继续流;返回错误则上层(传感器帧回调)应调用 `abort_capture` 并停止流。 + +```c +static int csi_rearm_capture(bus_adapter_t *self, + void *next_buffer, uint32_t size) +{ + csi_priv_t *priv = (csi_priv_t *)self->priv; + if (next_buffer == NULL || size == 0) + return -BUS_ERR_PARAM; + + priv->active_buffer = next_buffer; + priv->active_buffer_size = size; + + /* 仅更新 DMA 目标地址,不重新启动 DMA 控制器 */ + platform_dma_update_dst(priv->dma_handle, next_buffer, size); + return BUS_OK; +} +``` + +#### `abort_capture` ── 中止当前帧 + +**合约**:立即停止当前进行中的 DMA 传输,**不关闭硬件时钟**(调用方可能紧接着再次 `start_capture`)。可在线程或 ISR 上下文中调用。 + +```c +static int csi_abort_capture(bus_adapter_t *self) +{ + csi_priv_t *priv = (csi_priv_t *)self->priv; + platform_dma_stop(priv->dma_handle); + priv->active_buffer = NULL; + return BUS_OK; +} +``` + +#### `get_frame_size` ── 查询最近帧大小 + +**合约**:在帧回调调用 `bus_frame_t.length` 字段之前,上层也可通过此 op 查询。返回字节数,0 表示尚无有效帧。 + +```c +static uint32_t csi_get_frame_size(bus_adapter_t *self) +{ + return ((csi_priv_t *)self->priv)->last_frame_size; +} +``` + +#### `set_frame_callback` ── 注册连续流回调 + +**合约**:仅保存指针,不做任何硬件操作。连续流下每帧就绪时 ISR 调用此回调。 + +```c +static int csi_set_frame_callback(bus_adapter_t *self, + bus_frame_ready_callback_t callback, + void *user_data) +{ + csi_priv_t *priv = (csi_priv_t *)self->priv; + priv->user_callback = callback; + priv->user_data = user_data; + return BUS_OK; +} +``` + +#### 帧中断 ISR + +ISR 是连接硬件与上层的桥梁,须在其中:① 读取帧大小;② 构造 `bus_frame_t`;③ 调用 `user_callback`(若已注册)或释放 `snap_sem`(单帧模式)。 + +```c +/* 由平台中断向量调用,参数为 bus_adapter_t * */ +static void csi_frame_isr(void *arg) +{ + bus_adapter_t *self = (bus_adapter_t *)arg; + csi_priv_t *priv = (csi_priv_t *)self->priv; + + /* ① 读取本帧实际传输字节数(平台 DMA 寄存器)*/ + uint32_t frame_bytes = platform_dma_get_transferred(priv->dma_handle); + priv->last_frame_size = frame_bytes; + + /* ② 构造帧描述符(指向 DMA 缓冲区,不复制数据)*/ + bus_frame_t frame = { + .buffer = priv->active_buffer, + .length = frame_bytes, + .sequence = priv->frame_sequence++, + }; + + /* ③-a 连续流模式:调用上层回调(传感器帧回调),由其负责 rearm_capture */ + if (priv->user_callback != NULL) + { + priv->user_callback(self, &frame, priv->user_data); + return; + } + + /* ③-b 单帧模式:释放信号量,上层 read 退出阻塞 */ + rt_sem_release(priv->snap_sem); +} +``` + +> **注意**:ISR 结束后硬件会继续准备下一帧,但 DMA 目标地址仍是旧缓冲区,直到 `rearm_capture` 更新。如果上层回调执行时间过长,下一帧可能会覆盖旧数据——这是乒乓设计存在的固有窗口,乒乓缓冲越大、写入速度越快,此窗口越安全。 + +### 步骤三:定义并注册适配器实例 + +```c +static const bus_adapter_ops_t s_csi_ops = { + .init = csi_init, + .deinit = csi_deinit, + .start = csi_start, + .stop = csi_stop, + .set_frame_callback = csi_set_frame_callback, + .start_capture = csi_start_capture, + .rearm_capture = csi_rearm_capture, + .abort_capture = csi_abort_capture, + .get_frame_size = csi_get_frame_size, + /* 不支持的 op 留 NULL;调用方须先检查是否为 NULL */ + .update_buffer = NULL, + .set_pingpong_size = NULL, +}; + +static bus_adapter_t g_csi_adapter = { + .name = CSI_BUS_ADAPTER_NAME, + .type = BUS_TYPE_CSI, /* 若需新类型,在 bus_adapter.h 的 bus_type_t 枚举中添加 */ + .ops = &s_csi_ops, + .priv = &s_csi_priv, +}; + +static int csi_adapter_register(void) +{ + return bus_adapter_register(&g_csi_adapter); +} +INIT_BOARD_EXPORT(csi_adapter_register); +``` + +### 步骤四:传感器驱动选择总线 + +```c +/* ov5640_open 中 */ +cam->data_bus = bus_adapter_find(CSI_BUS_ADAPTER_NAME); +if (cam->data_bus == RT_NULL) +{ + LOG_E("ov5640: CSI adapter not found"); + return -RT_ERROR; +} +``` + +### `bus_adapter_ops_t` 完整字段合约 + +| 字段 | 是否必须 | 调用上下文 | 说明 | +|------|---------|-----------|------| +| `init` | 是 | 线程 | 硬件初始化;成功返回 `BUS_OK` | +| `deinit` | 是 | 线程 | 释放一切资源;在 `stop`/`abort_capture` 之后调用 | +| `start` | 是 | 线程 | 启动连续流;`set_frame_callback` 应在 `start` 之前调用 | +| `stop` | 是 | 线程 | 停止连续流;不释放资源 | +| `set_frame_callback` | 是 | 线程 | 仅保存指针;不做硬件操作 | +| `start_capture` | 推荐 | 线程 | 启动单帧 DMA;立即返回;完成由 ISR 通知 | +| `rearm_capture` | 推荐 | **ISR/回调** | 更新 DMA 目标地址;返回 `BUS_OK` 才能继续流 | +| `abort_capture` | 推荐 | 线程/ISR | 停止当前 DMA;不关时钟;幂等 | +| `get_frame_size` | 推荐 | 线程/ISR | 返回最近帧字节数;0 表示无有效帧 | +| `update_buffer` | 可选 | 线程 | 仅更换缓冲区指针,不改变 DMA 状态 | +| `set_pingpong_size` | 可选 | 线程 | 运行时调整内部中间缓冲大小(DVP 专用) | + +--- + +## 9. 分层交互时序 + +### 单帧拍照 + +``` +应用 Handle 层 OV2640 驱动 DVP + │ │ │ │ + ├─camera_capture_single() │ │ + │ ├─rt_device_read()─────► │ + │ │ ├─bus_adapter_start_capture() + │ │ │ ├─DMA start + │ │ │ │ ···帧到来··· + │ │ │◄──frame_isr()─────┤ + │ │ ├─rt_sem_release() │ + │ │◄─────────────────────┤ │ + │◄──frame_size───────┤ │ │ +``` + +### 连续流 + +``` +应用线程 Handle 层 OV2640 驱动 DVP + │ │ │ │ + ├─camera_start_stream() │ │ + │ ├─rt_device_control(START_STREAM) │ + │ │ ├─bus_adapter_set_frame_callback() + │ │ ├─bus_adapter_start() + │ │ │ ├─DMA start + │ │ │ │ + │ (等待) │ │◄──dvp DMA ISR────┤ + │ │◄──ov2640_frame_ready_callback() │ + │ ├─入队 + sem_release() │ │ + │◄─camera_get_stream_frame() 返回 │ │ + │ 处理帧 … │ │◄──dvp DMA ISR────┤(下一帧) + │ │◄──callback() │ │ + │ ├─入队(若消费慢则覆盖旧帧 + LOG_W) │ +``` + +--- + +## 10. 常见问题 + +**Q1:`camera_init()` 返回 `CAMERA_ERRORRESOURCE`** +确认 `ov2640_device_register()` 已被 `INIT_DEVICE_EXPORT` 调用,且 RT-Thread 设备树中存在名为 `"ov2640"` 的设备(可用 `list_device` MSH 命令验证)。 + +**Q2:`camera_init()` 返回 `CAMERA_ERRORRESOURCE`(device_ops 为 NULL)** +调用 `camera_handler_instance_init()` 时必须传入非 NULL 的 `input_arg->device_ops`,Handle 层没有内建的 ops 表。 + +**Q3:串流一段时间后日志出现 `camera: stream queue full`** +消费线程处理帧的速度慢于传感器出帧速度,`dropped_count` 持续增加。对策:降低帧率(`OV2640_CMD_SET_FRAMESIZE` 选小分辨率)、缩短消费线程处理时间,或提高消费线程优先级。 + +**Q4:`camera_change_settings()` 后首帧画面明显偏暗/偏色** +这是正常现象——函数内有 500 ms AEC/AWB 收敛等待,若业务对稳定性要求更高,可在拍照前丢弃 2~3 帧。 + +**Q5:`DVP_DEFAULT_RESOURCE_CONFIG` 中的 VSYNC 引脚怎么确定** +VSYNC 因板型差异较大,以宏参数形式传入: + +```c +dvp_resource_config_t res = DVP_DEFAULT_RESOURCE_CONFIG(PAD_PA42 /* vsync */); +``` + +具体引脚参见硬件原理图。 + +**Q6:如何开启 DVP 调试快照** +在 `dvp.c` 顶部将 `#define DEBUG_DVP 0` 改为 `1`,重新编译后 `dump_buffer_hex()` 和 `snapshot_and_dump_buffer()` 函数以及 6 KB 快照缓冲区才会编入固件。 + +**Q7:`bus_adapter_find()` 返回 NULL** +DVP 适配器通过 `INIT_BOARD_EXPORT(dvp_adapter_register)` 在板级初始化阶段自动注册。若在板级初始化完成前调用(例如在 `main()` 首行),可能找不到适配器。确保在 RT-Thread 初始化完成后再打开摄像头。 + +**Q8:`camera_capture_single` 超时后,下次再调用立即得到一帧脏数据** +超时时 DVP DMA 可能仍在运行。超时路径下应先调用 `camera_deinit`,再调用 `camera_init` 彻底重置硬件和信号量状态,否则残留 DMA 完成信号会令下次拍照提前退出并读到上次未完成的缓冲区内容。 + +**Q9:`camera_stop_stream` 之后队列里还剩一帧旧数据,影响下次流启动** +`camera_stop_stream` 会清零队列,但若应用在停流之前未消费全部帧,重新启动流后首次 `camera_get_stream_frame` 会返回第一帧新数据(队列已清空,不会有旧帧残留)。若对旧帧有疑虑,可在 `camera_start_stream` 之后以 `timeout=0` 非阻塞轮询一次,丢弃可能存在的遗留令牌: + +```c +camera_stream_frame_t stale; +while (camera_get_stream_frame(&g_cam, &stale, 0) == CAMERA_OK) + ; /* 丢弃 */ +``` + +**Q10:如何统计丢帧?** +Handle 层维护 `instance->dropped_count`,每次队列满覆盖旧帧时递增,并输出 `LOG_W("camera: stream queue full, dropped=%u")`。停流后读取: + +```c +camera_stop_stream(&g_cam); +LOG_I("dropped frames: %u", g_cam.dropped_count); +``` + +若希望在流运行期间实时监控,可在消费线程每次 `camera_get_stream_frame` 之后检查 `frame.sequence` 是否连续——相邻两次差值 > 1 表示 Handle 层队列发生了覆盖丢帧。