From 68b1ce82c466a974499d2abdf1113cd93ef3408a Mon Sep 17 00:00:00 2001 From: OKayJH Date: Wed, 22 Jul 2026 09:45:25 +0800 Subject: [PATCH 1/5] fix(imx296): deliver current fast-trigger frame after ROI Track fast-trigger mode across CIF so RKISP can identify IMX296. Finish V39 buffers at the last observable line. Delay vb2_done until tail DMA has drained. Restore full-frame ROI registers. Drain the invalid ROI frame during the free-run restart. Drop diagnostic and timing experiments from the production fix. Signed-off-by: OKayJH --- drivers/media/i2c/imx296.c | 79 ++++++++++++++-- drivers/media/platform/rockchip/isp/capture.c | 50 ++++++++++ drivers/media/platform/rockchip/isp/capture.h | 8 ++ .../media/platform/rockchip/isp/capture_v39.c | 17 +++- drivers/media/platform/rockchip/isp/rkisp.c | 93 ++++++++++++++++++- 5 files changed, 236 insertions(+), 11 deletions(-) diff --git a/drivers/media/i2c/imx296.c b/drivers/media/i2c/imx296.c index 0eae71b874623..c28b3e8f16bc3 100644 --- a/drivers/media/i2c/imx296.c +++ b/drivers/media/i2c/imx296.c @@ -111,6 +111,7 @@ #define IMX296_GAIN IMX296_REG_16BIT(0x3204) #define IMX296_GAINDLY IMX296_REG_8BIT(0x3212) +#define IMX296_GAINDLY_0FRAME 0x08 #define IMX296_GAINDLY_1FRAME 0x09 #define IMX296_BLKLEVEL IMX296_REG_16BIT(0x3254) @@ -146,6 +147,12 @@ */ #define IMX296_HMAX_DEFAULT 1100U #define IMX296_VBLANK_DEFAULT 1162U +/* + * Fast Trigger does not use the 30 fps free-run blanking budget. Sony's + * timing requirement is VTR = ROIWV1 + 30 for vertical ROI, otherwise the + * ROI frame stays open until the next XTRIG and vb2_done is delayed one shot. + */ +#define IMX296_XTRIG_VBLANK 30U #define IMX296_VBLANK_MAX (1048575U - IMX296_PIXEL_ARRAY_HEIGHT) #define IMX296_EXPOSURE_DEFAULT_LINES 1104U #define IMX296_ANALOG_GAIN_MIN 0U @@ -173,11 +180,29 @@ #define V4L2_CID_IMX296_LIGHT_SOURCE_ADVANCE_US (V4L2_CID_USER_IMX296_BASE + 0x4) #define V4L2_CID_IMX296_LIGHT_SOURCE_OFF_DELAY_US (V4L2_CID_USER_IMX296_BASE + 0x5) +/* Datasheet: changing ROI geometry produces one invalid output frame. */ +#define IMX296_ROI_INVALID_FRAME_WAIT_US 50000U + enum imx296_op_mode { IMX296_FREE_RUN = 0, IMX296_XTRIG_ONE_SHOT = 1, }; +/* + * RKISP sits behind the RKCIF bridge on this platform, so its active media + * entity is the CIF subdevice rather than the IMX296 subdevice. Keep a small + * read-only mode bridge for RKISP's fast-trigger early-done decision. The + * board has a single IMX296; atomic access also makes the IRQ/start paths + * independent of the sensor mutex. + */ +static atomic_t imx296_fast_trigger_mode = ATOMIC_INIT(0); + +bool imx296_is_fast_trigger_active(void) +{ + return atomic_read(&imx296_fast_trigger_mode) != 0; +} +EXPORT_SYMBOL_GPL(imx296_is_fast_trigger_active); + struct imx296_clk_params { unsigned int freq; u8 incksel[4]; @@ -239,6 +264,8 @@ struct imx296 { struct v4l2_subdev subdev; struct media_pad pad; struct v4l2_rect crop; + /* Set when ACTIVE crop changes; cleared after ROI invalid-frame discard. */ + bool roi_boundary_pending; struct v4l2_mbus_framefmt format; struct v4l2_ctrl_handler ctrls; @@ -559,7 +586,6 @@ static int imx296_trigger_once_locked(struct imx296 *sensor) return ret; } - static int imx296_set_ctrl(struct v4l2_ctrl *ctrl); static int imx296_mode_switch(struct imx296 *sensor, enum imx296_op_mode new_mode); @@ -765,10 +791,15 @@ static ssize_t trigger_pulse_us_store(struct device *dev, return -ERANGE; mutex_lock(&sensor->mutex); - sensor->trigger_pulse_us = pulse_us; - mutex_unlock(&sensor->mutex); + if (sensor->trigger_pulse_us != pulse_us) { + u32 old_pulse = sensor->trigger_pulse_us; - dev_info(dev, "trigger pulse width set to %u us\n", pulse_us); + sensor->trigger_pulse_us = pulse_us; + dev_info(dev, + "trigger pulse width set to %u us (was %u)\n", + pulse_us, old_pulse); + } + mutex_unlock(&sensor->mutex); return count; } @@ -1325,7 +1356,19 @@ static int imx296_setup(struct imx296 *sensor) imx296_write(sensor, IMX296_FID0_ROIWV1, crop->height, &ret); imx296_write(sensor, IMX296_MIPIC_AREA3W, crop->height, &ret); } else { + /* + * Restore the ROI geometry registers as well as disabling ROI. + * Leaving the previous window programmed is observable after an + * ROI-to-full-frame restart in fast-trigger mode: the frame identity + * is current, but the last output lines can still contain data from + * the preceding buffer. The power-on full-frame state has all four + * geometry registers cleared, so reproduce that state explicitly. + */ imx296_write(sensor, IMX296_FID0_ROI, 0, &ret); + imx296_write(sensor, IMX296_FID0_ROIPH1, 0, &ret); + imx296_write(sensor, IMX296_FID0_ROIPV1, 0, &ret); + imx296_write(sensor, IMX296_FID0_ROIWH1, 0, &ret); + imx296_write(sensor, IMX296_FID0_ROIWV1, 0, &ret); imx296_write(sensor, IMX296_MIPIC_AREA3W, IMX296_PIXEL_ARRAY_HEIGHT, &ret); } @@ -1348,7 +1391,10 @@ static int imx296_setup(struct imx296 *sensor) imx296_write(sensor, IMX296_GTTABLENUM, 0xc5, &ret); imx296_write(sensor, IMX296_CTRL418C, sensor->clk_params->ctrl418c, &ret); - imx296_write(sensor, IMX296_GAINDLY, IMX296_GAINDLY_1FRAME, &ret); + imx296_write(sensor, IMX296_GAINDLY, + sensor->pending_mode == IMX296_XTRIG_ONE_SHOT ? + IMX296_GAINDLY_0FRAME : IMX296_GAINDLY_1FRAME, + &ret); imx296_write(sensor, IMX296_BLKLEVEL, 0x03c, &ret); imx296_write(sensor, IMX296_VINT, IMX296_VINT_EN, &ret); @@ -1424,7 +1470,8 @@ static int imx296_restore_ctrls_for_mode_locked(struct imx296 *sensor, return imx296_apply_exposure_locked(sensor, sensor->exposure->val); } - return imx296_apply_vblank_locked(sensor, IMX296_VBLANK_DEFAULT); + /* XTRIG exposure is set by pulse width; VMAX must use the VTR baseline. */ + return imx296_apply_vblank_locked(sensor, IMX296_XTRIG_VBLANK); } static int imx296_stream_on(struct imx296 *sensor) @@ -1483,6 +1530,8 @@ static int imx296_mode_switch(struct imx296 *sensor, if (new_mode > IMX296_XTRIG_ONE_SHOT) return -EINVAL; + atomic_set(&imx296_fast_trigger_mode, + new_mode == IMX296_XTRIG_ONE_SHOT); sensor->pending_mode = new_mode; imx296_update_ctrl_visibility_locked(sensor, new_mode); @@ -1677,6 +1726,18 @@ static int imx296_s_stream(struct v4l2_subdev *sd, int enable) sensor->active_mode = sensor->pending_mode; sensor->streaming = true; + /* + * The datasheet specifies one invalid frame after ROI geometry changes. + * CamOS restarts the sensor in free-run before switching to fast trigger, + * so let that invalid frame drain before userspace can change run mode. + */ + if (sensor->roi_boundary_pending && + sensor->active_mode == IMX296_FREE_RUN) { + usleep_range(IMX296_ROI_INVALID_FRAME_WAIT_US, + IMX296_ROI_INVALID_FRAME_WAIT_US + 5000U); + sensor->roi_boundary_pending = false; + } + if (sensor->active_mode == IMX296_FREE_RUN && sensor->light_source_enabled) imx296_light_source_set_locked(sensor, true); @@ -1980,6 +2041,12 @@ static int imx296_set_selection(struct v4l2_subdev *sd, format->height = rect.height; } + if (sel->which == V4L2_SUBDEV_FORMAT_ACTIVE && + (rect.left != crop->left || rect.top != crop->top || + rect.width != crop->width || rect.height != crop->height)) { + sensor->roi_boundary_pending = true; + } + *crop = rect; sel->r = rect; diff --git a/drivers/media/platform/rockchip/isp/capture.c b/drivers/media/platform/rockchip/isp/capture.c index dd69891d0df60..06c16ab358822 100644 --- a/drivers/media/platform/rockchip/isp/capture.c +++ b/drivers/media/platform/rockchip/isp/capture.c @@ -600,6 +600,50 @@ void rkisp_stream_buf_done_early(struct rkisp_device *dev) } } +static enum hrtimer_restart rkisp_early_done_timer(struct hrtimer *timer) +{ + struct rkisp_capture_device *cap_dev = + container_of(timer, struct rkisp_capture_device, early_done_timer); + + rkisp_stream_buf_done_early(cap_dev->ispdev); + atomic_set(&cap_dev->early_done_pending, 0); + return HRTIMER_NORESTART; +} + +void rkisp_stream_buf_done_early_delayed(struct rkisp_device *dev, + u32 delay_us) +{ + struct rkisp_capture_device *cap_dev = &dev->cap_dev; + + if (!delay_us) { + rkisp_stream_buf_done_early(dev); + return; + } + + /* One line IRQ is expected per SOF. Do not move an already armed deadline. */ + if (atomic_cmpxchg(&cap_dev->early_done_pending, 0, 1)) + return; + + hrtimer_start(&cap_dev->early_done_timer, + ns_to_ktime((u64)delay_us * NSEC_PER_USEC), + HRTIMER_MODE_REL); +} + +void rkisp_stream_cancel_early_done(struct rkisp_device *dev) +{ + struct rkisp_capture_device *cap_dev = &dev->cap_dev; + int ret; + + if (in_interrupt()) { + ret = hrtimer_try_to_cancel(&cap_dev->early_done_timer); + if (ret < 0) + return; + } else { + hrtimer_cancel(&cap_dev->early_done_timer); + } + atomic_set(&cap_dev->early_done_pending, 0); +} + int rkisp_stream_buf_cnt(struct rkisp_stream *stream) { unsigned long lock_flags = 0; @@ -1911,6 +1955,10 @@ int rkisp_register_stream_vdevs(struct rkisp_device *dev) memset(cap_dev, 0, sizeof(*cap_dev)); cap_dev->ispdev = dev; atomic_set(&cap_dev->refcnt, 0); + atomic_set(&cap_dev->early_done_pending, 0); + hrtimer_init(&cap_dev->early_done_timer, CLOCK_MONOTONIC, + HRTIMER_MODE_REL); + cap_dev->early_done_timer.function = rkisp_early_done_timer; if (dev->isp_ver <= ISP_V13) { if (dev->isp_ver == ISP_V12) { @@ -1962,6 +2010,8 @@ int rkisp_register_stream_vdevs(struct rkisp_device *dev) void rkisp_unregister_stream_vdevs(struct rkisp_device *dev) { + rkisp_stream_cancel_early_done(dev); + if (dev->isp_ver <= ISP_V13) rkisp_unregister_stream_v1x(dev); else if (dev->isp_ver == ISP_V20) diff --git a/drivers/media/platform/rockchip/isp/capture.h b/drivers/media/platform/rockchip/isp/capture.h index 82fe5e0d7c66a..38c796fb44dbb 100644 --- a/drivers/media/platform/rockchip/isp/capture.h +++ b/drivers/media/platform/rockchip/isp/capture.h @@ -35,6 +35,7 @@ #ifndef _RKISP_PATH_VIDEO_H #define _RKISP_PATH_VIDEO_H +#include #include #include "common.h" @@ -302,6 +303,7 @@ struct rkisp_stream { bool is_crop_upd; bool is_using_resmem; bool frame_early; + u64 early_done_sof_ns; bool need_scl_upd; bool is_attach_info; wait_queue_head_t done; @@ -334,6 +336,9 @@ struct rkisp_capture_device { struct tasklet_struct rd_tasklet; atomic_t refcnt; u32 wait_line; + u32 early_done_delay_us; + struct hrtimer early_done_timer; + atomic_t early_done_pending; u32 wrap_width; u32 wrap_line; bool is_done_early; @@ -348,6 +353,9 @@ extern struct rockit_isp_ops rockit_isp_ops; void rkisp_stream_vir_cpy_image(struct work_struct *work); void rkisp_stream_buf_done_early(struct rkisp_device *dev); +void rkisp_stream_buf_done_early_delayed(struct rkisp_device *dev, + u32 delay_us); +void rkisp_stream_cancel_early_done(struct rkisp_device *dev); void rkisp_stream_buf_done(struct rkisp_stream *stream, struct rkisp_buffer *buf); void rkisp_unregister_stream_vdev(struct rkisp_stream *stream); diff --git a/drivers/media/platform/rockchip/isp/capture_v39.c b/drivers/media/platform/rockchip/isp/capture_v39.c index 0242e065c488d..a742d32ad72ea 100644 --- a/drivers/media/platform/rockchip/isp/capture_v39.c +++ b/drivers/media/platform/rockchip/isp/capture_v39.c @@ -1039,6 +1039,7 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) struct capture_fmt *isp_fmt = &stream->out_isp_fmt; unsigned long lock_flags = 0; struct rkisp_buffer *buf = NULL; + u64 sof_ns = dev->isp_sdev.frm_timestamp; u32 i, seq; if (stream->id == RKISP_STREAM_VIR) @@ -1050,12 +1051,26 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) if (stream->id == RKISP_STREAM_MP && dev->cap_dev.wrap_line) return 0; spin_lock_irqsave(&stream->vbq_lock, lock_flags); + /* + * wait_line can fire very close to the normal MI frame interrupt. + * If IRQ wins first, FRAME_WORK must not consume the next buffer; + * if FRAME_WORK wins first, IRQ must only advance the queue. The + * SOF timestamp uniquely identifies both callbacks as one frame. + */ + if (sof_ns && stream->early_done_sof_ns == sof_ns) { + spin_unlock_irqrestore(&stream->vbq_lock, lock_flags); + if (state == FRAME_IRQ) + goto end; + return 0; + } if (state == FRAME_IRQ && stream->curr_buf) stream->frame_early = false; else stream->frame_early = true; buf = stream->curr_buf; stream->curr_buf = NULL; + if (buf && sof_ns) + stream->early_done_sof_ns = sof_ns; spin_unlock_irqrestore(&stream->vbq_lock, lock_flags); if ((!stream->frame_early && state == FRAME_WORK) || (stream->frame_early && state == FRAME_IRQ)) @@ -1126,7 +1141,6 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) stream->dbg.delay = ns - dev->isp_sdev.frm_timestamp; stream->dbg.timestamp = ns; stream->dbg.id = seq; - if (vir->streaming && vir->conn_id == stream->id) { spin_lock_irqsave(&vir->vbq_lock, lock_flags); list_add_tail(&buf->queue, &dev->cap_dev.vir_cpy.queue); @@ -1490,6 +1504,7 @@ rkisp_start_streaming(struct vb2_queue *queue, unsigned int count) } memset(&stream->dbg, 0, sizeof(stream->dbg)); + stream->early_done_sof_ns = 0; atomic_inc(&dev->cap_dev.refcnt); if (!dev->isp_inp || !stream->linked) { diff --git a/drivers/media/platform/rockchip/isp/rkisp.c b/drivers/media/platform/rockchip/isp/rkisp.c index ca8d8e35bcae7..65cdad538eeda 100644 --- a/drivers/media/platform/rockchip/isp/rkisp.c +++ b/drivers/media/platform/rockchip/isp/rkisp.c @@ -56,6 +56,18 @@ #define ISP_V4L2_EVENT_ELEMS 4 #define ISP_SUBDEV_NAME DRIVER_NAME "-isp-subdev" + +#ifndef V4L2_CID_USER_IMX296_BASE +#define V4L2_CID_USER_IMX296_BASE (V4L2_CID_USER_BASE + 0x10d0) +#endif +#define V4L2_CID_IMX296_OP_MODE (V4L2_CID_USER_IMX296_BASE + 0x1) +#define IMX296_XTRIG_ONE_SHOT 1 +#define ISP39_OUT_LINE_COUNTER_TAIL 8 + +static unsigned int rkisp_imx296_tail_wait_us = 50000; +module_param_named(imx296_tail_wait_us, rkisp_imx296_tail_wait_us, uint, 0644); +MODULE_PARM_DESC(imx296_tail_wait_us, + "V39 IMX296 fast-trigger tail DMA hrtimer delay before buffer done (us)"); /* * NOTE: MIPI controller and input MUX are also configured in this file, * because ISP Subdev is not only describe ISP submodule(input size,format, output size, format), @@ -89,6 +101,36 @@ static void rkisp_config_cmsk(struct rkisp_device *dev); static void rkisp_config_aiisp(struct rkisp_device *dev); static void rkisp_config_fpn(struct rkisp_device *dev); +#if IS_REACHABLE(CONFIG_VIDEO_IMX296) +extern bool imx296_is_fast_trigger_active(void); +#endif + +static bool rkisp_imx296_fast_trigger_active(struct rkisp_device *dev) +{ + struct v4l2_subdev *sensor; + struct v4l2_ctrl *mode; + + /* Direct sensor-to-ISP topologies can read the control normally. */ + if (dev->active_sensor && dev->active_sensor->sd) { + sensor = dev->active_sensor->sd; + mode = v4l2_ctrl_find(sensor->ctrl_handler, + V4L2_CID_IMX296_OP_MODE); + if (mode) + return v4l2_ctrl_g_ctrl(mode) == IMX296_XTRIG_ONE_SHOT; + } + + /* + * RK3576 routes IMX296 -> RKCIF -> RKISP across two media devices. + * RKISP therefore sees the CIF bridge as active_sensor and cannot walk + * to the IMX296 control handler. Use the sensor's read-only mode bridge. + */ +#if IS_REACHABLE(CONFIG_VIDEO_IMX296) + return imx296_is_fast_trigger_active(); +#else + return false; +#endif +} + static inline struct rkisp_device *sd_to_isp_dev(struct v4l2_subdev *sd) { return container_of(sd->v4l2_dev, struct rkisp_device, v4l2_dev); @@ -2218,6 +2260,7 @@ static int rkisp_isp_stop(struct rkisp_device *dev) v4l2_dbg(1, rkisp_debug, &dev->v4l2_dev, "%s refcnt:%d\n", __func__, atomic_read(&hw->refcnt)); + rkisp_stream_cancel_early_done(dev); if (atomic_read(&hw->refcnt) > 1) goto end; @@ -2346,12 +2389,39 @@ static int rkisp_isp_stop(struct rkisp_device *dev) static int rkisp_isp_start(struct rkisp_device *dev) { struct rkisp_hw_dev *hw = dev->hw_dev; + u32 height = dev->isp_sdev.out_crop.height; u32 val; v4l2_dbg(1, rkisp_debug, &dev->v4l2_dev, "%s refcnt:%d link_num:%d\n", __func__, atomic_read(&hw->refcnt), hw->dev_link_num); + /* + * IMX296 Fast Trigger keeps the MIPI frame open until the next XTRIG. + * Complete the current ISP buffer at the last observable V39 output line + * so a trigger publishes its own frame instead of N-1. The V39 output + * line counter stops eight counts below the active height (for example, + * 1080 for a 1088-line frame), so height - 1 can never assert the line + * interrupt. Program one count before that terminal value, then keep the + * buffer owned by the ISP for a short tail-DMA drain interval before + * vb2_done. Free-run and other sensors retain the configured/default + * wait_line behavior and have no added delay. + */ + dev->cap_dev.early_done_delay_us = 0; + rkisp_stream_cancel_early_done(dev); + if (dev->isp_ver == ISP_V39 && height > 1 && + rkisp_imx296_fast_trigger_active(dev)) { + dev->cap_dev.wait_line = + height > ISP39_OUT_LINE_COUNTER_TAIL + 1 ? + height - ISP39_OUT_LINE_COUNTER_TAIL - 1 : 1; + dev->cap_dev.early_done_delay_us = + min_t(u32, rkisp_imx296_tail_wait_us, 100000U); + v4l2_info(&dev->v4l2_dev, + "IMX296 fast trigger: early buffer done at line %u/%u, tail wait %u us\n", + dev->cap_dev.wait_line, height, + dev->cap_dev.early_done_delay_us); + } + dev->cap_dev.is_done_early = false; if (dev->cap_dev.wait_line >= dev->isp_sdev.out_crop.height) dev->cap_dev.wait_line = 0; @@ -3858,7 +3928,15 @@ static void rkisp_queue_event_aiisp(struct rkisp_device *dev, u32 irq) rd_line += dev->aiisp_cfg.rd_linecnt; if (rd_line > h) rd_line = h - 1; - rkisp_write(dev, ISP32_ISP_IRQ_CFG0, rd_line, true); + /* + * IRQ_CFG0 low 16 bits belong to AIISP's quarter-line + * interrupt; high 16 bits belong to capture early-done. + * Preserve the IMX296 Fast Trigger threshold when AIISP + * advances its read line at runtime. + */ + rkisp_write(dev, ISP32_ISP_IRQ_CFG0, + (rd_line & 0xffff) | + (dev->cap_dev.wait_line << 16), true); } } else { wr_line = ISP39_AIISP_WR_LINECNT(val); @@ -3901,7 +3979,8 @@ static void rkisp_config_aiisp(struct rkisp_device *dev) irq_mask = ISP39_AIISP_LINECNT_DONE | ISP3X_OUT_FRM_QUARTER; en_mask = ISP39_AIISP_EN; - rd_line = dev->aiisp_cfg.rd_linecnt; + rd_line = (dev->aiisp_cfg.rd_linecnt & 0xffff) | + (dev->cap_dev.wait_line << 16); wr_line = dev->aiisp_cfg.wr_linecnt << 16; rkisp_write(dev, ISP32_ISP_IRQ_CFG0, rd_line, false); @@ -5087,7 +5166,14 @@ void rkisp_isp_isr(unsigned int isp_mis, if (isp_mis & ISP3X_OUT_FRM_HALF) { writel(ISP3X_OUT_FRM_HALF, base + CIF_ISP_ICR); rkisp_dvbm_event(dev, ISP3X_OUT_FRM_HALF); - rkisp_stream_buf_done_early(dev); + /* + * V39 exposes the last usable line interrupt before all tail lines + * have reached memory. Arm a high-resolution timer and keep ownership + * until those writes drain; doing vb2_done immediately can expose + * green/stale bottom rows to userspace. The timer avoids a long udelay + * in this hard-IRQ handler. + */ + rkisp_stream_buf_done_early_delayed(dev, dev->cap_dev.early_done_delay_us); } if (isp_mis & ISP3X_OUT_FRM_END) { writel(ISP3X_OUT_FRM_END, base + CIF_ISP_ICR); @@ -5112,4 +5198,3 @@ irqreturn_t rkisp_vs_isr_handler(int irq, void *ctx) return IRQ_HANDLED; } - From e8c10e0f65082d68f305987f6a718dc7cfded3c2 Mon Sep 17 00:00:00 2001 From: OKayJH Date: Wed, 22 Jul 2026 09:48:13 +0800 Subject: [PATCH 2/5] fix(imx296): restore PWM period after shorter exposure Cache the firmware PWM period instead of mutable runtime state. Recompute the period from the base period. Include the current pulse width and safety margin. Apply the disabled PWM state when trigger_pulse_us changes. This restores trigger rate after long-to-short exposure without reboot. Signed-off-by: OKayJH --- drivers/media/i2c/imx296.c | 55 ++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/drivers/media/i2c/imx296.c b/drivers/media/i2c/imx296.c index c28b3e8f16bc3..1128cd13984e3 100644 --- a/drivers/media/i2c/imx296.c +++ b/drivers/media/i2c/imx296.c @@ -243,6 +243,7 @@ struct imx296 { enum imx296_op_mode active_mode; enum imx296_op_mode pending_mode; u32 trigger_pulse_us; + u64 trigger_pwm_base_period_ns; struct gpio_desc *light_source_gpio; struct pinctrl_state *pins_active_high; @@ -385,20 +386,26 @@ static struct pwm_device *imx296_devm_pwm_get_optional(struct device *dev, return pwm; } -static u64 imx296_pwm_period_ns(struct pwm_device *pwm) +static u64 imx296_trigger_base_period_ns(struct imx296 *sensor) { - struct pwm_state state; struct pwm_args args; - if (!pwm) - return 0; + /* + * Cache the firmware-provided period once. The runtime PWM state may have + * been enlarged for an earlier long pulse and must never become the base + * for later calculations, otherwise a short pulse cannot restore the + * original trigger rate until reboot. + */ + if (!sensor->trigger_pwm) + return IMX296_TRIGGER_PERIOD_NS_DEFAULT; + if (sensor->trigger_pwm_base_period_ns) + return sensor->trigger_pwm_base_period_ns; - pwm_get_state(pwm, &state); - if (state.period) - return state.period; + pwm_get_args(sensor->trigger_pwm, &args); + sensor->trigger_pwm_base_period_ns = args.period ? + args.period : IMX296_TRIGGER_PERIOD_NS_DEFAULT; - pwm_get_args(pwm, &args); - return args.period; + return sensor->trigger_pwm_base_period_ns; } static u64 imx296_trigger_period_ns(u64 pulse_ns, u64 base_period_ns) @@ -411,17 +418,18 @@ static u64 imx296_trigger_period_ns(u64 pulse_ns, u64 base_period_ns) return max(base_period_ns, min_period_ns); } -static int imx296_init_trigger_pwm(struct imx296 *sensor) +static int imx296_apply_trigger_pwm_idle_locked(struct imx296 *sensor) { - struct pwm_state state = { 0 }; + struct pwm_state state; u64 pulse_ns; if (!sensor->trigger_pwm) return 0; pulse_ns = (u64)sensor->trigger_pulse_us * 1000ULL; + pwm_get_state(sensor->trigger_pwm, &state); state.period = imx296_trigger_period_ns( - pulse_ns, imx296_pwm_period_ns(sensor->trigger_pwm)); + pulse_ns, imx296_trigger_base_period_ns(sensor)); state.duty_cycle = 0; state.polarity = PWM_POLARITY_INVERSED; state.enabled = false; @@ -429,6 +437,16 @@ static int imx296_init_trigger_pwm(struct imx296 *sensor) return pwm_apply_state(sensor->trigger_pwm, &state); } +static int imx296_init_trigger_pwm(struct imx296 *sensor) +{ + if (!sensor->trigger_pwm) + return 0; + + imx296_trigger_base_period_ns(sensor); + + return imx296_apply_trigger_pwm_idle_locked(sensor); +} + static int imx296_light_source_value_locked(struct imx296 *sensor, bool on) { /* @@ -533,7 +551,7 @@ static int imx296_trigger_once_locked(struct imx296 *sensor) pwm_get_state(sensor->trigger_pwm, &state); state.period = imx296_trigger_period_ns( - duty_ns, imx296_pwm_period_ns(sensor->trigger_pwm)); + duty_ns, imx296_trigger_base_period_ns(sensor)); state.duty_cycle = duty_ns; state.polarity = PWM_POLARITY_INVERSED; state.enabled = true; @@ -780,6 +798,7 @@ static ssize_t trigger_pulse_us_store(struct device *dev, { struct imx296 *sensor = imx296_from_dev(dev); unsigned int pulse_us; + int ret = 0; if (!sensor) return -ENODEV; @@ -795,13 +814,21 @@ static ssize_t trigger_pulse_us_store(struct device *dev, u32 old_pulse = sensor->trigger_pulse_us; sensor->trigger_pulse_us = pulse_us; + ret = imx296_apply_trigger_pwm_idle_locked(sensor); + if (ret) { + sensor->trigger_pulse_us = old_pulse; + imx296_apply_trigger_pwm_idle_locked(sensor); + goto unlock; + } dev_info(dev, "trigger pulse width set to %u us (was %u)\n", pulse_us, old_pulse); } + +unlock: mutex_unlock(&sensor->mutex); - return count; + return ret ? ret : count; } static DEVICE_ATTR_RW(trigger_pulse_us); From 2c9b30ba12fe689a3bb45002a2f0bdd07a33ec06 Mon Sep 17 00:00:00 2001 From: OKayJH Date: Wed, 22 Jul 2026 12:10:55 +0800 Subject: [PATCH 3/5] build: use date-based DLCVCAM kernel build version --- DLCVCAM_BUILD_VERSION | 1 + ...15\344\275\234\346\211\213\345\206\214.md" | 58 ++++++++++++++++++- Makefile | 24 +++++++- 3 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 DLCVCAM_BUILD_VERSION diff --git a/DLCVCAM_BUILD_VERSION b/DLCVCAM_BUILD_VERSION new file mode 100644 index 0000000000000..0a6276b27fcd0 --- /dev/null +++ b/DLCVCAM_BUILD_VERSION @@ -0,0 +1 @@ +2026072201 diff --git "a/DLCVCAM\345\206\205\346\240\270\347\274\226\350\257\221\344\270\216\346\233\264\346\226\260\346\223\215\344\275\234\346\211\213\345\206\214.md" "b/DLCVCAM\345\206\205\346\240\270\347\274\226\350\257\221\344\270\216\346\233\264\346\226\260\346\223\215\344\275\234\346\211\213\345\206\214.md" index bafffd7c9b044..45ac3a4a637ac 100644 --- "a/DLCVCAM\345\206\205\346\240\270\347\274\226\350\257\221\344\270\216\346\233\264\346\226\260\346\223\215\344\275\234\346\211\213\345\206\214.md" +++ "b/DLCVCAM\345\206\205\346\240\270\347\274\226\350\257\221\344\270\216\346\233\264\346\226\260\346\223\215\344\275\234\346\211\213\345\206\214.md" @@ -20,7 +20,54 @@ which aarch64-linux-gnu-gcc ## 2. 编译内核 -### 2.1 初次配置(仅需执行一次) +### 2.1 日期构建编号规则 + +DLCVCAM 发布内核使用仓库根目录的 `DLCVCAM_BUILD_VERSION` 作为构建编号, +格式固定为: + +```text +YYYYMMDDNN +``` + +其中 `YYYYMMDD` 是发布日期,`NN` 是当天两位序号,从 `01` 开始。例如: + +```text +2026072201 +``` + +合入 `master` 前必须把该文件更新为实际合入日期;同一天发布多个内核时依次使用 +`01`、`02`、`03`。不要通过修改 `VERSION/PATCHLEVEL/SUBLEVEL` 记录发布日期, +这样可以保持 `uname -r` 和 `/lib/modules/6.1.99-rk3576` 路径稳定。 + +开发机查看仓库默认构建编号: + +```bash +cat DLCVCAM_BUILD_VERSION +make -s dlcvcam-build-version +``` + +板卡刷入对应内核后查看: + +```bash +uname -v +cat /proc/version +``` + +输出示例: + +```text +#2026072201 SMP Wed Jul 22 10:46:10 CST 2026 +``` + +正常发布构建不需要再手工传入 `KBUILD_BUILD_VERSION`。CI 或临时构建仍可显式传入 +该变量覆盖仓库值,但交付镜像必须使用 `DLCVCAM_BUILD_VERSION` 中已提交的编号。 +建议镜像文件名同时记录构建编号和 Git 短提交,例如: + +```text +boot-rk3576-6.1.99-2026072201-e8c10e0f.img +``` + +### 2.2 初次配置(仅需执行一次) ```bash cd /home/ypw/kernel @@ -36,7 +83,7 @@ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat_linux_rk3576_defconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig ``` -### 2.2 增量编译(日常修改后) +### 2.3 增量编译(日常修改后) ```bash cd /home/ypw/kernel @@ -67,6 +114,13 @@ BOOT_ITS=boot.its ./scripts/mkimg --dtb dlcvcam-rk3576.dtb mkimage -l boot.img ``` +同时确认本次编译使用的日期构建编号: + +```bash +make -s ARCH=arm64 O=/path/to/kernel-out dlcvcam-build-version +# 2026072201 +``` + --- ## 3. 烧录到板子 diff --git a/Makefile b/Makefile index 84f782580f962..2eb37484ffc7b 100644 --- a/Makefile +++ b/Makefile @@ -203,6 +203,21 @@ endif this-makefile := $(lastword $(MAKEFILE_LIST)) abs_srctree := $(realpath $(dir $(this-makefile))) +# DLCVCAM release build identifier. Keep the Linux release and module path +# stable (for example 6.1.99-rk3576), while exposing a date-based identifier +# through UTS_VERSION (`uname -v`) on the target board. A command-line +# KBUILD_BUILD_VERSION still takes precedence for temporary/CI builds. +DLCVCAM_BUILD_VERSION_FILE := $(abs_srctree)/DLCVCAM_BUILD_VERSION +DLCVCAM_BUILD_VERSION := $(strip $(shell cat $(DLCVCAM_BUILD_VERSION_FILE) 2>/dev/null)) +DLCVCAM_BUILD_VERSION_VALID := $(shell printf '%s\n' '$(DLCVCAM_BUILD_VERSION)' | \ + grep -Eq '^[0-9]{8}(0[1-9]|[1-9][0-9])$$' && echo y) +ifneq ($(DLCVCAM_BUILD_VERSION_VALID),y) +$(error DLCVCAM_BUILD_VERSION must use YYYYMMDDNN with NN in 01..99) +endif +KBUILD_BUILD_VERSION ?= $(DLCVCAM_BUILD_VERSION) + +export DLCVCAM_BUILD_VERSION KBUILD_BUILD_VERSION + ifneq ($(words $(subst :, ,$(abs_srctree))), 1) $(error source directory cannot contain spaces or colons) endif @@ -286,7 +301,8 @@ no-dot-config-targets := $(clean-targets) \ cscope gtags TAGS tags help% %docs check% coccicheck \ $(version_h) headers headers_% archheaders archscripts \ %asm-generic kernelversion %src-pkg dt_binding_check \ - outputmakefile rustavailable rustfmt rustfmtcheck + outputmakefile rustavailable rustfmt rustfmtcheck \ + dlcvcam-build-version # Installation targets should not require compiler. Unfortunately, vdso_install # is an exception where build artifacts may be updated. This must be fixed. no-compiler-targets := $(no-dot-config-targets) install dtbs_install \ @@ -1687,6 +1703,7 @@ help: @echo ' gtags - Generate GNU GLOBAL index' @echo ' kernelrelease - Output the release version string (use with make -s)' @echo ' kernelversion - Output the version stored in Makefile (use with make -s)' + @echo ' dlcvcam-build-version - Output the date-based DLCVCAM build identifier' @echo ' image_name - Output the image name (use with make -s)' @echo ' headers_install - Install sanitised kernel headers to INSTALL_HDR_PATH'; \ echo ' (default: $(INSTALL_HDR_PATH))'; \ @@ -2095,7 +2112,7 @@ coccicheck: export_report: $(PERL) $(srctree)/scripts/export_report.pl -PHONY += checkstack kernelrelease kernelversion image_name +PHONY += checkstack kernelrelease kernelversion dlcvcam-build-version image_name # UML needs a little special treatment here. It wants to use the host # toolchain, so needs $(SUBARCH) passed to checkstack.pl. Everyone @@ -2116,6 +2133,9 @@ kernelrelease: kernelversion: @echo $(KERNELVERSION) +dlcvcam-build-version: + @echo $(KBUILD_BUILD_VERSION) + image_name: @echo $(KBUILD_IMAGE) From 69ad979d02a4ff68bf806e0a48911659867c73d8 Mon Sep 17 00:00:00 2001 From: OKayJH Date: Wed, 22 Jul 2026 16:34:17 +0800 Subject: [PATCH 4/5] feat(trigger): gate external input during transitions --- drivers/input/misc/trigger-dev.c | 117 +++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 7 deletions(-) diff --git a/drivers/input/misc/trigger-dev.c b/drivers/input/misc/trigger-dev.c index a87762f4ab486..c31a60ed0ec2d 100644 --- a/drivers/input/misc/trigger-dev.c +++ b/drivers/input/misc/trigger-dev.c @@ -13,6 +13,8 @@ * 2. trigger-path: write to sysfs "echo 1 > ..." - fallback for software trigger * - Optional result LEDs: ok-led-gpios / ng-led-gpios; sysfs "result" (ok/ng/off), * "result_led_enable"; each external trigger clears LEDs before camera action. + * - Runtime gate: sysfs "enable" (0/1). Disabling masks the input IRQ, cancels + * queued work and drops pending triggers before returning. * * When trigger-output-gpios is set, keep a single owner for that output GPIO. * If another node already claims the same pin, remove one side in DT. @@ -87,6 +89,11 @@ struct trigger_dev { bool mode_use_gpio; struct mutex mode_mutex; + /* 外部触发总门闩;默认开启,模式/ROI 切换期间由 sysfs enable 关闭 */ + bool enabled; + /* Serializes IRQ masking and pending-work cancellation. */ + struct mutex enable_mutex; + /* 结果指示灯:DT ok-led-gpios / ng-led-gpios;逻辑 0=inactive(灭) 1=active(亮) */ struct gpio_desc *ok_led_gpiod; struct gpio_desc *ng_led_gpiod; @@ -101,6 +108,7 @@ struct trigger_dev { atomic64_t irq_count; atomic64_t trigger_ok_count; atomic64_t trigger_fail_count; + atomic64_t trigger_suppressed_count; u32 max_pending; u64 last_irq_ns; }; @@ -163,9 +171,13 @@ static int trigger_dev_set_input_mode(struct trigger_dev *tdev, enum trigger_dev_input_mode mode) { unsigned int irq_type = trigger_dev_irq_type_for_mode(tdev, mode); + bool enabled; int ret; - disable_irq(tdev->irq); + mutex_lock(&tdev->enable_mutex); + enabled = READ_ONCE(tdev->enabled); + if (enabled) + disable_irq(tdev->irq); cancel_delayed_work_sync(&tdev->debounce_work); ret = irq_set_irq_type(tdev->irq, irq_type); @@ -175,7 +187,51 @@ static int trigger_dev_set_input_mode(struct trigger_dev *tdev, WRITE_ONCE(tdev->input_mode, mode); } - enable_irq(tdev->irq); + if (enabled) + enable_irq(tdev->irq); + mutex_unlock(&tdev->enable_mutex); + return ret; +} + +static int trigger_dev_set_enabled(struct trigger_dev *tdev, bool enabled) +{ + int dropped; + int ret = 0; + + mutex_lock(&tdev->enable_mutex); + if (enabled == READ_ONCE(tdev->enabled)) + goto out; + + if (!enabled) { + /* Publish the closed gate before waiting for any in-flight handler. */ + WRITE_ONCE(tdev->enabled, false); + disable_irq(tdev->irq); + cancel_delayed_work_sync(&tdev->debounce_work); + cancel_work_sync(&tdev->trigger_work); + + dropped = atomic_xchg(&tdev->trigger_pending, 0); + if (dropped > 0) + atomic64_add(dropped, &tdev->trigger_suppressed_count); + + if (tdev->output_gpiod) + gpiod_set_value_cansleep(tdev->output_gpiod, 0); + + if (tdev->pressed) { + tdev->pressed = false; + input_report_key(tdev->input, tdev->key_code, 0); + input_sync(tdev->input); + } + } else { + atomic_set(&tdev->trigger_pending, 0); + ret = trigger_dev_refresh_pressed_state(tdev); + if (ret) + goto out; + WRITE_ONCE(tdev->enabled, true); + enable_irq(tdev->irq); + } + +out: + mutex_unlock(&tdev->enable_mutex); return ret; } @@ -283,6 +339,11 @@ static void trigger_dev_trigger_work(struct work_struct *work) /* Drain pending triggers (presses) */ while (atomic_dec_if_positive(&tdev->trigger_pending) >= 0) { + if (!READ_ONCE(tdev->enabled)) { + atomic64_inc(&tdev->trigger_suppressed_count); + continue; + } + mutex_lock(&tdev->led_mutex); if (tdev->result_led_enable) trigger_dev_leds_off_locked(tdev); @@ -327,6 +388,9 @@ static void trigger_dev_trigger_work(struct work_struct *work) static void trigger_dev_handle_state(struct trigger_dev *tdev, bool pressed_now) { + if (!READ_ONCE(tdev->enabled)) + return; + if (pressed_now == tdev->pressed) return; @@ -375,6 +439,10 @@ static irqreturn_t trigger_dev_irq(int irq, void *dev_id) tdev->last_irq_ns = ktime_get_ns(); atomic64_inc(&tdev->irq_count); + if (!READ_ONCE(tdev->enabled)) { + atomic64_inc(&tdev->trigger_suppressed_count); + return IRQ_HANDLED; + } /* * Fast edge mode: @@ -516,7 +584,9 @@ static int trigger_dev_probe(struct platform_device *pdev) tdev->dev = dev; platform_set_drvdata(pdev, tdev); mutex_init(&tdev->mode_mutex); + mutex_init(&tdev->enable_mutex); mutex_init(&tdev->led_mutex); + WRITE_ONCE(tdev->enabled, true); /* * Primary DT property: input-gpios (con_id = "input") @@ -555,6 +625,7 @@ static int trigger_dev_probe(struct platform_device *pdev) atomic64_set(&tdev->irq_count, 0); atomic64_set(&tdev->trigger_ok_count, 0); atomic64_set(&tdev->trigger_fail_count, 0); + atomic64_set(&tdev->trigger_suppressed_count, 0); tdev->max_pending = 0; tdev->last_irq_ns = 0; @@ -590,7 +661,7 @@ static int trigger_dev_probe(struct platform_device *pdev) gpio = desc_to_gpio(tdev->input_gpiod); active_low = gpiod_is_active_low(tdev->input_gpiod); - dev_info(dev, "ready: gpio=%d irq=%d active_low=%d input_mode=%s debounce_ms=%u key_code=%u mode=%s\n", + dev_info(dev, "ready: gpio=%d irq=%d active_low=%d input_mode=%s debounce_ms=%u key_code=%u mode=%s enabled=%d\n", gpio, tdev->irq, active_low, @@ -598,7 +669,8 @@ static int trigger_dev_probe(struct platform_device *pdev) tdev->debounce_ms, tdev->key_code, tdev->output_gpiod ? - "direct-gpio-pulse" : "sysfs-write"); + "direct-gpio-pulse" : "sysfs-write", + READ_ONCE(tdev->enabled)); if (tdev->input_mode == TRIGGER_DEV_INPUT_MODE_BUTTON && !tdev->debounce_ms) dev_warn(dev, "button input mode is selected with debounce-ms=0; mechanical keys may still bounce\n"); @@ -666,6 +738,36 @@ static ssize_t input_mode_store(struct device *dev, } static DEVICE_ATTR_RW(input_mode); +/* Sysfs: enable (0=屏蔽外部触发并清空 pending, 1=允许外部触发) */ +static ssize_t enable_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct trigger_dev *tdev = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", READ_ONCE(tdev->enabled)); +} + +static ssize_t enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct trigger_dev *tdev = dev_get_drvdata(dev); + bool enabled; + int ret; + + ret = kstrtobool(buf, &enabled); + if (ret) + return ret; + + ret = trigger_dev_set_enabled(tdev, enabled); + if (ret) + return ret; + + dev_info(dev, "external trigger %s\n", enabled ? "enabled" : "disabled"); + return count; +} +static DEVICE_ATTR_RW(enable); + /* Sysfs: mode (gpio|sysfs) */ static ssize_t mode_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -874,10 +976,12 @@ static ssize_t stats_show(struct device *dev, struct device_attribute *attr, cha u64 ago_us = tdev->last_irq_ns ? div_u64(now - tdev->last_irq_ns, 1000) : 0; return sysfs_emit(buf, - "irq=%lld ok=%lld fail=%lld pending=%d max_pending=%u last_irq_ago_us=%llu input_mode=%s debounce_ms=%u\n", + "enabled=%d irq=%lld ok=%lld fail=%lld suppressed=%lld pending=%d max_pending=%u last_irq_ago_us=%llu input_mode=%s debounce_ms=%u\n", + READ_ONCE(tdev->enabled), atomic64_read(&tdev->irq_count), atomic64_read(&tdev->trigger_ok_count), atomic64_read(&tdev->trigger_fail_count), + atomic64_read(&tdev->trigger_suppressed_count), atomic_read(&tdev->trigger_pending), tdev->max_pending, ago_us, @@ -888,6 +992,7 @@ static DEVICE_ATTR_RO(stats); static struct attribute *trigger_dev_attrs[] = { &dev_attr_input_mode.attr, + &dev_attr_enable.attr, &dev_attr_mode.attr, &dev_attr_pulse_count.attr, &dev_attr_pulse_interval_us.attr, @@ -930,5 +1035,3 @@ module_platform_driver(trigger_dev_driver); MODULE_DESCRIPTION("Generic GPIO input trigger device (camera trigger + optional result LEDs)"); MODULE_LICENSE("GPL"); MODULE_IMPORT_NS(VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver); - - From 627814412ed11792aa1842f53b96707d55de8187 Mon Sep 17 00:00:00 2001 From: OKayJH Date: Wed, 22 Jul 2026 17:52:22 +0800 Subject: [PATCH 5/5] build: bump DLCVCAM version to 2026072202 --- DLCVCAM_BUILD_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DLCVCAM_BUILD_VERSION b/DLCVCAM_BUILD_VERSION index 0a6276b27fcd0..18ef27d008985 100644 --- a/DLCVCAM_BUILD_VERSION +++ b/DLCVCAM_BUILD_VERSION @@ -1 +1 @@ -2026072201 +2026072202