// SPDX-License-Identifier: Apache-2.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define TAG "GC0308" static constexpr int VIDEO_BUFFER_COUNT = 2; static bool s_video_initialized = false; static SemaphoreHandle_t s_video_init_mutex = nullptr; static uint32_t s_video_handle_count = 0; struct Gc0308State : CameraHandleData { int fd; // Native sensor output dimensions uint32_t native_width; uint32_t native_height; // Frames are returned in the requested orientation. uint32_t width; uint32_t height; uint8_t* buffers[VIDEO_BUFFER_COUNT]; size_t buf_lengths[VIDEO_BUFFER_COUNT]; int last_dqbuf_index; uint8_t* frame_buf; size_t frame_buf_size; jpeg_enc_handle_t encoder; i2c_master_bus_handle_t sccb_bus; CameraRotation rotation; SemaphoreHandle_t rotation_mutex; // protects rotation, width, height uint32_t encoder_width; uint32_t encoder_height; }; #define GET_CONFIG(device) (static_cast((device)->config)) static gpio_num_t pin_or_nc(const GpioPinSpec& pin) { return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast(pin.pin); } static CameraRotation add_rotation(CameraRotation rotation, uint16_t offset) { return static_cast((static_cast(rotation) + offset) % 360); } static error_t reset_pulse(GpioDescriptor* descriptor) { // Release is attempted unconditionally, even if assert failed - leaving the expander output // asserted on an assert failure would hold the sensor in reset for the rest of boot. const bool assert_ok = gpio_descriptor_set_level(descriptor, true) == ERROR_NONE; // assert vTaskDelay(pdMS_TO_TICKS(10)); const bool release_ok = gpio_descriptor_set_level(descriptor, false) == ERROR_NONE; // release vTaskDelay(pdMS_TO_TICKS(10)); return (assert_ok && release_ok) ? ERROR_NONE : ERROR_RESOURCE; } static error_t start(Device* device) { auto* i2c = device_get_parent(device); if (device_get_type(i2c) != &I2C_CONTROLLER_TYPE) { LOG_E(TAG, "Parent is not an I2C controller"); return ERROR_RESOURCE; } const auto* config = GET_CONFIG(device); //auto address = config->address; // Reset is pulsed (not just held released) so the sensor reaches a known state regardless of // whatever it inherited from a previous boot if (config->pin_reset.gpio_controller != nullptr) { // Merge with the devicetree-supplied flags (e.g. GPIO_FLAG_ACTIVE_LOW) rather than // overwriting them, so a board that wires reset active-high isn't silently forced to // active-low polarity. auto* reset_descriptor = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, config->pin_reset.flags | GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO); if (reset_descriptor == nullptr) { LOG_E(TAG, "Failed to acquire reset pin"); return ERROR_RESOURCE; } error_t reset_error = reset_pulse(reset_descriptor); gpio_descriptor_release(reset_descriptor); if (reset_error != ERROR_NONE) { LOG_E(TAG, "Failed to pulse reset pin"); return reset_error; } } return ERROR_NONE; } static error_t stop([[maybe_unused]] Device* device) { return ERROR_NONE; } static SemaphoreHandle_t get_video_init_mutex() { static portMUX_TYPE creation_lock = portMUX_INITIALIZER_UNLOCKED; portENTER_CRITICAL(&creation_lock); if (!s_video_init_mutex) { s_video_init_mutex = xSemaphoreCreateMutex(); if (!s_video_init_mutex) { LOG_E(TAG, "Failed to create video init mutex"); } } portEXIT_CRITICAL(&creation_lock); return s_video_init_mutex; } extern "C" { error_t gc0308_open(Device* device, Gc0308Handle* out_handle) { if (!device || !out_handle) return ERROR_INVALID_ARGUMENT; auto* state = static_cast(heap_caps_calloc(1, sizeof(Gc0308State), MALLOC_CAP_DEFAULT)); if (!state) return ERROR_OUT_OF_MEMORY; state->device = device; state->fd = -1; state->last_dqbuf_index = -1; state->rotation_mutex = xSemaphoreCreateMutex(); if (!state->rotation_mutex) { heap_caps_free(state); return ERROR_OUT_OF_MEMORY; } state->rotation = CAMERA_ROTATION_0; struct v4l2_format fmt = {}; // Retrieve bus handle from the parent I2C controller auto* i2c = device_get_parent(device); state->sccb_bus = esp32_i2c_master_get_bus_handle(i2c); if (!state->sccb_bus) { LOG_E(TAG, "Failed to get i2c_master bus handle from parent device"); vSemaphoreDelete(state->rotation_mutex); heap_caps_free(state); return ERROR_RESOURCE; } const auto* config = GET_CONFIG(device); bool video_handle_acquired = false; // esp_video_init only needs to run once across all gc0308_open calls. { SemaphoreHandle_t init_mutex = get_video_init_mutex(); if (!init_mutex) { vSemaphoreDelete(state->rotation_mutex); heap_caps_free(state); return ERROR_OUT_OF_MEMORY; } xSemaphoreTake(init_mutex, portMAX_DELAY); if (!s_video_initialized) { esp_video_init_dvp_config_t dvp_config = { .sccb_config = { .init_sccb = false, .i2c_handle = state->sccb_bus, .freq = 100000, }, .reset_pin = GPIO_NUM_NC, .pwdn_pin = GPIO_NUM_NC, .dvp_pin = { .data_width = CAM_CTLR_DATA_WIDTH_8, .data_io = { static_cast(config->pin_d0.pin), static_cast(config->pin_d1.pin), static_cast(config->pin_d2.pin), static_cast(config->pin_d3.pin), static_cast(config->pin_d4.pin), static_cast(config->pin_d5.pin), static_cast(config->pin_d6.pin), static_cast(config->pin_d7.pin), }, .vsync_io = static_cast(config->pin_vsync.pin), .de_io = static_cast(config->pin_de.pin), .pclk_io = static_cast(config->pin_pclk.pin), .xclk_io = pin_or_nc(config->pin_xclk), }, .xclk_freq = config->xclk_frequency_hz, }; esp_video_init_config_t video_config = { .dvp = &dvp_config }; esp_err_t esp_err = esp_video_init_with_flags(&video_config, ESP_VIDEO_INIT_FLAGS_DVP); if (esp_err != ESP_OK) { LOG_E(TAG, "esp_video_init DVP failed: %s", esp_err_to_name(esp_err)); xSemaphoreGive(init_mutex); vSemaphoreDelete(state->rotation_mutex); heap_caps_free(state); return ERROR_RESOURCE; } s_video_initialized = true; } ++s_video_handle_count; video_handle_acquired = true; xSemaphoreGive(init_mutex); } state->fd = open(ESP_VIDEO_DVP_DEVICE_NAME, O_RDONLY); if (state->fd < 0) { LOG_E(TAG, "Failed to open %s", ESP_VIDEO_DVP_DEVICE_NAME); goto err_close; } fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(state->fd, VIDIOC_G_FMT, &fmt) != 0) { LOG_E(TAG, "VIDIOC_G_FMT failed"); goto err_close; } state->native_width = fmt.fmt.pix.width; state->native_height = fmt.fmt.pix.height; state->frame_buf_size = (size_t)state->native_width * state->native_height * 2; state->rotation = add_rotation(CAMERA_ROTATION_0, config->rotation_offset); if (state->rotation == CAMERA_ROTATION_90 || state->rotation == CAMERA_ROTATION_270) { state->width = state->native_height; state->height = state->native_width; } else { state->width = state->native_width; state->height = state->native_height; } state->frame_buf = static_cast(heap_caps_aligned_alloc(16, state->frame_buf_size, MALLOC_CAP_SPIRAM)); if (!state->frame_buf) goto err_close; { struct v4l2_requestbuffers request = {}; request.count = VIDEO_BUFFER_COUNT; request.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; request.memory = V4L2_MEMORY_MMAP; if (ioctl(state->fd, VIDIOC_REQBUFS, &request) != 0) goto err_close; } for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) { struct v4l2_buffer buffer = {}; buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buffer.memory = V4L2_MEMORY_MMAP; buffer.index = i; if (ioctl(state->fd, VIDIOC_QUERYBUF, &buffer) != 0) goto err_unmap; state->buf_lengths[i] = buffer.length; state->buffers[i] = static_cast(mmap(nullptr, buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED, state->fd, buffer.m.offset)); if (state->buffers[i] == MAP_FAILED) { state->buffers[i] = nullptr; goto err_unmap; } if (ioctl(state->fd, VIDIOC_QBUF, &buffer) != 0) goto err_unmap; } { int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(state->fd, VIDIOC_STREAMON, &type) != 0) goto err_unmap; } { jpeg_enc_config_t encoder_config = { .width = static_cast(state->width), .height = static_cast(state->height), .src_type = JPEG_PIXEL_FORMAT_RGB565_LE, .subsampling = JPEG_SUBSAMPLE_420, .quality = 80, .rotate = JPEG_ROTATE_0D, .task_enable = false, .hfm_task_priority = 13, .hfm_task_core = 1, }; if (jpeg_enc_open(&encoder_config, &state->encoder) != JPEG_ERR_OK) goto err_unmap; state->encoder_width = state->width; state->encoder_height = state->height; } LOG_I(TAG, "Video format: %lux%lu native", (unsigned long)state->native_width, (unsigned long)state->native_height); *out_handle = state; return ERROR_NONE; err_unmap: if (state->encoder) { jpeg_enc_close(state->encoder); state->encoder = nullptr; } { int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(state->fd, VIDIOC_STREAMOFF, &type); } for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) { if (state->buffers[i]) munmap(state->buffers[i], state->buf_lengths[i]); } err_close: if (state->fd >= 0) close(state->fd); if (video_handle_acquired) { SemaphoreHandle_t init_mutex = get_video_init_mutex(); xSemaphoreTake(init_mutex, portMAX_DELAY); --s_video_handle_count; if (s_video_handle_count == 0 && s_video_initialized) { esp_video_deinit_with_flags(ESP_VIDEO_INIT_FLAGS_DVP); s_video_initialized = false; } xSemaphoreGive(init_mutex); } vSemaphoreDelete(state->rotation_mutex); heap_caps_free(state->frame_buf); heap_caps_free(state); return ERROR_RESOURCE; } error_t gc0308_close(Gc0308Handle handle) { if (!handle) return ERROR_INVALID_ARGUMENT; auto* state = static_cast(handle); if (state->encoder) jpeg_enc_close(state->encoder); int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(state->fd, VIDIOC_STREAMOFF, &type); for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) { if (state->buffers[i]) munmap(state->buffers[i], state->buf_lengths[i]); } heap_caps_free(state->frame_buf); close(state->fd); SemaphoreHandle_t init_mutex = get_video_init_mutex(); xSemaphoreTake(init_mutex, portMAX_DELAY); if (s_video_handle_count > 0) --s_video_handle_count; if (s_video_handle_count == 0 && s_video_initialized) { esp_video_deinit_with_flags(ESP_VIDEO_INIT_FLAGS_DVP); s_video_initialized = false; } xSemaphoreGive(init_mutex); if (state->rotation_mutex) vSemaphoreDelete(state->rotation_mutex); // Bus handle is owned by esp32_i2c_master driver - do not delete it heap_caps_free(state); return ERROR_NONE; } error_t gc0308_set_rotation(Gc0308Handle handle, CameraRotation rotation) { if (!handle) return ERROR_INVALID_ARGUMENT; auto* state = static_cast(handle); CameraRotation effective_rotation = add_rotation(rotation, GET_CONFIG(state->device)->rotation_offset); xSemaphoreTake(state->rotation_mutex, portMAX_DELAY); if (state->rotation == effective_rotation) { xSemaphoreGive(state->rotation_mutex); return ERROR_NONE; } bool needs_swap = (effective_rotation == CAMERA_ROTATION_90 || effective_rotation == CAMERA_ROTATION_270); state->rotation = effective_rotation; if (needs_swap) { state->width = state->native_height; state->height = state->native_width; } else { state->width = state->native_width; state->height = state->native_height; } xSemaphoreGive(state->rotation_mutex); return ERROR_NONE; } error_t gc0308_get_frame(Gc0308Handle handle, uint8_t** buf, size_t* len, uint32_t timeout_ms, uint32_t* out_width, uint32_t* out_height) { if (!handle || !buf || !len) return ERROR_INVALID_ARGUMENT; auto* state = static_cast(handle); struct pollfd poll_descriptor = { .fd = state->fd, .events = POLLIN }; int poll_result = poll(&poll_descriptor, 1, static_cast(timeout_ms)); if (poll_result == 0) return ERROR_TIMEOUT; if (poll_result < 0) return ERROR_RESOURCE; struct v4l2_buffer buffer = {}; buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buffer.memory = V4L2_MEMORY_MMAP; if (ioctl(state->fd, VIDIOC_DQBUF, &buffer) != 0) return ERROR_RESOURCE; state->last_dqbuf_index = static_cast(buffer.index); if (buffer.index >= VIDEO_BUFFER_COUNT || buffer.bytesused != state->frame_buf_size || (buffer.flags & V4L2_BUF_FLAG_ERROR) != 0) { gc0308_release_frame(handle); return ERROR_RESOURCE; } xSemaphoreTake(state->rotation_mutex, portMAX_DELAY); CameraRotation rotation = state->rotation; uint32_t frame_width = state->width; uint32_t frame_height = state->height; size_t copy_length = state->frame_buf_size; const uint8_t* source = state->buffers[buffer.index]; for (uint32_t y = 0; y < frame_height; y++) { for (uint32_t x = 0; x < frame_width; x++) { uint32_t source_x; uint32_t source_y; switch (rotation) { case CAMERA_ROTATION_90: source_x = y; source_y = state->native_height - 1 - x; break; case CAMERA_ROTATION_180: source_x = state->native_width - 1 - x; source_y = state->native_height - 1 - y; break; case CAMERA_ROTATION_270: source_x = state->native_width - 1 - y; source_y = x; break; default: source_x = x; source_y = y; break; } size_t source_offset = ((size_t)source_y * state->native_width + source_x) * 2; size_t destination_offset = ((size_t)y * frame_width + x) * 2; if (source_offset + 1 >= copy_length) continue; state->frame_buf[destination_offset] = source[source_offset + 1]; state->frame_buf[destination_offset + 1] = source[source_offset]; } } xSemaphoreGive(state->rotation_mutex); *buf = state->frame_buf; *len = (size_t)frame_width * frame_height * 2; if (out_width != nullptr) { *out_width = frame_width; } if (out_height != nullptr) { *out_height = frame_height; } return ERROR_NONE; } error_t gc0308_release_frame(Gc0308Handle handle) { if (!handle) return ERROR_INVALID_ARGUMENT; auto* state = static_cast(handle); if (state->last_dqbuf_index < 0) return ERROR_INVALID_STATE; struct v4l2_buffer buffer = {}; buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buffer.memory = V4L2_MEMORY_MMAP; buffer.index = static_cast(state->last_dqbuf_index); state->last_dqbuf_index = -1; return ioctl(state->fd, VIDIOC_QBUF, &buffer) == 0 ? ERROR_NONE : ERROR_RESOURCE; } uint32_t gc0308_get_width(Gc0308Handle handle) { if (!handle) return 0; auto* state = static_cast(handle); xSemaphoreTake(state->rotation_mutex, portMAX_DELAY); uint32_t width = state->width; xSemaphoreGive(state->rotation_mutex); return width; } uint32_t gc0308_get_height(Gc0308Handle handle) { if (!handle) return 0; auto* state = static_cast(handle); xSemaphoreTake(state->rotation_mutex, portMAX_DELAY); uint32_t height = state->height; xSemaphoreGive(state->rotation_mutex); return height; } error_t gc0308_capture_jpeg(Gc0308Handle handle, uint8_t** out_buf, size_t* out_len, uint8_t quality) { if (!handle || !out_buf || !out_len) return ERROR_INVALID_ARGUMENT; auto* state = static_cast(handle); // Dequeue one frame. Dimensions come back atomically with the frame data itself, // so they can't drift if a concurrent gc0308_set_rotation() changes state->width/height. uint8_t* frame = nullptr; size_t frame_len = 0; uint32_t frame_width = 0; uint32_t frame_height = 0; error_t err = gc0308_get_frame(handle, &frame, &frame_len, 500, &frame_width, &frame_height); if (err != ERROR_NONE) return err; if (frame == nullptr || frame_len == 0) { gc0308_release_frame(handle); return ERROR_RESOURCE; } size_t image_size = (size_t)frame_width * frame_height * 2; uint8_t* aligned_input = static_cast(heap_caps_aligned_alloc(16, image_size, MALLOC_CAP_SPIRAM)); uint8_t* jpeg_output = static_cast(heap_caps_aligned_alloc(16, image_size, MALLOC_CAP_SPIRAM)); if (!aligned_input || !jpeg_output) { heap_caps_free(aligned_input); heap_caps_free(jpeg_output); gc0308_release_frame(handle); return ERROR_OUT_OF_MEMORY; } memcpy(aligned_input, frame, frame_len); if (state->encoder_width != frame_width || state->encoder_height != frame_height) { if (state->encoder) jpeg_enc_close(state->encoder); state->encoder = nullptr; jpeg_enc_config_t cfg = DEFAULT_JPEG_ENC_CONFIG(); cfg.width = static_cast(frame_width); cfg.height = static_cast(frame_height); cfg.src_type = JPEG_PIXEL_FORMAT_RGB565_LE; cfg.subsampling = JPEG_SUBSAMPLE_420; cfg.quality = quality; if (jpeg_enc_open(&cfg, &state->encoder) != JPEG_ERR_OK) { heap_caps_free(aligned_input); heap_caps_free(jpeg_output); gc0308_release_frame(handle); return ERROR_RESOURCE; } state->encoder_width = frame_width; state->encoder_height = frame_height; } jpeg_enc_set_quality(state->encoder, quality); int encoded_size = 0; jpeg_error_t encode_error = jpeg_enc_process(state->encoder, aligned_input, static_cast(frame_len), jpeg_output, static_cast(image_size), &encoded_size); heap_caps_free(aligned_input); gc0308_release_frame(handle); if (encode_error != JPEG_ERR_OK || encoded_size <= 0) { LOG_E(TAG, "JPEG conversion failed: %d", static_cast(encode_error)); heap_caps_free(jpeg_output); return ERROR_RESOURCE; } *out_buf = jpeg_output; *out_len = encoded_size; return ERROR_NONE; } CameraApi gc0308_camera_api = { .open = gc0308_open, .close = gc0308_close, .get_frame = gc0308_get_frame, .release_frame = gc0308_release_frame, .get_width = gc0308_get_width, .get_height = gc0308_get_height, .set_rotation = gc0308_set_rotation, .capture_jpeg = gc0308_capture_jpeg }; Driver gc0308_driver = { .name = "gc0308", .compatible = (const char*[]) { "galaxycore,gc0308", nullptr }, .start_device = start, .stop_device = stop, .api = &gc0308_camera_api, .device_type = &CAMERA_TYPE, .owner = &gc0308_module, .internal = nullptr }; } // extern "C"