Files
tactility/Drivers/sc2356-module/source/sc2356.cpp
T
Shadowtrance 7a7f09be35 Tab5 camera + other stuff (#558)
* Tab5 camera + other stuff

Tab5 camera driver - SC2356
Custom SliderBox widget - slider with plus and minus buttons + value label and snapping
Rtc Time service + rtc api
Sdk release - only include drivers built for that specific target, eg: sc2356 driver is mipi / p4 only.
No more hardcoded manual sdk cmakelists
New function to find device by compatible string match. no more static cast bool wildness when trying to match a single device (like M5Stack PaperS3 for example)

* feedback + fixes

Fixed external app user data path.
fix(gui): block app teardown until onHide() completes, preventing ELF unload racing a still-running app
added camera device type and api

* drain the snake sssem

---------

Co-authored-by: Ken Van Hoeylandt <git@kenvanhoeylandt.net>
2026-07-10 08:45:03 +02:00

563 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: Apache-2.0
#include <drivers/sc2356.h>
#include <sc2356_module.h>
#include <tactility/device.h>
#include <tactility/drivers/esp32_i2c_master.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#include <esp_heap_caps.h>
#include <esp_video_device.h>
#include <esp_video_init.h>
#include <freertos/task.h>
#include <fcntl.h>
#include <linux/videodev2.h>
#include <sys/errno.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/poll.h>
#include <driver/jpeg_encode.h>
#include <driver/ppa.h>
#include <cstring>
#include <freertos/semphr.h>
#define TAG "SC2356"
// SC2356 (SC202CS) chip ID registers (16-bit address, 8-bit value)
// PID = 0xEB52: high byte 0xEB at register 0x3107, low byte 0x52 at 0x3108
static constexpr uint8_t SC2356_REG_CHIP_ID_H[2] = { 0x31, 0x07 };
static constexpr uint8_t SC2356_CHIP_ID_H = 0xEB;
static constexpr uint8_t SC2356_REG_CHIP_ID_L[2] = { 0x31, 0x08 };
static constexpr uint8_t SC2356_CHIP_ID_L = 0x52;
static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(20);
static constexpr int VIDEO_BUFFER_COUNT = 2;
static bool s_video_initialized = false;
static SemaphoreHandle_t s_video_init_mutex = nullptr;
struct Sc2356State : CameraHandleData {
int fd;
// Native sensor output dimensions (always 1280×720 from the sensor)
uint32_t native_width;
uint32_t native_height;
// Post-rotation dimensions (swapped for 90/270)
uint32_t width;
uint32_t height;
uint8_t* buffers[VIDEO_BUFFER_COUNT];
size_t buf_lengths[VIDEO_BUFFER_COUNT];
int last_dqbuf_index;
jpeg_encoder_handle_t enc;
i2c_master_bus_handle_t sccb_bus;
CameraRotation rotation;
ppa_client_handle_t ppa;
// PPA output buffer — fixed size, large enough for any rotation (native_width * native_height * 2)
uint8_t* ppa_out_buf;
size_t ppa_out_buf_size;
SemaphoreHandle_t rotation_mutex; // protects rotation, width, height
};
#define GET_CONFIG(device) (static_cast<const Sc2356Config*>((device)->config))
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;
}
auto address = GET_CONFIG(device)->address;
uint8_t chip_id_h = 0, chip_id_l = 0;
error_t err = i2c_controller_write_read(i2c, address, SC2356_REG_CHIP_ID_H, sizeof(SC2356_REG_CHIP_ID_H), &chip_id_h, 1, I2C_TIMEOUT);
if (err != ERROR_NONE) {
LOG_W(TAG, "WHO_AM_I read failed: %s — sensor may still be starting up", error_to_string(err));
return ERROR_NONE;
}
err = i2c_controller_write_read(i2c, address, SC2356_REG_CHIP_ID_L, sizeof(SC2356_REG_CHIP_ID_L), &chip_id_l, 1, I2C_TIMEOUT);
if (err != ERROR_NONE) {
LOG_W(TAG, "WHO_AM_I read (low) failed: %s", error_to_string(err));
return ERROR_NONE;
}
if (chip_id_h != SC2356_CHIP_ID_H || chip_id_l != SC2356_CHIP_ID_L) {
LOG_E(TAG, "WHO_AM_I mismatch: got 0x%02X%02X, expected 0x%02X%02X", chip_id_h, chip_id_l, SC2356_CHIP_ID_H, SC2356_CHIP_ID_L);
return ERROR_RESOURCE;
}
LOG_I(TAG, "WHO_AM_I OK (0x%02X%02X)", chip_id_h, chip_id_l);
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;
}
static ppa_srm_rotation_angle_t to_ppa_rotation(CameraRotation rotation) {
switch (rotation) {
case CAMERA_ROTATION_90: return PPA_SRM_ROTATION_ANGLE_90;
case CAMERA_ROTATION_180: return PPA_SRM_ROTATION_ANGLE_180;
case CAMERA_ROTATION_270: return PPA_SRM_ROTATION_ANGLE_270;
default: return PPA_SRM_ROTATION_ANGLE_0;
}
}
extern "C" {
error_t sc2356_open(Device* device, Sc2356Handle* out_handle) {
if (!device || !out_handle) return ERROR_INVALID_ARGUMENT;
auto* state = static_cast<Sc2356State*>(heap_caps_calloc(1, sizeof(Sc2356State), 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;
// 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;
}
// esp_video_init only needs to run once across all sc2356_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_csi_config_t csi_config = {};
csi_config.sccb_config.init_sccb = false;
csi_config.sccb_config.i2c_handle = state->sccb_bus;
csi_config.sccb_config.freq = 400000;
csi_config.reset_pin = static_cast<gpio_num_t>(-1);
csi_config.pwdn_pin = static_cast<gpio_num_t>(-1);
esp_video_init_config_t cam_config = {};
cam_config.csi = &csi_config;
esp_err_t esp_err = esp_video_init(&cam_config);
if (esp_err != ESP_OK) {
LOG_E(TAG, "esp_video_init 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;
}
xSemaphoreGive(init_mutex);
}
// Open the CSI video device. The ISP pipeline controller (running in the background
// after esp_video_init) applies AE/AWB/demosaicing automatically; pixel output
// is still on /dev/video0, which delivers processed RGB565 frames.
state->fd = open(ESP_VIDEO_MIPI_CSI_DEVICE_NAME, O_RDONLY);
if (state->fd < 0) {
LOG_E(TAG, "Failed to open %s", ESP_VIDEO_MIPI_CSI_DEVICE_NAME);
vSemaphoreDelete(state->rotation_mutex);
heap_caps_free(state);
return ERROR_RESOURCE;
}
// Query current format to get sensor dimensions
struct v4l2_format fmt = {};
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;
}
// Set RGB565 output format
if (fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_RGB565) {
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
if (ioctl(state->fd, VIDIOC_S_FMT, &fmt) != 0) {
LOG_E(TAG, "VIDIOC_S_FMT RGB565 failed");
goto err_close;
}
// Re-read to get the actual negotiated dimensions
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(state->fd, VIDIOC_G_FMT, &fmt) != 0) {
LOG_E(TAG, "VIDIOC_G_FMT after set failed");
goto err_close;
}
}
state->native_width = fmt.fmt.pix.width;
state->native_height = fmt.fmt.pix.height;
// Post-rotation dimensions (swapped for 90/270)
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;
}
LOG_I(TAG, "Video format: %lux%lu native",
(unsigned long)state->native_width, (unsigned long)state->native_height);
// PPA output buffer — fixed size (max of any rotation), cache-line aligned per PPA requirements
state->ppa_out_buf_size = (size_t)state->native_width * state->native_height * 2;
state->ppa_out_buf = static_cast<uint8_t*>(
heap_caps_aligned_alloc(64, state->ppa_out_buf_size, MALLOC_CAP_SPIRAM));
if (!state->ppa_out_buf) {
LOG_E(TAG, "Failed to alloc PPA output buffer (%zu bytes)", state->ppa_out_buf_size);
goto err_close;
}
// PPA client for hardware-accelerated rotation
{
ppa_client_config_t ppa_cfg = {};
ppa_cfg.oper_type = PPA_OPERATION_SRM;
ppa_cfg.max_pending_trans_num = 1;
if (ppa_register_client(&ppa_cfg, &state->ppa) != ESP_OK) {
LOG_E(TAG, "ppa_register_client failed");
goto err_close;
}
}
// Request mmap buffers
{
struct v4l2_requestbuffers req = {};
req.count = VIDEO_BUFFER_COUNT;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(state->fd, VIDIOC_REQBUFS, &req) != 0) {
LOG_E(TAG, "VIDIOC_REQBUFS failed");
goto err_close;
}
}
// Query, mmap, and queue each buffer
for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) {
struct v4l2_buffer buf = {};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (ioctl(state->fd, VIDIOC_QUERYBUF, &buf) != 0) {
LOG_E(TAG, "VIDIOC_QUERYBUF[%d] failed", i);
goto err_unmap;
}
state->buf_lengths[i] = buf.length;
state->buffers[i] = static_cast<uint8_t*>(
mmap(nullptr, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, state->fd, buf.m.offset)
);
if (state->buffers[i] == MAP_FAILED) {
state->buffers[i] = nullptr;
LOG_E(TAG, "mmap[%d] failed", i);
goto err_unmap;
}
if (ioctl(state->fd, VIDIOC_QBUF, &buf) != 0) {
LOG_E(TAG, "VIDIOC_QBUF[%d] failed", i);
goto err_unmap;
}
}
// Start streaming
{
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(state->fd, VIDIOC_STREAMON, &type) != 0) {
LOG_E(TAG, "VIDIOC_STREAMON failed");
goto err_unmap;
}
}
// Create hardware JPEG encoder once; reused across all sc2356_capture_jpeg() calls
{
jpeg_encode_engine_cfg_t eng_cfg = { .intr_priority = 0, .timeout_ms = 1000 };
esp_err_t je = jpeg_new_encoder_engine(&eng_cfg, &state->enc);
if (je != ESP_OK) {
LOG_E(TAG, "jpeg_new_encoder_engine failed: %s", esp_err_to_name(je));
goto err_unmap;
}
}
*out_handle = state;
return ERROR_NONE;
err_unmap:
{
// Stop streaming before unmapping - correct V4L2 cleanup order, avoids DMA-into-
// unmapped-buffer races. Harmless no-op if STREAMON never succeeded (e.g. it's the
// failure that brought us here).
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->ppa) ppa_unregister_client(state->ppa);
if (state->ppa_out_buf) heap_caps_free(state->ppa_out_buf);
if (state->rotation_mutex) vSemaphoreDelete(state->rotation_mutex);
close(state->fd);
heap_caps_free(state);
return ERROR_RESOURCE;
}
error_t sc2356_close(Sc2356Handle handle) {
if (!handle) return ERROR_INVALID_ARGUMENT;
auto* state = static_cast<Sc2356State*>(handle);
if (state->enc) jpeg_del_encoder_engine(state->enc);
if (state->ppa) ppa_unregister_client(state->ppa);
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]);
}
if (state->ppa_out_buf) heap_caps_free(state->ppa_out_buf);
if (state->rotation_mutex) vSemaphoreDelete(state->rotation_mutex);
close(state->fd);
// Bus handle is owned by esp32_i2c_master driver - do not delete it
heap_caps_free(state);
return ERROR_NONE;
}
error_t sc2356_set_rotation(Sc2356Handle handle, CameraRotation rotation) {
if (!handle) return ERROR_INVALID_ARGUMENT;
auto* state = static_cast<Sc2356State*>(handle);
xSemaphoreTake(state->rotation_mutex, portMAX_DELAY);
if (state->rotation == rotation) {
xSemaphoreGive(state->rotation_mutex);
return ERROR_NONE;
}
bool needs_swap = (rotation == CAMERA_ROTATION_90 || rotation == CAMERA_ROTATION_270);
state->rotation = 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 sc2356_get_frame(Sc2356Handle 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<Sc2356State*>(handle);
// Use poll to enforce timeout
struct pollfd pfd = {};
pfd.fd = state->fd;
pfd.events = POLLIN;
int ret = poll(&pfd, 1, static_cast<int>(timeout_ms));
if (ret == 0) return ERROR_TIMEOUT;
if (ret < 0) {
LOG_E(TAG, "poll failed: %d", errno);
return ERROR_RESOURCE;
}
struct v4l2_buffer vbuf = {};
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vbuf.memory = V4L2_MEMORY_MMAP;
if (ioctl(state->fd, VIDIOC_DQBUF, &vbuf) != 0) {
LOG_E(TAG, "VIDIOC_DQBUF failed: %d", errno);
return ERROR_RESOURCE;
}
state->last_dqbuf_index = static_cast<int>(vbuf.index);
uint8_t* raw = state->buffers[vbuf.index];
xSemaphoreTake(state->rotation_mutex, portMAX_DELAY);
uint32_t frame_width = state->width;
uint32_t frame_height = state->height;
ppa_srm_oper_config_t srm_cfg = {};
srm_cfg.in.buffer = raw;
srm_cfg.in.pic_w = srm_cfg.in.block_w = state->native_width;
srm_cfg.in.pic_h = srm_cfg.in.block_h = state->native_height;
srm_cfg.in.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
srm_cfg.out.buffer = state->ppa_out_buf;
srm_cfg.out.buffer_size = state->ppa_out_buf_size;
srm_cfg.out.pic_w = frame_width;
srm_cfg.out.pic_h = frame_height;
srm_cfg.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
srm_cfg.rotation_angle = to_ppa_rotation(state->rotation);
srm_cfg.scale_x = srm_cfg.scale_y = 1.0f;
srm_cfg.mode = PPA_TRANS_MODE_BLOCKING;
esp_err_t ppa_err = ppa_do_scale_rotate_mirror(state->ppa, &srm_cfg);
xSemaphoreGive(state->rotation_mutex);
if (ppa_err != ESP_OK) {
LOG_E(TAG, "ppa_do_scale_rotate_mirror failed: %s", esp_err_to_name(ppa_err));
// Buffer was already dequeued (VIDIOC_DQBUF above) - the caller receives an error
// and may never call sc2356_release_frame(), so return it to the queue here or
// repeated PPA failures exhaust VIDEO_BUFFER_COUNT and poll() blocks forever.
sc2356_release_frame(handle);
return ERROR_RESOURCE;
}
*buf = state->ppa_out_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 sc2356_release_frame(Sc2356Handle handle) {
if (!handle) return ERROR_INVALID_ARGUMENT;
auto* state = static_cast<Sc2356State*>(handle);
if (state->last_dqbuf_index < 0) return ERROR_INVALID_STATE;
struct v4l2_buffer vbuf = {};
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vbuf.memory = V4L2_MEMORY_MMAP;
vbuf.index = static_cast<uint32_t>(state->last_dqbuf_index);
state->last_dqbuf_index = -1;
if (ioctl(state->fd, VIDIOC_QBUF, &vbuf) != 0) {
LOG_E(TAG, "VIDIOC_QBUF failed: %d", errno);
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
uint32_t sc2356_get_width(Sc2356Handle handle) {
if (!handle) return 0;
auto* state = static_cast<Sc2356State*>(handle);
xSemaphoreTake(state->rotation_mutex, portMAX_DELAY);
uint32_t width = state->width;
xSemaphoreGive(state->rotation_mutex);
return width;
}
uint32_t sc2356_get_height(Sc2356Handle handle) {
if (!handle) return 0;
auto* state = static_cast<Sc2356State*>(handle);
xSemaphoreTake(state->rotation_mutex, portMAX_DELAY);
uint32_t height = state->height;
xSemaphoreGive(state->rotation_mutex);
return height;
}
error_t sc2356_capture_jpeg(Sc2356Handle 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<Sc2356State*>(handle);
// Dequeue one frame. Dimensions come back atomically with the frame data itself,
// so they can't drift if a concurrent sc2356_set_rotation() changes state->width/height.
uint8_t* frame = nullptr;
size_t frame_len = 0;
uint32_t capture_width = 0;
uint32_t capture_height = 0;
error_t err = sc2356_get_frame(handle, &frame, &frame_len, 500, &capture_width, &capture_height);
if (err != ERROR_NONE) return err;
// Output buffer must be allocated with jpeg_alloc_encoder_mem for DMA alignment.
// jpeg_alloc_encoder_mem() allocates exactly the requested size with no headroom, so the
// hint must cover the worst case, not the typical case. At high quality settings (low
// compression) on detailed/noisy images, a width*height (1 byte/pixel) hint can be smaller
// than the actual encoded output, causing the DMA write to overrun the buffer - this was
// observed as JPEG corruption confined to the tail of the image. width*height*2 matches
// the uncompressed RGB565 source size, which JPEG output can never exceed.
size_t out_sz = 0;
jpeg_encode_memory_alloc_cfg_t mem_cfg = { .buffer_direction = JPEG_ENC_ALLOC_OUTPUT_BUFFER };
uint8_t* jpeg_buf = static_cast<uint8_t*>(
jpeg_alloc_encoder_mem(capture_width * capture_height * 2, &mem_cfg, &out_sz));
if (!jpeg_buf) {
sc2356_release_frame(handle);
return ERROR_OUT_OF_MEMORY;
}
jpeg_encode_cfg_t enc_cfg = {
.height = capture_height,
.width = capture_width,
.src_type = JPEG_ENCODE_IN_FORMAT_RGB565,
.sub_sample = JPEG_DOWN_SAMPLING_YUV420,
.image_quality = quality,
};
uint32_t encoded_size = 0;
esp_err_t esp_err = jpeg_encoder_process(state->enc, &enc_cfg, frame,
static_cast<uint32_t>(frame_len),
jpeg_buf, static_cast<uint32_t>(out_sz),
&encoded_size);
sc2356_release_frame(handle);
if (esp_err != ESP_OK || encoded_size == 0) {
LOG_E(TAG, "jpeg_encoder_process failed: %s", esp_err_to_name(esp_err));
heap_caps_free(jpeg_buf);
return ERROR_RESOURCE;
}
*out_buf = jpeg_buf;
*out_len = static_cast<size_t>(encoded_size);
return ERROR_NONE;
}
CameraApi sc2356_camera_api = {
.open = sc2356_open,
.close = sc2356_close,
.get_frame = sc2356_get_frame,
.release_frame = sc2356_release_frame,
.get_width = sc2356_get_width,
.get_height = sc2356_get_height,
.set_rotation = sc2356_set_rotation,
.capture_jpeg = sc2356_capture_jpeg
};
Driver sc2356_driver = {
.name = "sc2356",
.compatible = (const char*[]) { "smartsens,sc2356", nullptr },
.start_device = start,
.stop_device = stop,
.api = &sc2356_camera_api,
.device_type = &CAMERA_TYPE,
.owner = &sc2356_module,
.internal = nullptr
};
} // extern "C"