From 3eef7dac8080cca3b43191ff007c55dcf4b6f9e0 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Wed, 22 Jul 2026 09:45:25 +0800 Subject: [PATCH 01/21] 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 4d2a190de13f814c58bc9e658d65c66f1d751b62 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Wed, 22 Jul 2026 09:48:13 +0800 Subject: [PATCH 02/21] 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 3baa88293b0c25bd14ecafc6e52762f7231ef0af Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Wed, 22 Jul 2026 12:10:55 +0800 Subject: [PATCH 03/21] 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 3e34ca8bbb5cbadacae5695159aab42c377f9d2e Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Wed, 22 Jul 2026 16:34:17 +0800 Subject: [PATCH 04/21] 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 1baecda3da11e173fa736db916e8e8670f77b8f1 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Wed, 22 Jul 2026 17:52:22 +0800 Subject: [PATCH 05/21] 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 From 1dfe51e3e77d2b870a51ca5a9ad754a6f417bfa6 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Thu, 23 Jul 2026 15:42:18 +0800 Subject: [PATCH 06/21] build: bump DLCVCAM version to 2026072301 --- DLCVCAM_BUILD_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DLCVCAM_BUILD_VERSION b/DLCVCAM_BUILD_VERSION index 18ef27d008985..63dd368ca5caa 100644 --- a/DLCVCAM_BUILD_VERSION +++ b/DLCVCAM_BUILD_VERSION @@ -1 +1 @@ -2026072202 +2026072301 From 23f0e169299107ecf2a392d0877f13865d80e4a0 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Tue, 4 Aug 2026 10:02:21 +0800 Subject: [PATCH 07/21] =?UTF-8?q?build(dlcvcam):=20boot.img=20=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E6=89=93=E5=8C=85=E4=B8=8E=20sidecar=20=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build_bootimg.sh:配置/编译/打包命名镜像 + 自动 bump - scripts/dlcvcam_verify_bootimg.py:整包 SHA 与 FIT 内嵌 hash - 手册:编包与烧录前校验;防砖改外链 u-boot / CamOS Admin - gitignore:本地 bad_packages 与镜像 sidecar boot-try 用户态与 recovery sync 不在本仓(见 u-boot tools/dlcvcam、Admin)。 Co-Authored-By: Claude --- .gitignore | 6 + ...15\344\275\234\346\211\213\345\206\214.md" | 114 +++- build_bootimg.sh | 294 +++++++++ scripts/dlcvcam_verify_bootimg.py | 556 ++++++++++++++++++ 4 files changed, 952 insertions(+), 18 deletions(-) create mode 100755 build_bootimg.sh create mode 100755 scripts/dlcvcam_verify_bootimg.py diff --git a/.gitignore b/.gitignore index 73f36f5bc5b78..761caecd81153 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,9 @@ sphinx_*/ *.dtb* *.scr extboot* + +# DLCVCAM local build / test artifacts (not sources) +/bad_packages/ +*.img.sha256 +*.img.dlcvcam.json +/boot-rk3576-*.img 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..b84e796403c5c 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" @@ -113,36 +113,111 @@ sudo rkdeveloptool rd # 重启 --- -## 4. 完整编译更新脚本 +## 4. 完整编译更新脚本(推荐) -在编译主机上创建 `build_bootimg.sh`: +> **推荐日常使用仓库根目录的 `build_bootimg.sh`**(见下)。 +> 防砖(U-Boot FIT fallback / boot-try、recovery 金镜像、confirm 服务)**不在本仓库实现**: +> - U-Boot:https://github.com/dl-cv/u-boot/pull/1 (`tools/dlcvcam/` 含 confirm 脚本协议副本) +> - CamOS Admin:seed recovery、安装 boot-try / WDT、维护脚本 `dlcvcam_sync_boot_to_recovery.sh` + +### 4.1 一键编译 ```bash -#!/bin/bash -set -e +cd /path/to/kernel # 本仓库 -cd /home/ypw/kernel +./build_bootimg.sh -echo "[1/4] 增量编译内核..." -make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image dtbs modules -j8 +# 内存较小时降低并行度 +./build_bootimg.sh -j4 -echo "[2/4] 生成 Image.lz4..." -lz4 -f arch/arm64/boot/Image arch/arm64/boot/Image.lz4 +# 强制重新 defconfig + 合并裁剪配置 +./build_bootimg.sh --force-config -echo "[3/4] 打包 boot.img..." -BOOT_ITS=boot.its ./scripts/mkimg --dtb dlcvcam-rk3576.dtb +# 删除 .config 后走完整初次配置 +./build_bootimg.sh --clean-config -echo "[4/4] 提示:如需更新内核模块(如 WiFi/BT 驱动等),请执行:" -echo " sudo make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules_install INSTALL_MOD_PATH=/path/to/rootfs" +# 不自动改写 DLCVCAM_BUILD_VERSION(沿用文件当前值) +./build_bootimg.sh --no-bump +``` + +### 4.2 脚本会做什么 + +1. **构建编号**:按 `DLCVCAM_BUILD_VERSION`(`YYYYMMDDNN`)规则自动 bump(见 §2.1)。 +2. **配置**:无合适 `.config` 时 `lubancat_linux_rk3576_defconfig` + `dlcvcam_rk3576_kernel_cut.config`。 +3. **编译打包**:`Image` / `dtbs` / `modules` → `Image.lz4` → `boot.img`(FIT)。 +4. **交付文件名**(同时保留根目录 `boot.img`): + +```text +boot-rk3576-6.1.99--.img +``` -echo "" -echo "完成!" -ls -lh boot.img +5. **完整性 sidecar(无签名)**:为 `boot.img` 与交付镜像各生成: -echo "" -echo "下一步:通过 RKDevTool 或 rkdeveloptool 烧录 boot.img" +```text +<镜像>.sha256 # 整包 SHA-256 +<镜像>.dlcvcam.json # 整包哈希 + FIT 内嵌分量哈希清单 + 构建元数据 ``` +FIT 镜像本身在打包时已写入各 image 的内嵌 `sha256`(见 `boot.its`)。 +sidecar 用于上传后、烧录前做**损坏/截断/误传**检测;**不能替代签名**。 + +### 4.3 编译完成后 + +```bash +ls -lh boot.img boot-rk3576-*.img boot-rk3576-*.img.sha256 boot-rk3576-*.img.dlcvcam.json +mkimage -l boot.img + +# 本地再验一次(打包脚本结束时已自检) +python3 scripts/dlcvcam_verify_bootimg.py verify boot-rk3576-*.img --require-sidecar +``` + +### 4.4 板卡上传后、烧录前校验(推荐) + +把**镜像 + `.sha256`(建议连同 `.dlcvcam.json` 与校验脚本)**一起拷到板子,例如 `/tmp`: + +```bash +# 在板卡上(需 python3) +python3 scripts/dlcvcam_verify_bootimg.py verify /tmp/boot-rk3576-....img \ + --require-sidecar +# 退出码 0 才允许烧录;非 0 则重新传输,不要 wl/dd +``` + +| 检查 | 依据 | 作用 | +|------|------|------| +| 整包 SHA-256 | `.sha256` | 发现下载截断、拷贝损坏、传错文件 | +| FIT 内嵌 hash | boot.img 内 fdt/kernel/resource 的 sha256 节点 | 发现 payload 被改;**不依赖 sidecar** 也能查内容损坏 | + +只带了镜像、没有 sidecar 时,仍可只验 FIT 内嵌 hash: + +```bash +python3 scripts/dlcvcam_verify_bootimg.py verify /tmp/boot-rk3576-....img +``` + +对已有镜像补生成 sidecar(开发机): + +```bash +python3 scripts/dlcvcam_verify_bootimg.py gen-sidecar boot-rk3576-....img +``` + +下一步:校验通过后,用第 3 节的 RKDevTool 或 `rkdeveloptool` 烧录(或经 **CamOS Admin** 内核升级面板)。 + +### 4.5 防砖与 recovery(外链,本仓不提供脚本) + +| 能力 | 仓库 / 位置 | +|------|-------------| +| boot FIT 坏 → 读 recovery | [u-boot#1](https://github.com/dl-cv/u-boot/pull/1) `fit.c` | +| 合法 FIT 起不来 → boot-try 计数 | 同上;misc@48KiB `DCBT` | +| multi-user 后清计数 | u-boot `tools/dlcvcam/` + **Admin** 安装 confirm unit | +| recovery 金镜像 status/sync/restore | **CamOS Admin** `dlcvcam_sync_boot_to_recovery.sh` | +| 硬挂复位 | Admin `10-dlcvcam-watchdog.conf`(`RuntimeWatchdogSec`) | +| 升级前 seed recovery / 只写 boot | Admin 内核升级逻辑 | + +**原则(产品路径):** + +- 日常升级**只写 boot**;recovery 为金镜像,稳定后才手工提升。 +- 不使用完整 userspace bootguard 状态机。 +- 板端安装与操作以 Admin / u-boot `tools/dlcvcam/README.md` 为准。 + --- ## 附录:常用排查命令 @@ -151,6 +226,9 @@ echo "下一步:通过 RKDevTool 或 rkdeveloptool 烧录 boot.img" # 查看 FIT 镜像内容 mkimage -l boot.img +# 完整性校验(整包 + FIT 内嵌 hash,无签名) +python3 scripts/dlcvcam_verify_bootimg.py verify boot.img --require-sidecar + # 查看 eMMC 分区表 fdisk -l /dev/mmcblk0 diff --git a/build_bootimg.sh b/build_bootimg.sh new file mode 100755 index 0000000000000..6322eeab7c39f --- /dev/null +++ b/build_bootimg.sh @@ -0,0 +1,294 @@ +#!/bin/bash +# DLCVCAM RK3576 内核自动编译脚本 +# - 自动判断初次配置 / 增量编译 +# - 若 DLCVCAM_BUILD_VERSION 对应提交与当前 HEAD 不同,则按当天日期自动 bump +# - 产出带构建编号与短提交的 boot 镜像:boot-rk3576-6.1.99-YYYYMMDDNN-.img +# - 同时生成完整性 sidecar(不使用签名): +# .sha256 整包 SHA-256 +# .dlcvcam.json 整包哈希 + FIT 内嵌分量哈希清单 +# 板端烧录前:python3 scripts/dlcvcam_verify_bootimg.py verify --require-sidecar +# +# 用法: +# ./build_bootimg.sh # 自动判断配置并编译 +# ./build_bootimg.sh --force-config # 强制重新 defconfig + merge +# ./build_bootimg.sh --no-bump # 不自动更新 DLCVCAM_BUILD_VERSION +# ./build_bootimg.sh -j4 # 指定并行度 +# ./build_bootimg.sh --clean-config # 删除 .config 后走初次配置 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +ARCH=arm64 +CROSS_COMPILE=aarch64-linux-gnu- +DEFCONFIG=lubancat_linux_rk3576_defconfig +CUT_CONFIG=arch/arm64/configs/dlcvcam_rk3576_kernel_cut.config +DTB_NAME=dlcvcam-rk3576.dtb +BUILD_VERSION_FILE=DLCVCAM_BUILD_VERSION +KERNEL_VER_PREFIX="6.1.99" +JOBS="$(nproc 2>/dev/null || echo 8)" +FORCE_CONFIG=0 +NO_BUMP=0 +CLEAN_CONFIG=0 + +log() { printf '\033[1;34m[%s]\033[0m %s\n' "$(date '+%H:%M:%S')" "$*"; } +ok() { printf '\033[1;32m[%s]\033[0m %s\n' "$(date '+%H:%M:%S')" "$*"; } +warn() { printf '\033[1;33m[%s]\033[0m %s\n' "$(date '+%H:%M:%S')" "$*" >&2; } +die() { printf '\033[1;31m[%s]\033[0m %s\n' "$(date '+%H:%M:%S')" "$*" >&2; exit 1; } + +usage() { + sed -n '2,12p' "$0" | sed 's/^# \?//' + exit 0 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage ;; + --force-config) FORCE_CONFIG=1; shift ;; + --clean-config) CLEAN_CONFIG=1; shift ;; + --no-bump) NO_BUMP=1; shift ;; + -j*) + if [[ "$1" == "-j" ]]; then + JOBS="${2:?-j 需要并行数}" + shift 2 + else + JOBS="${1#-j}" + shift + fi + ;; + *) + die "未知参数: $1 (见 --help)" + ;; + esac +done + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1" +} + +need_cmd make +need_cmd lz4 +need_cmd git +need_cmd "${CROSS_COMPILE}gcc" + +# ---------- 构建编号:相对上次提交该文件的 commit 是否变化 ---------- +# 规则: +# 1. 若 DLCVCAM_BUILD_VERSION 相对 HEAD 有未提交修改,且内容已是合法编号,则沿用工作区值 +# 2. 否则看「最后一次提交该文件的 commit」是否等于当前 HEAD: +# - 相等:说明本提交就是 bump 提交,沿用文件内容 +# - 不等:说明 HEAD 相对上次 bump 又有新提交,按当天日期自动 bump(同日递增 NN) +bump_build_version_if_needed() { + local today current last_bump_commit head_commit date_part seq new_ver + + [[ -f "${BUILD_VERSION_FILE}" ]] || die "缺少 ${BUILD_VERSION_FILE}" + + current="$(tr -d '[:space:]' < "${BUILD_VERSION_FILE}")" + [[ "${current}" =~ ^[0-9]{8}(0[1-9]|[1-9][0-9])$ ]] \ + || die "${BUILD_VERSION_FILE} 格式非法: '${current}'(需要 YYYYMMDDNN,NN=01..99)" + + if [[ "${NO_BUMP}" -eq 1 ]]; then + warn "跳过版本自动 bump(--no-bump),使用: ${current}" + echo "${current}" + return + fi + + # 工作区已改过该文件:尊重用户/上次脚本写好的值 + if ! git diff --quiet -- "${BUILD_VERSION_FILE}" 2>/dev/null \ + || ! git diff --cached --quiet -- "${BUILD_VERSION_FILE}" 2>/dev/null; then + log "${BUILD_VERSION_FILE} 工作区已修改,沿用: ${current}" + echo "${current}" + return + fi + + head_commit="$(git rev-parse HEAD)" + last_bump_commit="$(git log -1 --format='%H' -- "${BUILD_VERSION_FILE}" 2>/dev/null || true)" + + if [[ -n "${last_bump_commit}" && "${last_bump_commit}" == "${head_commit}" ]]; then + log "${BUILD_VERSION_FILE} 已在当前 HEAD 提交,沿用: ${current}" + echo "${current}" + return + fi + + today="$(date +%Y%m%d)" + date_part="${current:0:8}" + seq="${current:8:2}" + # 去掉前导零做算术 + seq=$((10#${seq})) + + if [[ "${date_part}" == "${today}" ]]; then + seq=$((seq + 1)) + if [[ "${seq}" -gt 99 ]]; then + die "当天构建序号已超过 99,请手工处理 ${BUILD_VERSION_FILE}" + fi + new_ver="$(printf '%s%02d' "${today}" "${seq}")" + else + new_ver="${today}01" + fi + + printf '%s\n' "${new_ver}" > "${BUILD_VERSION_FILE}" + ok "HEAD 相对上次 ${BUILD_VERSION_FILE} 提交已变化,自动 bump: ${current} -> ${new_ver}" + echo "${new_ver}" +} + +# ---------- 初次配置 vs 增量 ---------- +need_initial_config() { + [[ "${FORCE_CONFIG}" -eq 1 ]] && return 0 + [[ "${CLEAN_CONFIG}" -eq 1 ]] && return 0 + [[ ! -f .config ]] && return 0 + + # 关键配置标记:本地版本与 ARM64,避免误用其它板级 .config + if ! grep -q '^CONFIG_ARM64=y' .config 2>/dev/null; then + warn ".config 不是 ARM64 配置,将重新配置" + return 0 + fi + if ! grep -q 'CONFIG_LOCALVERSION="-rk3576"' .config 2>/dev/null; then + # 裁剪配置合并后通常带 -rk3576;没有也不强制失败,仅提示 + warn ".config 未包含 CONFIG_LOCALVERSION=\"-rk3576\",仍按增量处理(可用 --force-config)" + fi + return 1 +} + +do_initial_config() { + log "初次配置:${DEFCONFIG} + ${CUT_CONFIG}" + if [[ "${CLEAN_CONFIG}" -eq 1 && -f .config ]]; then + rm -f .config .config.old + fi + + make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" "${DEFCONFIG}" + [[ -f "${CUT_CONFIG}" ]] || die "缺少裁剪配置: ${CUT_CONFIG}" + ./scripts/kconfig/merge_config.sh -m .config "${CUT_CONFIG}" + make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" olddefconfig + ok "配置完成" +} + +do_build() { + log "编译 Image / dtbs / modules (-j${JOBS})..." + make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" \ + Image dtbs modules -j"${JOBS}" + + log "压缩 Image -> Image.lz4 ..." + lz4 -f arch/arm64/boot/Image arch/arm64/boot/Image.lz4 + + log "打包 boot.img (DTB=${DTB_NAME})..." + BOOT_ITS=boot.its ./scripts/mkimg --dtb "${DTB_NAME}" + + [[ -f boot.img ]] || die "未生成 boot.img" +} + +is_worktree_dirty_except_build_version() { + # 忽略 DLCVCAM_BUILD_VERSION 本身(脚本可能刚 bump 过) + # 以及常见编译产物,避免误标 dirty + local line path + while IFS= read -r line; do + [[ -z "${line}" ]] && continue + path="${line:3}" + path="${path#\"}" + path="${path%\"}" + # rename: "old -> new" + if [[ "${path}" == *" -> "* ]]; then + path="${path##* -> }" + fi + case "${path}" in + "${BUILD_VERSION_FILE}") continue ;; + boot.img|resource.img|zboot.img|out|out/*) continue ;; + boot.img.sha256|boot.img.dlcvcam.json) continue ;; + boot-rk3576-*.img|boot-rk3576-*.img.sha256|boot-rk3576-*.img.dlcvcam.json) continue ;; + arch/arm64/boot/Image|arch/arm64/boot/Image.lz4) continue ;; + .config|.config.old) continue ;; + bad_packages|bad_packages/*) continue ;; + esac + return 0 + done < <(git status --porcelain --untracked-files=no 2>/dev/null || true) + return 1 +} + +write_bootimg_integrity_sidecars() { + # 生成整包 sha256 + JSON 清单;板端烧录前用 scripts/dlcvcam_verify_bootimg.py 校验 + # 不使用签名,仅防传输损坏 / 误传 / 非密码学意义上的篡改发现 + local img="$1" + local build_id="$2" + local kernelrelease="$3" + local git_desc="$4" + local verify_py="${SCRIPT_DIR}/scripts/dlcvcam_verify_bootimg.py" + + [[ -f "${img}" ]] || die "write_bootimg_integrity_sidecars: 缺少 ${img}" + + if [[ -f "${verify_py}" ]] && command -v python3 >/dev/null 2>&1; then + python3 "${verify_py}" gen-sidecar "${img}" \ + --build-version "${build_id}" \ + --kernelrelease "${kernelrelease:-}" \ + --git "${git_desc}" \ + || die "生成完整性 sidecar 失败: ${img}" + # 自检:刚生成的包必须能通过 FIT 内嵌 hash + 整包 sha256 + python3 "${verify_py}" verify "${img}" --require-sidecar \ + || die "打包后自检失败(不应发生): ${img}" + return + fi + + # 兜底:无 python 时只写整包 sha256 + warn "python3 或 scripts/dlcvcam_verify_bootimg.py 不可用,仅写入 ${img}.sha256" + need_cmd sha256sum + sha256sum "${img}" | awk -v n="$(basename "${img}")" '{print $1 " " n}' > "${img}.sha256" +} + +package_named_image() { + local build_ver short_sha out_name kernelrelease build_id git_desc + + build_ver="$(tr -d '[:space:]' < "${BUILD_VERSION_FILE}")" + short_sha="$(git rev-parse --short=8 HEAD)" + # 除版本文件/编译产物外还有改动时加 -dirty,避免与干净提交产物混淆 + if is_worktree_dirty_except_build_version; then + short_sha="${short_sha}-dirty" + fi + + out_name="boot-rk3576-${KERNEL_VER_PREFIX}-${build_ver}-${short_sha}.img" + cp -f boot.img "${out_name}" + + if command -v mkimage >/dev/null 2>&1; then + log "FIT 信息:" + mkimage -l boot.img || true + fi + + kernelrelease="$(make -s ARCH="${ARCH}" kernelrelease 2>/dev/null || true)" + build_id="$(make -s ARCH="${ARCH}" dlcvcam-build-version 2>/dev/null || echo "${build_ver}")" + git_desc="$(git rev-parse --short HEAD) ($(git rev-parse --abbrev-ref HEAD))" + + log "生成完整性校验文件(整包 sha256 + FIT 清单)..." + write_bootimg_integrity_sidecars "boot.img" "${build_id}" "${kernelrelease:-}" "${git_desc}" + write_bootimg_integrity_sidecars "${out_name}" "${build_id}" "${kernelrelease:-}" "${git_desc}" + + ok "完成" + echo + echo " kernelrelease : ${kernelrelease:-n/a}" + echo " build version : ${build_id}" + echo " git : ${git_desc}" + echo " boot.img : $(ls -lh boot.img | awk '{print $5}')" + echo " 交付镜像 : ${out_name} ($(ls -lh "${out_name}" | awk '{print $5}'))" + echo " 整包校验 : ${out_name}.sha256" + echo " 清单 : ${out_name}.dlcvcam.json" + echo + echo "烧录前在板卡上校验(不验签,仅 hash):" + echo " python3 scripts/dlcvcam_verify_bootimg.py verify ${out_name} --require-sidecar" + echo " # 或只验 FIT 内嵌 sha256(无 sidecar 时):" + echo " python3 scripts/dlcvcam_verify_bootimg.py verify ${out_name}" + echo + echo "下一步:校验通过后再用 RKDevTool / rkdeveloptool 烧录 ${out_name}" + echo " (或直接烧录 boot.img,内容相同)" +} + +# ---------- main ---------- +log "工作目录: ${SCRIPT_DIR}" +log "交叉编译: ${CROSS_COMPILE}gcc ($(${CROSS_COMPILE}gcc -dumpmachine 2>/dev/null || echo '?'))" + +BUILD_VER="$(bump_build_version_if_needed)" +log "本次 KBUILD_BUILD_VERSION / DLCVCAM_BUILD_VERSION = ${BUILD_VER}" + +if need_initial_config; then + do_initial_config +else + log "检测到已有 .config,走增量编译" +fi + +do_build +package_named_image diff --git a/scripts/dlcvcam_verify_bootimg.py b/scripts/dlcvcam_verify_bootimg.py new file mode 100755 index 0000000000000..5903472a72988 --- /dev/null +++ b/scripts/dlcvcam_verify_bootimg.py @@ -0,0 +1,556 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +DLCVCAM boot.img (FIT) integrity checker — hash only, no signature. + +Verifies: + 1) Optional whole-file SHA-256 against .sha256 or --expect-sha256 + 2) FIT/FDT structure parse + 3) Embedded per-image hash nodes (fdt / kernel / resource, ...) + +Exit codes: + 0 OK + 1 verification failed + 2 usage / I/O / parse error +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import struct +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + + +FDT_MAGIC = 0xD00DFEED +FDT_BEGIN_NODE = 1 +FDT_END_NODE = 2 +FDT_PROP = 3 +FDT_NOP = 4 +FDT_END = 9 + +SUPPORTED_HASH = { + "sha1": (hashlib.sha1, 20), + "sha256": (hashlib.sha256, 32), + "sha512": (hashlib.sha512, 64), +} + + +class VerifyError(Exception): + pass + + +def eprint(*args: object) -> None: + print(*args, file=sys.stderr) + + +def sha256_file(path: Path, chunk: int = 1024 * 1024) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + while True: + b = f.read(chunk) + if not b: + break + h.update(b) + return h.hexdigest() + + +def parse_sha256_sidecar(text: str) -> str: + """ + Accept: + + + * + """ + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + part = line.split()[0].strip().lower() + if len(part) == 64 and all(c in "0123456789abcdef" for c in part): + return part + raise VerifyError("sidecar 中未找到合法 sha256") + + +def align4(n: int) -> int: + return (n + 3) & ~3 + + +def parse_fdt_tree(data: bytes, fdt_offset: int = 0) -> Dict[str, Dict[str, bytes]]: + if len(data) < fdt_offset + 40: + raise VerifyError("文件过短,无法读取 FDT 头") + + magic, totalsize, off_dt_struct, off_dt_strings, _off_mem, version, _last, _cpu, size_dt_strings, size_dt_struct = struct.unpack_from( + ">10I", data, fdt_offset + ) + if magic != FDT_MAGIC: + raise VerifyError(f"非 FIT/FDT 魔数: 0x{magic:08x} (期望 0xd00dfeed)") + if version < 16: + raise VerifyError(f"不支持的 FDT version: {version}") + if totalsize < 40 or fdt_offset + totalsize > len(data): + # external-data FIT: totalsize 只覆盖 header tree,payload 在后面 —— 允许 totalsize 小于文件 + if totalsize < 40: + raise VerifyError(f"非法 FDT totalsize: {totalsize}") + + struct_base = fdt_offset + off_dt_struct + strings_base = fdt_offset + off_dt_strings + struct_end = struct_base + size_dt_struct + strings_end = strings_base + size_dt_strings + if struct_end > len(data) or strings_end > len(data): + raise VerifyError("FDT struct/strings 超出文件范围") + + def get_string(nameoff: int) -> str: + start = strings_base + nameoff + if start >= strings_end: + raise VerifyError("FDT 字符串偏移越界") + end = data.find(b"\x00", start, strings_end) + if end < 0: + raise VerifyError("FDT 字符串缺少 NUL") + return data[start:end].decode("ascii", "replace") + + pos = struct_base + depth = 0 + stack: List[str] = [] + props: Dict[str, Dict[str, bytes]] = {} + + while pos + 4 <= struct_end: + tag = struct.unpack_from(">I", data, pos)[0] + pos += 4 + if tag == FDT_BEGIN_NODE: + end = data.find(b"\x00", pos, struct_end) + if end < 0: + raise VerifyError("FDT 节点名缺少 NUL") + name = data[pos:end].decode("ascii", "replace") + pos = align4(end + 1) + depth += 1 + if depth == 1: + full = "/" + else: + parent = stack[-1] + full = f"{parent}{name}" if parent.endswith("/") else f"{parent}/{name}" + if parent == "/": + full = f"/{name}" + stack.append(full) + props.setdefault(full, {}) + elif tag == FDT_END_NODE: + if not stack: + raise VerifyError("FDT END_NODE 不匹配") + stack.pop() + depth -= 1 + elif tag == FDT_PROP: + if pos + 8 > struct_end: + raise VerifyError("FDT PROP 头截断") + plen, nameoff = struct.unpack_from(">II", data, pos) + pos += 8 + if pos + plen > struct_end: + raise VerifyError("FDT PROP 值截断") + pname = get_string(nameoff) + pval = data[pos : pos + plen] + pos = align4(pos + plen) + if not stack: + raise VerifyError("FDT PROP 无当前节点") + props[stack[-1]][pname] = pval + elif tag == FDT_NOP: + continue + elif tag == FDT_END: + break + else: + raise VerifyError(f"未知 FDT tag {tag} @ {pos - 4}") + + return props + + +def _cstr(val: bytes) -> str: + return val.split(b"\x00", 1)[0].decode("ascii", "replace") + + +def _u32(val: bytes) -> Optional[int]: + if len(val) != 4: + return None + return struct.unpack(">I", val)[0] + + +def collect_fit_images(props: Dict[str, Dict[str, bytes]]) -> List[Dict[str, Any]]: + images: List[Dict[str, Any]] = [] + for path, p in props.items(): + # image nodes: /images/ (exactly one name segment under /images) + if not path.startswith("/images/"): + continue + rest = path[len("/images/") :] + if not rest or "/" in rest: + continue + name = rest + img: Dict[str, Any] = { + "name": name, + "path": path, + "type": _cstr(p["type"]) if "type" in p else "", + "compression": _cstr(p["compression"]) if "compression" in p else "", + "data_position": _u32(p["data-position"]) if "data-position" in p else None, + "data_offset": _u32(p["data-offset"]) if "data-offset" in p else None, + "data_size": _u32(p["data-size"]) if "data-size" in p else None, + "hashes": [], + } + # inline data property (rare for our external FIT) + if "data" in p: + img["inline_data_len"] = len(p["data"]) + img["inline_data"] = p["data"] + + # hash subnodes: /images//hash or hash@1 ... + prefix = path + "/" + for hpath, hp in props.items(): + if not hpath.startswith(prefix): + continue + sub = hpath[len(prefix) :] + if "/" in sub: + continue + if not (sub == "hash" or sub.startswith("hash@") or sub.startswith("hash-")): + continue + if "algo" not in hp or "value" not in hp: + continue + img["hashes"].append( + { + "node": sub, + "algo": _cstr(hp["algo"]).lower(), + "value": hp["value"], + } + ) + images.append(img) + images.sort(key=lambda x: x["name"]) + return images + + +def resolve_image_blob(data: bytes, img: Dict[str, Any], fdt_totalsize_hint: int) -> bytes: + if "inline_data" in img: + return img["inline_data"] + + size = img.get("data_size") + if size is None: + raise VerifyError(f"镜像 {img['name']}: 缺少 data-size/data") + + pos = img.get("data_position") + if pos is not None: + end = pos + size + if end > len(data): + raise VerifyError( + f"镜像 {img['name']}: data-position/size 越界 ({pos}+{size} > {len(data)})" + ) + return data[pos:end] + + off = img.get("data_offset") + if off is not None: + # external data: offset relative to end of FIT header (aligned external area) + # U-Boot -E external data: payload starts after FIT totalsize, often aligned. + # data-offset is relative to the external data base (right after FIT). + base = fdt_totalsize_hint + # mkimage -E -p 0x800: external data base is max(totalsize aligned, -p alignment) + # For our images, data-position is absolute; data-offset path kept for completeness. + pos2 = base + off + end = pos2 + size + if end > len(data): + raise VerifyError( + f"镜像 {img['name']}: data-offset/size 越界 ({pos2}+{size} > {len(data)})" + ) + return data[pos2:end] + + raise VerifyError(f"镜像 {img['name']}: 无 data / data-position / data-offset") + + +def verify_fit_hashes(data: bytes) -> Tuple[bool, List[str], List[Dict[str, Any]]]: + props = parse_fdt_tree(data, 0) + magic, totalsize = struct.unpack_from(">2I", data, 0) + assert magic == FDT_MAGIC + + images = collect_fit_images(props) + if not images: + raise VerifyError("FIT 中未找到 /images/* 节点") + + messages: List[str] = [] + details: List[Dict[str, Any]] = [] + all_ok = True + + for img in images: + entry: Dict[str, Any] = { + "name": img["name"], + "type": img["type"], + "data_size": img.get("data_size"), + "data_position": img.get("data_position"), + "hashes": [], + } + try: + blob = resolve_image_blob(data, img, totalsize) + except VerifyError as ex: + all_ok = False + messages.append(f"[FAIL] {img['name']}: {ex}") + entry["error"] = str(ex) + details.append(entry) + continue + + if not img["hashes"]: + all_ok = False + messages.append(f"[FAIL] {img['name']}: 无内嵌 hash 节点") + entry["error"] = "no hash node" + details.append(entry) + continue + + for h in img["hashes"]: + algo = h["algo"] + val = h["value"] + hinfo: Dict[str, Any] = { + "node": h["node"], + "algo": algo, + "expected": val.hex(), + } + if algo not in SUPPORTED_HASH: + all_ok = False + hinfo["ok"] = False + hinfo["error"] = f"不支持的 hash 算法: {algo}" + messages.append(f"[FAIL] {img['name']}/{h['node']}: 不支持算法 {algo}") + entry["hashes"].append(hinfo) + continue + factory, digest_len = SUPPORTED_HASH[algo] + if len(val) != digest_len: + # some FIT blobs store value with padding + if len(val) > digest_len and all(b == 0 for b in val[digest_len:]): + val = val[:digest_len] + hinfo["expected"] = val.hex() + else: + all_ok = False + hinfo["ok"] = False + hinfo["error"] = f"hash 长度 {len(val)} != {digest_len}" + messages.append( + f"[FAIL] {img['name']}/{h['node']}: hash 长度异常 {len(val)}" + ) + entry["hashes"].append(hinfo) + continue + actual = factory(blob).digest() + hinfo["actual"] = actual.hex() + ok = hmac_compare(actual, val) + hinfo["ok"] = ok + if ok: + messages.append( + f"[OK] {img['name']}/{h['node']} {algo} ({len(blob)} bytes)" + ) + else: + all_ok = False + messages.append( + f"[FAIL] {img['name']}/{h['node']} {algo} mismatch\n" + f" expect {val.hex()}\n" + f" actual {actual.hex()}" + ) + entry["hashes"].append(hinfo) + details.append(entry) + + return all_ok, messages, details + + +def hmac_compare(a: bytes, b: bytes) -> bool: + if len(a) != len(b): + return False + diff = 0 + for x, y in zip(a, b): + diff |= x ^ y + return diff == 0 + + +def load_expect_sha256(img: Path, explicit: Optional[str], sidecar: Optional[Path]) -> Optional[str]: + if explicit: + v = explicit.strip().lower() + if len(v) != 64 or any(c not in "0123456789abcdef" for c in v): + raise VerifyError("--expect-sha256 不是 64 位 hex") + return v + candidates: List[Path] = [] + if sidecar: + candidates.append(sidecar) + candidates.append(Path(str(img) + ".sha256")) + candidates.append(img.with_suffix(img.suffix + ".sha256")) + # also boot-xxx.img.sha256 already covered; try stem.sha256 + candidates.append(img.with_suffix(".sha256")) + for c in candidates: + if c.is_file(): + return parse_sha256_sidecar(c.read_text(encoding="utf-8", errors="replace")) + return None + + +def write_manifest(img: Path, file_sha: str, fit_details: List[Dict[str, Any]], meta: Dict[str, Any]) -> Path: + out = Path(str(img) + ".dlcvcam.json") + doc = { + "format": "dlcvcam-bootimg-manifest-v1", + "file": img.name, + "size": img.stat().st_size, + "sha256": file_sha, + "fit_images": [ + { + "name": d.get("name"), + "type": d.get("type"), + "data_size": d.get("data_size"), + "data_position": d.get("data_position"), + "hashes": [ + { + "node": h.get("node"), + "algo": h.get("algo"), + "sha": h.get("expected") or h.get("actual"), + } + for h in d.get("hashes", []) + ], + } + for d in fit_details + ], + "meta": meta, + } + out.write_text(json.dumps(doc, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + return out + + +def cmd_verify(args: argparse.Namespace) -> int: + img = Path(args.image) + if not img.is_file(): + eprint(f"文件不存在: {img}") + return 2 + + size = img.stat().st_size + if size <= 0: + eprint("[FAIL] 空文件") + return 1 + + data = img.read_bytes() + file_sha = hashlib.sha256(data).hexdigest() + print(f"file : {img}") + print(f"size : {size}") + print(f"sha256 : {file_sha}") + + failed = False + + # whole-file hash + try: + expect = load_expect_sha256(img, args.expect_sha256, Path(args.sha256_file) if args.sha256_file else None) + except VerifyError as ex: + eprint(f"[ERR] {ex}") + return 2 + + if expect is None: + if args.require_sidecar: + eprint("[FAIL] 未提供/未找到整包 .sha256(指定了 --require-sidecar)") + return 1 + print("whole-file: (no sidecar / --expect-sha256, skip)") + else: + if hmac_compare(bytes.fromhex(expect), bytes.fromhex(file_sha)): + print(f"[OK] whole-file sha256 matches") + else: + failed = True + print(f"[FAIL] whole-file sha256 mismatch") + print(f" expect {expect}") + print(f" actual {file_sha}") + + # FIT internal hashes + try: + ok, messages, details = verify_fit_hashes(data) + except VerifyError as ex: + eprint(f"[FAIL] FIT 解析/校验错误: {ex}") + return 1 + + print("fit-hash :") + for m in messages: + print(f" {m}") + if not ok: + failed = True + + if args.json_out: + Path(args.json_out).write_text( + json.dumps( + { + "file": str(img), + "size": size, + "sha256": file_sha, + "whole_file_ok": (expect is None) or (expect == file_sha), + "fit_ok": ok, + "fit_images": details, + }, + indent=2, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", + ) + + if failed: + print("RESULT : FAIL") + return 1 + print("RESULT : OK") + return 0 + + +def cmd_gen_sidecar(args: argparse.Namespace) -> int: + img = Path(args.image) + if not img.is_file(): + eprint(f"文件不存在: {img}") + return 2 + data = img.read_bytes() + file_sha = hashlib.sha256(data).hexdigest() + + sha_path = Path(args.sha256_file) if args.sha256_file else Path(str(img) + ".sha256") + sha_path.write_text(f"{file_sha} {img.name}\n", encoding="utf-8") + + fit_details: List[Dict[str, Any]] = [] + try: + _ok, _msgs, fit_details = verify_fit_hashes(data) + except VerifyError as ex: + eprint(f"警告: FIT 解析失败,仍写入整包 sha256: {ex}") + + meta = { + "build_version": args.build_version or "", + "git": args.git or "", + "kernelrelease": args.kernelrelease or "", + "generated_by": "scripts/dlcvcam_verify_bootimg.py", + } + man_path = write_manifest(img, file_sha, fit_details, meta) + + print(f"wrote {sha_path}") + print(f"wrote {man_path}") + print(f"sha256 {file_sha}") + return 0 + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description="DLCVCAM boot.img hash verifier (no signature)") + sub = p.add_subparsers(dest="cmd", required=True) + + v = sub.add_parser("verify", help="校验 boot.img(整包 sha256 + FIT 内嵌 hash)") + v.add_argument("image", help="boot.img / boot-rk3576-*.img 路径") + v.add_argument("--expect-sha256", help="期望的整包 sha256 hex") + v.add_argument("--sha256-file", help="整包 sha256 sidecar 路径") + v.add_argument("--require-sidecar", action="store_true", help="必须存在整包 sha256") + v.add_argument("--json-out", help="把详细结果写到 JSON") + v.set_defaults(func=cmd_verify) + + g = sub.add_parser("gen-sidecar", help="为 boot.img 生成 .sha256 与 .dlcvcam.json") + g.add_argument("image", help="boot.img 路径") + g.add_argument("--sha256-file", help="输出 .sha256 路径") + g.add_argument("--build-version", default="") + g.add_argument("--git", default="") + g.add_argument("--kernelrelease", default="") + g.set_defaults(func=cmd_gen_sidecar) + + return p + + +def main(argv: Optional[List[str]] = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + try: + return int(args.func(args)) + except BrokenPipeError: + return 0 + except VerifyError as ex: + eprint(f"[ERR] {ex}") + return 2 + except OSError as ex: + eprint(f"[ERR] {ex}") + return 2 + + +if __name__ == "__main__": + sys.exit(main()) From 050b74ca80bfd3ee16125a2f1865d9ef9090da5b Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Tue, 4 Aug 2026 10:39:53 +0800 Subject: [PATCH 08/21] =?UTF-8?q?docs:=20=C2=A74.5=20=E5=A4=96=E9=93=BE=20?= =?UTF-8?q?CamOS=20=E9=98=B2=E7=A0=96=E4=B8=8E=E9=87=91=E9=95=9C=E5=83=8F?= =?UTF-8?q?=E7=BB=B4=E6=8A=A4=E6=89=8B=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- ...26\260\346\223\215\344\275\234\346\211\213\345\206\214.md" | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 b84e796403c5c..43538c81c42cb 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" @@ -212,11 +212,13 @@ python3 scripts/dlcvcam_verify_bootimg.py gen-sidecar boot-rk3576-....img | 硬挂复位 | Admin `10-dlcvcam-watchdog.conf`(`RuntimeWatchdogSec`) | | 升级前 seed recovery / 只写 boot | Admin 内核升级逻辑 | +板端完整说明(sync / boot-try / confirm / 看门狗 / 检查表):CamOS `docs/admin/DLCVCAM内核防砖与金镜像维护.md`。 + **原则(产品路径):** - 日常升级**只写 boot**;recovery 为金镜像,稳定后才手工提升。 - 不使用完整 userspace bootguard 状态机。 -- 板端安装与操作以 Admin / u-boot `tools/dlcvcam/README.md` 为准。 +- 板端安装与操作以 Admin 上述文档及 u-boot `tools/dlcvcam/README.md` 为准。 --- From 03cf5016e756d34ff71e6e7306575bd2536580ce Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Wed, 5 Aug 2026 11:26:08 +0800 Subject: [PATCH 09/21] fix(imx296): close FT frame and harden V39 early-done drain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Monitor↔trigger green bottom lines and full green frames after ROI come from two residual races on top of the N-1 early-done path: 1. The 50 ms tail-DMA hrtimer can fire after the next SOF and complete the wrong buffer. Stamp the armed SOF, drop late timers, cut the default drain to 2 ms, and clear wait_line/OUT_FRM_HALF when leaving fast-trigger so free-run restarts do not keep early-done. 2. Fast-trigger leaves the MIPI frame open until the next XTRIG. Emit a light-free dummy pulse before stream-off / leave-FT, drain the datasheet ROI invalid frame on free-run and FT stream-on, and keep GAINDLY in sync on hot mode_switch. Board flashed with build 2026080501 for validation. --- DLCVCAM_BUILD_VERSION | 2 +- drivers/media/i2c/imx296.c | 118 +++++++++++++++++- drivers/media/platform/rockchip/isp/capture.c | 18 ++- drivers/media/platform/rockchip/isp/capture.h | 2 + drivers/media/platform/rockchip/isp/common.h | 1 + drivers/media/platform/rockchip/isp/dev.c | 2 +- drivers/media/platform/rockchip/isp/rkisp.c | 23 +++- 7 files changed, 156 insertions(+), 10 deletions(-) diff --git a/DLCVCAM_BUILD_VERSION b/DLCVCAM_BUILD_VERSION index 63dd368ca5caa..6f412232a9fb8 100644 --- a/DLCVCAM_BUILD_VERSION +++ b/DLCVCAM_BUILD_VERSION @@ -1 +1 @@ -2026072301 +2026080501 diff --git a/drivers/media/i2c/imx296.c b/drivers/media/i2c/imx296.c index 1128cd13984e3..f252918c86bcd 100644 --- a/drivers/media/i2c/imx296.c +++ b/drivers/media/i2c/imx296.c @@ -182,6 +182,12 @@ /* Datasheet: changing ROI geometry produces one invalid output frame. */ #define IMX296_ROI_INVALID_FRAME_WAIT_US 50000U +/* + * After leaving fast-trigger, one short XTRIG is needed so the sensor closes the + * open MIPI frame before free-run stream_on. Without it ISP can latch a green + * or half-written buffer on the next free-run frames. + */ +#define IMX296_XTRIG_CLOSE_FRAME_WAIT_US 50000U enum imx296_op_mode { IMX296_FREE_RUN = 0, @@ -604,6 +610,47 @@ static int imx296_trigger_once_locked(struct imx296 *sensor) return ret; } + +/* + * Emit one XTRIG without light-source side effects so an open fast-trigger + * frame can finish readout. Caller must hold sensor->mutex. + */ +static int imx296_dummy_trigger_locked(struct imx296 *sensor, const char *reason) +{ + bool saved_light_en; + int ret; + + if (!sensor->trigger_pwm) { + dev_dbg(sensor->dev, + "dummy trigger skipped (no pwm) reason=%s\n", reason); + return -ENODEV; + } + + if (!sensor->streaming || + sensor->active_mode != IMX296_XTRIG_ONE_SHOT) { + dev_dbg(sensor->dev, + "dummy trigger skipped (streaming=%u active=%s) reason=%s\n", + sensor->streaming, + imx296_op_mode_name(sensor->active_mode), reason); + return 0; + } + + saved_light_en = sensor->light_source_enabled; + sensor->light_source_enabled = false; + ret = imx296_trigger_once_locked(sensor); + sensor->light_source_enabled = saved_light_en; + if (ret) { + dev_warn(sensor->dev, + "dummy trigger failed (%d) reason=%s\n", ret, reason); + return ret; + } + + usleep_range(IMX296_XTRIG_CLOSE_FRAME_WAIT_US, + IMX296_XTRIG_CLOSE_FRAME_WAIT_US + 5000U); + dev_dbg(sensor->dev, "dummy trigger done reason=%s\n", reason); + return 0; +} + static int imx296_set_ctrl(struct v4l2_ctrl *ctrl); static int imx296_mode_switch(struct imx296 *sensor, enum imx296_op_mode new_mode); @@ -1442,6 +1489,8 @@ static int imx296_apply_mode_regs_locked(struct imx296 *sensor, imx296_write(sensor, IMX296_SYNCSEL, IMX296_SYNCSEL_NORMAL, &ret); imx296_disable_trigger_outputs_locked(sensor, &ret); + /* Free-run uses next-frame gain application. */ + imx296_write(sensor, IMX296_GAINDLY, IMX296_GAINDLY_1FRAME, &ret); break; case IMX296_XTRIG_ONE_SHOT: @@ -1457,6 +1506,12 @@ static int imx296_apply_mode_regs_locked(struct imx296 *sensor, IMX296_LOWLAGTRG_FAST, &ret); imx296_write(sensor, IMX296_SYNCSEL, IMX296_SYNCSEL_HIZ, &ret); imx296_disable_trigger_outputs_locked(sensor, &ret); + /* + * Fast-trigger gain must take effect on the first XTRIG after a + * control update. setup() already picks 0x08 for pending FT, but + * hot mode_switch does not re-run setup — program it here too. + */ + imx296_write(sensor, IMX296_GAINDLY, IMX296_GAINDLY_0FRAME, &ret); break; default: @@ -1552,11 +1607,17 @@ static int imx296_quick_stream(struct imx296 *sensor, bool on) static int imx296_mode_switch(struct imx296 *sensor, enum imx296_op_mode new_mode) { + enum imx296_op_mode old_mode; int ret = 0; if (new_mode > IMX296_XTRIG_ONE_SHOT) return -EINVAL; + /* + * Publish the requested mode to RKISP before the sensor is stopped so + * the next isp_start (CamOS PAUSED->READY->PLAYING) programs the matching + * early-done policy for the target mode. + */ atomic_set(&imx296_fast_trigger_mode, new_mode == IMX296_XTRIG_ONE_SHOT); sensor->pending_mode = new_mode; @@ -1572,6 +1633,16 @@ static int imx296_mode_switch(struct imx296 *sensor, if (ret < 0) return ret; + old_mode = sensor->active_mode; + + /* + * Fast-trigger leaves the MIPI frame open until the next XTRIG. Close it + * before STANDBY so CIF/ISP do not keep a half-written buffer that later + * surfaces as a green frame when returning to free-run / monitor. + */ + if (old_mode == IMX296_XTRIG_ONE_SHOT) + imx296_dummy_trigger_locked(sensor, "leave-fast-trigger"); + ret = imx296_stream_off(sensor); if (ret) goto out_pm; @@ -1593,8 +1664,26 @@ static int imx296_mode_switch(struct imx296 *sensor, sensor->active_mode = new_mode; sensor->streaming = true; + /* + * Datasheet: one invalid frame after ROI geometry change. If the crop + * changed while we were still in FT (or ROI was pending across the + * switch), drain it now that free-run is producing frames again. For a + * switch into FT the free-run path in s_stream already drained it when + * CamOS restarts in free-run first; keep a free-run drain here for the + * hot streaming path used by sysfs without a full STREAMOFF. + */ + 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; + } + out_pm: if (ret && !sensor->streaming) { + /* Roll the mode bridge back so ISP does not keep FT early-done. */ + atomic_set(&imx296_fast_trigger_mode, + sensor->active_mode == IMX296_XTRIG_ONE_SHOT); pm_runtime_mark_last_busy(sensor->dev); pm_runtime_put_autosuspend(sensor->dev); } else { @@ -1714,6 +1803,13 @@ static int imx296_s_stream(struct v4l2_subdev *sd, int enable) goto unlock; if (!enable) { + /* + * Close any open fast-trigger frame before STANDBY so the last + * ISP buffer is not left half-written (green) across STREAMOFF. + */ + if (sensor->active_mode == IMX296_XTRIG_ONE_SHOT) + imx296_dummy_trigger_locked(sensor, "stream-off"); + ret = imx296_stream_off(sensor); imx296_light_source_set_locked(sensor, false); if (!ret) { @@ -1726,6 +1822,10 @@ static int imx296_s_stream(struct v4l2_subdev *sd, int enable) imx296_update_ctrl_visibility_locked(sensor, sensor->pending_mode); + /* Keep the RKISP early-done bridge aligned with the mode about to start. */ + atomic_set(&imx296_fast_trigger_mode, + sensor->pending_mode == IMX296_XTRIG_ONE_SHOT); + ret = pm_runtime_resume_and_get(sensor->dev); if (ret < 0) goto unlock; @@ -1757,12 +1857,18 @@ static int imx296_s_stream(struct v4l2_subdev *sd, int enable) * 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 stream-on lands directly in fast-trigger (no free-run settle), close + * the invalid frame with a dummy XTRIG instead. */ - 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->roi_boundary_pending) { + if (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; + } else if (sensor->active_mode == IMX296_XTRIG_ONE_SHOT) { + imx296_dummy_trigger_locked(sensor, "roi-boundary"); + sensor->roi_boundary_pending = false; + } } if (sensor->active_mode == IMX296_FREE_RUN && sensor->light_source_enabled) @@ -1771,6 +1877,8 @@ static int imx296_s_stream(struct v4l2_subdev *sd, int enable) goto unlock; err_pm: + atomic_set(&imx296_fast_trigger_mode, + sensor->active_mode == IMX296_XTRIG_ONE_SHOT); pm_runtime_put_sync(sensor->dev); unlock: mutex_unlock(&sensor->mutex); diff --git a/drivers/media/platform/rockchip/isp/capture.c b/drivers/media/platform/rockchip/isp/capture.c index 06c16ab358822..b125677b00537 100644 --- a/drivers/media/platform/rockchip/isp/capture.c +++ b/drivers/media/platform/rockchip/isp/capture.c @@ -604,8 +604,20 @@ 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); + struct rkisp_device *dev = cap_dev->ispdev; + u64 armed_sof = READ_ONCE(cap_dev->early_done_armed_sof_ns); + u64 cur_sof = dev->isp_sdev.frm_timestamp; - rkisp_stream_buf_done_early(cap_dev->ispdev); + /* + * Only complete the frame that armed this timer. A late timer after the + * next SOF would otherwise vb2_done the following buffer with an empty + * tail (full/bottom green), which is the free-run↔trigger transition + * failure mode observed on V39 + IMX296. + */ + if (!armed_sof || !cur_sof || armed_sof == cur_sof) + rkisp_stream_buf_done_early(dev); + + WRITE_ONCE(cap_dev->early_done_armed_sof_ns, 0); atomic_set(&cap_dev->early_done_pending, 0); return HRTIMER_NORESTART; } @@ -614,6 +626,7 @@ void rkisp_stream_buf_done_early_delayed(struct rkisp_device *dev, u32 delay_us) { struct rkisp_capture_device *cap_dev = &dev->cap_dev; + u64 sof_ns = dev->isp_sdev.frm_timestamp; if (!delay_us) { rkisp_stream_buf_done_early(dev); @@ -624,6 +637,7 @@ void rkisp_stream_buf_done_early_delayed(struct rkisp_device *dev, if (atomic_cmpxchg(&cap_dev->early_done_pending, 0, 1)) return; + WRITE_ONCE(cap_dev->early_done_armed_sof_ns, sof_ns); hrtimer_start(&cap_dev->early_done_timer, ns_to_ktime((u64)delay_us * NSEC_PER_USEC), HRTIMER_MODE_REL); @@ -641,6 +655,7 @@ void rkisp_stream_cancel_early_done(struct rkisp_device *dev) } else { hrtimer_cancel(&cap_dev->early_done_timer); } + WRITE_ONCE(cap_dev->early_done_armed_sof_ns, 0); atomic_set(&cap_dev->early_done_pending, 0); } @@ -1956,6 +1971,7 @@ int rkisp_register_stream_vdevs(struct rkisp_device *dev) cap_dev->ispdev = dev; atomic_set(&cap_dev->refcnt, 0); atomic_set(&cap_dev->early_done_pending, 0); + cap_dev->early_done_armed_sof_ns = 0; hrtimer_init(&cap_dev->early_done_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); cap_dev->early_done_timer.function = rkisp_early_done_timer; diff --git a/drivers/media/platform/rockchip/isp/capture.h b/drivers/media/platform/rockchip/isp/capture.h index 38c796fb44dbb..ad5e1bce1b481 100644 --- a/drivers/media/platform/rockchip/isp/capture.h +++ b/drivers/media/platform/rockchip/isp/capture.h @@ -339,6 +339,8 @@ struct rkisp_capture_device { u32 early_done_delay_us; struct hrtimer early_done_timer; atomic_t early_done_pending; + /* SOF ns of the frame for which early_done_timer is armed (0 = none). */ + u64 early_done_armed_sof_ns; u32 wrap_width; u32 wrap_line; bool is_done_early; diff --git a/drivers/media/platform/rockchip/isp/common.h b/drivers/media/platform/rockchip/isp/common.h index f77364e922ed9..853b66db9807c 100644 --- a/drivers/media/platform/rockchip/isp/common.h +++ b/drivers/media/platform/rockchip/isp/common.h @@ -145,6 +145,7 @@ extern bool rkisp_monitor; extern bool rkisp_irq_dbg; extern bool rkisp_buf_dbg; extern u64 rkisp_debug_reg; +extern unsigned int rkisp_wait_line; extern unsigned int rkisp_vicap_buf[DEV_MAX]; extern unsigned int rkisp_hdr_wrap_line[DEV_MAX]; extern struct platform_driver rkisp_plat_drv; diff --git a/drivers/media/platform/rockchip/isp/dev.c b/drivers/media/platform/rockchip/isp/dev.c index f614f4129b080..8e1f0bca41dcc 100644 --- a/drivers/media/platform/rockchip/isp/dev.c +++ b/drivers/media/platform/rockchip/isp/dev.c @@ -91,7 +91,7 @@ u64 rkisp_debug_reg = 0xFFFFFFFFFLL; module_param_named(debug_reg, rkisp_debug_reg, ullong, 0644); MODULE_PARM_DESC(debug_reg, "rkisp debug register"); -static unsigned int rkisp_wait_line; +unsigned int rkisp_wait_line; module_param_named(wait_line, rkisp_wait_line, uint, 0644); MODULE_PARM_DESC(wait_line, "rkisp wait line to buf done early"); diff --git a/drivers/media/platform/rockchip/isp/rkisp.c b/drivers/media/platform/rockchip/isp/rkisp.c index 65cdad538eeda..d5c41903d59fd 100644 --- a/drivers/media/platform/rockchip/isp/rkisp.c +++ b/drivers/media/platform/rockchip/isp/rkisp.c @@ -63,8 +63,17 @@ #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 +/* + * After the V39 last-usable-line IRQ the remaining ~8 output lines still need + * to reach DRAM before vb2_done. At full-width 10-bit / ~74.25 MHz pixel clock + * that is well under 1 ms; keep a small safety margin. A multi-frame delay + * (e.g. 50 ms) lets the timer fire after the next SOF and green-out the wrong + * buffer on free-run↔trigger transitions. + */ +#define ISP39_IMX296_TAIL_WAIT_US_DEFAULT 2000U +#define ISP39_IMX296_TAIL_WAIT_US_MAX 10000U -static unsigned int rkisp_imx296_tail_wait_us = 50000; +static unsigned int rkisp_imx296_tail_wait_us = ISP39_IMX296_TAIL_WAIT_US_DEFAULT; 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)"); @@ -2406,16 +2415,22 @@ static int rkisp_isp_start(struct rkisp_device *dev) * 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. + * + * Always re-seed wait_line from the module/DT default first: pipeline + * open sets it once, but a previous fast-trigger session must not leave + * is_done_early enabled across a free-run restart. */ dev->cap_dev.early_done_delay_us = 0; rkisp_stream_cancel_early_done(dev); + dev->cap_dev.wait_line = rkisp_wait_line; 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); + min_t(u32, rkisp_imx296_tail_wait_us, + ISP39_IMX296_TAIL_WAIT_US_MAX); 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, @@ -2441,6 +2456,10 @@ static int rkisp_isp_start(struct rkisp_device *dev) rkisp_unite_clear_bits(dev, CIF_ISP_IMSC, ISP2X_LSC_LUT_ERR, false); dev->rawaf_irq_cnt = 0; } + } else if (dev->isp_ver >= ISP_V32) { + /* Drop a stale OUT_FRM_HALF threshold left by the previous session. */ + rkisp_write(dev, ISP32_ISP_IRQ_CFG0, 0, false); + rkisp_clear_bits(dev, CIF_ISP_IMSC, ISP3X_OUT_FRM_HALF, false); } /* Activate ISP */ From 142cd0ca219bafcc0ba5d5979f2bc424119bf7dc Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Wed, 5 Aug 2026 15:22:18 +0800 Subject: [PATCH 10/21] build(dlcvcam): drop .dlcvcam.json, keep img+sha256 only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 交付物收敛为 boot.img / boot-rk3576-*.img 与整包 .sha256。 FIT 内嵌 hash 仍由 verify 检查;JSON 清单不再生成。 --- .gitignore | 2 +- ...15\344\275\234\346\211\213\345\206\214.md" | 33 +++--- build_bootimg.sh | 38 +++---- scripts/dlcvcam_verify_bootimg.py | 100 ++++-------------- 4 files changed, 58 insertions(+), 115 deletions(-) diff --git a/.gitignore b/.gitignore index 761caecd81153..be04ab3aaefaf 100644 --- a/.gitignore +++ b/.gitignore @@ -179,5 +179,5 @@ extboot* # DLCVCAM local build / test artifacts (not sources) /bad_packages/ *.img.sha256 -*.img.dlcvcam.json +*.img.dlcvcam.json # legacy; no longer generated /boot-rk3576-*.img 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 43538c81c42cb..3e2ac3d1e88d9 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" @@ -151,20 +151,27 @@ cd /path/to/kernel # 本仓库 boot-rk3576-6.1.99--.img ``` -5. **完整性 sidecar(无签名)**:为 `boot.img` 与交付镜像各生成: +5. **完整性 sidecar(无签名,仅整包 hash)**:为 `boot.img` 与交付镜像各生成: ```text -<镜像>.sha256 # 整包 SHA-256 -<镜像>.dlcvcam.json # 整包哈希 + FIT 内嵌分量哈希清单 + 构建元数据 +<镜像>.sha256 # 整包 SHA-256(sha256sum 格式) ``` -FIT 镜像本身在打包时已写入各 image 的内嵌 `sha256`(见 `boot.its`)。 -sidecar 用于上传后、烧录前做**损坏/截断/误传**检测;**不能替代签名**。 +交付物就是 **`.img` + `.sha256`**,不再生成 `.dlcvcam.json`。 + +FIT 镜像本身在打包时已写入各 image 的内嵌 `sha256`(见 `boot.its`)。校验时两层分工: + +| 检查 | 依据 | 作用 | +|------|------|------| +| 整包 SHA-256 | `.sha256` | 发现下载截断、拷贝损坏、**传错成另一份合法旧包** | +| FIT 内嵌 hash | boot.img 内 fdt/kernel/resource 的 sha256 节点 | 发现 payload 被改;**不依赖 sidecar** 也能查内容损坏 | + +`.sha256` **不能替代签名**,只作运维防呆。 ### 4.3 编译完成后 ```bash -ls -lh boot.img boot-rk3576-*.img boot-rk3576-*.img.sha256 boot-rk3576-*.img.dlcvcam.json +ls -lh boot.img boot-rk3576-*.img boot-rk3576-*.img.sha256 mkimage -l boot.img # 本地再验一次(打包脚本结束时已自检) @@ -173,27 +180,25 @@ python3 scripts/dlcvcam_verify_bootimg.py verify boot-rk3576-*.img --require-sid ### 4.4 板卡上传后、烧录前校验(推荐) -把**镜像 + `.sha256`(建议连同 `.dlcvcam.json` 与校验脚本)**一起拷到板子,例如 `/tmp`: +把**镜像 + `.sha256`(建议连同校验脚本)**一起拷到板子,例如 `/tmp`: ```bash # 在板卡上(需 python3) python3 scripts/dlcvcam_verify_bootimg.py verify /tmp/boot-rk3576-....img \ --require-sidecar # 退出码 0 才允许烧录;非 0 则重新传输,不要 wl/dd -``` -| 检查 | 依据 | 作用 | -|------|------|------| -| 整包 SHA-256 | `.sha256` | 发现下载截断、拷贝损坏、传错文件 | -| FIT 内嵌 hash | boot.img 内 fdt/kernel/resource 的 sha256 节点 | 发现 payload 被改;**不依赖 sidecar** 也能查内容损坏 | +# 无 python 时也可用: +# sha256sum -c /tmp/boot-rk3576-....img.sha256 +``` -只带了镜像、没有 sidecar 时,仍可只验 FIT 内嵌 hash: +只带了镜像、没有 `.sha256` 时,仍可只验 FIT 内嵌 hash: ```bash python3 scripts/dlcvcam_verify_bootimg.py verify /tmp/boot-rk3576-....img ``` -对已有镜像补生成 sidecar(开发机): +对已有镜像补生成 `.sha256`(开发机): ```bash python3 scripts/dlcvcam_verify_bootimg.py gen-sidecar boot-rk3576-....img diff --git a/build_bootimg.sh b/build_bootimg.sh index 6322eeab7c39f..a928a6902eea8 100755 --- a/build_bootimg.sh +++ b/build_bootimg.sh @@ -3,9 +3,8 @@ # - 自动判断初次配置 / 增量编译 # - 若 DLCVCAM_BUILD_VERSION 对应提交与当前 HEAD 不同,则按当天日期自动 bump # - 产出带构建编号与短提交的 boot 镜像:boot-rk3576-6.1.99-YYYYMMDDNN-.img -# - 同时生成完整性 sidecar(不使用签名): -# .sha256 整包 SHA-256 -# .dlcvcam.json 整包哈希 + FIT 内嵌分量哈希清单 +# - 同时生成整包完整性 sidecar(不使用签名,仅 .sha256): +# .sha256 整包 SHA-256(防传错包/截断;FIT 内嵌 hash 另由 verify 检查) # 板端烧录前:python3 scripts/dlcvcam_verify_bootimg.py verify --require-sidecar # # 用法: @@ -194,6 +193,7 @@ is_worktree_dirty_except_build_version() { boot.img|resource.img|zboot.img|out|out/*) continue ;; boot.img.sha256|boot.img.dlcvcam.json) continue ;; boot-rk3576-*.img|boot-rk3576-*.img.sha256|boot-rk3576-*.img.dlcvcam.json) continue ;; + # legacy *.dlcvcam.json ignored if leftover from older builds arch/arm64/boot/Image|arch/arm64/boot/Image.lz4) continue ;; .config|.config.old) continue ;; bad_packages|bad_packages/*) continue ;; @@ -203,31 +203,28 @@ is_worktree_dirty_except_build_version() { return 1 } -write_bootimg_integrity_sidecars() { - # 生成整包 sha256 + JSON 清单;板端烧录前用 scripts/dlcvcam_verify_bootimg.py 校验 - # 不使用签名,仅防传输损坏 / 误传 / 非密码学意义上的篡改发现 +write_bootimg_sha256_sidecar() { + # 只生成整包 .sha256;板端烧录前用 scripts/dlcvcam_verify_bootimg.py 做 + # 整包 SHA + FIT 内嵌 hash(无签名) local img="$1" - local build_id="$2" - local kernelrelease="$3" - local git_desc="$4" local verify_py="${SCRIPT_DIR}/scripts/dlcvcam_verify_bootimg.py" - [[ -f "${img}" ]] || die "write_bootimg_integrity_sidecars: 缺少 ${img}" + [[ -f "${img}" ]] || die "write_bootimg_sha256_sidecar: 缺少 ${img}" + + # 清理旧版 JSON 清单(若存在),避免误当交付物 + rm -f "${img}.dlcvcam.json" if [[ -f "${verify_py}" ]] && command -v python3 >/dev/null 2>&1; then python3 "${verify_py}" gen-sidecar "${img}" \ - --build-version "${build_id}" \ - --kernelrelease "${kernelrelease:-}" \ - --git "${git_desc}" \ - || die "生成完整性 sidecar 失败: ${img}" + || die "生成 .sha256 失败: ${img}" # 自检:刚生成的包必须能通过 FIT 内嵌 hash + 整包 sha256 python3 "${verify_py}" verify "${img}" --require-sidecar \ || die "打包后自检失败(不应发生): ${img}" return fi - # 兜底:无 python 时只写整包 sha256 - warn "python3 或 scripts/dlcvcam_verify_bootimg.py 不可用,仅写入 ${img}.sha256" + # 兜底:无 python 时只写整包 sha256(无法做 FIT 自检) + warn "python3 或 scripts/dlcvcam_verify_bootimg.py 不可用,仅写入 ${img}.sha256(跳过 FIT 自检)" need_cmd sha256sum sha256sum "${img}" | awk -v n="$(basename "${img}")" '{print $1 " " n}' > "${img}.sha256" } @@ -254,9 +251,9 @@ package_named_image() { build_id="$(make -s ARCH="${ARCH}" dlcvcam-build-version 2>/dev/null || echo "${build_ver}")" git_desc="$(git rev-parse --short HEAD) ($(git rev-parse --abbrev-ref HEAD))" - log "生成完整性校验文件(整包 sha256 + FIT 清单)..." - write_bootimg_integrity_sidecars "boot.img" "${build_id}" "${kernelrelease:-}" "${git_desc}" - write_bootimg_integrity_sidecars "${out_name}" "${build_id}" "${kernelrelease:-}" "${git_desc}" + log "生成整包 .sha256 并自检(FIT 内嵌 hash + 整包 sha256)..." + write_bootimg_sha256_sidecar "boot.img" + write_bootimg_sha256_sidecar "${out_name}" ok "完成" echo @@ -266,11 +263,10 @@ package_named_image() { echo " boot.img : $(ls -lh boot.img | awk '{print $5}')" echo " 交付镜像 : ${out_name} ($(ls -lh "${out_name}" | awk '{print $5}'))" echo " 整包校验 : ${out_name}.sha256" - echo " 清单 : ${out_name}.dlcvcam.json" echo echo "烧录前在板卡上校验(不验签,仅 hash):" echo " python3 scripts/dlcvcam_verify_bootimg.py verify ${out_name} --require-sidecar" - echo " # 或只验 FIT 内嵌 sha256(无 sidecar 时):" + echo " # 或只验 FIT 内嵌 sha256(无 .sha256 时):" echo " python3 scripts/dlcvcam_verify_bootimg.py verify ${out_name}" echo echo "下一步:校验通过后再用 RKDevTool / rkdeveloptool 烧录 ${out_name}" diff --git a/scripts/dlcvcam_verify_bootimg.py b/scripts/dlcvcam_verify_bootimg.py index 5903472a72988..c7c4dc9b29386 100755 --- a/scripts/dlcvcam_verify_bootimg.py +++ b/scripts/dlcvcam_verify_bootimg.py @@ -8,6 +8,9 @@ 2) FIT/FDT structure parse 3) Embedded per-image hash nodes (fdt / kernel / resource, ...) +Delivery sidecar is only: + .sha256 whole-file SHA-256 (sha256sum format) + Exit codes: 0 OK 1 verification failed @@ -18,8 +21,8 @@ import argparse import hashlib +import hmac import json -import os import struct import sys from pathlib import Path @@ -242,13 +245,9 @@ def resolve_image_blob(data: bytes, img: Dict[str, Any], fdt_totalsize_hint: int off = img.get("data_offset") if off is not None: - # external data: offset relative to end of FIT header (aligned external area) - # U-Boot -E external data: payload starts after FIT totalsize, often aligned. - # data-offset is relative to the external data base (right after FIT). - base = fdt_totalsize_hint - # mkimage -E -p 0x800: external data base is max(totalsize aligned, -p alignment) - # For our images, data-position is absolute; data-offset path kept for completeness. - pos2 = base + off + # external data: offset relative to end of FIT header (aligned external area). + # Our images use absolute data-position; data-offset kept as fallback only. + pos2 = fdt_totalsize_hint + off end = pos2 + size if end > len(data): raise VerifyError( @@ -328,7 +327,7 @@ def verify_fit_hashes(data: bytes) -> Tuple[bool, List[str], List[Dict[str, Any] continue actual = factory(blob).digest() hinfo["actual"] = actual.hex() - ok = hmac_compare(actual, val) + ok = hmac.compare_digest(actual, val) hinfo["ok"] = ok if ok: messages.append( @@ -347,15 +346,6 @@ def verify_fit_hashes(data: bytes) -> Tuple[bool, List[str], List[Dict[str, Any] return all_ok, messages, details -def hmac_compare(a: bytes, b: bytes) -> bool: - if len(a) != len(b): - return False - diff = 0 - for x, y in zip(a, b): - diff |= x ^ y - return diff == 0 - - def load_expect_sha256(img: Path, explicit: Optional[str], sidecar: Optional[Path]) -> Optional[str]: if explicit: v = explicit.strip().lower() @@ -365,46 +355,14 @@ def load_expect_sha256(img: Path, explicit: Optional[str], sidecar: Optional[Pat candidates: List[Path] = [] if sidecar: candidates.append(sidecar) + # Preferred: .img.sha256 candidates.append(Path(str(img) + ".sha256")) - candidates.append(img.with_suffix(img.suffix + ".sha256")) - # also boot-xxx.img.sha256 already covered; try stem.sha256 - candidates.append(img.with_suffix(".sha256")) for c in candidates: if c.is_file(): return parse_sha256_sidecar(c.read_text(encoding="utf-8", errors="replace")) return None -def write_manifest(img: Path, file_sha: str, fit_details: List[Dict[str, Any]], meta: Dict[str, Any]) -> Path: - out = Path(str(img) + ".dlcvcam.json") - doc = { - "format": "dlcvcam-bootimg-manifest-v1", - "file": img.name, - "size": img.stat().st_size, - "sha256": file_sha, - "fit_images": [ - { - "name": d.get("name"), - "type": d.get("type"), - "data_size": d.get("data_size"), - "data_position": d.get("data_position"), - "hashes": [ - { - "node": h.get("node"), - "algo": h.get("algo"), - "sha": h.get("expected") or h.get("actual"), - } - for h in d.get("hashes", []) - ], - } - for d in fit_details - ], - "meta": meta, - } - out.write_text(json.dumps(doc, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - return out - - def cmd_verify(args: argparse.Namespace) -> int: img = Path(args.image) if not img.is_file(): @@ -426,7 +384,9 @@ def cmd_verify(args: argparse.Namespace) -> int: # whole-file hash try: - expect = load_expect_sha256(img, args.expect_sha256, Path(args.sha256_file) if args.sha256_file else None) + expect = load_expect_sha256( + img, args.expect_sha256, Path(args.sha256_file) if args.sha256_file else None + ) except VerifyError as ex: eprint(f"[ERR] {ex}") return 2 @@ -437,11 +397,11 @@ def cmd_verify(args: argparse.Namespace) -> int: return 1 print("whole-file: (no sidecar / --expect-sha256, skip)") else: - if hmac_compare(bytes.fromhex(expect), bytes.fromhex(file_sha)): - print(f"[OK] whole-file sha256 matches") + if hmac.compare_digest(bytes.fromhex(expect), bytes.fromhex(file_sha)): + print("[OK] whole-file sha256 matches") else: failed = True - print(f"[FAIL] whole-file sha256 mismatch") + print("[FAIL] whole-file sha256 mismatch") print(f" expect {expect}") print(f" actual {file_sha}") @@ -484,32 +444,17 @@ def cmd_verify(args: argparse.Namespace) -> int: def cmd_gen_sidecar(args: argparse.Namespace) -> int: + """Write whole-file .sha256 only (no JSON manifest).""" img = Path(args.image) if not img.is_file(): eprint(f"文件不存在: {img}") return 2 - data = img.read_bytes() - file_sha = hashlib.sha256(data).hexdigest() + file_sha = sha256_file(img) sha_path = Path(args.sha256_file) if args.sha256_file else Path(str(img) + ".sha256") sha_path.write_text(f"{file_sha} {img.name}\n", encoding="utf-8") - fit_details: List[Dict[str, Any]] = [] - try: - _ok, _msgs, fit_details = verify_fit_hashes(data) - except VerifyError as ex: - eprint(f"警告: FIT 解析失败,仍写入整包 sha256: {ex}") - - meta = { - "build_version": args.build_version or "", - "git": args.git or "", - "kernelrelease": args.kernelrelease or "", - "generated_by": "scripts/dlcvcam_verify_bootimg.py", - } - man_path = write_manifest(img, file_sha, fit_details, meta) - print(f"wrote {sha_path}") - print(f"wrote {man_path}") print(f"sha256 {file_sha}") return 0 @@ -522,16 +467,13 @@ def build_parser() -> argparse.ArgumentParser: v.add_argument("image", help="boot.img / boot-rk3576-*.img 路径") v.add_argument("--expect-sha256", help="期望的整包 sha256 hex") v.add_argument("--sha256-file", help="整包 sha256 sidecar 路径") - v.add_argument("--require-sidecar", action="store_true", help="必须存在整包 sha256") - v.add_argument("--json-out", help="把详细结果写到 JSON") + v.add_argument("--require-sidecar", action="store_true", help="必须存在整包 .sha256") + v.add_argument("--json-out", help="把详细校验结果写到 JSON(调试用,非交付物)") v.set_defaults(func=cmd_verify) - g = sub.add_parser("gen-sidecar", help="为 boot.img 生成 .sha256 与 .dlcvcam.json") + g = sub.add_parser("gen-sidecar", help="为 boot.img 生成 .sha256(整包 hash)") g.add_argument("image", help="boot.img 路径") - g.add_argument("--sha256-file", help="输出 .sha256 路径") - g.add_argument("--build-version", default="") - g.add_argument("--git", default="") - g.add_argument("--kernelrelease", default="") + g.add_argument("--sha256-file", help="输出 .sha256 路径(默认 .sha256)") g.set_defaults(func=cmd_gen_sidecar) return p From 8868421a55901b65a12b492aff41f4c150319277 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Thu, 6 Aug 2026 15:10:21 +0800 Subject: [PATCH 11/21] =?UTF-8?q?fix(imx296/isp):=20=E5=BC=BA=E5=8C=96=20V?= =?UTF-8?q?39=20early-done=20=E5=AE=8C=E6=88=90=E6=9D=A1=E4=BB=B6=E5=B9=B6?= =?UTF-8?q?=E5=85=B3=E9=97=AD=20FT=20=E5=85=A5=E6=A8=A1=E8=84=8F=E5=B8=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IMX296 fast-trigger 下 V39 在 wait_line 处 early-done 过早,MI Y/CB 尚未 live 写满即 vb2_done,导致底行残帧/绿线。 - MI 完成改为 live ramp + full,再 post-MI hold(warmup 更保守) - 入 FT 前若干 SOF 走真实 MI FE,并用 early_done_drop_left 持久丢头帧 - free-run 清理 early-done 状态,不 arm - 进入 XTRIG 时 dummy 触发 ×2,关掉残留打开帧 板上 #2026080518/#2026080601 长期 ROI/mode thrash green=0。 --- drivers/media/i2c/imx296.c | 19 +- drivers/media/platform/rockchip/isp/capture.c | 215 ++++++++++++++++-- drivers/media/platform/rockchip/isp/capture.h | 38 +++- .../media/platform/rockchip/isp/capture_v39.c | 11 + drivers/media/platform/rockchip/isp/rkisp.c | 118 ++++++++-- 5 files changed, 356 insertions(+), 45 deletions(-) diff --git a/drivers/media/i2c/imx296.c b/drivers/media/i2c/imx296.c index f252918c86bcd..03b253bd48941 100644 --- a/drivers/media/i2c/imx296.c +++ b/drivers/media/i2c/imx296.c @@ -1667,10 +1667,7 @@ static int imx296_mode_switch(struct imx296 *sensor, /* * Datasheet: one invalid frame after ROI geometry change. If the crop * changed while we were still in FT (or ROI was pending across the - * switch), drain it now that free-run is producing frames again. For a - * switch into FT the free-run path in s_stream already drained it when - * CamOS restarts in free-run first; keep a free-run drain here for the - * hot streaming path used by sysfs without a full STREAMOFF. + * switch), drain it now that free-run is producing frames again. */ if (sensor->roi_boundary_pending && sensor->active_mode == IMX296_FREE_RUN) { @@ -1679,6 +1676,20 @@ static int imx296_mode_switch(struct imx296 *sensor, sensor->roi_boundary_pending = false; } + /* + * Entering FT from free-run (typical CamOS ROI→trigger path): the MIPI + * link / ISP pipeline often publishes one half-open or green first frame + * on the first XTRIG even when MI FE completes. Emit a light-free dummy + * pulse so that invalid frame is closed and discarded before userspace + * triggers. ISP skip_frame also drops the first completed buffers. + */ + if (old_mode != IMX296_XTRIG_ONE_SHOT && + new_mode == IMX296_XTRIG_ONE_SHOT) { + imx296_dummy_trigger_locked(sensor, "enter-fast-trigger"); + /* Second pulse: first dummy often only closes the free-run tail. */ + imx296_dummy_trigger_locked(sensor, "enter-fast-trigger-2"); + } + out_pm: if (ret && !sensor->streaming) { /* Roll the mode bridge back so ISP does not keep FT early-done. */ diff --git a/drivers/media/platform/rockchip/isp/capture.c b/drivers/media/platform/rockchip/isp/capture.c index b125677b00537..908b20d477f2b 100644 --- a/drivers/media/platform/rockchip/isp/capture.c +++ b/drivers/media/platform/rockchip/isp/capture.c @@ -588,6 +588,8 @@ void rkisp_stream_buf_done_early(struct rkisp_device *dev) if (!dev->cap_dev.is_done_early) return; + /* Buffer drop is applied in mi_frame_end via early_done_drop_left. */ + for (i = 0; i < RKISP_MAX_STREAM; i++) { if (i == RKISP_STREAM_VIR || i == RKISP_STREAM_LUMA || i == RKISP_STREAM_DMATX0 || i == RKISP_STREAM_DMATX1 || @@ -600,6 +602,109 @@ void rkisp_stream_buf_done_early(struct rkisp_device *dev) } } +static void rkisp_early_done_store_deadline(struct rkisp_capture_device *cap_dev, + u64 deadline_ns) +{ + WRITE_ONCE(cap_dev->early_done_deadline_ns_lo, lower_32_bits(deadline_ns)); + WRITE_ONCE(cap_dev->early_done_deadline_ns_hi, upper_32_bits(deadline_ns)); +} + +static u64 rkisp_early_done_load_deadline(struct rkisp_capture_device *cap_dev) +{ + u32 lo = READ_ONCE(cap_dev->early_done_deadline_ns_lo); + u32 hi = READ_ONCE(cap_dev->early_done_deadline_ns_hi); + + return ((u64)hi << 32) | lo; +} + +static void rkisp_early_done_cache_mi_sizes(struct rkisp_device *dev) +{ + struct rkisp_capture_device *cap_dev = &dev->cap_dev; + struct rkisp_stream *mp = &cap_dev->stream[RKISP_STREAM_MP]; + u32 y_size = 0, cb_size = 0; + + /* + * Prefer the shadow programmed size (bytes the MI will write this + * frame). Fall back to plane sizeimage from the MP format. + */ + if (mp->streaming) { + y_size = rkisp_read(dev, CIF_MI_MP_Y_SIZE_SHD, true); + cb_size = rkisp_read(dev, CIF_MI_MP_CB_SIZE_SHD, true); + if (!y_size && mp->out_fmt.num_planes >= 1) + y_size = mp->out_fmt.plane_fmt[0].sizeimage; + if (!cb_size && mp->out_fmt.num_planes >= 2) + cb_size = mp->out_fmt.plane_fmt[1].sizeimage; + /* Single-plane NV12: chroma is the second half. */ + if (y_size && !cb_size && mp->out_fmt.num_planes == 1) + cb_size = y_size / 2; + } + + WRITE_ONCE(cap_dev->early_done_mi_y_size, y_size); + WRITE_ONCE(cap_dev->early_done_mi_cb_size, cb_size); + WRITE_ONCE(cap_dev->early_done_mi_saw_y, 0); + WRITE_ONCE(cap_dev->early_done_mi_saw_cb, 0); + WRITE_ONCE(cap_dev->early_done_mi_ready, 0); +} + +/* + * MI SHD offs often still hold the previous buffer's final counts across an + * FT SOF. Require a live ramp (offs < need) before trusting full (== need). + */ +static bool rkisp_early_done_mi_finished(struct rkisp_device *dev) +{ + struct rkisp_capture_device *cap_dev = &dev->cap_dev; + u32 y_need = READ_ONCE(cap_dev->early_done_mi_y_size); + u32 cb_need = READ_ONCE(cap_dev->early_done_mi_cb_size); + u32 y_offs, cb_offs; + bool y_full = true, cb_full = true; + + if (!y_need && !cb_need) + return false; + + if (y_need) { + y_offs = rkisp_read(dev, CIF_MI_MP_Y_OFFS_CNT_SHD, true); + if (y_offs < y_need) + WRITE_ONCE(cap_dev->early_done_mi_saw_y, 1); + y_full = READ_ONCE(cap_dev->early_done_mi_saw_y) && + (y_offs >= y_need); + } + + if (cb_need) { + cb_offs = rkisp_read(dev, CIF_MI_MP_CB_OFFS_CNT_SHD, true); + if (cb_offs < cb_need) + WRITE_ONCE(cap_dev->early_done_mi_saw_cb, 1); + cb_full = READ_ONCE(cap_dev->early_done_mi_saw_cb) && + (cb_offs >= cb_need); + } + + return y_full && cb_full; +} + +static u32 rkisp_early_done_post_hold_us(struct rkisp_capture_device *cap_dev) +{ + u32 left = READ_ONCE(cap_dev->early_done_warmup_left); + u32 warm = READ_ONCE(cap_dev->early_done_warmup_post_mi_us); + u32 steady = READ_ONCE(cap_dev->early_done_post_mi_us); + + if (left && warm) + return warm; + return steady; +} + +/* + * Fast-trigger completion after wait_line (OUT_FRM_HALF): + * + * Completing when ISP_OUT_LINE hits the V39 terminal (height - 8) publishes + * buffers with an ~8-row green bottom band — the counter saturates before MI + * finishes the last rows / chroma. MI SHD can also hold the previous buffer's + * final offsets across FT SOF, so require a live ramp before trusting full. + * + * Even after MI reports full, last NV12 chroma may still be in flight → hold + * post_mi_us (longer for the first few FT frames after mode/ROI switch). + * + * On residual ceiling without verified MI: do NOT force vb2_done; leave the + * buffer to the real MI FE IRQ. + */ static enum hrtimer_restart rkisp_early_done_timer(struct hrtimer *timer) { struct rkisp_capture_device *cap_dev = @@ -607,42 +712,121 @@ static enum hrtimer_restart rkisp_early_done_timer(struct hrtimer *timer) struct rkisp_device *dev = cap_dev->ispdev; u64 armed_sof = READ_ONCE(cap_dev->early_done_armed_sof_ns); u64 cur_sof = dev->isp_sdev.frm_timestamp; + u64 deadline = rkisp_early_done_load_deadline(cap_dev); + u64 now = ktime_get_ns(); + u32 poll_us = READ_ONCE(cap_dev->early_done_poll_us); + u32 post_us; + + if (!armed_sof || (cur_sof && armed_sof != cur_sof)) { + /* Stale arm — next SOF already won. */ + WRITE_ONCE(cap_dev->early_done_armed_sof_ns, 0); + WRITE_ONCE(cap_dev->early_done_mi_ready, 0); + atomic_set(&cap_dev->early_done_pending, 0); + return HRTIMER_NORESTART; + } + + if (!READ_ONCE(cap_dev->early_done_mi_ready)) { + if (rkisp_early_done_mi_finished(dev)) { + WRITE_ONCE(cap_dev->early_done_mi_ready, 1); + post_us = rkisp_early_done_post_hold_us(cap_dev); + if (post_us) { + hrtimer_forward_now(timer, + ns_to_ktime((u64)post_us * NSEC_PER_USEC)); + return HRTIMER_RESTART; + } + /* fall through to complete */ + } else if (!deadline || now < deadline) { + if (!poll_us) + poll_us = 50; + hrtimer_forward_now(timer, + ns_to_ktime((u64)poll_us * NSEC_PER_USEC)); + return HRTIMER_RESTART; + } else { + /* + * Ceiling hit without verified MI. Leave buffer to MI FE; + * forcing done here is the green-band failure mode. + */ + WRITE_ONCE(cap_dev->early_done_armed_sof_ns, 0); + WRITE_ONCE(cap_dev->early_done_mi_ready, 0); + atomic_set(&cap_dev->early_done_pending, 0); + return HRTIMER_NORESTART; + } + } - /* - * Only complete the frame that armed this timer. A late timer after the - * next SOF would otherwise vb2_done the following buffer with an empty - * tail (full/bottom green), which is the free-run↔trigger transition - * failure mode observed on V39 + IMX296. - */ - if (!armed_sof || !cur_sof || armed_sof == cur_sof) - rkisp_stream_buf_done_early(dev); + /* MI ready + post-MI hold elapsed (or post_us was 0). */ + if (READ_ONCE(cap_dev->early_done_warmup_left)) + WRITE_ONCE(cap_dev->early_done_warmup_left, + READ_ONCE(cap_dev->early_done_warmup_left) - 1); + rkisp_stream_buf_done_early(dev); WRITE_ONCE(cap_dev->early_done_armed_sof_ns, 0); + WRITE_ONCE(cap_dev->early_done_mi_ready, 0); + rkisp_early_done_store_deadline(cap_dev, 0); 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) +void rkisp_stream_buf_done_early_drain(struct rkisp_device *dev, + u32 ceiling_us) { struct rkisp_capture_device *cap_dev = &dev->cap_dev; u64 sof_ns = dev->isp_sdev.frm_timestamp; + u32 skip, poll_us; + u64 deadline; - if (!delay_us) { - rkisp_stream_buf_done_early(dev); + if (!cap_dev->is_done_early) + return; + + /* First SOFs after free-run/ROI→FT: use real MI FE only. */ + skip = READ_ONCE(cap_dev->early_done_skip_frames); + if (skip) { + WRITE_ONCE(cap_dev->early_done_skip_frames, skip - 1); 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; + rkisp_early_done_cache_mi_sizes(dev); WRITE_ONCE(cap_dev->early_done_armed_sof_ns, sof_ns); + + if (!ceiling_us) + ceiling_us = READ_ONCE(cap_dev->early_done_delay_us); + if (!ceiling_us) + ceiling_us = 10000; + deadline = ktime_get_ns() + (u64)ceiling_us * NSEC_PER_USEC; + rkisp_early_done_store_deadline(cap_dev, deadline); + + poll_us = READ_ONCE(cap_dev->early_done_poll_us); + if (!poll_us) + poll_us = 50; + hrtimer_start(&cap_dev->early_done_timer, - ns_to_ktime((u64)delay_us * NSEC_PER_USEC), + ns_to_ktime((u64)poll_us * NSEC_PER_USEC), HRTIMER_MODE_REL); } +void rkisp_stream_buf_done_early_delayed(struct rkisp_device *dev, + u32 delay_us) +{ + /* Prefer MI-drain path; delay_us is the safety ceiling. */ + rkisp_stream_buf_done_early_drain(dev, delay_us); +} + +void rkisp_early_done_clear_state(struct rkisp_device *dev) +{ + struct rkisp_capture_device *cap_dev = &dev->cap_dev; + + WRITE_ONCE(cap_dev->early_done_armed_sof_ns, 0); + WRITE_ONCE(cap_dev->early_done_mi_y_size, 0); + WRITE_ONCE(cap_dev->early_done_mi_cb_size, 0); + WRITE_ONCE(cap_dev->early_done_mi_saw_y, 0); + WRITE_ONCE(cap_dev->early_done_mi_saw_cb, 0); + WRITE_ONCE(cap_dev->early_done_mi_ready, 0); + rkisp_early_done_store_deadline(cap_dev, 0); + atomic_set(&cap_dev->early_done_pending, 0); +} + void rkisp_stream_cancel_early_done(struct rkisp_device *dev) { struct rkisp_capture_device *cap_dev = &dev->cap_dev; @@ -655,8 +839,7 @@ void rkisp_stream_cancel_early_done(struct rkisp_device *dev) } else { hrtimer_cancel(&cap_dev->early_done_timer); } - WRITE_ONCE(cap_dev->early_done_armed_sof_ns, 0); - atomic_set(&cap_dev->early_done_pending, 0); + rkisp_early_done_clear_state(dev); } int rkisp_stream_buf_cnt(struct rkisp_stream *stream) diff --git a/drivers/media/platform/rockchip/isp/capture.h b/drivers/media/platform/rockchip/isp/capture.h index ad5e1bce1b481..bcdad462eeeaf 100644 --- a/drivers/media/platform/rockchip/isp/capture.h +++ b/drivers/media/platform/rockchip/isp/capture.h @@ -336,11 +336,43 @@ struct rkisp_capture_device { struct tasklet_struct rd_tasklet; atomic_t refcnt; u32 wait_line; + /* + * Fast-trigger early-done (V39 + IMX296): + * Arm at wait_line (OUT_FRM_HALF). Complete only when MI MP Y/CB write + * offsets have ramped live and reached the programmed plane sizes, then + * hold post_mi_us so the last NV12 chroma beats land. ISP_OUT_LINE + * saturates ~8 lines early and must not drive completion. + * early_done_delay_us is only the residual safety ceiling (no force + * vb2_done without verified MI). First early_done_skip_frames SOFs leave + * completion to the real MI FE IRQ; stream->skip_frame may also drop + * the first FT buffers after free-run/ROI→trigger. + */ u32 early_done_delay_us; struct hrtimer early_done_timer; atomic_t early_done_pending; - /* SOF ns of the frame for which early_done_timer is armed (0 = none). */ + /* SOF ns of the frame for which early-done is armed (0 = none). */ u64 early_done_armed_sof_ns; + u32 early_done_poll_us; + u32 early_done_terminal_line; + u32 early_done_deadline_ns_lo; + u32 early_done_deadline_ns_hi; + u32 early_done_mi_y_size; + u32 early_done_mi_cb_size; + /* Observed live MI ramp (y/cb offs below programmed size) this arm. */ + u32 early_done_mi_saw_y; + u32 early_done_mi_saw_cb; + /* Y+CB live+full; then wait post_mi_us before vb2_done. */ + u32 early_done_mi_ready; + u32 early_done_post_mi_us; + u32 early_done_warmup_left; + u32 early_done_warmup_post_mi_us; + /* + * SOFs after FT start that must not arm early-done (use MI FE only). + * Decremented in the drain path; 0 = normal MI-poll early-done. + */ + u32 early_done_skip_frames; + /* Drop next N completed MP buffers (survives stream->skip_frame=0). */ + u32 early_done_drop_left; u32 wrap_width; u32 wrap_line; bool is_done_early; @@ -355,9 +387,13 @@ 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_drain(struct rkisp_device *dev, + u32 ceiling_us); +/* Legacy name kept for non-IMX296 call sites. */ 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_early_done_clear_state(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 a742d32ad72ea..657757e16ae19 100644 --- a/drivers/media/platform/rockchip/isp/capture_v39.c +++ b/drivers/media/platform/rockchip/isp/capture_v39.c @@ -1087,6 +1087,17 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) struct rkisp_stream *vir = &dev->cap_dev.stream[RKISP_STREAM_VIR]; u64 ns = 0; + if (stream->id == RKISP_STREAM_MP) { + u32 drop = READ_ONCE(dev->cap_dev.early_done_drop_left); + + if (drop) { + WRITE_ONCE(dev->cap_dev.early_done_drop_left, drop - 1); + spin_lock_irqsave(&stream->vbq_lock, lock_flags); + list_add_tail(&buf->queue, &stream->buf_queue); + spin_unlock_irqrestore(&stream->vbq_lock, lock_flags); + goto end; + } + } if (dev->skip_frame || stream->skip_frame) { spin_lock_irqsave(&stream->vbq_lock, lock_flags); list_add_tail(&buf->queue, &stream->buf_queue); diff --git a/drivers/media/platform/rockchip/isp/rkisp.c b/drivers/media/platform/rockchip/isp/rkisp.c index d5c41903d59fd..105e7eae260a9 100644 --- a/drivers/media/platform/rockchip/isp/rkisp.c +++ b/drivers/media/platform/rockchip/isp/rkisp.c @@ -64,19 +64,61 @@ #define IMX296_XTRIG_ONE_SHOT 1 #define ISP39_OUT_LINE_COUNTER_TAIL 8 /* - * After the V39 last-usable-line IRQ the remaining ~8 output lines still need - * to reach DRAM before vb2_done. At full-width 10-bit / ~74.25 MHz pixel clock - * that is well under 1 ms; keep a small safety margin. A multi-frame delay - * (e.g. 50 ms) lets the timer fire after the next SOF and green-out the wrong - * buffer on free-run↔trigger transitions. + * Residual / MI-drain ceiling after wait_line. Completion is driven by MI + * plane progress + post-MI hold, not by this delay alone. Keep a generous + * ceiling so a slow first FT frame is not cut short; never force vb2_done + * on expiry without verified MI (see capture.c). */ -#define ISP39_IMX296_TAIL_WAIT_US_DEFAULT 2000U -#define ISP39_IMX296_TAIL_WAIT_US_MAX 10000U +#define ISP39_IMX296_TAIL_WAIT_US_DEFAULT 15000U +#define ISP39_IMX296_TAIL_WAIT_US_MAX 40000U +#define ISP39_IMX296_LINE_US_MIN 80U +#define ISP39_IMX296_LINE_US_MAX 250U +#define ISP39_IMX296_EARLY_DONE_POLL_US 50U +#define ISP39_IMX296_POST_MI_US 6000U +#define ISP39_IMX296_WARMUP_POST_MI_US 16000U +#define ISP39_IMX296_WARMUP_FRAMES 4U +/* SOFs that skip early-done (MI FE only) after free-run/ROI->FT. */ +#define ISP39_IMX296_SKIP_EARLY_FRAMES 3U +/* Drop first completed MP buffers after FT start (invalid / half-open). */ +#define ISP39_IMX296_SKIP_BUF_FRAMES 3U static unsigned int rkisp_imx296_tail_wait_us = ISP39_IMX296_TAIL_WAIT_US_DEFAULT; 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)"); + "V39 IMX296 FT MI-drain residual ceiling (us); completion prefers MI offs"); + +static u32 rkisp_imx296_residual_ceiling_us(u32 width, u32 height, + u32 wait_line) +{ + u32 remain, line_us, tail_us, param; + + if (height <= wait_line) + remain = ISP39_OUT_LINE_COUNTER_TAIL; + else + remain = height - wait_line; + + /* + * Rough line time from width (10-bit / ~74.25 MHz class). Clamp so + * small ROI does not under-estimate and huge width does not explode. + */ + line_us = width / 8; + if (line_us < ISP39_IMX296_LINE_US_MIN) + line_us = ISP39_IMX296_LINE_US_MIN; + if (line_us > ISP39_IMX296_LINE_US_MAX) + line_us = ISP39_IMX296_LINE_US_MAX; + + /* Safety ceiling only — completion prefers MI offs. */ + tail_us = (remain + ISP39_OUT_LINE_COUNTER_TAIL * 2) * line_us + 500; + if (tail_us < 10000) + tail_us = 10000; + + param = rkisp_imx296_tail_wait_us; + if (param < tail_us) + param = tail_us; + if (param > ISP39_IMX296_TAIL_WAIT_US_MAX) + param = ISP39_IMX296_TAIL_WAIT_US_MAX; + return param; +} /* * 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), @@ -2409,32 +2451,60 @@ static int rkisp_isp_start(struct rkisp_device *dev) * 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. + * line counter stops eight counts below the active height, so height - 1 + * never asserts. Program one count before that terminal, then complete + * when MI plane offsets finish (+ post-MI hold). Free-run clears early-done. * - * Always re-seed wait_line from the module/DT default first: pipeline - * open sets it once, but a previous fast-trigger session must not leave - * is_done_early enabled across a free-run restart. + * Always re-seed wait_line from the module/DT default first: a previous + * fast-trigger session must not leave is_done_early across free-run. */ dev->cap_dev.early_done_delay_us = 0; + dev->cap_dev.early_done_poll_us = ISP39_IMX296_EARLY_DONE_POLL_US; + dev->cap_dev.early_done_terminal_line = 0; + dev->cap_dev.early_done_post_mi_us = 0; + dev->cap_dev.early_done_warmup_post_mi_us = 0; + dev->cap_dev.early_done_warmup_left = 0; + dev->cap_dev.early_done_skip_frames = 0; + dev->cap_dev.early_done_drop_left = 0; rkisp_stream_cancel_early_done(dev); dev->cap_dev.wait_line = rkisp_wait_line; 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; + struct rkisp_stream *mp = &dev->cap_dev.stream[RKISP_STREAM_MP]; + u32 terminal = height > ISP39_OUT_LINE_COUNTER_TAIL ? + height - ISP39_OUT_LINE_COUNTER_TAIL : 1; + + dev->cap_dev.early_done_terminal_line = terminal; + dev->cap_dev.wait_line = terminal > 1 ? terminal - 1 : 1; dev->cap_dev.early_done_delay_us = - min_t(u32, rkisp_imx296_tail_wait_us, - ISP39_IMX296_TAIL_WAIT_US_MAX); + rkisp_imx296_residual_ceiling_us(dev->isp_sdev.out_crop.width, + height, + dev->cap_dev.wait_line); + dev->cap_dev.early_done_post_mi_us = ISP39_IMX296_POST_MI_US; + dev->cap_dev.early_done_warmup_post_mi_us = + ISP39_IMX296_WARMUP_POST_MI_US; + dev->cap_dev.early_done_warmup_left = ISP39_IMX296_WARMUP_FRAMES; + dev->cap_dev.early_done_skip_frames = ISP39_IMX296_SKIP_EARLY_FRAMES; + /* + * Drop the first FT-completed buffers entirely. After free-run/ROI + * the first published frames are often half-open / invalid even when + * MI FE fires. Use cap_dev.early_done_drop_left so it survives + * stream->skip_frame being cleared in rkisp_start(). + */ + dev->cap_dev.early_done_drop_left = ISP39_IMX296_SKIP_BUF_FRAMES; + if (mp->streaming) + mp->skip_frame = ISP39_IMX296_SKIP_BUF_FRAMES; v4l2_info(&dev->v4l2_dev, - "IMX296 fast trigger: early buffer done at line %u/%u, tail wait %u us\n", + "IMX296 fast trigger: early buffer done at line %u/%u terminal %u, MI-drain ceiling %u us post-MI %u/%u us (warmup %u) skip %u drop %u poll %u us\n", dev->cap_dev.wait_line, height, - dev->cap_dev.early_done_delay_us); + dev->cap_dev.early_done_terminal_line, + dev->cap_dev.early_done_delay_us, + dev->cap_dev.early_done_post_mi_us, + dev->cap_dev.early_done_warmup_post_mi_us, + dev->cap_dev.early_done_warmup_left, + dev->cap_dev.early_done_skip_frames, + ISP39_IMX296_SKIP_BUF_FRAMES, + dev->cap_dev.early_done_poll_us); } dev->cap_dev.is_done_early = false; From 1a89f1c997715a43ce43915e69ed2a18c8b1621e Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Thu, 6 Aug 2026 15:10:22 +0800 Subject: [PATCH 12/21] =?UTF-8?q?fix(rkisp):=20procfs=20MMIO=20=E9=97=A8?= =?UTF-8?q?=E9=97=A9=EF=BC=8C=E9=81=BF=E5=85=8D=20teardown=20=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=20SError=20panic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CamOS 经 /proc/rkisp* 写 WB 寄存器(mode=0x100 / 400=0x6197)时, 若 ISP 未 START 或已 teardown,非法 MMIO 会触发 Asynchronous SError。 - FIL_AIQ/FIL_SW 写路径要求 ISP_START,并 pm_runtime_get/put - 拒绝越界/非对齐寄存器;限制单次写入长度 - mode= 仍可设置;streaming 时正常 MMIO 不受影响 已在 180 刷入 #2026080601 冒烟:accept/reject/skip 路径正常。 --- drivers/media/platform/rockchip/isp/procfs.c | 109 +++++++++++++++++-- 1 file changed, 98 insertions(+), 11 deletions(-) diff --git a/drivers/media/platform/rockchip/isp/procfs.c b/drivers/media/platform/rockchip/isp/procfs.c index d52e406d0fd85..edeb97e37dc38 100644 --- a/drivers/media/platform/rockchip/isp/procfs.c +++ b/drivers/media/platform/rockchip/isp/procfs.c @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-2.0 /* Copyright (c) Rockchip Electronics Co., Ltd. */ #include +#include #include #include #include +#include #include "dev.h" #include "procfs.h" @@ -17,6 +19,9 @@ #ifdef CONFIG_PROC_FS +/* Cap userspace proc write size; CamOS WB path only needs a few dozen bytes. */ +#define RKISP_PROC_WRITE_MAX PAGE_SIZE + static void isp20_show(struct rkisp_device *dev, struct seq_file *p) { u32 full_range_flg = CIF_ISP_CTRL_ISP_CSM_Y_FULL_ENA | CIF_ISP_CTRL_ISP_CSM_C_FULL_ENA; @@ -1377,20 +1382,51 @@ static void rkisp_proc_dump_mem(struct rkisp_device *dev) } } +static bool rkisp_proc_hw_ready_for_mmio(struct rkisp_device *dev) +{ + struct rkisp_hw_dev *hw = dev->hw_dev; + + if (!hw || !hw->base_addr) + return false; + if (dev->is_suspend) + return false; + /* + * ISP_START is set while the pipeline is streaming. Writing ISP + * registers (FIL_AIQ/FIL_SW) while clocks/power are off, or during + * teardown thrash, has been observed to raise Asynchronous SError + * (python CamOS WB path: mode=0x100 / 400=0x6197) and panic the SoC. + */ + if (!(dev->isp_state & ISP_START)) + return false; + return true; +} + static ssize_t rkisp_proc_write(struct file *file, const char __user *user_buf, size_t user_len, loff_t *pos) { struct rkisp_device *dev = pde_data(file_inode(file)); - char *tmp, *buf = vmalloc(user_len + 1); + struct device *pm_dev = NULL; + char *tmp, *buf; u32 val, reg; int ret; + int pm_ret = 0; + ssize_t out = user_len; + bool need_pm = false; + + if (!dev) + return -ENODEV; + if (!user_len) + return 0; + if (user_len > RKISP_PROC_WRITE_MAX) + return -E2BIG; + buf = vmalloc(user_len + 1); if (!buf) return -ENOMEM; if (copy_from_user(buf, user_buf, user_len) != 0) { - vfree(buf); - return -EFAULT; + out = -EFAULT; + goto free_buf; } if (buf[user_len - 1] == '\n') buf[user_len - 1] = 0; @@ -1401,8 +1437,10 @@ static ssize_t rkisp_proc_write(struct file *file, if (tmp) { tmp += 5; ret = kstrtou32(tmp, 16, &val); - if (ret) - goto end; + if (ret) { + out = ret; + goto free_buf; + } dev->procfs.mode = val; if (val & RKISP_PROCFS_DUMP_MEM) rkisp_proc_dump_mem(dev); @@ -1410,13 +1448,57 @@ static ssize_t rkisp_proc_write(struct file *file, (RKISP_PROCFS_FIL_AIQ | RKISP_PROCFS_FIL_SW)) { char *p_reg, *p_val; + if (!rkisp_proc_hw_ready_for_mmio(dev)) { + dev_warn_ratelimited(dev->dev, + "%s: skip MMIO (isp_state=0x%x suspend=%d mode=0x%x) cmd='%s'\n", + __func__, dev->isp_state, dev->is_suspend, + dev->procfs.mode, buf); + out = -ENODEV; + goto free_buf; + } + + /* + * Keep the HW powered for the whole register burst. On + * multi-device ISP the vir device's RPM get pulls hw_dev. + * Prefer hw_dev->dev when available (actual MMIO owner). + */ + pm_dev = dev->hw_dev->dev ? dev->hw_dev->dev : dev->dev; + pm_ret = pm_runtime_get_sync(pm_dev); + if (pm_ret < 0) { + pm_runtime_put_noidle(pm_dev); + dev_err_ratelimited(dev->dev, + "%s: pm_runtime_get_sync failed (%d), skip MMIO cmd='%s'\n", + __func__, pm_ret, buf); + out = pm_ret; + goto free_buf; + } + need_pm = true; + + if (!rkisp_proc_hw_ready_for_mmio(dev)) { + dev_warn_ratelimited(dev->dev, + "%s: ISP left ready window after RPM get (state=0x%x), skip\n", + __func__, dev->isp_state); + out = -ENODEV; + goto put_pm; + } + p_reg = buf; tmp = strstr(p_reg, "="); while (tmp) { *tmp = '\0'; ret = kstrtou32(p_reg, 16, ®); - if (ret) - goto end; + if (ret) { + out = ret; + goto put_pm; + } + /* Refuse obviously out-of-range offsets into the ISP SW map. */ + if (reg >= RKISP_ISP_SW_REG_SIZE || (reg & 0x3)) { + dev_warn_ratelimited(dev->dev, + "%s: reject bad reg 0x%x\n", + __func__, reg); + out = -EINVAL; + goto put_pm; + } p_val = tmp + 1; tmp = strstr(p_val, " "); if (tmp) { @@ -1424,8 +1506,10 @@ static ssize_t rkisp_proc_write(struct file *file, p_reg = tmp + 1; } ret = kstrtou32(p_val, 16, &val); - if (ret) - goto end; + if (ret) { + out = ret; + goto put_pm; + } if (dev->procfs.mode & RKISP_PROCFS_FIL_SW) writel(val, dev->hw_dev->base_addr + reg); else @@ -1434,9 +1518,12 @@ static ssize_t rkisp_proc_write(struct file *file, tmp = strstr(p_reg, "="); } } -end: +put_pm: + if (need_pm) + pm_runtime_put_sync(pm_dev); +free_buf: vfree(buf); - return user_len; + return out; } static int isp_open(struct inode *inode, struct file *file) From efdc50869b21fe6ae999cce0e05fb56917b0b072 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Thu, 6 Aug 2026 15:10:22 +0800 Subject: [PATCH 13/21] =?UTF-8?q?build:=20=E5=B0=86=20DLCVCAM=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E5=8F=B7=E6=9B=B4=E6=96=B0=E4=B8=BA=202026080601?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对应已刷板镜像 boot-rk3576-6.1.99-2026080601(含绿线策略与 procfs 门闩)。 --- DLCVCAM_BUILD_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DLCVCAM_BUILD_VERSION b/DLCVCAM_BUILD_VERSION index 6f412232a9fb8..da2a87bded858 100644 --- a/DLCVCAM_BUILD_VERSION +++ b/DLCVCAM_BUILD_VERSION @@ -1 +1 @@ -2026080501 +2026080601 From 35f0daed04d51af8e87ac51e839677fc20a5a26c Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Fri, 7 Aug 2026 17:43:03 +0800 Subject: [PATCH 14/21] =?UTF-8?q?fix(imx296/isp):=20=E4=BF=AE=E5=A4=8D=20F?= =?UTF-8?q?T=20N-1=20=E7=9B=B8=E4=BD=8D=E5=B9=B6=E5=8A=A0=E5=9B=BA=20V39?= =?UTF-8?q?=20early-done=20=E7=BB=BF=E7=BA=BF=E9=98=B2=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IMX296 master_fast_trigger 下 V39 MI OFFS 常为 0,early-done 放弃到下一 次 XTRIG 的 MI FE 会导致发布内容落后一拍(N-1);free_run 期间外部 XTRIG 也会把进 FT 相位打乱。 主要变更: - imx296: free_run 忽略 XTRIG;进 FT / STREAMON 后 short dummy×3; enter/ROI dummy 强制 800us 脉宽,leave/stream-off 仍等满帧关闭 - rkisp capture: SHD+START 双读 MI offs;ceiling-force + force_post 持有, 不再 bare wait_line 完成;n1trace 诊断计数 - capture_v39: FT mi_drop/skip/pub 与 early 路径对齐 - rkisp: 放宽 residual/post/force_post,warmup=6、skip/drop=2 - 补充 N-1/绿线诊断文档与 bootimg 构建/校验脚本 --- ...56\345\244\215\350\256\260\345\275\225.md" | 294 +++++++++ ...14\350\257\201\350\256\260\345\275\225.md" | 213 +++++++ build_bootimg.sh | 294 +++++++++ drivers/media/i2c/imx296.c | 119 +++- drivers/media/platform/rockchip/isp/capture.c | 171 +++++- drivers/media/platform/rockchip/isp/capture.h | 21 +- .../media/platform/rockchip/isp/capture_v39.c | 33 ++ drivers/media/platform/rockchip/isp/rkisp.c | 75 ++- scripts/dlcvcam_verify_bootimg.py | 556 ++++++++++++++++++ 9 files changed, 1719 insertions(+), 57 deletions(-) create mode 100644 "IMX296_N1\347\233\270\344\275\215\346\273\236\345\220\216_\350\257\212\346\226\255\344\270\216\344\277\256\345\244\215\350\256\260\345\275\225.md" create mode 100644 "IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" create mode 100755 build_bootimg.sh create mode 100755 scripts/dlcvcam_verify_bootimg.py diff --git "a/IMX296_N1\347\233\270\344\275\215\346\273\236\345\220\216_\350\257\212\346\226\255\344\270\216\344\277\256\345\244\215\350\256\260\345\275\225.md" "b/IMX296_N1\347\233\270\344\275\215\346\273\236\345\220\216_\350\257\212\346\226\255\344\270\216\344\277\256\345\244\215\350\256\260\345\275\225.md" new file mode 100644 index 0000000000000..3a81fd75c2ebb --- /dev/null +++ "b/IMX296_N1\347\233\270\344\275\215\346\273\236\345\220\216_\350\257\212\346\226\255\344\270\216\344\277\256\345\244\215\350\256\260\345\275\225.md" @@ -0,0 +1,294 @@ +# IMX296 N-1 相位滞后 — 诊断与修复记录 + +> 独立于绿线文档。本文件跟踪 **触发第 N 次却拿到第 N−1 次曝光内容** 的排查与修复。 +> 创建:2026-08-06 +> 板端:root@192.168.1.180,CamOS :8000,sensor `4-001a`,capture `/dev/video11` +> 验证内核:`#2026080601 SMP Thu Aug 6 17:59:30 CST 2026`,boot sha256 `70490539408638b86673fcde3785296eede15b59d8387ae576cb31bbe1d57301` + +--- + +## 0. 问题描述(用户确认) + +- **现象**:进入 trigger 模式后,第 N 次触发返回的是第 N−1 次触发对应的帧内容(相位晚一帧),不是简单的「少一帧」。 +- **诱发条件**:监控模式(free_run)下,或 **监控→触发切换过程中**,存在外部触发(可用 `echo 1 > /sys/bus/i2c/devices/4-001a/trigger` 模拟);问题在进入触发模式后 **稳定保持**。 +- **判定法**:gain=0,曝光在 min≈21 与 max≈33289 间交替,每次改曝光后触发一次;用画面中心 ROI 均值区分 LO/HI,看是否与 **当前** 曝光一致。 + +--- + +## 1. 根因(日志确认) + +### 1.1 主因:V39 early-done 永久 ceiling-miss → 只能靠「下一拍 XTRIG」的 MI FE 关帧 + +IMX296 master_fast_trigger(FT)语义:一拍 XTRIG **打开** MIPI 帧,**下一拍** 才关帧。 +ISP V39 的 early-done 本意是在 OUT_FRM_HALF + residual ceiling 后,用 MI 进度验证写完再 `vb2_done`,使 **当前触发** 对应的曝光内容在本拍内发布。 + +实测(n1trace): + +``` +early ceiling-force ... y=0/1584128 base=0 saw=1 cb=0/792064 + y_shd=0 y_st=0 cb_shd=0 cb_st=0 +``` + +- `CIF_MI_MP_Y/CB_OFFS_CNT_SHD` 与 `*_START` 在整段 FT 开帧期间 **始终为 0**。 +- live-ramp 条件永远无法把 offs 推到 `need` → 旧逻辑 **abandon to MI FE**。 +- MI FE 只在 **下一用户 XTRIG 关帧** 时到来 → 用户拿到的永远是上一拍曝光 → **稳定 N-1**。 + +这与 mon+ext / cold 无关时是 early-done 在本硅上的结构性失效;mon+ext 只是把入口相位搅得更脏、更容易锁死。 + +### 1.2 次因 A:CamOS STREAMOFF→STREAMON 路径跳过 enter-FT dummy + +`imx296_mode_switch()` 在 `!streaming` 时 early-return,不发 enter dummy。 +CamOS run_mode 边界是 PAUSED→READY→apply→PLAYING(STREAMOFF/ON),因此 dummy 从未跑到 → 首拍用户 XTRIG 只关半开帧,相位锁 N-1。 + +**修复**:`imx296_s_stream(1)` 在 FT 上电后固定 dummy×3(`enter-fast-trigger` / `-2` / `-3`,短脉宽 800µs)。 + +### 1.3 次因 B:free_run 期间 XTRIG 脏写 PWM + +mon 期外部触发曾真实打出 PWM(`trigger pulse was emitted while active mode is free_run`),污染 FT 入口。 + +**修复**:`imx296_trigger_once_locked` 在 `streaming && active != XTRIG_ONE_SHOT` 时 **直接忽略**(`return 0` + ratelimited warn)。 + +--- + +## 2. 修复内容(内核) + +| 文件 | 改动 | +|---|---| +| `drivers/media/platform/rockchip/isp/capture.c` | early-done residual ceiling:**不再 abandon**;记 `ceiling-force`,置 `mi_ready`,走 post-MI hold,再 early complete | +| 同上 | `rkisp_early_done_read_y/cb_offs`:`max(SHD, START)`(双视图采样,仍被本硅全 0 击穿,故需 ceiling-force) | +| `drivers/media/i2c/imx296.c` | free_run 忽略 XTRIG | +| 同上 | `s_stream` + `mode_switch` 进入 FT 时 dummy×3,`IMX296_DUMMY_PULSE_US=800` | +| `drivers/media/platform/rockchip/isp/rkisp.c` | FT arm:`SKIP_EARLY=4` / `SKIP_BUF=4` / `POST_MI=8000` / `WARMUP_POST_MI=18000` / `WARMUP_FRAMES=4` + n1trace | + +### 2.1 ceiling-force 关键路径(capture.c) + +``` +if (!mi_ready) { + if (mi_finished) → post-MI hold → complete + else if (now < deadline) → poll + else { + /* V39 OFFS 全 0:强制 mi_ready + post-MI hold → complete */ + diag_ceiling++; log ceiling-force; + mi_ready = 1; + if (post_us) restart timer; + /* else fall through complete */ + } +} +``` + +相对「在 wait_line 立刻 force」:仍吃满 residual ceiling + post_mi hold,降低绿带风险。 +相对「abandon to MI FE」:本拍即可 `mi_pub state=WORK early=1`,相位对齐当前曝光。 + +--- + +## 3. 验证结果(ceiling-force 内核) + +脚本:`/tmp/n1_confirm_v3.py`(低对比阈值:span≈3.3 时 thr≈max(0.35·span, 0.4))。 + +现场 gain=0 时 LO mean≈21.67,HI mean≈24.9–25.1(span≈3.3);bot48 在 HI 约 27–28(非完整绿带形态)。旧脚本 `span<8 → NO CONTRAST` 会误杀,不可用。 + +| 用例 | mon 外触发 | 结果(T2–T10) | +|---|---|---| +| L0_MONEXT_0 | 8 | **CUR 9/9,N1=0** | +| L0_MONEXT_1 | 8 | **CUR 9/9,N1=0** | +| L0_MONEXT_2 | 8 | **CUR 9/9,N1=0** | +| L0_COLD | 0 | **CUR 9/9,N1=0** | +| SW_WS_MONEXT | 8 | **CUR 9/9,N1=0** | +| SW_WS_COLD | 0 | **CUR 9/9,N1=0** | + +**SUMMARY:`['CUR','CUR','CUR','CUR','CUR','CUR']`** + +### 3.1 dmesg 旁证(同一次矩阵) + +| 计数 | 含义 | +|---|---| +| COMPLETE=89 | early complete 成功 | +| ceiling-force=89 | 本硅 OFFS 全 0,全部走 force 路径 | +| mi_pub WORK early=1 ≥43 | 用户可见帧走 early 发布 | +| mi_pub IRQ =22 | 部分 drop/warmup 仍走 IRQ | +| enter-fast-trigger-3 =12 | 每次进 FT 的第三 dummy 到位 | +| pulse ignored =32 | mon 期 8×4 场景 XTRIG 被吞 | + +时间线样例: + +``` +ceiling-force sof=S ... y_shd=0 y_st=0 +early complete sof=S ... +mi_pub state=WORK vb_seq=N early=1 ... sof=S +``` + +同 sof 上 force→complete→WORK,与用户触发同拍。 + +--- + +## 4. 历史复现矩阵(修复前,供对照) + +脚本 `/tmp/n1_confirm.py`(高对比阈值): + +| 用例 | 模式 | 触发 | mon 外触发 | 结果 | +|---|---|---|---|---| +| L0_SYS_NODISC | line0 | sysfs | 0 | CUR | +| L0_SYS_MONEXT | line0 | sysfs | 8 | **N-1(9/9)** | +| SW_WS_NODISC | software | WS | 0 | CUR | +| SW_WS_DISC2 | software | WS | 0 | N-1(人为打乱 pending) | +| SW_BOTH_NODISC | software | WS+sysfs | 0 | N-1(双路径 pending) | +| SW_WS_MONEXT | software | WS | 8 | CUR(当时 early-done 偶发仍工作 + soft pending) | + +MONEXT 首帧特征(修前 L0): + +``` +T1 LO tag=hold mean=HI dt=4s → 切换前 hold-last +T2 HI tag=1 mean=LO → 实为 T1 的 LO +… 全程 N-1 +``` + +--- + +## 5. 相关代码锚点 + +### 5.1 内核 + +- `drivers/media/i2c/imx296.c` + - `imx296_trigger_once_locked`:free_run ignore + - `imx296_dummy_trigger_locked` + `IMX296_DUMMY_PULSE_US` + - `imx296_mode_switch` / `imx296_s_stream`:enter-FT dummy×3 +- `drivers/media/platform/rockchip/isp/capture.c` + - `rkisp_early_done_timer`:**ceiling-force** + - `rkisp_early_done_mi_finished` / `read_*_offs` +- `drivers/media/platform/rockchip/isp/rkisp.c`:FT skip/drop/warmup 常数 +- `capture_v39.c`:`mi_frame_end` drop / FRAME_WORK vs IRQ + +### 5.2 CamOS(板端,非本树) + +- `gst_v4l2_grabber.py`:run_mode boundary、pending_soft、line0 latest-only、warmup 6×150ms + +--- + +## 6. 风险与后续 + +1. **ceiling-force 与绿线**:强制 complete 依赖 residual ceiling + `POST_MI=8ms`(warmup 18ms)。本轮 HI bot48≈27–28 vs center≈25,未见典型底 8 行纯绿带;建议 endurance 再抽检 `gre=0`。 +2. **OFFS 永久 0**:ceiling-force 是本硅上的务实解;若后续固件/寄存器视图恢复 live ramp,force 路径会自然减少(mi_finished 先成功)。 +3. **n1trace 日志量**:验证完成后可降为 `dev_dbg` / 采样打印,避免产线 dmesg 刷屏。 +4. **测试脚本**:产线/回归请用低对比阈值版本(span 可能仅 3–4);或提高 gain/补光再测。 +5. **版本号**:当前板端仍标 `#2026080601`(内容已含本修复);正式合入前建议 bump DLCVCAM 构建号并清理 n1trace。 + +--- + +## 7. 工作日志 + +### 2026-08-06 复现 + +- 脚本:`/tmp/n1_confirm.py`,L0_MONEXT 稳定 N-1。 +- 基线内核:绿线 early-done + procfs 门闩。 + +### 2026-08-06 诊断 + +- 加 n1trace:pulse / dummy / early arm·complete·ceiling / mi_pub。 +- 确认 mon 期 free_run 曾 emit PWM;STREAMOFF 路径无 enter dummy。 +- 确认 early-done:`y_shd=y_st=0` 永久 ceiling-miss,mi_pub 仅在下一 XTRIG 的 IRQ。 + +### 2026-08-06 修复 + +1. free_run ignore XTRIG +2. s_stream enter dummy×3(补 mode_switch 在 !streaming 的空洞) +3. early-done:**ceiling-force**(取代 abandon-to-MI-FE) +4. 刷机 `17:59:30` / sha `70490539…` + +### 2026-08-06 验证(本轮收口) + +- 全矩阵 6/6 **CUR**,N1=0。 +- COMPLETE≈ceiling-force,WORK early=1 与用户触发同 sof。 +- free_run ignore + enter-fast-trigger-3 计数符合预期。 + +**结论:N-1 相位滞后根因已定位,内核修复已在板端矩阵验证通过。** + +--- + +## 8. 2026-08-07 ROI 绿线回归与共存修复 + +**现象**:N-1 ceiling-force 合入后,触发模式 ROI 后前几帧再出现绿线/串帧风险。 + +**根因并存**: +- N-1 需要 ceiling-force(本硅 MI OFFS 全 0,abandon→下一拍 MI FE = N-1) +- 绿线需要足够 residual + post-MI;旧 force 仅 8ms post + drop/skip=4,ROI 短 height 时不够 + +### 8.1 第一轮共存(14:12 刷板,仍绿) + +| 参数 | 旧 (N-1) | 第一轮 | +|---|---|---| +| residual default/max | 15/40 ms | 20/60 ms | +| POST_MI | 8 ms | 12 ms | +| FORCE_POST_MI | (=POST) | 16 ms | +| WARMUP_POST / frames | 18 ms / 4 | 28 ms / 8 | +| SKIP_EARLY / DROP | 4 / 4 | 6 / 6 | + +**用户样帧回归(14:32:55)**: +- `images/2026-08-07/NG/20260807_143255_431_012242.jpg`,1456×592 +- bot8 ≈ `[57.7, 73.0, 36.6]`,末 ~8–9 行经典绿带 +- dmesg:`isp_ft_arm height=592 ... ceiling_us=22930 force_post=16000 skip=6 drop=6 warmup=8` +- 全程 `ceiling-force y_shd=y_st=0`;drop/skip 耗尽后首个 `mi_pub WORK early=1` 即对上 NG 时间戳 +- **结论**:16ms force_post + ~23ms residual 对 592 高 ROI 仍不够 UV 收尾 + +### 8.2 第二轮加强(本轮) + +| 参数 | 第一轮 | 第二轮 | +|---|---|---| +| residual default/max | 20/60 ms | **35/100 ms**,min floor 30ms;公式 `(remain+TAIL*6)*line + (h/2)*(line/2)+5ms` | +| POST_MI | 12 ms | **20 ms** | +| FORCE_POST_MI | 16 ms | **45 ms** | +| WARMUP_POST / frames | 28 ms / 8 | **55 ms / 12** | +| SKIP_EARLY / DROP | 6 / 6 | **8 / 10** | + +实现增量: +- `capture.c`:`rkisp_early_done_post_hold_us(..., force_path)` + - force 路径:`hold = max(hold, force_post) + residual/2` + - 验证路径仍用 steady/warmup,不加 residual 半额 +- `rkisp.c`:上表 + residual 公式 +- sensor 侧 mon 禁 XTRIG、enter dummy×3 **保留** + +预期 592 高:residual≥30ms,force hold warmup≈55+15=70ms(steady≈45+15=60ms), +从 wait_line 到 complete 合计 ~90–100ms,覆盖末行 UV;ROI stream-on 多丢 10 帧半开缓冲。 + +验证清单: +1. dmesg `isp_ft_arm ... force_post=45000 skip=8 drop=10 warmup=12` +2. FT+ROI 1456×592 前 20 用户可见帧 bot8 无 G 主导绿带 +3. N-1 矩阵仍全 CUR(force 路径仍在,仅 hold 变长) + +### 8.3 第二轮板端验证(2026-08-07 14:38 内核) + +- 内核:`#2026080601 SMP Fri Aug 7 14:38:29 CST 2026` +- boot sha256:`7f9d1424612aab11983e2070cf93e057bf981aaa0d71975b7d202d9cb2fd164b` +- dmesg arm 样例:`isp_ft_arm height=592 ... skip=8 drop=10 warmup=12 ceiling_us=42310 force_post=45000` + +**绿线(用户复现几何 1456×592)** +- `/tmp/roi592_patient.py`:6 次 ROI(含 592×3、400×1),**96/96 帧 ok,green=0,miss=0** +- 592 bot8 典型 ≈ `[31, 26, 37]`(G 不主导,对比用户 NG bot8 `[57.7, 73.0, 36.6]`) +- 仍全程 `ceiling-force`(OFFS 全 0),force→complete 持有加长后无底绿带 + +**N-1 矩阵**(`/tmp/n1_confirm_v4.py`,进 FT 后 prime 14 次触发烧 drop) +| 用例 | 结果 | +|---|---| +| SW_WS_COLD | **CUR** 9/9 | +| SW_WS_MONEXT | **CUR** 9/9 | +| L0_MONEXT_0 | **CUR** 9/9 | +| L0_MONEXT_1 | **CUR** 9/9 | +| L0_COLD | **CUR** 9/9 | + +**SUMMARY:`['CUR','CUR','CUR','CUR','CUR']`,N1=0** + +**结论**:第二轮共存参数在 ROI 绿线与 N-1 上同时成立;请 UI 再抠 1456×592 做人工 spot-check。 + +### 8.4 drop 收敛(产品:触发必出图,3–5 fps) + +**现象**:切 trigger 后头 10–12 次用户触发无图。dmesg 显示 `pulse emitted` 成功,但 `mi_skip_frame skip=10…1`,第 11 次才 `mi_pub`。 + +**原因**:第二轮为压绿把 `SKIP_BUF=10`/`SKIP_EARLY=8` 叠在 CamOS warmup×6 之上;绿线主防护已是 residual+force_post,大 drop 只伤害产品指标。 + +**调整**: +| 参数 | 8.2 | 8.4 | +|---|---|---| +| SKIP_BUF / SKIP_EARLY | 10 / 8 | **2 / 2** | +| WARMUP_FRAMES | 12 | **6**(只加长 hold,不丢用户帧) | +| FORCE_POST / residual | 45ms / 35–100ms | **保持** | + +预期:mode-switch 后 CamOS warmup 结束开 gate,内核最多再丢 2 帧半开缓冲;用户触发应几乎张张 `mi_pub`。 diff --git "a/IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" "b/IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" new file mode 100644 index 0000000000000..1d7556d9f69a8 --- /dev/null +++ "b/IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" @@ -0,0 +1,213 @@ +# IMX296 绿线/串帧修复 — 内核修改与长期验证记录 + +> 长期记忆与人工复核文件。本文件随每次新修改、新发现持续更新。 +> 最后一次更新:2026-08-06 13:40(内核 procfs 门闩已刷板 `#2026080601`,accept/reject/skip 冒烟通过) + +--- + +## 0. 问题背景 + +IMX296 在 **trigger 模式(master_fast_trigger)** 下,尤其是 free-run → trigger 切换、ROI 修改/重置后的最初几帧,出现 **绿线(底部残帧/串帧)**。根因不在 sensor 曝光本身,而在 ISP 通路帧完成与 buffer 交接的竞态。 + +测试痛点:板端无硬件触发源时通过 sysfs `run_mode` + WebSocket 软触发驱动。真正有意义的 bug 几乎都出现在 **模式切换 / ROI 修改** 时。 + +--- + +## 1. 根因分析(结论) + +IMX296 FT(fast trigger / XTRIG)下 MIPI 帧「保持打开」直到下一次触发到来,造成: + +1. **ISP V39 提前完成(early-done)竞态**:`wait_line` 在 height−8−1 处触发 `ISP3X_OUT_FRM_HALF`,但此时 **MI 的 Y/CB 写尚未完成**,尤其 FT 帧被 XTRIG 提前关闭时,chroma(CB)最后几行可能还没落盘。 +2. **V39 的 ISP_OUT_LINE 提前饱和**:约 8 行提前饱和,不能作为完成依据。 +3. **MI Y/CB_OFFS_CNT_SHD 可能残留上一帧 final 值**:FT 的 SOF 到来时 MI 的 SHD 寄存器还持有上一帧的写计数,导致「读到 full」就立即放行的假象。 +4. **进入 FT 后第一帧**:上一轮 free-run 的打开帧 + 首个 XTRIG,invalid 帧关闭与首个 valid 帧交接。 +5. **`rkisp_start` 里 `stream->skip_frame` 会被清零**,所以「丢掉头 N 帧」不能靠它,需要独立的持久计数器。 + +--- + +## 2. 内核修改文件清单 + +| 文件 | 修改点 | 作用 | +|---|---|---| +| `drivers/media/platform/rockchip/isp/capture.h` | 新增 early-done 状态字段与 API | 结构体字段:`early_done_delay_us/timer/pending/armed_sof_ns/poll_us/terminal_line/deadline_ns_lo/hi/mi_y_size/mi_cb_size/mi_saw_y/mi_saw_cb/mi_ready/post_mi_us/warmup_left/warmup_post_mi_us/skip_frames/drop_left`。API:`rkisp_stream_buf_done_early_drain`、`rkisp_early_done_clear_state` | +| `drivers/media/platform/rockchip/isp/capture.c` | MI 写计数缓存、`rkisp_early_done_mi_finished()`、定时器轮询后 hold | 读取 MI Y/CB_OFFS_CNT_SHD 得到 `mi_y_size/mi_cb_size`;判定「live ramp(saw_y/saw_cb 低于目标值)+ full」才 ready;ready 后按 `post_mi_us` 再 hold 若干 µs 让 chroma 尾行落盘;超 residual 上限只记日志,不强制 `vb2_done` | +| `drivers/media/platform/rockchip/isp/capture_v39.c` | `mi_frame_end` 里 early-done 前丢弃帧 | 若 `early_done_drop_left > 0`:递减、requeue buffer、goto end(不触发 vb2_done),可跨 `stream->skip_frame` 清零存活 | +| `drivers/media/platform/rockchip/isp/rkisp.c` | 0518 策略常量与 FT 启动逻辑 | TAIL=8、residual 上限默认 15000(max 40000)、line_us 80–250、poll 50µs、POST_MI=6000µs、WARMUP_POST_MI=16000µs、WARMUP_FRAMES=4、SKIP_EARLY_FRAMES=3、SKIP_BUF_FRAMES=3 | +| `drivers/media/i2c/imx296.c` | 进入 FT 的双 dummy 触发 | `imx296_dummy_trigger_locked()` 两次(日志 `enter-fast-trigger` / `enter-fast-trigger-2`),关闭进入 FT 后残留的 invalid 打开帧 | + +--- + +## 3. Early-done 完成策略(capture.c 核心逻辑) + +``` +FT SOF → + early_done_skip_frames > 0 ? 不做 early-done(走真实 MI FE IRQ),递减 + : 在 wait_line(OUT_FRM_HALF) 处 arm early_done 定时器 +定时器每 poll(50µs) 轮询一次: + MI Y_OFFS_CNT_SHD / CB_OFFS_CNT_SHD 与 programmed size 比较 + saw_y/saw_cb 记录「这一臂里见过 < size 的值」→ 证明不是残留 SHD + 只有 (saw_y && saw_cb && Y_full && CB_full) → mi_ready=1 + mi_ready 后计时 post_mi_us(FT 用 6000,warmup 用 16000)再完成 vb2_done +residual 超上限:仅 dev_dbg 告警,绝不强制 vb2_done(避免残帧出流) +``` + +设计要点: +- **不信任单次 SHD 读值**,必须看到 live ramp(从低于 size 增长到 size)才认为本帧 MI 真写完。 +- **不做「超时强制 done」**。残帧宁可丢(被 drop_left / skip 掉),也不能带着未写完的 chroma 出流形成绿线。 +- **post-MI hold** 让最后几行 NV12 色度落盘后再上报帧完成。 +- warmup(进入 FT 后前 `WARMUP_FRAMES=4` 帧)用更保守的 `WARMUP_POST_MI=16000µs`。 +- `SKIP_EARLY_FRAMES=3`:前 3 个 SOF 完全交给 MI FE IRQ。 +- `SKIP_BUF_FRAMES=3`:`stream->skip_frame` 额外丢 3 个 FT 头帧(free-run→FT 后首个 XTRIG 帧最脏)。 +- `early_done_drop_left`(capture_v39)提供**跨 start 清零存活的丢弃**。 + +--- + +## 4. Free-run 路径处理 + +free-run 下 **不 arm early-done**(`rkisp_early_done_clear_state` 清理状态),完全走真实 MI FE IRQ 完成,保持出厂路径不变,避免误伤 free-run 稳定性。 + +--- + +## 5. 测试方法论(board: root@192.168.1.180) + +- **CamOS**:`/root/smartcam_bs/0_测试运行_camos.sh`,软触发走 sysfs + WS。 +- **连续采集脚本**:`/root/roi_endurance_live.py`(持久副本;`/tmp/roi_endurance_live.py` 是运行副本)。 + - 每个 sample:软触发 → 等 `X-Frame-Tag` 变化(**绝不用 hold-last 预览当新帧**)→ 存 `frames/`。 + - free-run 与 trigger 两模式都连续存图。 + - 事件集:`random_roi`(随机尺寸+随机偏移+重置全幅)、`roi_thrash`、`mode_flip`(free↔trigger↔monitor)、`min_settle`。 +- **绿线判定**:每帧底部区域 RGB,任一通道异常偏高记为 green;绿帧存 `green/`。 +- **MISS 判定**:触发后未见新 X-Frame-Tag。 +- **skip_free**:ROI 变更后 free-run 的前 4 帧丢弃(`skip_free_left=4`),因实测 free tag=1(变更后首帧)确为脏帧。 + +### 关键运行参数(board 端) +``` +sysfs run_mode: /sys/bus/i2c/devices/4-001a/run_mode +free_run / master_fast_trigger / monitor +capture node: /dev/video11 (rkisp_mainpath) +API: http://127.0.0.1:8000/api/auth/login {"username":"admin","password":"dlcv2026"} → {"success":true} +``` + +--- + +## 6. 长期验证进展(更新记录) + +### 6.1 内核构建 `#2026080518`(6.1.99-rk3576) +- 首次承载本修复全量策略(MI live+full、post-MI hold、skip/drop、dummy×2)。 +- 已刷入板端 flash(多次重启后仍生效,验证通过)。 + +### 6.2 长时间 endurance 结果(多段) +| 时间段 | free 存图 | trig 存图 | green | miss | roi 次数 | mode 次数 | 结论 | +|---|---|---|---|---|---|---|---| +| 首段 ~18:00–20:30(含 3 次板端重启) | 累计增长 | 累计增长 | **0** | 0 | 数十 | 上百 | 稳定,重启为网络/系统问题非 bug | +| 20:30–00:40 段(单进程 4535) | ~20.7k | ~8.2k | **0** | 58(集中在 00:11 一次 burst 超时卡死窗口,非绿线) | 212 | 309 | 稳定;miss 为 API/burst 瞬时超时 | +| 00:40 板端重启后新周期(10h,监控至 09:13 结束) | 跨 4 次重启(00:40/05:02/06:54/08:52)多段累计 free≈1.2 万+、trig≈5 千+(每段被重启清零重新累计) | 同左 | **0** | 0(各恢复后段) | 数百(含随机偏移) | 数百 | 全段 green=0;重启为 systemd 看门狗 30s 触发(非绿线) | + +**核心结论:green=0 已跨多段、多 ROI 组合、多模式切换、多板端重启保持。** 偶发 MISS 均为 API 软触发/网络瞬时故障导致的自愈事件,**与绿线无关**。 + +### 6.3 典型自愈事件(记录留档) +- **20:11 MISS 风暴**:ROI `1456x400_0_474` 后触发 MISS(46 次),free 帧恢复正常。手动复位:kill endurance + CamOS → 重启 → 恢复。 +- **00:11 burst 超时**:`event_reset_roi → shoot_burst` 抛 `TimeoutError`,seq 卡死 25437,watchdog 判 stall → 重启 CamOS;endurance 自愈 recover,miss=58 冻结不再增长。 +- **00:40 板端重启**:内核保留(flash 验证),CamOS 自启,endurance 需手动重启(见 §7 命令)。 +- **04:32:50 例行恢复**:api=False → 04:33:31 自愈 api=True,green=0,seq=24858。00:11 后第 5 次同类 API 瞬时故障(00:11/01:36/02:45/03:03/04:32),全部自愈,miss 计数自 02:45 后冻结在 158 不再增长。均为软触发/API 链路瞬时问题,与绿线无关。 +- **04:36:18 例行恢复**:api=False(seq 冻结 24947)→ 04:36:59 自愈 api=True,green=0,seq=25582(追帧正常)。第 6 次同类事件,~40s 自愈。**观察:04:32–04:36 窗口内两次 API 瞬时故障,软触发/API 链路抖动频率略有上升**,但 green 恒 0、seq 持续推进,未影响帧质量(详见 §8 待办)。 +- **04:38:21 例行恢复**:api=False → 04:39:02 自愈 api=True,green=0,seq=25659。第 7 次同类事件(04:32/04:36/04:38 三连),全部 ~40s 内自愈,小时报确认板端 streaming=1 正常。**结论:API 链路抖动加剧但自愈极快,帧质量不受影响;10h 周期内暂不需介入,周期结束后建议单独排查 CamOS 软触发链路(§8)。** +- **04:46:35 例行恢复**:api=False → 04:47:17 自愈 api=True,green=0,seq=26588,~42s。第 8 次事件,04:32 起约 15 分钟内 4 次抖动(~1次/5min),全部自愈,watchdog 全程正确 defer 不双杀。**green 恒 0、seq 每次恢复后推进,帧质量不受影响。** +- **04:50:01 例行恢复**:api=False(seq 一度冻结 26588 达 ~5min)→ 04:50:43 自愈 api=True,seq=26644(恢复推进)。第 9 次事件,04:32–04:50 约 18 分钟内 5 次抖动,全部 ~40s 自愈,watchdog 按 streak 逻辑 defer、未误判。**链路抖动持续但帧质量保持 green=0。** +- **05:02:30 板端重启(第 2 次)**:/tmp 整体清空 → endurance/camos 全灭,watchdog 判定 endurance dead 并自动重启。手动恢复:CamOS 拉起(i=18 OK)、endurance PID 4279、END_TS 保留原 10h 窗口(1785984201,至 08-06 10:43)、left=5.66h。**dmesg/journal 无 panic/oops/watchdog 记录**,重启原因不明;两次重启(00:40、05:02)均发生在 API 抖动之后,疑为板端外部复位(硬件看门狗/供电/外部指令),与绿线无关(green 全程 0)。**待办:查板端复位来源(§8)。** +- **05:27 小时报新发现(ROI→recover 触发条件定位)**:重启后(05:03 起)7 次 recover 全部在 `roi set_*` 后 ~15s 触发(05:15/05:19/05:22/05:25 均如此)。即 endurance 在 ROI 变更后的健康检查(等新 X-Frame-Tag)超时,判定异常 → 重启 CamOS。恢复后 seq 持续推进、green=0、miss=0。**结论更新:API 抖动主因是 ROI 变更后的软触发链路超时自愈,非随机故障;与绿线无关,但每次 ROI 变更耗 ~60-90s 恢复窗口。** +- **06:54:32 板端重启(第 3 次,根因确认)**:/tmp 清空 → 恢复流程同前(endurance PID 4090、left=3.8h、END 保留)。**诊断结论**:`/etc/systemd/system.conf.d/10-dlcvcam-watchdog.conf` 配置 `RuntimeWatchdogSec=30s`,systemd(PID 1)持有 /dev/watchdog0 每 30s 喂硬件看门狗(2ace0000)。**三次重启(00:40/05:02/06:54)均发生在重度流切换/API 抖动之后,机制为:系统阻塞 >30s(软锁/D-state 风暴)→ 硬件复位**。上次启动日志在流状态正常后无 panic 结束,符合看门狗复位。重启间隔缩短(4.4h→1.9h)与 recover 频率上升同步。**与绿线无关(green 全程 0)。** +- **08:52:10 板端重启(第 4 次)**:/tmp 清空 → 恢复(endurance PID 4361、left=1.84h、END 保留至 10:43)。重启间隔稳定在 ~2h(00:40/05:02/06:54/08:52)。**看门狗 30s 机制下,重度流切换期间系统 >30s 阻塞反复触发硬件复位;每次重启清零已积累数据,但 green=0 在所有跨重启周期保持。** + +--- + +## 7. 运维命令速查 + +### 板端 +```bash +# 重启 endurance(10 小时) +OUT=/tmp/roi_endurance_live; mkdir -p $OUT/frames $OUT/green +END_TS=$(( $(date +%s) + 36000 )); echo $END_TS > $OUT/end_ts +cp -f /root/roi_endurance_live.py /tmp/roi_endurance_live.py +setsid nohup env ENDURANCE_OUT=$OUT python3 /tmp/roi_endurance_live.py --hours 10 >> $OUT/console.log 2>&1 < /dev/null & + +# 看状态 +cat /tmp/roi_endurance_live/summary.txt +tail -n 20 /tmp/roi_endurance_live/console.log +``` + +### 本机 watchdog(dev 机) +```bash +# 常驻:/tmp/endurance_watchdog.py (PID 385900),每 40s 采集 +# 输出:/tmp/endurance_watchdog_state/status.json +cat /tmp/endurance_watchdog_state/status.json +``` +Watchdog 行为:API 优先健康判定;endurance 自愈窗口(recover)不抢杀;seq stall>360s → CamOS 重启;endurance 死 → 重启(若未过 end_ts)。 + +### 内核 +```bash +# 编译 & 刷板(见 DLCVCAM内核编译与更新操作手册.md) +# 本仓库最新 fix 提交:416f32d22c72 fix(imx296): close FT frame and harden V39 early-done drain +``` + +--- + +## 8. 后续关注点(待办/风险) + +- [x] 00:40 重启后新 10h 周期跑完,复核绿色 0 与 roi/mode 覆盖率(09:13 监控结束判定 green=0 可交付)。 +- [ ] 若再次出现 free tag=1 绿线,考虑把 skip_free 的丢弃逻辑前移到内核(而非测试脚本)。 +- [x] **板端重启根因复核(2026-08-06 10:10,纠正先前“仅 30s 看门狗误复位”判断)**: + - 配置属实:`/etc/systemd/system.conf.d/10-dlcvcam-watchdog.conf` → `RuntimeWatchdogSec=30s`,`systemd` PID1 持有 `/dev/watchdog0`(`2ace0000.watchdog`)。设计意图是 hard hang → 停 pet → SoC 复位 → U-Boot boot-try。 + - **pstore 实锤不是“单纯 >30s 软锁”**:`/var/lib/systemd/pstore/dmesg-ramoops-0` 捕获 + `Kernel panic - not syncing: Asynchronous SError Interrupt`, + `Comm: python`,`pc: vfree` / `lr: rkisp_proc_write+0xf8`(`drivers/.../isp/procfs.c` 写路径末尾 `vfree(buf)`;SError 多为此前 `writel`/`rkisp_write` 非法访问 ISP 寄存器的异步报错)。 + 同一签名在 `dmesg.txt`(build `#2026080504`)也出现过 → 可复现,非偶发。 + - **崩溃前序(重度流切换 thrash)**:反复 `stream on/off` → `MIPI_CSI2 ERR2` → 大量 `CIF_ISP_PIC_SIZE_ERROR` → `start pipeline failed -22` → 再 `run_mode=master_fast_trigger`(streaming=0 仍打触发)→ userspace 写 rkisp procfs:`mode=0x100`、`400=0x6197` → SError panic。另有 `rkisp_stream_stop id:0 timeout`。 + - **重启形态分类**: + - boot -2/-3/-4/-5:journal 无 `System is rebooting`,末尾停在流错误/PIC_SIZE → **硬死/panic 后复位**(看门狗是收尸,不是首因)。 + - boot -1(09:40):有 `systemd-logind: System is rebooting` → **软件 reboot**(endurance 周期尾/人工/脚本),与 panic 不同。 + - **处置建议**(改配置仍需用户确认): + 1. **优先修根因**:CamOS/python 经 `/proc` 写 ISP 寄存器的时机与 ISP power/clock/pipeline 状态;以及 ROI/mode thrash 下 `PIC_SIZE_ERROR` / `pipeline failed -22` / `stream_stop timeout`。 + 2. **RuntimeWatchdogSec=30s 可临时放宽到 2–5min 作辅助**,但 **放宽 alone 挡不住 SError panic**;panic 路径仍会复位。仅当确认存在“无 panic 的长阻塞”时放宽才有独立收益。 + 3. 保留 pstore/ramoops;下次复位后优先 `cp /var/lib/systemd/pstore/dmesg-ramoops-*` 归档。 +- [ ] **ROI→recover 触发条件已定位**:endurance 在 ROI 变更后等新 X-Frame-Tag 超时(~15s)即重启 CamOS。建议排查 endurance 的 ROI 变更后健康检查超时阈值/触发逻辑(与绿线无关,但影响测试效率,每次 ROI 变更耗 ~60-90s)。 +- [ ] MISS 风暴根因在 API/WS 软触发链路,与绿线无关,但值得单独跟进稳定性(可选)。 +- [ ] 内核层面可将 warmup 帧数/skip 数做成可调参数,便于回归调参。 +- [x] **rkisp_proc_write SError 写者定位(2026-08-06)**:CamOS `smartcam_backend/camera/rkcam/_runtime/gst_v4l2_controls.py` → `V4L2ControlsRoiManager._write_wb_register_from_ratio()`(默认 `SMARTCAM_WB_REGISTER_ENABLE=1`)。写入序列: + 1. `mode=0x100`(`RKISP_PROCFS_FIL_AIQ`) + 2. `400=0x6197`(`ISP3X_ISP_CTRL0`,开 AWB 等) + 3. `538/53c/...` AWB gain(`ISP3X_ISP_AWB_GAIN*_G/RB`) + 调用链:`set_balance_ratio` / load user set;路径 `/proc/rkisp-vir0`。**无 streaming 门闩**,ROI/mode thrash teardown 窗口仍会写 → SError。板端已确认 **V4L2 无 red/blue balance**,不能靠关 WB-reg 代替(会丢手动白平衡功能)。 +- [x] **最小功能保留修复(板端 CamOS,2026-08-06 10:57 已落地并重启验证)**:同文件加 streaming/ISP 门闩,**不关** `SMARTCAM_WB_REGISTER_ENABLE`: + - 默认 `SMARTCAM_WB_REGISTER_REQUIRE_STREAMING=1` + - 写 MMIO 前要求:非 `camera_offline`,且 `run_mode streaming=1` 或 `/proc/rkisp*` 显示 `Isp online/offline`(内核 `ISP_START`);否则 skip 并打 `[wb-reg] skip MMIO (...)` + - `set_balance_ratio` **先写入 state 目标 ratio**,MMIO 失败也不丢用户设置(ROI/mode 恢复后 grabber restore 会再写) + - 备份:`gst_v4l2_controls.py.bak.20260806_serror_gate` + - 验证:streaming=1 时 write=True(启动加载用户集2 `r=1383 g=1024 b=2000` 成功);`camera_offline` / `streaming=0` / `isp_not_start` 时 write=False;`/preview/frame` JPEG 正常;未关 WB 功能。 +- [x] **ROI/mode thrash 门闩验证(2026-08-06 10:59:55–11:21:38,~22 min)**: + - 输出:`/tmp/roi_endurance_wb_gate`,`ENDURANCE_OUT=... python3 ... --hours 0.33` + - **结果**:`free=772 trig=565 green=0 miss=0 roi=17 mode=29 recover=8 camos_rst=4 err=0`,存图 1337 帧,`green/` 空 + - **无板端重启**:`boot_same=y`,uptime 连续,dmesg 无 SError/panic,pstore 未刷新新 panic(仍为 08:51/09:41 旧档) + - 事件覆盖:`random_roi×8`、`roi_thrash×2`、`mode_flip×2`、`reset_roi×2`、`min_settle×1` + - **WB 功能保留**:streaming 窗口内多次 `[wb-reg] write ... ratio r=1383 g=1024 b=2000`(`camos_server_wb_gate.log` 14 条 + camos_restart 后 `camos_boot.log` 7 条);门闩代码仍在板端 + - **残留(非 panic)**:ROI 后 endurance 健康检查仍触发 recover/camos_rst(与绿线/SError 无关,测试效率问题) +- [x] **内核门闩补丁已刷板(2026-08-06 13:37,`#2026080601`)**:`drivers/media/platform/rockchip/isp/procfs.c` + - 镜像:`boot-rk3576-6.1.99-2026080601-416f32d2-dirty.img`(sha256 `33eff39a…b516244`),`dd` → `/dev/disk/by-partlabel/boot` + - 含绿线策略工作区变更 + procfs 门闩(ISP_START + RPM get/put + reg `<0x6000` 且 4 字节对齐 + 写长度 cap) + - **冒烟**: + 1. streaming=1:`mode=0x100` / `400=0x6197` / `538`/`53c` → rc=0,dmesg 正常 `rkisp_proc_write`,**无 skip** + 2. 坏寄存器:`6000=0x1`、`401=0x1` → rc≠0,dmesg `reject bad reg` + 3. CamOS 停、streaming=0:`400=…`/`538=…` → `-ENODEV`,dmesg `skip MMIO (isp_state=0x10001 …)`(**mode= 仍可写**,只拦 FIL_AIQ/FIL_SW MMIO) + 4. CamOS 重启后:gain/pulse 可调(gain=55、pulse≈8986)、preview JPEG OK、CamOS `[wb-reg] write` 仍成功;dmesg 无 SError/panic + - 与 CamOS streaming 门闩双层防护;watchdog 未改 + +--- + +## 9. 结论 + +当前内核策略在长期混合负载(free+trigger、随机 ROI 含偏移、模式频繁切换、板端多次重启)下 **green=0 稳定保持**。修复核心:**MI 真实完成(live ramp + full)+ post-MI hold + 早期 SOF skip + 持久 buffer drop + 进入 FT dummy 触发**。测试脚本侧以 skip_free 丢弃 ROI 变更后脏 free 帧为辅。 + +**判定标准**:单段 ≥ 数小时、覆盖多种 ROI(含随机偏移)与模式切换、green=0 且 free/trig 连续存图正常,即可判定修复稳定可交付。**2026-08-06 09:13 监控结束:00:43 起 10h 周期累计 ~8.5h(含 4 次板端重启、数十次 CamOS 重启、数百 ROI/mode 切换),全程 green=0,判定修复稳定。** + +**SError / WB-reg(2026-08-06)**:硬复位主因更正为 thrash 窗口 CamOS 经 `/proc/rkisp-vir0` 写 ISP 寄存器触发 Asynchronous SError。**双层修复已上板**:① CamOS streaming/ISP 门闩(不关 WB-reg,thrash ~22min 无复位);② 内核 `procfs.c` 门闩 build `#2026080601` 已刷 180,accept/reject/skip 冒烟通过。`RuntimeWatchdogSec` 未改。 + +**遗留问题**(均与绿线无关,见 §8):① ROI 变更后 endurance 健康检查超时自愈;② 可选更长 thrash 提高 no-SError 置信度;③ `#2026080601` 为 dirty 树构建(含未提交绿线/procfs 工作区改动),合入前需正式 commit。 diff --git a/build_bootimg.sh b/build_bootimg.sh new file mode 100755 index 0000000000000..6322eeab7c39f --- /dev/null +++ b/build_bootimg.sh @@ -0,0 +1,294 @@ +#!/bin/bash +# DLCVCAM RK3576 内核自动编译脚本 +# - 自动判断初次配置 / 增量编译 +# - 若 DLCVCAM_BUILD_VERSION 对应提交与当前 HEAD 不同,则按当天日期自动 bump +# - 产出带构建编号与短提交的 boot 镜像:boot-rk3576-6.1.99-YYYYMMDDNN-.img +# - 同时生成完整性 sidecar(不使用签名): +# .sha256 整包 SHA-256 +# .dlcvcam.json 整包哈希 + FIT 内嵌分量哈希清单 +# 板端烧录前:python3 scripts/dlcvcam_verify_bootimg.py verify --require-sidecar +# +# 用法: +# ./build_bootimg.sh # 自动判断配置并编译 +# ./build_bootimg.sh --force-config # 强制重新 defconfig + merge +# ./build_bootimg.sh --no-bump # 不自动更新 DLCVCAM_BUILD_VERSION +# ./build_bootimg.sh -j4 # 指定并行度 +# ./build_bootimg.sh --clean-config # 删除 .config 后走初次配置 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +ARCH=arm64 +CROSS_COMPILE=aarch64-linux-gnu- +DEFCONFIG=lubancat_linux_rk3576_defconfig +CUT_CONFIG=arch/arm64/configs/dlcvcam_rk3576_kernel_cut.config +DTB_NAME=dlcvcam-rk3576.dtb +BUILD_VERSION_FILE=DLCVCAM_BUILD_VERSION +KERNEL_VER_PREFIX="6.1.99" +JOBS="$(nproc 2>/dev/null || echo 8)" +FORCE_CONFIG=0 +NO_BUMP=0 +CLEAN_CONFIG=0 + +log() { printf '\033[1;34m[%s]\033[0m %s\n' "$(date '+%H:%M:%S')" "$*"; } +ok() { printf '\033[1;32m[%s]\033[0m %s\n' "$(date '+%H:%M:%S')" "$*"; } +warn() { printf '\033[1;33m[%s]\033[0m %s\n' "$(date '+%H:%M:%S')" "$*" >&2; } +die() { printf '\033[1;31m[%s]\033[0m %s\n' "$(date '+%H:%M:%S')" "$*" >&2; exit 1; } + +usage() { + sed -n '2,12p' "$0" | sed 's/^# \?//' + exit 0 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage ;; + --force-config) FORCE_CONFIG=1; shift ;; + --clean-config) CLEAN_CONFIG=1; shift ;; + --no-bump) NO_BUMP=1; shift ;; + -j*) + if [[ "$1" == "-j" ]]; then + JOBS="${2:?-j 需要并行数}" + shift 2 + else + JOBS="${1#-j}" + shift + fi + ;; + *) + die "未知参数: $1 (见 --help)" + ;; + esac +done + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1" +} + +need_cmd make +need_cmd lz4 +need_cmd git +need_cmd "${CROSS_COMPILE}gcc" + +# ---------- 构建编号:相对上次提交该文件的 commit 是否变化 ---------- +# 规则: +# 1. 若 DLCVCAM_BUILD_VERSION 相对 HEAD 有未提交修改,且内容已是合法编号,则沿用工作区值 +# 2. 否则看「最后一次提交该文件的 commit」是否等于当前 HEAD: +# - 相等:说明本提交就是 bump 提交,沿用文件内容 +# - 不等:说明 HEAD 相对上次 bump 又有新提交,按当天日期自动 bump(同日递增 NN) +bump_build_version_if_needed() { + local today current last_bump_commit head_commit date_part seq new_ver + + [[ -f "${BUILD_VERSION_FILE}" ]] || die "缺少 ${BUILD_VERSION_FILE}" + + current="$(tr -d '[:space:]' < "${BUILD_VERSION_FILE}")" + [[ "${current}" =~ ^[0-9]{8}(0[1-9]|[1-9][0-9])$ ]] \ + || die "${BUILD_VERSION_FILE} 格式非法: '${current}'(需要 YYYYMMDDNN,NN=01..99)" + + if [[ "${NO_BUMP}" -eq 1 ]]; then + warn "跳过版本自动 bump(--no-bump),使用: ${current}" + echo "${current}" + return + fi + + # 工作区已改过该文件:尊重用户/上次脚本写好的值 + if ! git diff --quiet -- "${BUILD_VERSION_FILE}" 2>/dev/null \ + || ! git diff --cached --quiet -- "${BUILD_VERSION_FILE}" 2>/dev/null; then + log "${BUILD_VERSION_FILE} 工作区已修改,沿用: ${current}" + echo "${current}" + return + fi + + head_commit="$(git rev-parse HEAD)" + last_bump_commit="$(git log -1 --format='%H' -- "${BUILD_VERSION_FILE}" 2>/dev/null || true)" + + if [[ -n "${last_bump_commit}" && "${last_bump_commit}" == "${head_commit}" ]]; then + log "${BUILD_VERSION_FILE} 已在当前 HEAD 提交,沿用: ${current}" + echo "${current}" + return + fi + + today="$(date +%Y%m%d)" + date_part="${current:0:8}" + seq="${current:8:2}" + # 去掉前导零做算术 + seq=$((10#${seq})) + + if [[ "${date_part}" == "${today}" ]]; then + seq=$((seq + 1)) + if [[ "${seq}" -gt 99 ]]; then + die "当天构建序号已超过 99,请手工处理 ${BUILD_VERSION_FILE}" + fi + new_ver="$(printf '%s%02d' "${today}" "${seq}")" + else + new_ver="${today}01" + fi + + printf '%s\n' "${new_ver}" > "${BUILD_VERSION_FILE}" + ok "HEAD 相对上次 ${BUILD_VERSION_FILE} 提交已变化,自动 bump: ${current} -> ${new_ver}" + echo "${new_ver}" +} + +# ---------- 初次配置 vs 增量 ---------- +need_initial_config() { + [[ "${FORCE_CONFIG}" -eq 1 ]] && return 0 + [[ "${CLEAN_CONFIG}" -eq 1 ]] && return 0 + [[ ! -f .config ]] && return 0 + + # 关键配置标记:本地版本与 ARM64,避免误用其它板级 .config + if ! grep -q '^CONFIG_ARM64=y' .config 2>/dev/null; then + warn ".config 不是 ARM64 配置,将重新配置" + return 0 + fi + if ! grep -q 'CONFIG_LOCALVERSION="-rk3576"' .config 2>/dev/null; then + # 裁剪配置合并后通常带 -rk3576;没有也不强制失败,仅提示 + warn ".config 未包含 CONFIG_LOCALVERSION=\"-rk3576\",仍按增量处理(可用 --force-config)" + fi + return 1 +} + +do_initial_config() { + log "初次配置:${DEFCONFIG} + ${CUT_CONFIG}" + if [[ "${CLEAN_CONFIG}" -eq 1 && -f .config ]]; then + rm -f .config .config.old + fi + + make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" "${DEFCONFIG}" + [[ -f "${CUT_CONFIG}" ]] || die "缺少裁剪配置: ${CUT_CONFIG}" + ./scripts/kconfig/merge_config.sh -m .config "${CUT_CONFIG}" + make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" olddefconfig + ok "配置完成" +} + +do_build() { + log "编译 Image / dtbs / modules (-j${JOBS})..." + make ARCH="${ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" \ + Image dtbs modules -j"${JOBS}" + + log "压缩 Image -> Image.lz4 ..." + lz4 -f arch/arm64/boot/Image arch/arm64/boot/Image.lz4 + + log "打包 boot.img (DTB=${DTB_NAME})..." + BOOT_ITS=boot.its ./scripts/mkimg --dtb "${DTB_NAME}" + + [[ -f boot.img ]] || die "未生成 boot.img" +} + +is_worktree_dirty_except_build_version() { + # 忽略 DLCVCAM_BUILD_VERSION 本身(脚本可能刚 bump 过) + # 以及常见编译产物,避免误标 dirty + local line path + while IFS= read -r line; do + [[ -z "${line}" ]] && continue + path="${line:3}" + path="${path#\"}" + path="${path%\"}" + # rename: "old -> new" + if [[ "${path}" == *" -> "* ]]; then + path="${path##* -> }" + fi + case "${path}" in + "${BUILD_VERSION_FILE}") continue ;; + boot.img|resource.img|zboot.img|out|out/*) continue ;; + boot.img.sha256|boot.img.dlcvcam.json) continue ;; + boot-rk3576-*.img|boot-rk3576-*.img.sha256|boot-rk3576-*.img.dlcvcam.json) continue ;; + arch/arm64/boot/Image|arch/arm64/boot/Image.lz4) continue ;; + .config|.config.old) continue ;; + bad_packages|bad_packages/*) continue ;; + esac + return 0 + done < <(git status --porcelain --untracked-files=no 2>/dev/null || true) + return 1 +} + +write_bootimg_integrity_sidecars() { + # 生成整包 sha256 + JSON 清单;板端烧录前用 scripts/dlcvcam_verify_bootimg.py 校验 + # 不使用签名,仅防传输损坏 / 误传 / 非密码学意义上的篡改发现 + local img="$1" + local build_id="$2" + local kernelrelease="$3" + local git_desc="$4" + local verify_py="${SCRIPT_DIR}/scripts/dlcvcam_verify_bootimg.py" + + [[ -f "${img}" ]] || die "write_bootimg_integrity_sidecars: 缺少 ${img}" + + if [[ -f "${verify_py}" ]] && command -v python3 >/dev/null 2>&1; then + python3 "${verify_py}" gen-sidecar "${img}" \ + --build-version "${build_id}" \ + --kernelrelease "${kernelrelease:-}" \ + --git "${git_desc}" \ + || die "生成完整性 sidecar 失败: ${img}" + # 自检:刚生成的包必须能通过 FIT 内嵌 hash + 整包 sha256 + python3 "${verify_py}" verify "${img}" --require-sidecar \ + || die "打包后自检失败(不应发生): ${img}" + return + fi + + # 兜底:无 python 时只写整包 sha256 + warn "python3 或 scripts/dlcvcam_verify_bootimg.py 不可用,仅写入 ${img}.sha256" + need_cmd sha256sum + sha256sum "${img}" | awk -v n="$(basename "${img}")" '{print $1 " " n}' > "${img}.sha256" +} + +package_named_image() { + local build_ver short_sha out_name kernelrelease build_id git_desc + + build_ver="$(tr -d '[:space:]' < "${BUILD_VERSION_FILE}")" + short_sha="$(git rev-parse --short=8 HEAD)" + # 除版本文件/编译产物外还有改动时加 -dirty,避免与干净提交产物混淆 + if is_worktree_dirty_except_build_version; then + short_sha="${short_sha}-dirty" + fi + + out_name="boot-rk3576-${KERNEL_VER_PREFIX}-${build_ver}-${short_sha}.img" + cp -f boot.img "${out_name}" + + if command -v mkimage >/dev/null 2>&1; then + log "FIT 信息:" + mkimage -l boot.img || true + fi + + kernelrelease="$(make -s ARCH="${ARCH}" kernelrelease 2>/dev/null || true)" + build_id="$(make -s ARCH="${ARCH}" dlcvcam-build-version 2>/dev/null || echo "${build_ver}")" + git_desc="$(git rev-parse --short HEAD) ($(git rev-parse --abbrev-ref HEAD))" + + log "生成完整性校验文件(整包 sha256 + FIT 清单)..." + write_bootimg_integrity_sidecars "boot.img" "${build_id}" "${kernelrelease:-}" "${git_desc}" + write_bootimg_integrity_sidecars "${out_name}" "${build_id}" "${kernelrelease:-}" "${git_desc}" + + ok "完成" + echo + echo " kernelrelease : ${kernelrelease:-n/a}" + echo " build version : ${build_id}" + echo " git : ${git_desc}" + echo " boot.img : $(ls -lh boot.img | awk '{print $5}')" + echo " 交付镜像 : ${out_name} ($(ls -lh "${out_name}" | awk '{print $5}'))" + echo " 整包校验 : ${out_name}.sha256" + echo " 清单 : ${out_name}.dlcvcam.json" + echo + echo "烧录前在板卡上校验(不验签,仅 hash):" + echo " python3 scripts/dlcvcam_verify_bootimg.py verify ${out_name} --require-sidecar" + echo " # 或只验 FIT 内嵌 sha256(无 sidecar 时):" + echo " python3 scripts/dlcvcam_verify_bootimg.py verify ${out_name}" + echo + echo "下一步:校验通过后再用 RKDevTool / rkdeveloptool 烧录 ${out_name}" + echo " (或直接烧录 boot.img,内容相同)" +} + +# ---------- main ---------- +log "工作目录: ${SCRIPT_DIR}" +log "交叉编译: ${CROSS_COMPILE}gcc ($(${CROSS_COMPILE}gcc -dumpmachine 2>/dev/null || echo '?'))" + +BUILD_VER="$(bump_build_version_if_needed)" +log "本次 KBUILD_BUILD_VERSION / DLCVCAM_BUILD_VERSION = ${BUILD_VER}" + +if need_initial_config; then + do_initial_config +else + log "检测到已有 .config,走增量编译" +fi + +do_build +package_named_image diff --git a/drivers/media/i2c/imx296.c b/drivers/media/i2c/imx296.c index 03b253bd48941..e2c009e8b1f0c 100644 --- a/drivers/media/i2c/imx296.c +++ b/drivers/media/i2c/imx296.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -188,6 +189,12 @@ * or half-written buffer on the next free-run frames. */ #define IMX296_XTRIG_CLOSE_FRAME_WAIT_US 50000U +/* + * Enter-FT / ROI-boundary dummies temporarily force this short pulse so drain + * time is bounded by readout, not by a multi-10ms user exposure left in + * trigger_pulse_us from a previous session / CamOS replay (N-1 entry hygiene). + */ +#define IMX296_DUMMY_PULSE_US 800U enum imx296_op_mode { IMX296_FREE_RUN = 0, @@ -202,6 +209,8 @@ enum imx296_op_mode { * independent of the sensor mutex. */ static atomic_t imx296_fast_trigger_mode = ATOMIC_INIT(0); +/* Monotonic XTRIG counter for N-1 phase correlation (user + dummy pulses). */ +static atomic64_t imx296_xtrig_seq = ATOMIC64_INIT(0); bool imx296_is_fast_trigger_active(void) { @@ -250,6 +259,8 @@ struct imx296 { enum imx296_op_mode pending_mode; u32 trigger_pulse_us; u64 trigger_pwm_base_period_ns; + /* Transient N-1 trace tag; set only under sensor->mutex by dummy path. */ + const char *pulse_trace_tag; struct gpio_desc *light_source_gpio; struct pinctrl_state *pins_active_high; @@ -533,6 +544,20 @@ static int imx296_trigger_once_locked(struct imx296 *sensor) if (!sensor->trigger_pwm) return -ENODEV; + /* + * Free-run / monitor ignores XTRIG. Emitting a pulse while free_run is + * active was observed to leave the FT entry path one frame out of phase + * (N-1) after CamOS switches to master_fast_trigger — mon-period external + * edges must not poke the PWM. + */ + if (sensor->streaming && + sensor->active_mode != IMX296_XTRIG_ONE_SHOT) { + dev_warn_ratelimited(sensor->dev, + "trigger pulse ignored: active mode is %s (need master_fast_trigger)\n", + imx296_op_mode_name(sensor->active_mode)); + return 0; + } + pulse_us = clamp_t(u32, sensor->trigger_pulse_us, IMX296_TRIGGER_PULSE_US_MIN, IMX296_TRIGGER_PULSE_US_MAX); @@ -582,18 +607,23 @@ static int imx296_trigger_once_locked(struct imx296 *sensor) if (ret) goto out_light; - dev_info(sensor->dev, - "trigger pulse emitted: width=%u us idle=high active=low mode=%s streaming=%u\n", - pulse_us, - imx296_op_mode_name(sensor->streaming ? - sensor->active_mode : - sensor->pending_mode), - sensor->streaming); - - if (sensor->streaming && sensor->active_mode != IMX296_XTRIG_ONE_SHOT) - dev_warn(sensor->dev, - "trigger pulse was emitted while active mode is %s; switch run_mode to master_fast_trigger for one-shot capture\n", - imx296_op_mode_name(sensor->active_mode)); + { + u64 xseq = (u64)atomic64_inc_return(&imx296_xtrig_seq); + const char *tag = sensor->pulse_trace_tag ? + sensor->pulse_trace_tag : "user"; + const char *mode_name = imx296_op_mode_name( + sensor->streaming ? sensor->active_mode : + sensor->pending_mode); + + dev_info(sensor->dev, + "trigger pulse emitted: width=%u us idle=high active=low mode=%s streaming=%u\n", + pulse_us, mode_name, sensor->streaming); + /* n1trace: correlate with ISP mi_fe / early-done lines */ + dev_info(sensor->dev, + "n1trace pulse seq=%llu width=%u mode=%s streaming=%u tag=%s t_ns=%llu\n", + xseq, pulse_us, mode_name, sensor->streaming, tag, + ktime_get_ns()); + } out_light: if (sensor->light_source_enabled && sensor->light_source_gpio) { @@ -614,10 +644,19 @@ static int imx296_trigger_once_locked(struct imx296 *sensor) /* * Emit one XTRIG without light-source side effects so an open fast-trigger * frame can finish readout. Caller must hold sensor->mutex. + * + * Enter-FT / ROI-boundary dummies temporarily force IMX296_DUMMY_PULSE_US so + * drain is not stretched by a leftover multi-10ms user exposure width. + * leave/stream-off keep the current pulse_us (may be long) and always wait + * IMX296_XTRIG_CLOSE_FRAME_WAIT_US for the open frame to close. */ static int imx296_dummy_trigger_locked(struct imx296 *sensor, const char *reason) { bool saved_light_en; + bool short_pulse; + u32 saved_pulse_us = 0; + u32 wait_us = IMX296_XTRIG_CLOSE_FRAME_WAIT_US; + u32 done_pulse_us; int ret; if (!sensor->trigger_pwm) { @@ -635,9 +674,25 @@ static int imx296_dummy_trigger_locked(struct imx296 *sensor, const char *reason return 0; } + /* Short pulse only for enter-FT / ROI drain — not a latency shortcut. */ + short_pulse = reason && + (!strcmp(reason, "enter-fast-trigger") || + !strcmp(reason, "enter-fast-trigger-2") || + !strcmp(reason, "enter-fast-trigger-3") || + !strcmp(reason, "roi-boundary")); + saved_light_en = sensor->light_source_enabled; sensor->light_source_enabled = false; + if (short_pulse) { + saved_pulse_us = sensor->trigger_pulse_us; + sensor->trigger_pulse_us = IMX296_DUMMY_PULSE_US; + } + sensor->pulse_trace_tag = reason; ret = imx296_trigger_once_locked(sensor); + sensor->pulse_trace_tag = NULL; + done_pulse_us = short_pulse ? IMX296_DUMMY_PULSE_US : sensor->trigger_pulse_us; + if (short_pulse) + sensor->trigger_pulse_us = saved_pulse_us; sensor->light_source_enabled = saved_light_en; if (ret) { dev_warn(sensor->dev, @@ -645,9 +700,10 @@ static int imx296_dummy_trigger_locked(struct imx296 *sensor, const char *reason return ret; } - usleep_range(IMX296_XTRIG_CLOSE_FRAME_WAIT_US, - IMX296_XTRIG_CLOSE_FRAME_WAIT_US + 5000U); - dev_dbg(sensor->dev, "dummy trigger done reason=%s\n", reason); + usleep_range(wait_us, wait_us + 5000U); + dev_info(sensor->dev, + "n1trace dummy done reason=%s wait_us=%u pulse_us=%u t_ns=%llu\n", + reason, wait_us, done_pulse_us, ktime_get_ns()); return 0; } @@ -1635,6 +1691,14 @@ static int imx296_mode_switch(struct imx296 *sensor, old_mode = sensor->active_mode; + dev_info(sensor->dev, + "n1trace mode_switch begin %s->%s streaming=%u xtrig_seq=%llu t_ns=%llu\n", + imx296_op_mode_name(old_mode), + imx296_op_mode_name(new_mode), + sensor->streaming, + (u64)atomic64_read(&imx296_xtrig_seq), + ktime_get_ns()); + /* * Fast-trigger leaves the MIPI frame open until the next XTRIG. Close it * before STANDBY so CIF/ISP do not keep a half-written buffer that later @@ -1686,10 +1750,20 @@ static int imx296_mode_switch(struct imx296 *sensor, if (old_mode != IMX296_XTRIG_ONE_SHOT && new_mode == IMX296_XTRIG_ONE_SHOT) { imx296_dummy_trigger_locked(sensor, "enter-fast-trigger"); - /* Second pulse: first dummy often only closes the free-run tail. */ + /* Extra pulses: first dummy often only closes the free-run tail. */ imx296_dummy_trigger_locked(sensor, "enter-fast-trigger-2"); + imx296_dummy_trigger_locked(sensor, "enter-fast-trigger-3"); } + dev_info(sensor->dev, + "n1trace mode_switch end %s->%s active=%s streaming=%u ret=%d xtrig_seq=%llu t_ns=%llu\n", + imx296_op_mode_name(old_mode), + imx296_op_mode_name(new_mode), + imx296_op_mode_name(sensor->active_mode), + sensor->streaming, ret, + (u64)atomic64_read(&imx296_xtrig_seq), + ktime_get_ns()); + out_pm: if (ret && !sensor->streaming) { /* Roll the mode bridge back so ISP does not keep FT early-done. */ @@ -1882,6 +1956,19 @@ static int imx296_s_stream(struct v4l2_subdev *sd, int enable) } } + /* + * CamOS run_mode boundary is STREAMOFF → write pending FT → STREAMON. + * imx296_mode_switch() early-returns while !streaming, so its enter-FT + * dummies never run on that path. Without these dummies the first + * userspace XTRIG only closes a half-open frame and phase locks at N-1. + * Always drain three short light-free pulses after any FT stream-on. + */ + if (sensor->active_mode == IMX296_XTRIG_ONE_SHOT) { + imx296_dummy_trigger_locked(sensor, "enter-fast-trigger"); + imx296_dummy_trigger_locked(sensor, "enter-fast-trigger-2"); + imx296_dummy_trigger_locked(sensor, "enter-fast-trigger-3"); + } + if (sensor->active_mode == IMX296_FREE_RUN && sensor->light_source_enabled) imx296_light_source_set_locked(sensor, true); diff --git a/drivers/media/platform/rockchip/isp/capture.c b/drivers/media/platform/rockchip/isp/capture.c index 908b20d477f2b..e6f168b7cec48 100644 --- a/drivers/media/platform/rockchip/isp/capture.c +++ b/drivers/media/platform/rockchip/isp/capture.c @@ -617,6 +617,29 @@ static u64 rkisp_early_done_load_deadline(struct rkisp_capture_device *cap_dev) return ((u64)hi << 32) | lo; } +/* + * Live MI byte progress. On V39 FT the classic OFFS_CNT_SHD often sticks at 0 + * for the whole open frame (logs: y=0/need base=0 saw=1 → permanent + * ceiling-miss → MI FE only on next XTRIG → N-1 phase). Sample both the SHD + * and the START (live) views and take the larger — whichever register is + * actually ticking on this silicon wins. + */ +static u32 rkisp_early_done_read_y_offs(struct rkisp_device *dev) +{ + u32 shd = rkisp_read(dev, CIF_MI_MP_Y_OFFS_CNT_SHD, true); + u32 start = rkisp_read(dev, CIF_MI_MP_Y_OFFS_CNT_START, true); + + return max(shd, start); +} + +static u32 rkisp_early_done_read_cb_offs(struct rkisp_device *dev) +{ + u32 shd = rkisp_read(dev, CIF_MI_MP_CB_OFFS_CNT_SHD, true); + u32 start = rkisp_read(dev, CIF_MI_MP_CB_OFFS_CNT_START, true); + + return max(shd, start); +} + static void rkisp_early_done_cache_mi_sizes(struct rkisp_device *dev) { struct rkisp_capture_device *cap_dev = &dev->cap_dev; @@ -641,37 +664,51 @@ static void rkisp_early_done_cache_mi_sizes(struct rkisp_device *dev) WRITE_ONCE(cap_dev->early_done_mi_y_size, y_size); WRITE_ONCE(cap_dev->early_done_mi_cb_size, cb_size); + /* Baseline at arm: detect later decrease/reset as live activity. */ + WRITE_ONCE(cap_dev->early_done_mi_y_base, + rkisp_early_done_read_y_offs(dev)); + WRITE_ONCE(cap_dev->early_done_mi_cb_base, + rkisp_early_done_read_cb_offs(dev)); WRITE_ONCE(cap_dev->early_done_mi_saw_y, 0); WRITE_ONCE(cap_dev->early_done_mi_saw_cb, 0); WRITE_ONCE(cap_dev->early_done_mi_ready, 0); } /* - * MI SHD offs often still hold the previous buffer's final counts across an - * FT SOF. Require a live ramp (offs < need) before trusting full (== need). + * MI offs often still hold the previous buffer's final counts across an FT + * SOF, or stay stuck at 0 on SHD while START advances (V39). Require evidence + * of a live frame before trusting full (== need): + * - offs below programmed size (classic ramp), or + * - offs decreased vs the value sampled at arm (counter finally rolled), or + * - offs is zero (counter reset at SOF / new buffer). + * Without this, residual offs==need makes saw_* stuck at 0; with SHD stuck at + * 0, every FT frame ceiling-misses and forces completion onto the next XTRIG + * (N-1 phase). */ static bool rkisp_early_done_mi_finished(struct rkisp_device *dev) { struct rkisp_capture_device *cap_dev = &dev->cap_dev; u32 y_need = READ_ONCE(cap_dev->early_done_mi_y_size); u32 cb_need = READ_ONCE(cap_dev->early_done_mi_cb_size); - u32 y_offs, cb_offs; + u32 y_base = READ_ONCE(cap_dev->early_done_mi_y_base); + u32 cb_base = READ_ONCE(cap_dev->early_done_mi_cb_base); + u32 y_offs = 0, cb_offs = 0; bool y_full = true, cb_full = true; if (!y_need && !cb_need) return false; if (y_need) { - y_offs = rkisp_read(dev, CIF_MI_MP_Y_OFFS_CNT_SHD, true); - if (y_offs < y_need) + y_offs = rkisp_early_done_read_y_offs(dev); + if (y_offs < y_need || y_offs < y_base || y_offs == 0) WRITE_ONCE(cap_dev->early_done_mi_saw_y, 1); y_full = READ_ONCE(cap_dev->early_done_mi_saw_y) && (y_offs >= y_need); } if (cb_need) { - cb_offs = rkisp_read(dev, CIF_MI_MP_CB_OFFS_CNT_SHD, true); - if (cb_offs < cb_need) + cb_offs = rkisp_early_done_read_cb_offs(dev); + if (cb_offs < cb_need || cb_offs < cb_base || cb_offs == 0) WRITE_ONCE(cap_dev->early_done_mi_saw_cb, 1); cb_full = READ_ONCE(cap_dev->early_done_mi_saw_cb) && (cb_offs >= cb_need); @@ -680,15 +717,49 @@ static bool rkisp_early_done_mi_finished(struct rkisp_device *dev) return y_full && cb_full; } -static u32 rkisp_early_done_post_hold_us(struct rkisp_capture_device *cap_dev) +/* Snapshot current offs for ceiling-miss diagnostics (IRQ/timer context). */ +static void rkisp_early_done_sample_offs(struct rkisp_device *dev, + u32 *y_offs, u32 *cb_offs) +{ + *y_offs = rkisp_early_done_read_y_offs(dev); + *cb_offs = rkisp_early_done_read_cb_offs(dev); +} + +static u32 rkisp_early_done_post_hold_us(struct rkisp_capture_device *cap_dev, + bool force_path) { u32 left = READ_ONCE(cap_dev->early_done_warmup_left); u32 warm = READ_ONCE(cap_dev->early_done_warmup_post_mi_us); u32 steady = READ_ONCE(cap_dev->early_done_post_mi_us); + u32 force = READ_ONCE(cap_dev->early_done_force_post_mi_us); + u32 residual = READ_ONCE(cap_dev->early_done_delay_us); + u32 hold; + /* Warmup after free-run/ROI→FT is always the most conservative. */ if (left && warm) - return warm; - return steady; + hold = warm; + else + hold = steady; + + /* + * Ceiling-force means MI offs never verified. Board evidence + * (1456x592 green bot~8): residual+16ms still UV-incomplete. + * Floor at force_post, and add a slice of residual again so short + * ROI cannot under-hold after wait_line. + */ + if (force_path) { + u32 extra = residual / 2; + + if (force > hold) + hold = force; + if (extra) { + if (hold < U32_MAX - extra) + hold += extra; + else + hold = U32_MAX; + } + } + return hold; } /* @@ -702,8 +773,8 @@ static u32 rkisp_early_done_post_hold_us(struct rkisp_capture_device *cap_dev) * Even after MI reports full, last NV12 chroma may still be in flight → hold * post_mi_us (longer for the first few FT frames after mode/ROI switch). * - * On residual ceiling without verified MI: do NOT force vb2_done; leave the - * buffer to the real MI FE IRQ. + * On residual ceiling without verified MI: ceiling-force + force_post hold + * (N-1 phase). Still never complete at bare wait_line without that hold. */ static enum hrtimer_restart rkisp_early_done_timer(struct hrtimer *timer) { @@ -728,7 +799,7 @@ static enum hrtimer_restart rkisp_early_done_timer(struct hrtimer *timer) if (!READ_ONCE(cap_dev->early_done_mi_ready)) { if (rkisp_early_done_mi_finished(dev)) { WRITE_ONCE(cap_dev->early_done_mi_ready, 1); - post_us = rkisp_early_done_post_hold_us(cap_dev); + post_us = rkisp_early_done_post_hold_us(cap_dev, false); if (post_us) { hrtimer_forward_now(timer, ns_to_ktime((u64)post_us * NSEC_PER_USEC)); @@ -743,13 +814,51 @@ static enum hrtimer_restart rkisp_early_done_timer(struct hrtimer *timer) return HRTIMER_RESTART; } else { /* - * Ceiling hit without verified MI. Leave buffer to MI FE; - * forcing done here is the green-band failure mode. + * Ceiling without verified MI. On V39+IMX296 FT the + * MI OFFS_CNT_* views often stay 0 for the whole open + * frame (see y_shd/y_st logs), so the live-ramp check + * can never succeed. Abandoning to MI FE then waits + * for the *next* XTRIG to close the MIPI frame → + * published content is always one shot behind (N-1). + * + * Residual ceiling already covers remaining lines + * after wait_line; fall through the post-MI hold so + * last NV12 chroma can land, then complete. This is + * safer than green-band force-at-wait_line and fixes + * the N-1 phase lock. */ - WRITE_ONCE(cap_dev->early_done_armed_sof_ns, 0); - WRITE_ONCE(cap_dev->early_done_mi_ready, 0); - atomic_set(&cap_dev->early_done_pending, 0); - return HRTIMER_NORESTART; + WRITE_ONCE(cap_dev->early_done_diag_ceiling, + READ_ONCE(cap_dev->early_done_diag_ceiling) + 1); + { + u32 yo = 0, co = 0; + u32 y_shd, y_st, cb_shd, cb_st; + + rkisp_early_done_sample_offs(dev, &yo, &co); + y_shd = rkisp_read(dev, CIF_MI_MP_Y_OFFS_CNT_SHD, true); + y_st = rkisp_read(dev, CIF_MI_MP_Y_OFFS_CNT_START, true); + cb_shd = rkisp_read(dev, CIF_MI_MP_CB_OFFS_CNT_SHD, true); + cb_st = rkisp_read(dev, CIF_MI_MP_CB_OFFS_CNT_START, true); + v4l2_info(&dev->v4l2_dev, + "n1trace early ceiling-force sof=%llu cnt=%u y=%u/%u base=%u saw=%u cb=%u/%u base=%u saw=%u y_shd=%u y_st=%u cb_shd=%u cb_st=%u t_ns=%llu\n", + armed_sof, + READ_ONCE(cap_dev->early_done_diag_ceiling), + yo, READ_ONCE(cap_dev->early_done_mi_y_size), + READ_ONCE(cap_dev->early_done_mi_y_base), + READ_ONCE(cap_dev->early_done_mi_saw_y), + co, READ_ONCE(cap_dev->early_done_mi_cb_size), + READ_ONCE(cap_dev->early_done_mi_cb_base), + READ_ONCE(cap_dev->early_done_mi_saw_cb), + y_shd, y_st, cb_shd, cb_st, + ktime_get_ns()); + } + WRITE_ONCE(cap_dev->early_done_mi_ready, 1); + post_us = rkisp_early_done_post_hold_us(cap_dev, true); + if (post_us) { + hrtimer_forward_now(timer, + ns_to_ktime((u64)post_us * NSEC_PER_USEC)); + return HRTIMER_RESTART; + } + /* fall through to complete */ } } @@ -758,6 +867,15 @@ static enum hrtimer_restart rkisp_early_done_timer(struct hrtimer *timer) WRITE_ONCE(cap_dev->early_done_warmup_left, READ_ONCE(cap_dev->early_done_warmup_left) - 1); + WRITE_ONCE(cap_dev->early_done_diag_complete, + READ_ONCE(cap_dev->early_done_diag_complete) + 1); + v4l2_info(&dev->v4l2_dev, + "n1trace early complete sof=%llu warm_left=%u cnt=%u t_ns=%llu\n", + armed_sof, + READ_ONCE(cap_dev->early_done_warmup_left), + READ_ONCE(cap_dev->early_done_diag_complete), + ktime_get_ns()); + rkisp_stream_buf_done_early(dev); WRITE_ONCE(cap_dev->early_done_armed_sof_ns, 0); WRITE_ONCE(cap_dev->early_done_mi_ready, 0); @@ -781,6 +899,13 @@ void rkisp_stream_buf_done_early_drain(struct rkisp_device *dev, skip = READ_ONCE(cap_dev->early_done_skip_frames); if (skip) { WRITE_ONCE(cap_dev->early_done_skip_frames, skip - 1); + WRITE_ONCE(cap_dev->early_done_diag_skip_sof, + READ_ONCE(cap_dev->early_done_diag_skip_sof) + 1); + v4l2_info(&dev->v4l2_dev, + "n1trace early skip-sof left=%u cnt=%u sof=%llu t_ns=%llu\n", + skip - 1, + READ_ONCE(cap_dev->early_done_diag_skip_sof), + sof_ns, ktime_get_ns()); return; } @@ -789,6 +914,14 @@ void rkisp_stream_buf_done_early_drain(struct rkisp_device *dev, rkisp_early_done_cache_mi_sizes(dev); WRITE_ONCE(cap_dev->early_done_armed_sof_ns, sof_ns); + WRITE_ONCE(cap_dev->early_done_diag_arm, + READ_ONCE(cap_dev->early_done_diag_arm) + 1); + v4l2_info(&dev->v4l2_dev, + "n1trace early arm sof=%llu ceiling_us=%u cnt=%u t_ns=%llu\n", + sof_ns, + ceiling_us ? ceiling_us : READ_ONCE(cap_dev->early_done_delay_us), + READ_ONCE(cap_dev->early_done_diag_arm), + ktime_get_ns()); if (!ceiling_us) ceiling_us = READ_ONCE(cap_dev->early_done_delay_us); diff --git a/drivers/media/platform/rockchip/isp/capture.h b/drivers/media/platform/rockchip/isp/capture.h index bcdad462eeeaf..92ac2ab15e27b 100644 --- a/drivers/media/platform/rockchip/isp/capture.h +++ b/drivers/media/platform/rockchip/isp/capture.h @@ -342,10 +342,9 @@ struct rkisp_capture_device { * offsets have ramped live and reached the programmed plane sizes, then * hold post_mi_us so the last NV12 chroma beats land. ISP_OUT_LINE * saturates ~8 lines early and must not drive completion. - * early_done_delay_us is only the residual safety ceiling (no force - * vb2_done without verified MI). First early_done_skip_frames SOFs leave - * completion to the real MI FE IRQ; stream->skip_frame may also drop - * the first FT buffers after free-run/ROI→trigger. + * residual ceiling may ceiling-force when MI OFFS stay 0 (N-1 fix) but + * still applies force_post_mi hold. First skip_frames SOFs use MI FE; + * drop_left discards dirty MP buffers after free-run/ROI→FT. */ u32 early_done_delay_us; struct hrtimer early_done_timer; @@ -358,12 +357,17 @@ struct rkisp_capture_device { u32 early_done_deadline_ns_hi; u32 early_done_mi_y_size; u32 early_done_mi_cb_size; - /* Observed live MI ramp (y/cb offs below programmed size) this arm. */ + /* SHD offs sampled when early-done was armed (live-decrease baseline). */ + u32 early_done_mi_y_base; + u32 early_done_mi_cb_base; + /* Observed live MI activity (ramp / decrease / zero) this arm. */ u32 early_done_mi_saw_y; u32 early_done_mi_saw_cb; /* Y+CB live+full; then wait post_mi_us before vb2_done. */ u32 early_done_mi_ready; u32 early_done_post_mi_us; + /* Floor hold after ceiling-force (unverified MI); >= post_mi_us. */ + u32 early_done_force_post_mi_us; u32 early_done_warmup_left; u32 early_done_warmup_post_mi_us; /* @@ -373,6 +377,13 @@ struct rkisp_capture_device { u32 early_done_skip_frames; /* Drop next N completed MP buffers (survives stream->skip_frame=0). */ u32 early_done_drop_left; + /* N-1 phase diagnostics (FT only): counts published / dropped MP frames. */ + u32 early_done_diag_pub; + u32 early_done_diag_drop; + u32 early_done_diag_skip_sof; + u32 early_done_diag_arm; + u32 early_done_diag_complete; + u32 early_done_diag_ceiling; u32 wrap_width; u32 wrap_line; bool is_done_early; diff --git a/drivers/media/platform/rockchip/isp/capture_v39.c b/drivers/media/platform/rockchip/isp/capture_v39.c index 657757e16ae19..5bee66bba1a3c 100644 --- a/drivers/media/platform/rockchip/isp/capture_v39.c +++ b/drivers/media/platform/rockchip/isp/capture_v39.c @@ -1086,12 +1086,26 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) struct vb2_buffer *vb2_buf = &buf->vb.vb2_buf; struct rkisp_stream *vir = &dev->cap_dev.stream[RKISP_STREAM_VIR]; u64 ns = 0; + bool ft_diag = dev->cap_dev.is_done_early && + stream->id == RKISP_STREAM_MP; if (stream->id == RKISP_STREAM_MP) { u32 drop = READ_ONCE(dev->cap_dev.early_done_drop_left); if (drop) { WRITE_ONCE(dev->cap_dev.early_done_drop_left, drop - 1); + if (ft_diag) { + WRITE_ONCE(dev->cap_dev.early_done_diag_drop, + READ_ONCE(dev->cap_dev.early_done_diag_drop) + 1); + v4l2_info(&dev->v4l2_dev, + "n1trace mi_drop state=%s left=%u cnt=%u early=%d sof=%llu t_ns=%llu\n", + state == FRAME_WORK ? "WORK" : + state == FRAME_IRQ ? "IRQ" : "OTH", + drop - 1, + READ_ONCE(dev->cap_dev.early_done_diag_drop), + stream->frame_early, sof_ns, + ktime_get_ns()); + } spin_lock_irqsave(&stream->vbq_lock, lock_flags); list_add_tail(&buf->queue, &stream->buf_queue); spin_unlock_irqrestore(&stream->vbq_lock, lock_flags); @@ -1099,6 +1113,13 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) } } if (dev->skip_frame || stream->skip_frame) { + if (ft_diag) + v4l2_info(&dev->v4l2_dev, + "n1trace mi_skip_frame state=%s skip=%u dev_skip=%u sof=%llu t_ns=%llu\n", + state == FRAME_WORK ? "WORK" : + state == FRAME_IRQ ? "IRQ" : "OTH", + stream->skip_frame, dev->skip_frame, + sof_ns, ktime_get_ns()); spin_lock_irqsave(&stream->vbq_lock, lock_flags); list_add_tail(&buf->queue, &stream->buf_queue); spin_unlock_irqrestore(&stream->vbq_lock, lock_flags); @@ -1152,6 +1173,18 @@ 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 (ft_diag) { + WRITE_ONCE(dev->cap_dev.early_done_diag_pub, + READ_ONCE(dev->cap_dev.early_done_diag_pub) + 1); + v4l2_info(&dev->v4l2_dev, + "n1trace mi_pub state=%s vb_seq=%u early=%d drop_left=%u pub=%u sof=%llu t_ns=%llu\n", + state == FRAME_WORK ? "WORK" : + state == FRAME_IRQ ? "IRQ" : "OTH", + seq, stream->frame_early, + READ_ONCE(dev->cap_dev.early_done_drop_left), + READ_ONCE(dev->cap_dev.early_done_diag_pub), + sof_ns, ns); + } 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); diff --git a/drivers/media/platform/rockchip/isp/rkisp.c b/drivers/media/platform/rockchip/isp/rkisp.c index 105e7eae260a9..2155f21961492 100644 --- a/drivers/media/platform/rockchip/isp/rkisp.c +++ b/drivers/media/platform/rockchip/isp/rkisp.c @@ -64,23 +64,35 @@ #define IMX296_XTRIG_ONE_SHOT 1 #define ISP39_OUT_LINE_COUNTER_TAIL 8 /* - * Residual / MI-drain ceiling after wait_line. Completion is driven by MI - * plane progress + post-MI hold, not by this delay alone. Keep a generous - * ceiling so a slow first FT frame is not cut short; never force vb2_done - * on expiry without verified MI (see capture.c). + * Residual / MI-drain ceiling after wait_line. Prefer live MI offs; when + * OFFS stays 0 on this V39 silicon, capture.c ceiling-forces after this + * residual and still applies force_post_mi hold so N-1 stays aligned + * without publishing at bare wait_line. ROI stream-on re-seeds skip/drop. */ -#define ISP39_IMX296_TAIL_WAIT_US_DEFAULT 15000U -#define ISP39_IMX296_TAIL_WAIT_US_MAX 40000U +#define ISP39_IMX296_TAIL_WAIT_US_DEFAULT 35000U +#define ISP39_IMX296_TAIL_WAIT_US_MAX 100000U #define ISP39_IMX296_LINE_US_MIN 80U #define ISP39_IMX296_LINE_US_MAX 250U #define ISP39_IMX296_EARLY_DONE_POLL_US 50U -#define ISP39_IMX296_POST_MI_US 6000U -#define ISP39_IMX296_WARMUP_POST_MI_US 16000U -#define ISP39_IMX296_WARMUP_FRAMES 4U +#define ISP39_IMX296_POST_MI_US 20000U +/* + * Ceiling-force path floor: MI OFFS stay 0 so we never see live full. + * Green is held by residual + force_post, NOT by dropping user shots. + * Product 3–5 fps: CamOS already warms 6 frames before opening the + * external gate; kernel must not burn another ~10 user triggers. + */ +#define ISP39_IMX296_FORCE_POST_MI_US 45000U +/* First FT frames after free-run/ROI stream-on (longer hold only). */ +#define ISP39_IMX296_WARMUP_POST_MI_US 55000U +#define ISP39_IMX296_WARMUP_FRAMES 6U /* SOFs that skip early-done (MI FE only) after free-run/ROI->FT. */ -#define ISP39_IMX296_SKIP_EARLY_FRAMES 3U -/* Drop first completed MP buffers after FT start (invalid / half-open). */ -#define ISP39_IMX296_SKIP_BUF_FRAMES 3U +#define ISP39_IMX296_SKIP_EARLY_FRAMES 2U +/* + * Drop only the first completed MP buffers after FT stream-on (half-open). + * Keep tiny: CamOS residual_discard/warmup covers mode-switch quarantine. + * Large drop was the "trigger but no photo" for the first 10–12 shots. + */ +#define ISP39_IMX296_SKIP_BUF_FRAMES 2U static unsigned int rkisp_imx296_tail_wait_us = ISP39_IMX296_TAIL_WAIT_US_DEFAULT; module_param_named(imx296_tail_wait_us, rkisp_imx296_tail_wait_us, uint, 0644); @@ -107,10 +119,20 @@ static u32 rkisp_imx296_residual_ceiling_us(u32 width, u32 height, if (line_us > ISP39_IMX296_LINE_US_MAX) line_us = ISP39_IMX296_LINE_US_MAX; - /* Safety ceiling only — completion prefers MI offs. */ - tail_us = (remain + ISP39_OUT_LINE_COUNTER_TAIL * 2) * line_us + 500; - if (tail_us < 10000) - tail_us = 10000; + /* + * Residual after wait_line must cover remaining luma + chroma tail. + * NV12 chroma trails the last Y rows — budget extra lines so + * ceiling-force is not early on short ROI heights (green/tear). + */ + /* + * Budget remaining ISP lines after wait_line PLUS a large chroma + * tail. Observed green band is ~OUT_LINE tail (8–10 rows) of bad UV + * when force completes too early — use ~half-frame chroma time. + */ + tail_us = (remain + ISP39_OUT_LINE_COUNTER_TAIL * 6) * line_us + + (height / 2) * (line_us / 2) + 5000; + if (tail_us < 30000) + tail_us = 30000; param = rkisp_imx296_tail_wait_us; if (param < tail_us) @@ -2462,10 +2484,17 @@ static int rkisp_isp_start(struct rkisp_device *dev) dev->cap_dev.early_done_poll_us = ISP39_IMX296_EARLY_DONE_POLL_US; dev->cap_dev.early_done_terminal_line = 0; dev->cap_dev.early_done_post_mi_us = 0; + dev->cap_dev.early_done_force_post_mi_us = 0; dev->cap_dev.early_done_warmup_post_mi_us = 0; dev->cap_dev.early_done_warmup_left = 0; dev->cap_dev.early_done_skip_frames = 0; dev->cap_dev.early_done_drop_left = 0; + dev->cap_dev.early_done_diag_pub = 0; + dev->cap_dev.early_done_diag_drop = 0; + dev->cap_dev.early_done_diag_skip_sof = 0; + dev->cap_dev.early_done_diag_arm = 0; + dev->cap_dev.early_done_diag_complete = 0; + dev->cap_dev.early_done_diag_ceiling = 0; rkisp_stream_cancel_early_done(dev); dev->cap_dev.wait_line = rkisp_wait_line; if (dev->isp_ver == ISP_V39 && height > 1 && @@ -2481,6 +2510,8 @@ static int rkisp_isp_start(struct rkisp_device *dev) height, dev->cap_dev.wait_line); dev->cap_dev.early_done_post_mi_us = ISP39_IMX296_POST_MI_US; + dev->cap_dev.early_done_force_post_mi_us = + ISP39_IMX296_FORCE_POST_MI_US; dev->cap_dev.early_done_warmup_post_mi_us = ISP39_IMX296_WARMUP_POST_MI_US; dev->cap_dev.early_done_warmup_left = ISP39_IMX296_WARMUP_FRAMES; @@ -2495,16 +2526,26 @@ static int rkisp_isp_start(struct rkisp_device *dev) if (mp->streaming) mp->skip_frame = ISP39_IMX296_SKIP_BUF_FRAMES; v4l2_info(&dev->v4l2_dev, - "IMX296 fast trigger: early buffer done at line %u/%u terminal %u, MI-drain ceiling %u us post-MI %u/%u us (warmup %u) skip %u drop %u poll %u us\n", + "IMX296 fast trigger: early buffer done at line %u/%u terminal %u, MI-drain ceiling %u us post-MI %u/%u force %u us (warmup %u) skip %u drop %u poll %u us\n", dev->cap_dev.wait_line, height, dev->cap_dev.early_done_terminal_line, dev->cap_dev.early_done_delay_us, dev->cap_dev.early_done_post_mi_us, dev->cap_dev.early_done_warmup_post_mi_us, + dev->cap_dev.early_done_force_post_mi_us, dev->cap_dev.early_done_warmup_left, dev->cap_dev.early_done_skip_frames, ISP39_IMX296_SKIP_BUF_FRAMES, dev->cap_dev.early_done_poll_us); + v4l2_info(&dev->v4l2_dev, + "n1trace isp_ft_arm height=%u wait_line=%u skip=%u drop=%u warmup=%u ceiling_us=%u force_post=%u t_ns=%llu\n", + height, dev->cap_dev.wait_line, + dev->cap_dev.early_done_skip_frames, + ISP39_IMX296_SKIP_BUF_FRAMES, + dev->cap_dev.early_done_warmup_left, + dev->cap_dev.early_done_delay_us, + dev->cap_dev.early_done_force_post_mi_us, + ktime_get_ns()); } dev->cap_dev.is_done_early = false; diff --git a/scripts/dlcvcam_verify_bootimg.py b/scripts/dlcvcam_verify_bootimg.py new file mode 100755 index 0000000000000..5903472a72988 --- /dev/null +++ b/scripts/dlcvcam_verify_bootimg.py @@ -0,0 +1,556 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +DLCVCAM boot.img (FIT) integrity checker — hash only, no signature. + +Verifies: + 1) Optional whole-file SHA-256 against .sha256 or --expect-sha256 + 2) FIT/FDT structure parse + 3) Embedded per-image hash nodes (fdt / kernel / resource, ...) + +Exit codes: + 0 OK + 1 verification failed + 2 usage / I/O / parse error +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import struct +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + + +FDT_MAGIC = 0xD00DFEED +FDT_BEGIN_NODE = 1 +FDT_END_NODE = 2 +FDT_PROP = 3 +FDT_NOP = 4 +FDT_END = 9 + +SUPPORTED_HASH = { + "sha1": (hashlib.sha1, 20), + "sha256": (hashlib.sha256, 32), + "sha512": (hashlib.sha512, 64), +} + + +class VerifyError(Exception): + pass + + +def eprint(*args: object) -> None: + print(*args, file=sys.stderr) + + +def sha256_file(path: Path, chunk: int = 1024 * 1024) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + while True: + b = f.read(chunk) + if not b: + break + h.update(b) + return h.hexdigest() + + +def parse_sha256_sidecar(text: str) -> str: + """ + Accept: + + + * + """ + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + part = line.split()[0].strip().lower() + if len(part) == 64 and all(c in "0123456789abcdef" for c in part): + return part + raise VerifyError("sidecar 中未找到合法 sha256") + + +def align4(n: int) -> int: + return (n + 3) & ~3 + + +def parse_fdt_tree(data: bytes, fdt_offset: int = 0) -> Dict[str, Dict[str, bytes]]: + if len(data) < fdt_offset + 40: + raise VerifyError("文件过短,无法读取 FDT 头") + + magic, totalsize, off_dt_struct, off_dt_strings, _off_mem, version, _last, _cpu, size_dt_strings, size_dt_struct = struct.unpack_from( + ">10I", data, fdt_offset + ) + if magic != FDT_MAGIC: + raise VerifyError(f"非 FIT/FDT 魔数: 0x{magic:08x} (期望 0xd00dfeed)") + if version < 16: + raise VerifyError(f"不支持的 FDT version: {version}") + if totalsize < 40 or fdt_offset + totalsize > len(data): + # external-data FIT: totalsize 只覆盖 header tree,payload 在后面 —— 允许 totalsize 小于文件 + if totalsize < 40: + raise VerifyError(f"非法 FDT totalsize: {totalsize}") + + struct_base = fdt_offset + off_dt_struct + strings_base = fdt_offset + off_dt_strings + struct_end = struct_base + size_dt_struct + strings_end = strings_base + size_dt_strings + if struct_end > len(data) or strings_end > len(data): + raise VerifyError("FDT struct/strings 超出文件范围") + + def get_string(nameoff: int) -> str: + start = strings_base + nameoff + if start >= strings_end: + raise VerifyError("FDT 字符串偏移越界") + end = data.find(b"\x00", start, strings_end) + if end < 0: + raise VerifyError("FDT 字符串缺少 NUL") + return data[start:end].decode("ascii", "replace") + + pos = struct_base + depth = 0 + stack: List[str] = [] + props: Dict[str, Dict[str, bytes]] = {} + + while pos + 4 <= struct_end: + tag = struct.unpack_from(">I", data, pos)[0] + pos += 4 + if tag == FDT_BEGIN_NODE: + end = data.find(b"\x00", pos, struct_end) + if end < 0: + raise VerifyError("FDT 节点名缺少 NUL") + name = data[pos:end].decode("ascii", "replace") + pos = align4(end + 1) + depth += 1 + if depth == 1: + full = "/" + else: + parent = stack[-1] + full = f"{parent}{name}" if parent.endswith("/") else f"{parent}/{name}" + if parent == "/": + full = f"/{name}" + stack.append(full) + props.setdefault(full, {}) + elif tag == FDT_END_NODE: + if not stack: + raise VerifyError("FDT END_NODE 不匹配") + stack.pop() + depth -= 1 + elif tag == FDT_PROP: + if pos + 8 > struct_end: + raise VerifyError("FDT PROP 头截断") + plen, nameoff = struct.unpack_from(">II", data, pos) + pos += 8 + if pos + plen > struct_end: + raise VerifyError("FDT PROP 值截断") + pname = get_string(nameoff) + pval = data[pos : pos + plen] + pos = align4(pos + plen) + if not stack: + raise VerifyError("FDT PROP 无当前节点") + props[stack[-1]][pname] = pval + elif tag == FDT_NOP: + continue + elif tag == FDT_END: + break + else: + raise VerifyError(f"未知 FDT tag {tag} @ {pos - 4}") + + return props + + +def _cstr(val: bytes) -> str: + return val.split(b"\x00", 1)[0].decode("ascii", "replace") + + +def _u32(val: bytes) -> Optional[int]: + if len(val) != 4: + return None + return struct.unpack(">I", val)[0] + + +def collect_fit_images(props: Dict[str, Dict[str, bytes]]) -> List[Dict[str, Any]]: + images: List[Dict[str, Any]] = [] + for path, p in props.items(): + # image nodes: /images/ (exactly one name segment under /images) + if not path.startswith("/images/"): + continue + rest = path[len("/images/") :] + if not rest or "/" in rest: + continue + name = rest + img: Dict[str, Any] = { + "name": name, + "path": path, + "type": _cstr(p["type"]) if "type" in p else "", + "compression": _cstr(p["compression"]) if "compression" in p else "", + "data_position": _u32(p["data-position"]) if "data-position" in p else None, + "data_offset": _u32(p["data-offset"]) if "data-offset" in p else None, + "data_size": _u32(p["data-size"]) if "data-size" in p else None, + "hashes": [], + } + # inline data property (rare for our external FIT) + if "data" in p: + img["inline_data_len"] = len(p["data"]) + img["inline_data"] = p["data"] + + # hash subnodes: /images//hash or hash@1 ... + prefix = path + "/" + for hpath, hp in props.items(): + if not hpath.startswith(prefix): + continue + sub = hpath[len(prefix) :] + if "/" in sub: + continue + if not (sub == "hash" or sub.startswith("hash@") or sub.startswith("hash-")): + continue + if "algo" not in hp or "value" not in hp: + continue + img["hashes"].append( + { + "node": sub, + "algo": _cstr(hp["algo"]).lower(), + "value": hp["value"], + } + ) + images.append(img) + images.sort(key=lambda x: x["name"]) + return images + + +def resolve_image_blob(data: bytes, img: Dict[str, Any], fdt_totalsize_hint: int) -> bytes: + if "inline_data" in img: + return img["inline_data"] + + size = img.get("data_size") + if size is None: + raise VerifyError(f"镜像 {img['name']}: 缺少 data-size/data") + + pos = img.get("data_position") + if pos is not None: + end = pos + size + if end > len(data): + raise VerifyError( + f"镜像 {img['name']}: data-position/size 越界 ({pos}+{size} > {len(data)})" + ) + return data[pos:end] + + off = img.get("data_offset") + if off is not None: + # external data: offset relative to end of FIT header (aligned external area) + # U-Boot -E external data: payload starts after FIT totalsize, often aligned. + # data-offset is relative to the external data base (right after FIT). + base = fdt_totalsize_hint + # mkimage -E -p 0x800: external data base is max(totalsize aligned, -p alignment) + # For our images, data-position is absolute; data-offset path kept for completeness. + pos2 = base + off + end = pos2 + size + if end > len(data): + raise VerifyError( + f"镜像 {img['name']}: data-offset/size 越界 ({pos2}+{size} > {len(data)})" + ) + return data[pos2:end] + + raise VerifyError(f"镜像 {img['name']}: 无 data / data-position / data-offset") + + +def verify_fit_hashes(data: bytes) -> Tuple[bool, List[str], List[Dict[str, Any]]]: + props = parse_fdt_tree(data, 0) + magic, totalsize = struct.unpack_from(">2I", data, 0) + assert magic == FDT_MAGIC + + images = collect_fit_images(props) + if not images: + raise VerifyError("FIT 中未找到 /images/* 节点") + + messages: List[str] = [] + details: List[Dict[str, Any]] = [] + all_ok = True + + for img in images: + entry: Dict[str, Any] = { + "name": img["name"], + "type": img["type"], + "data_size": img.get("data_size"), + "data_position": img.get("data_position"), + "hashes": [], + } + try: + blob = resolve_image_blob(data, img, totalsize) + except VerifyError as ex: + all_ok = False + messages.append(f"[FAIL] {img['name']}: {ex}") + entry["error"] = str(ex) + details.append(entry) + continue + + if not img["hashes"]: + all_ok = False + messages.append(f"[FAIL] {img['name']}: 无内嵌 hash 节点") + entry["error"] = "no hash node" + details.append(entry) + continue + + for h in img["hashes"]: + algo = h["algo"] + val = h["value"] + hinfo: Dict[str, Any] = { + "node": h["node"], + "algo": algo, + "expected": val.hex(), + } + if algo not in SUPPORTED_HASH: + all_ok = False + hinfo["ok"] = False + hinfo["error"] = f"不支持的 hash 算法: {algo}" + messages.append(f"[FAIL] {img['name']}/{h['node']}: 不支持算法 {algo}") + entry["hashes"].append(hinfo) + continue + factory, digest_len = SUPPORTED_HASH[algo] + if len(val) != digest_len: + # some FIT blobs store value with padding + if len(val) > digest_len and all(b == 0 for b in val[digest_len:]): + val = val[:digest_len] + hinfo["expected"] = val.hex() + else: + all_ok = False + hinfo["ok"] = False + hinfo["error"] = f"hash 长度 {len(val)} != {digest_len}" + messages.append( + f"[FAIL] {img['name']}/{h['node']}: hash 长度异常 {len(val)}" + ) + entry["hashes"].append(hinfo) + continue + actual = factory(blob).digest() + hinfo["actual"] = actual.hex() + ok = hmac_compare(actual, val) + hinfo["ok"] = ok + if ok: + messages.append( + f"[OK] {img['name']}/{h['node']} {algo} ({len(blob)} bytes)" + ) + else: + all_ok = False + messages.append( + f"[FAIL] {img['name']}/{h['node']} {algo} mismatch\n" + f" expect {val.hex()}\n" + f" actual {actual.hex()}" + ) + entry["hashes"].append(hinfo) + details.append(entry) + + return all_ok, messages, details + + +def hmac_compare(a: bytes, b: bytes) -> bool: + if len(a) != len(b): + return False + diff = 0 + for x, y in zip(a, b): + diff |= x ^ y + return diff == 0 + + +def load_expect_sha256(img: Path, explicit: Optional[str], sidecar: Optional[Path]) -> Optional[str]: + if explicit: + v = explicit.strip().lower() + if len(v) != 64 or any(c not in "0123456789abcdef" for c in v): + raise VerifyError("--expect-sha256 不是 64 位 hex") + return v + candidates: List[Path] = [] + if sidecar: + candidates.append(sidecar) + candidates.append(Path(str(img) + ".sha256")) + candidates.append(img.with_suffix(img.suffix + ".sha256")) + # also boot-xxx.img.sha256 already covered; try stem.sha256 + candidates.append(img.with_suffix(".sha256")) + for c in candidates: + if c.is_file(): + return parse_sha256_sidecar(c.read_text(encoding="utf-8", errors="replace")) + return None + + +def write_manifest(img: Path, file_sha: str, fit_details: List[Dict[str, Any]], meta: Dict[str, Any]) -> Path: + out = Path(str(img) + ".dlcvcam.json") + doc = { + "format": "dlcvcam-bootimg-manifest-v1", + "file": img.name, + "size": img.stat().st_size, + "sha256": file_sha, + "fit_images": [ + { + "name": d.get("name"), + "type": d.get("type"), + "data_size": d.get("data_size"), + "data_position": d.get("data_position"), + "hashes": [ + { + "node": h.get("node"), + "algo": h.get("algo"), + "sha": h.get("expected") or h.get("actual"), + } + for h in d.get("hashes", []) + ], + } + for d in fit_details + ], + "meta": meta, + } + out.write_text(json.dumps(doc, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + return out + + +def cmd_verify(args: argparse.Namespace) -> int: + img = Path(args.image) + if not img.is_file(): + eprint(f"文件不存在: {img}") + return 2 + + size = img.stat().st_size + if size <= 0: + eprint("[FAIL] 空文件") + return 1 + + data = img.read_bytes() + file_sha = hashlib.sha256(data).hexdigest() + print(f"file : {img}") + print(f"size : {size}") + print(f"sha256 : {file_sha}") + + failed = False + + # whole-file hash + try: + expect = load_expect_sha256(img, args.expect_sha256, Path(args.sha256_file) if args.sha256_file else None) + except VerifyError as ex: + eprint(f"[ERR] {ex}") + return 2 + + if expect is None: + if args.require_sidecar: + eprint("[FAIL] 未提供/未找到整包 .sha256(指定了 --require-sidecar)") + return 1 + print("whole-file: (no sidecar / --expect-sha256, skip)") + else: + if hmac_compare(bytes.fromhex(expect), bytes.fromhex(file_sha)): + print(f"[OK] whole-file sha256 matches") + else: + failed = True + print(f"[FAIL] whole-file sha256 mismatch") + print(f" expect {expect}") + print(f" actual {file_sha}") + + # FIT internal hashes + try: + ok, messages, details = verify_fit_hashes(data) + except VerifyError as ex: + eprint(f"[FAIL] FIT 解析/校验错误: {ex}") + return 1 + + print("fit-hash :") + for m in messages: + print(f" {m}") + if not ok: + failed = True + + if args.json_out: + Path(args.json_out).write_text( + json.dumps( + { + "file": str(img), + "size": size, + "sha256": file_sha, + "whole_file_ok": (expect is None) or (expect == file_sha), + "fit_ok": ok, + "fit_images": details, + }, + indent=2, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", + ) + + if failed: + print("RESULT : FAIL") + return 1 + print("RESULT : OK") + return 0 + + +def cmd_gen_sidecar(args: argparse.Namespace) -> int: + img = Path(args.image) + if not img.is_file(): + eprint(f"文件不存在: {img}") + return 2 + data = img.read_bytes() + file_sha = hashlib.sha256(data).hexdigest() + + sha_path = Path(args.sha256_file) if args.sha256_file else Path(str(img) + ".sha256") + sha_path.write_text(f"{file_sha} {img.name}\n", encoding="utf-8") + + fit_details: List[Dict[str, Any]] = [] + try: + _ok, _msgs, fit_details = verify_fit_hashes(data) + except VerifyError as ex: + eprint(f"警告: FIT 解析失败,仍写入整包 sha256: {ex}") + + meta = { + "build_version": args.build_version or "", + "git": args.git or "", + "kernelrelease": args.kernelrelease or "", + "generated_by": "scripts/dlcvcam_verify_bootimg.py", + } + man_path = write_manifest(img, file_sha, fit_details, meta) + + print(f"wrote {sha_path}") + print(f"wrote {man_path}") + print(f"sha256 {file_sha}") + return 0 + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description="DLCVCAM boot.img hash verifier (no signature)") + sub = p.add_subparsers(dest="cmd", required=True) + + v = sub.add_parser("verify", help="校验 boot.img(整包 sha256 + FIT 内嵌 hash)") + v.add_argument("image", help="boot.img / boot-rk3576-*.img 路径") + v.add_argument("--expect-sha256", help="期望的整包 sha256 hex") + v.add_argument("--sha256-file", help="整包 sha256 sidecar 路径") + v.add_argument("--require-sidecar", action="store_true", help="必须存在整包 sha256") + v.add_argument("--json-out", help="把详细结果写到 JSON") + v.set_defaults(func=cmd_verify) + + g = sub.add_parser("gen-sidecar", help="为 boot.img 生成 .sha256 与 .dlcvcam.json") + g.add_argument("image", help="boot.img 路径") + g.add_argument("--sha256-file", help="输出 .sha256 路径") + g.add_argument("--build-version", default="") + g.add_argument("--git", default="") + g.add_argument("--kernelrelease", default="") + g.set_defaults(func=cmd_gen_sidecar) + + return p + + +def main(argv: Optional[List[str]] = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + try: + return int(args.func(args)) + except BrokenPipeError: + return 0 + except VerifyError as ex: + eprint(f"[ERR] {ex}") + return 2 + except OSError as ex: + eprint(f"[ERR] {ex}") + return 2 + + +if __name__ == "__main__": + sys.exit(main()) From 12345700ef70a3ae01b2a78c576607b053a431ae Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Fri, 7 Aug 2026 17:47:14 +0800 Subject: [PATCH 15/21] =?UTF-8?q?build:=20=E5=B0=86=20DLCVCAM=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E5=8F=B7=E6=9B=B4=E6=96=B0=E4=B8=BA=202026080701?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对应 fix(imx296/isp) N-1/绿线合入后的 boot 镜像 #2026080701 (8950d5ff)。 --- DLCVCAM_BUILD_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DLCVCAM_BUILD_VERSION b/DLCVCAM_BUILD_VERSION index da2a87bded858..8f791578917ec 100644 --- a/DLCVCAM_BUILD_VERSION +++ b/DLCVCAM_BUILD_VERSION @@ -1 +1 @@ -2026080601 +2026080701 From 01726d6fa95e5c2962e8890841bab8e45d5c80e3 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Mon, 10 Aug 2026 14:24:50 +0800 Subject: [PATCH 16/21] =?UTF-8?q?fix(imx296/isp):=20=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=85=B3=E9=97=AD=20n1trace=EF=BC=8C=E9=81=BF=E5=85=8D=20FT=20?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=89=93=E6=BB=A1=20rootfs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 8950d5ff 引入的 per-frame n1trace/dev_info/v4l2_info 改为 module_param 门控(imx296.n1trace / video_rkisp.n1trace,默认 0),保留诊断可热开。 同步 bump DLCVCAM 构建号至 2026081001,并清理本地 worktree 忽略项。 --- .gitignore | 6 +++ DLCVCAM_BUILD_VERSION | 2 +- ...56\345\244\215\350\256\260\345\275\225.md" | 7 +++- drivers/media/i2c/imx296.c | 41 +++++++++++++------ drivers/media/platform/rockchip/isp/capture.c | 8 ++-- .../media/platform/rockchip/isp/capture_v39.c | 6 +-- drivers/media/platform/rockchip/isp/common.h | 8 ++++ drivers/media/platform/rockchip/isp/rkisp.c | 14 ++++++- 8 files changed, 69 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index be04ab3aaefaf..d0720779beff6 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,9 @@ extboot* *.img.sha256 *.img.dlcvcam.json # legacy; no longer generated /boot-rk3576-*.img + +# local worktree junk +kernel_backup/ +scripts/__pycache__/ +trigger +*.bak.* diff --git a/DLCVCAM_BUILD_VERSION b/DLCVCAM_BUILD_VERSION index 8f791578917ec..442dfff0d1584 100644 --- a/DLCVCAM_BUILD_VERSION +++ b/DLCVCAM_BUILD_VERSION @@ -1 +1 @@ -2026080701 +2026081001 diff --git "a/IMX296_N1\347\233\270\344\275\215\346\273\236\345\220\216_\350\257\212\346\226\255\344\270\216\344\277\256\345\244\215\350\256\260\345\275\225.md" "b/IMX296_N1\347\233\270\344\275\215\346\273\236\345\220\216_\350\257\212\346\226\255\344\270\216\344\277\256\345\244\215\350\256\260\345\275\225.md" index 3a81fd75c2ebb..4d2d8b1108376 100644 --- "a/IMX296_N1\347\233\270\344\275\215\346\273\236\345\220\216_\350\257\212\346\226\255\344\270\216\344\277\256\345\244\215\350\256\260\345\275\225.md" +++ "b/IMX296_N1\347\233\270\344\275\215\346\273\236\345\220\216_\350\257\212\346\226\255\344\270\216\344\277\256\345\244\215\350\256\260\345\275\225.md" @@ -168,7 +168,12 @@ T2 HI tag=1 mean=LO → 实为 T1 的 LO 1. **ceiling-force 与绿线**:强制 complete 依赖 residual ceiling + `POST_MI=8ms`(warmup 18ms)。本轮 HI bot48≈27–28 vs center≈25,未见典型底 8 行纯绿带;建议 endurance 再抽检 `gre=0`。 2. **OFFS 永久 0**:ceiling-force 是本硅上的务实解;若后续固件/寄存器视图恢复 live ramp,force 路径会自然减少(mi_finished 先成功)。 -3. **n1trace 日志量**:验证完成后可降为 `dev_dbg` / 采样打印,避免产线 dmesg 刷屏。 +3. **n1trace 日志量**:已默认关闭(`imx296.n1trace=0` / `video_rkisp.n1trace=0`)。诊断时: + ```bash + echo 1 > /sys/module/imx296/parameters/n1trace + echo 1 > /sys/module/video_rkisp/parameters/n1trace + ``` + 产线保持 0,避免 rsyslog 双写打满 rootfs。 4. **测试脚本**:产线/回归请用低对比阈值版本(span 可能仅 3–4);或提高 gain/补光再测。 5. **版本号**:当前板端仍标 `#2026080601`(内容已含本修复);正式合入前建议 bump DLCVCAM 构建号并清理 n1trace。 diff --git a/drivers/media/i2c/imx296.c b/drivers/media/i2c/imx296.c index e2c009e8b1f0c..2edabee194b1c 100644 --- a/drivers/media/i2c/imx296.c +++ b/drivers/media/i2c/imx296.c @@ -211,6 +211,21 @@ enum imx296_op_mode { static atomic_t imx296_fast_trigger_mode = ATOMIC_INIT(0); /* Monotonic XTRIG counter for N-1 phase correlation (user + dummy pulses). */ static atomic64_t imx296_xtrig_seq = ATOMIC64_INIT(0); +/* + * Per-frame FT diagnostics (pulse / dummy / mode_switch / stream-state). + * Default off: left on during N-1 bring-up these flooded rsyslog and filled the rootfs. + * Enable: echo 1 > /sys/module/imx296/parameters/n1trace + */ +static bool imx296_n1trace; +module_param_named(n1trace, imx296_n1trace, bool, 0644); +MODULE_PARM_DESC(n1trace, + "IMX296 FT n1trace/dev_info diagnostics (0=off default, 1=verbose)"); + +#define imx296_n1trace_info(dev, fmt, ...) \ + do { \ + if (imx296_n1trace) \ + dev_info((dev), fmt, ##__VA_ARGS__); \ + } while (0) bool imx296_is_fast_trigger_active(void) { @@ -615,14 +630,14 @@ static int imx296_trigger_once_locked(struct imx296 *sensor) sensor->streaming ? sensor->active_mode : sensor->pending_mode); - dev_info(sensor->dev, - "trigger pulse emitted: width=%u us idle=high active=low mode=%s streaming=%u\n", - pulse_us, mode_name, sensor->streaming); /* n1trace: correlate with ISP mi_fe / early-done lines */ - dev_info(sensor->dev, - "n1trace pulse seq=%llu width=%u mode=%s streaming=%u tag=%s t_ns=%llu\n", - xseq, pulse_us, mode_name, sensor->streaming, tag, - ktime_get_ns()); + imx296_n1trace_info(sensor->dev, + "trigger pulse emitted: width=%u us idle=high active=low mode=%s streaming=%u\n", + pulse_us, mode_name, sensor->streaming); + imx296_n1trace_info(sensor->dev, + "n1trace pulse seq=%llu width=%u mode=%s streaming=%u tag=%s t_ns=%llu\n", + xseq, pulse_us, mode_name, sensor->streaming, tag, + ktime_get_ns()); } out_light: @@ -701,9 +716,9 @@ static int imx296_dummy_trigger_locked(struct imx296 *sensor, const char *reason } usleep_range(wait_us, wait_us + 5000U); - dev_info(sensor->dev, - "n1trace dummy done reason=%s wait_us=%u pulse_us=%u t_ns=%llu\n", - reason, wait_us, done_pulse_us, ktime_get_ns()); + imx296_n1trace_info(sensor->dev, + "n1trace dummy done reason=%s wait_us=%u pulse_us=%u t_ns=%llu\n", + reason, wait_us, done_pulse_us, ktime_get_ns()); return 0; } @@ -987,7 +1002,7 @@ static void imx296_log_stream_state_locked(struct imx296 *sensor, const char *ta #undef IMX296_READBACK - dev_info(sensor->dev, + imx296_n1trace_info(sensor->dev, "stream-state[%s]: pending=%s active=%s test_pattern=%u ctrl00=0x%02x standby=%u ctrl08=0x%02x reghold=%u ctrl0a=0x%02x xmsta=%u ctrl0b=0x%02x trigen=%u syncsel=0x%02x lowlag=0x%02x pgctrl=0x%02x regen=%u clken=%u pg_mode=%u\n", tag, imx296_op_mode_name(sensor->pending_mode), @@ -1691,7 +1706,7 @@ static int imx296_mode_switch(struct imx296 *sensor, old_mode = sensor->active_mode; - dev_info(sensor->dev, + imx296_n1trace_info(sensor->dev, "n1trace mode_switch begin %s->%s streaming=%u xtrig_seq=%llu t_ns=%llu\n", imx296_op_mode_name(old_mode), imx296_op_mode_name(new_mode), @@ -1755,7 +1770,7 @@ static int imx296_mode_switch(struct imx296 *sensor, imx296_dummy_trigger_locked(sensor, "enter-fast-trigger-3"); } - dev_info(sensor->dev, + imx296_n1trace_info(sensor->dev, "n1trace mode_switch end %s->%s active=%s streaming=%u ret=%d xtrig_seq=%llu t_ns=%llu\n", imx296_op_mode_name(old_mode), imx296_op_mode_name(new_mode), diff --git a/drivers/media/platform/rockchip/isp/capture.c b/drivers/media/platform/rockchip/isp/capture.c index e6f168b7cec48..c53578d840da2 100644 --- a/drivers/media/platform/rockchip/isp/capture.c +++ b/drivers/media/platform/rockchip/isp/capture.c @@ -838,7 +838,7 @@ static enum hrtimer_restart rkisp_early_done_timer(struct hrtimer *timer) y_st = rkisp_read(dev, CIF_MI_MP_Y_OFFS_CNT_START, true); cb_shd = rkisp_read(dev, CIF_MI_MP_CB_OFFS_CNT_SHD, true); cb_st = rkisp_read(dev, CIF_MI_MP_CB_OFFS_CNT_START, true); - v4l2_info(&dev->v4l2_dev, + rkisp_n1trace_info(&dev->v4l2_dev, "n1trace early ceiling-force sof=%llu cnt=%u y=%u/%u base=%u saw=%u cb=%u/%u base=%u saw=%u y_shd=%u y_st=%u cb_shd=%u cb_st=%u t_ns=%llu\n", armed_sof, READ_ONCE(cap_dev->early_done_diag_ceiling), @@ -869,7 +869,7 @@ static enum hrtimer_restart rkisp_early_done_timer(struct hrtimer *timer) WRITE_ONCE(cap_dev->early_done_diag_complete, READ_ONCE(cap_dev->early_done_diag_complete) + 1); - v4l2_info(&dev->v4l2_dev, + rkisp_n1trace_info(&dev->v4l2_dev, "n1trace early complete sof=%llu warm_left=%u cnt=%u t_ns=%llu\n", armed_sof, READ_ONCE(cap_dev->early_done_warmup_left), @@ -901,7 +901,7 @@ void rkisp_stream_buf_done_early_drain(struct rkisp_device *dev, WRITE_ONCE(cap_dev->early_done_skip_frames, skip - 1); WRITE_ONCE(cap_dev->early_done_diag_skip_sof, READ_ONCE(cap_dev->early_done_diag_skip_sof) + 1); - v4l2_info(&dev->v4l2_dev, + rkisp_n1trace_info(&dev->v4l2_dev, "n1trace early skip-sof left=%u cnt=%u sof=%llu t_ns=%llu\n", skip - 1, READ_ONCE(cap_dev->early_done_diag_skip_sof), @@ -916,7 +916,7 @@ void rkisp_stream_buf_done_early_drain(struct rkisp_device *dev, WRITE_ONCE(cap_dev->early_done_armed_sof_ns, sof_ns); WRITE_ONCE(cap_dev->early_done_diag_arm, READ_ONCE(cap_dev->early_done_diag_arm) + 1); - v4l2_info(&dev->v4l2_dev, + rkisp_n1trace_info(&dev->v4l2_dev, "n1trace early arm sof=%llu ceiling_us=%u cnt=%u t_ns=%llu\n", sof_ns, ceiling_us ? ceiling_us : READ_ONCE(cap_dev->early_done_delay_us), diff --git a/drivers/media/platform/rockchip/isp/capture_v39.c b/drivers/media/platform/rockchip/isp/capture_v39.c index 5bee66bba1a3c..e022b385aaa2a 100644 --- a/drivers/media/platform/rockchip/isp/capture_v39.c +++ b/drivers/media/platform/rockchip/isp/capture_v39.c @@ -1097,7 +1097,7 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) if (ft_diag) { WRITE_ONCE(dev->cap_dev.early_done_diag_drop, READ_ONCE(dev->cap_dev.early_done_diag_drop) + 1); - v4l2_info(&dev->v4l2_dev, + rkisp_n1trace_info(&dev->v4l2_dev, "n1trace mi_drop state=%s left=%u cnt=%u early=%d sof=%llu t_ns=%llu\n", state == FRAME_WORK ? "WORK" : state == FRAME_IRQ ? "IRQ" : "OTH", @@ -1114,7 +1114,7 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) } if (dev->skip_frame || stream->skip_frame) { if (ft_diag) - v4l2_info(&dev->v4l2_dev, + rkisp_n1trace_info(&dev->v4l2_dev, "n1trace mi_skip_frame state=%s skip=%u dev_skip=%u sof=%llu t_ns=%llu\n", state == FRAME_WORK ? "WORK" : state == FRAME_IRQ ? "IRQ" : "OTH", @@ -1176,7 +1176,7 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) if (ft_diag) { WRITE_ONCE(dev->cap_dev.early_done_diag_pub, READ_ONCE(dev->cap_dev.early_done_diag_pub) + 1); - v4l2_info(&dev->v4l2_dev, + rkisp_n1trace_info(&dev->v4l2_dev, "n1trace mi_pub state=%s vb_seq=%u early=%d drop_left=%u pub=%u sof=%llu t_ns=%llu\n", state == FRAME_WORK ? "WORK" : state == FRAME_IRQ ? "IRQ" : "OTH", diff --git a/drivers/media/platform/rockchip/isp/common.h b/drivers/media/platform/rockchip/isp/common.h index 853b66db9807c..2d593a4142e5a 100644 --- a/drivers/media/platform/rockchip/isp/common.h +++ b/drivers/media/platform/rockchip/isp/common.h @@ -144,7 +144,15 @@ extern int rkisp_debug; extern bool rkisp_monitor; extern bool rkisp_irq_dbg; extern bool rkisp_buf_dbg; +/* FT N-1 diagnostics; default off (see module_param n1trace in rkisp.c). */ +extern bool rkisp_n1trace; extern u64 rkisp_debug_reg; + +#define rkisp_n1trace_info(v4l2_dev, fmt, ...) \ + do { \ + if (rkisp_n1trace) \ + v4l2_info((v4l2_dev), fmt, ##__VA_ARGS__); \ + } while (0) extern unsigned int rkisp_wait_line; extern unsigned int rkisp_vicap_buf[DEV_MAX]; extern unsigned int rkisp_hdr_wrap_line[DEV_MAX]; diff --git a/drivers/media/platform/rockchip/isp/rkisp.c b/drivers/media/platform/rockchip/isp/rkisp.c index 2155f21961492..0cdf3093b0e62 100644 --- a/drivers/media/platform/rockchip/isp/rkisp.c +++ b/drivers/media/platform/rockchip/isp/rkisp.c @@ -99,6 +99,18 @@ module_param_named(imx296_tail_wait_us, rkisp_imx296_tail_wait_us, uint, 0644); MODULE_PARM_DESC(imx296_tail_wait_us, "V39 IMX296 FT MI-drain residual ceiling (us); completion prefers MI offs"); +/* + * Per-frame FT n1trace (early arm/complete/ceiling, mi_pub/drop). Default off: + * bring-up logging filled rsyslog (kern+syslog) and exhausted rootfs. + * Enable: echo 1 > /sys/module/video_rkisp/parameters/n1trace + * (pair with imx296.n1trace=1 for full pulse↔mi correlation). + */ +bool rkisp_n1trace; +module_param_named(n1trace, rkisp_n1trace, bool, 0644); +MODULE_PARM_DESC(n1trace, + "RKISP FT n1trace diagnostics (0=off default, 1=verbose)"); +EXPORT_SYMBOL_GPL(rkisp_n1trace); + static u32 rkisp_imx296_residual_ceiling_us(u32 width, u32 height, u32 wait_line) { @@ -2537,7 +2549,7 @@ static int rkisp_isp_start(struct rkisp_device *dev) dev->cap_dev.early_done_skip_frames, ISP39_IMX296_SKIP_BUF_FRAMES, dev->cap_dev.early_done_poll_us); - v4l2_info(&dev->v4l2_dev, + rkisp_n1trace_info(&dev->v4l2_dev, "n1trace isp_ft_arm height=%u wait_line=%u skip=%u drop=%u warmup=%u ceiling_us=%u force_post=%u t_ns=%llu\n", height, dev->cap_dev.wait_line, dev->cap_dev.early_done_skip_frames, From 821edce1ab22dbcf105d0b7b23173c9d20808656 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Thu, 13 Aug 2026 10:52:08 +0800 Subject: [PATCH 17/21] =?UTF-8?q?fix(imx296/isp):=20#1016=20=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=20UV=20=E5=B0=BE=E4=BF=AE=E8=A1=A5=E5=B9=B6=E4=BA=A4?= =?UTF-8?q?=E4=BB=98=20#2026081301?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ceiling-force early-done 底行 UV 不完整导致 mon→FT/ROI 绿带;#1013 整尾 0x80 虽消 classic 却引入产品灰接缝。#1016 仅在 mean/neu 判定为 坏行时用 last-good UV 扩展,neu 占比仅在 ref 有 chroma 时生效。 主要变更: - capture_v39: rkisp_ft_sanitize_nv12_bot 条件修补(#1016) - rkisp: warm/gate/skip 收至 2,靠 UV 修补而非加长 quarantine - capture/h: FT sanitize 钩子 - DLCVCAM_BUILD_VERSION → 2026081301 - 多信号底带检测与验收脚本;绿线排查/验收记录 板端 smartcam_bs 用户场景验收:classic/seam/tear=0,N-1 pass, mon→trigger apply ~2.83s(≤3s)。 --- .gitignore | 6 + DLCVCAM_BUILD_VERSION | 2 +- ...0\247\346\265\213\350\257\225_20260807.md" | 281 +++ ...0\247\345\244\215\346\265\213_20260810.md" | 89 + ...22\346\237\245\350\256\260\345\275\225.md" | 535 ++++++ ...15\344\270\216\351\252\214\350\257\201.md" | 477 ++++++ ...14\350\257\201\350\256\260\345\275\225.md" | 3 +- drivers/media/platform/rockchip/isp/capture.c | 12 +- drivers/media/platform/rockchip/isp/capture.h | 6 + .../media/platform/rockchip/isp/capture_v39.c | 278 ++- drivers/media/platform/rockchip/isp/rkisp.c | 64 +- scripts/imx296_bottom_quality.py | 269 +++ scripts/imx296_day_endurance.py | 1507 +++++++++++++++++ scripts/imx296_day_watchdog.py | 426 +++++ scripts/verify_accept_user.py | 644 +++++++ scripts/verify_k1301_lat.py | 332 ++++ 16 files changed, 4900 insertions(+), 31 deletions(-) create mode 100644 "IMX296_24h\347\250\263\345\256\232\346\200\247\346\265\213\350\257\225_20260807.md" create mode 100644 "IMX296_4h\347\250\263\345\256\232\346\200\247\345\244\215\346\265\213_20260810.md" create mode 100644 "IMX296_\347\273\277\347\272\277_20260810_ceiling_force\346\216\222\346\237\245\350\256\260\345\275\225.md" create mode 100644 "IMX296_\347\273\277\347\272\277_20260812_gate_IRQ\344\277\256\345\244\215\344\270\216\351\252\214\350\257\201.md" create mode 100644 scripts/imx296_bottom_quality.py create mode 100644 scripts/imx296_day_endurance.py create mode 100644 scripts/imx296_day_watchdog.py create mode 100644 scripts/verify_accept_user.py create mode 100644 scripts/verify_k1301_lat.py diff --git a/.gitignore b/.gitignore index d0720779beff6..8da8fc0c46027 100644 --- a/.gitignore +++ b/.gitignore @@ -187,3 +187,9 @@ kernel_backup/ scripts/__pycache__/ trigger *.bak.* + +# local IMX296 verification dumps / build logs (not sources) +/IMX296_verify_*/ +/IMX296_2h_*/ +/IMX296_user_*/ +/IMX296_build_*.log diff --git a/DLCVCAM_BUILD_VERSION b/DLCVCAM_BUILD_VERSION index 442dfff0d1584..3bb86e818f0e5 100644 --- a/DLCVCAM_BUILD_VERSION +++ b/DLCVCAM_BUILD_VERSION @@ -1 +1 @@ -2026081001 +2026081301 diff --git "a/IMX296_24h\347\250\263\345\256\232\346\200\247\346\265\213\350\257\225_20260807.md" "b/IMX296_24h\347\250\263\345\256\232\346\200\247\346\265\213\350\257\225_20260807.md" new file mode 100644 index 0000000000000..05603de8ab321 --- /dev/null +++ "b/IMX296_24h\347\250\263\345\256\232\346\200\247\346\265\213\350\257\225_20260807.md" @@ -0,0 +1,281 @@ +# IMX296 24h 稳定性测试 — 总结报告 + +- **板卡**: `root@192.168.1.180` +- **内核**: `6.1.99-rk3576 #2026080701` / commit `8950d5ff`(分支 `fix/imx296-fast-trigger-n1-pwm-period`) +- **关注点**: 模式切换、ROI set/reset、触发、底部绿线、N-1 相位 +- **报告写入**: 2026-08-08(监控已停;板端 endurance 进程当时仍可能存活) +- **原始数据**: 板端 `/tmp/imx296_day_endurance/`(`summary.*` / `bugs.jsonl` / `events.jsonl` / `console.log`) +- **主机快照**: `/tmp/imx296_day_watchdog/` + +--- + +## 1. 测试怎么跑的 + +### 1.1 两阶段路径(重要) + +| 阶段 | 时间(约) | 路径 | 说明 | +|------|------------|------|------| +| A. Grabber 直连 | 08-07 18:14 → 约 22:40 | `GstV4L2Grabber` 占 `/dev/video11` | **错误路径**。未走 CamOS;后与手动 CamOS 抢设备,TOO_FEW/no_frame 爆炸,结果交叉污染 | +| B. CamOS 前后端 | 08-07 22:52 → 监控截止约 08-08 00:57 | `0_测试运行_camos.sh` + API/WS | **正确路径**。login → PATCH `/api/flow_config` → WS `rkcam_*` → `/preview/frame` + `X-Frame-Tag` | + +**下文结论以阶段 B(CamOS)为准。** 阶段 A 仅作对照说明,不计入 CamOS 轮次统计。 + +### 1.2 CamOS 轮次内容(每 cycle) + +1. `monitor/close`(free_run)抽几帧 +2. `trigger/software`:BASE LO/HI 相位 + 随机 ROI set/reset + 再 ROI +3. 同 run_mode 下切 `line0`:BASE + ROI set/reset +4. 回到 `monitor/close` +5. 判据:增益固定 0 → LO/HI 曝光交替 → `phase`;JPEG 底行 → 绿线;等新 `X-Frame-Tag` + +脚本:worktree `scripts/imx296_day_endurance.py` → 板端 `/tmp/imx296_day_endurance.py` +输出:`/tmp/imx296_day_endurance/` +CamOS 轮次起始 pid:`188060`,`--hours 24` + +### 1.3 测法注意(已写入脚本) + +- **判 N-1 前 `rkcam_set_gain(0)`**:默认用户集增益常 ~12 dB,暗场下会把 LO/HI 对比压扁,假 MIX/LOW_CONTRAST。 +- 曝光:LO≈100 µs / HI≈16000 µs;ROI/模式切换后丢弃 1 枪再采样。 +- 绿线:JPEG RGB 底行启发式(非 NV12 底 48 行;与 grabber 期算法不同)。 + +--- + +## 2. CamOS 阶段终态结果 + +**窗口**: 2026-08-07 22:52:25 → 约 2026-08-08 00:57(uptime ≈ 7519 s,**约 2.1 h**;计划 24h,监控提前结束) + +| 指标 | 数值 | 解读 | +|------|------|------| +| cycles | 39 | 后半程切换失败拖慢 | +| cases | 271 | | +| frames / valid | 1894 / 1788 | valid 率 ≈ **94.4%** | +| **green** | **0** | **绿线未复现** | +| **n1** | **1** | 仅 1 条,且对比度极低(见下) | +| mix | 24 | 多为 ROI/暗场相关 | +| lowc | 2 | 曝光几乎无差分 | +| too_few | 15 | 整段采不够有效帧 | +| no_frame | 106 | 单次触发未等到新 tag | +| switch_fail | **17** | **后半程主矛盾** | +| recoveries | 12 | | +| crashes | 0 | 测试进程未崩 | +| bugs 总计 | **57** | 见分类 | + +### 2.1 Bug 分类(bugs.jsonl 标题归类) + +| 类别 | 约计数 | 代表标题 | +|------|--------|----------| +| 相位 MIX | ~22 | `SW_ROI_*:phase=MIX`、`SW_ROI_RESET:phase=MIX`、`LINE0_ROI_*:phase=MIX` | +| 切换/流失败 | ~17 | `switch_fail:MON` / `MON_retry` / `MON_END` / `LINE0`;`stream_fail:MON*` | +| TOO_FEW | ~15 | ROI 后 / LINE0_BASE / LINE0_ROI_RESET `valid=0/6` 或 `0/8` | +| LOW_CONTRAST | 2 | `SW_BASE:phase=LOW_CONTRAST` | +| N1 | 1 | `SW_BASE:phase=N1` @ cycle 26 | +| green | 0 | — | + +### 2.2 对照「内核绿线 / N-1 修复」目标 + +| 目标 | CamOS 轮结果 | 判断 | +|------|----------------|------| +| 底部绿线 | 0 | **本轮未复现,正面** | +| 明确 N-1 回归 | 1 条边缘样本 | **不能当铁证**(见 §3.2) | +| 长稳切换 + ROI + 触发 | switch_fail / TOO_FEW / no_frame 突出 | **可用性主风险在 CamOS 路径与取帧,不在绿线** | + +### 2.3 与阶段 A(Grabber)对照(污染轮,仅参考) + +Grabber 直连曾到:cycles≈22,bugs≈307,**n1=7**,green=0,但 **noframe≈1945、valid 卡在 530**——典型 **与 CamOS 抢 `/dev/video11` 后流死亡**,N1/MIX 统计不可信。 +该阶段已归档:`/tmp/imx296_day_endurance_archive/grabber_*.tgz`。 + +--- + +## 3. 发现的问题与原因分析 + +### 3.1 【主问题 A】模式切换失败堆积(后半程主导) + +**现象** + +- `switch_fail` / `stream_fail` 合计约 17 次。 +- 约 00:00 后大量 `switch_fail:MON` / `MON_retry`,cycle 几乎耗在切换重试上,summary 一度长时间不刷新。 +- 日志里常见:`info=timed out`;有时 sysfs 已是 `active=free_run` 且 `streaming=1`,脚本仍判失败;也有 `streaming=0` 窗口。 + +**可能原因(按可能性)** + +1. **脚本等待条件过严 / 与 CamOS 语义不一致** + - `wait_stream(need_free=True)` 要求 `active=free_run`;`need_ft=True` 要求 `master_fast_trigger`。 + - CamOS `PATCH /api/flow_config` 成功 ≠ 立刻满足脚本假设;冷却 `RUN_MODE_SWITCH_COOLDOWN_S=5s`、硬件 apply 耗时、ROI 后 pipeline 重建都会拉长窗口。 + - 已 free_run + streaming=1 仍 timeout → 更像 **判定逻辑/超时预算** 问题,而非 sensor 彻底挂死。 + +2. **CamOS 切换路径偶发卡住或恢复不完整** + - `apply_flow_config_patch` 串行 + 硬件 `set_trigger_mode`;失败会 rollback,但前端/脚本若连续狂切,易叠加 cooldown 拒绝与 recover 重启。 + - recover 会 `pkill server.py` + 再跑 `0_测试运行_camos.sh`(含 **vite build**),一次恢复可达数十秒~数分钟,期间全是 fail。 + +3. **preview/流在 monitor 边界短暂 streaming=0** + - 切换瞬间 grabber 停-起,脚本把「短暂无流」记成 stream_fail。 + +**影响**: 不是绿线/N-1,但会 **严重稀释 24h 有效 cycle 数**,并把 bugs 数抬高。 + +**建议改法** + +| 层级 | 动作 | +|------|------| +| 测试脚本 | 放宽 `wait_stream`:monitor 只需 `streaming=1`(不必死磕 free_run 字符串);拉长 settle/timeout;cooldown 显式 sleep ≥5.5s 且对「已是目标模式」短路径成功 | +| 测试脚本 | recover **禁止**动辄整站 `0_测试运行_camos.sh`(会 rebuild 前端);改为仅 `patch monitor` + `rkcam_reset_roi` + 重连 WS;真死再用轻量 `python server.py` 拉起 | +| CamOS | 给 flow 切换增加「硬件已达目标 + streaming」的统一 ready 查询;超时错误区分 cooldown / hardware fail / stream down | +| 运维 | 长稳前确认单实例 server.py;避免测试中手动再开 CamOS | + +--- + +### 3.2 【主问题 B】触发后无新帧 → no_frame / TOO_FEW + +**现象** + +- `no_frame=106`,`too_few=15`(整 case valid 不足一半)。 +- 多发于:**ROI set/reset 之后**、`LINE0_BASE` / `LINE0_ROI_RESET`。 +- 机制:`fire_trigger`(WS `rkcam_trigger_once` + `/debug/trigger_once` + sysfs XTRIG)后轮询 `/preview/frame`,要求 **新的 `X-Frame-Tag`**。 + +**可能原因** + +1. **ROI 切换后 pipeline/encoder 重建窗口** + - CamOS ROI 会停流重建;脚本 settle 2.5s 可能不够,或首几枪 tag 不涨。 +2. **LINE0 与 software 触发路径不一致** + - line0 更依赖外部/XTRIG;仅 WS soft trigger 可能无效;sysfs 脉冲时序与 sensor 窗口不对齐会丢帧。 +3. **preview JPEG 路径与「原始触发帧」不同步** + - 测的是编码后 preview,不是 raw NV12;tag 更新策略若合并/跳帧,会表现为「触发了但 tag 不变」。 +4. **暗场 + 极短曝光** 不直接导致 no_frame,但会与相位问题叠加。 + +**建议改法** + +| 层级 | 动作 | +|------|------| +| 测试脚本 | ROI 后:更长 settle(≥ ROI 实测 latency,历史 CamOS ~3.7s)+ 多 discard 帧;LINE0 明确只靠 XTRIG 或只靠已验证的 trigger API | +| 测试脚本 | tag 不变时区分「HTTP 失败 / 旧 tag / 空 body」,写入 bug 字段,便于分流 | +| CamOS | ROI 完成后 ready 信号;trigger_once 与 preview tag 原子绑定(每成功触发必新 tag 或明确 error) | +| 内核/驱动 | 若 ready 后仍系统性丢触发,再查 FT/XTRIG;**先排除 CamOS/脚本** | + +--- + +### 3.3 【次要】相位 MIX / LOW_CONTRAST(多与测法 + ROI) + +**现象** + +- MIX ≈22,LOW_CONTRAST=2;集中 SW/LINE0 的 ROI 与 RESET。 +- 多数 means 贴在 ~18.3–19.5,**span 常 <1**,暗场 JPEG 对比极差。 + +**原因** + +1. **场景本身极暗**:中心 luma 几乎贴地,LO/HI 差分小,标签抖动 → MIX。 +2. **曝光/增益生效滞后**:WS 异步队列;settle 不足时 HI 帧仍像 LO。 +3. **ROI 后首帧/尺寸变化** 污染序列(已有 1 枪 discard,可能不够)。 +4. **与真 N-1 的区分**:真 N-1 在 **span 足够大** 时 labels 整体错一位;本轮多数 MIX **不具备** 这种清晰对比。 + +**建议改法** + +- 相位用例加可控照明或提高 HI 上限 / 拉长 HI,目标 span≫5。 +- 或对 span<阈值的 case **降级为 inconclusive**,不记 bug(与 green/N1 硬故障分开)。 +- 保持 gain=0;曝光 set 后 wait 可调到 1 帧时间以上。 + +--- + +### 3.4 【N-1】唯一样本 — 证据弱 + +- **唯一**: `SW_BASE:phase=N1` @ **cycle 26**(2026-08-07 23:36:16) +- means ≈ `[19.024, 18.39, 18.39, 18.391, 18.39, 18.39, 18.39]`,labels 几乎全 LO,span≈0.63 +- **解读**: 更像「只有一点略亮、其余无对比」被算法打成 N1,**不是** grabber 早期那种 span~4、整段错位的经典 N-1。 +- **结论**: **不能单独作为内核 N-1 回归失败的证据**;需在高对比条件下复测。 + +--- + +### 3.5 【绿线】本轮未复现 + +- CamOS 轮 `green_frames=0`,bugs 无 green 类。 +- 注意:本轮绿线判据是 **JPEG 底行 RGB**,与早期 NV12 底 48 行不同;若只关心 ISP raw 底行,建议另加 raw/NV12 探针或内核侧 n1trace/绿线专用 case。 +- 结合阶段 A 在污染前也长期 green=0,**倾向:当前 `#2026080701` 上绿线不是主复发项**。 + +--- + +## 4. 根因分层结论(给后续排期) + +``` + ┌─────────────────────────┐ + 本轮主噪声 ──► │ CamOS 切换超时/等待过严 │ → 改脚本判定 + CamOS ready API + │ ROI/触发后 preview 无新帧 │ → settle/tag 绑定 + LINE0 触发路径 + └─────────────────────────┘ + ┌─────────────────────────┐ + 测法噪声 ──► │ 暗场 JPEG LO/HI 对比不足 │ → 照明/曝光/span 门闩 + └─────────────────────────┘ + ┌─────────────────────────┐ + 内核目标 ──► │ 绿线:未复现 │ → 保持观察,可加 NV12 探针 + │ N-1:1 条弱样本 │ → 高对比专用复现后再定 + └─────────────────────────┘ +``` + +**一句话**: +当前 `#2026080701` 在 **CamOS 真实路径** 上,**绿线没出来、N-1 没有可靠复现**;真正拖垮长稳的是 **模式切换失败** 和 **ROI/触发后等不到 preview 新帧**,应优先从 **CamOS + 测试脚本** 修,而不是先再刷一版内核。 + +--- + +## 5. 建议的解决顺序(可执行) + +### P0 — 让 24h 跑得动、统计可信 + +1. 改 endurance:`wait_stream` 语义、cooldown、禁止 recover 触发前端 build。 +2. ROI 后 settle ≥ 实测 latency;LINE0 触发路径单一化并打日志。 +3. span 过低的相位结果标 `INCONCLUSIVE`,不计入 n1/mix bug。 +4. 确认板上始终 **单** `python server.py`,测试期间无人手开第二套 CamOS。 + +### P1 — 验证内核修复是否仍站得住 + +5. 固定高对比场景(灯/白墙 + gain0 + LO/HI span 目标)跑 software/line0/ROI 各 N 次。 +6. 可选:并行短时 NV12/raw 底行绿线探针,与 JPEG 解耦。 +7. 只有 P0 稳定后,再开真正 24h,并以 green/n1(高对比)为门禁。 + +### P2 — CamOS 产品侧 + +8. flow 切换 ready 状态机;ROI/trigger 与 `X-Frame-Tag` 契约。 +9. 避免测试/恢复路径走完整 `vite build`。 + +--- + +## 6. 过程时间线(精简) + +| 时间 | 事件 | +|------|------| +| 08-07 18:14 | Grabber 版 24h 启动(路径错误) | +| 18:14–22:xx | Grabber:早期有 N1;中后期抢设备 → no_frame/TOO_FEW 主导 | +| ~22:40 | 停 Grabber,归档;CamOS 单实例拉起 | +| **22:52** | **CamOS endurance 启动** pid=188060;冒烟 SW/LINE0 多为 CUR,green=0 | +| 22:52–23:30 | 正常推进;出现 MIX/TOO_FEW;**23:36 唯一 N1(弱)** | +| ~00:00 后 | **switch_fail:MON\*** 连发,有效 case 增速下降 | +| 08-08 ~00:57 | 监控停止时快照:cycles=39 bugs=57 green=0 n1=1 | +| 08-08 | 主机 cron / watchdog 已停;本总结写入 | + +--- + +## 7. 附件与命令 + +```bash +# 板端结果 +ssh root@192.168.1.180 'cat /tmp/imx296_day_endurance/summary.txt; wc -l /tmp/imx296_day_endurance/bugs.jsonl' + +# 归档(示例) +ssh root@192.168.1.180 'tar czf /tmp/imx296_day_endurance_archive/camos_$(date +%Y%m%d_%H%M%S).tgz -C /tmp imx296_day_endurance' + +# 停板端测试(若仍需要) +ssh root@192.168.1.180 'pkill -f /tmp/imx296_day_endurance.py' +``` + +- 脚本: `scripts/imx296_day_endurance.py`(CamOS)、`scripts/imx296_day_watchdog.py`(已停) +- 内核记录入口: 记忆/既有 `IMX296_绿线修复_*`、`IMX296_N1相位滞后_*` +- 构建号: DLCVCAM `#2026080701` + +--- + +## 8. 最终判定(给决策) + +| 问题 | 是否本轮确认 | 优先级 | 主责方向 | +|------|--------------|--------|----------| +| 底部绿线复发 | **否** | 观察 | 内核已像稳住;可选 NV12 补测 | +| N-1 回归 | **未确认**(1 弱样本) | 中(要高对比复测) | 测法 + 必要时内核 | +| 模式切换超时 | **是** | **P0** | 测试脚本 + CamOS | +| ROI/触发失帧 | **是** | **P0** | CamOS preview/trigger + 脚本 settle | +| 暗场相位 MIX | **是(噪声为主)** | P1 | 测法门闩 | + +**总体**: CamOS 路径约 2.1h 样本支持「绿线/硬 N-1 不是当前头号问题」;**先修切换与取帧,再重开干净 24h。** diff --git "a/IMX296_4h\347\250\263\345\256\232\346\200\247\345\244\215\346\265\213_20260810.md" "b/IMX296_4h\347\250\263\345\256\232\346\200\247\345\244\215\346\265\213_20260810.md" new file mode 100644 index 0000000000000..6fe930a96b9dc --- /dev/null +++ "b/IMX296_4h\347\250\263\345\256\232\346\200\247\345\244\215\346\265\213_20260810.md" @@ -0,0 +1,89 @@ +# IMX296 4h 稳定性复测(#2026081008 绿线 / N-1 / ROI) + +- **开始时间**: 2026-08-10 20:57 (CST) +- **计划时长**: 4 小时(约至 2026-08-11 00:57) +- **板卡**: `root@192.168.1.180` +- **内核**: `#2026081008` pub_gate=6(drop+warm 后计数)+ CamOS `SMARTCAM_TRIGGER_WARMUP_FRAMES=12` +- **脚本**: 板端 `/tmp/imx296_4h_endurance.py` → `/tmp/imx296_4h_endurance_1008/` +- **主机产物**: `/tmp/imx296_4h_watchdog_1008/summary.json`(板端 `/tmp` 为 tmpfs,重启/崩溃后脚本与目录曾丢失) +- **关注故障**: 底绿、N-1、模式切换/ROI、首枪无图 + +## 成功标准 + +| 项 | 标准 | 结果 | +|----|------|------| +| classic 绿线 `green_frames` | **0** | **0** ✅ | +| hard N-1 `n1_cases` | **0** | **0** ✅ | +| 切换/ROI 硬失败 | 0 | switch_fail=0 roi_fail=0 ✅ | +| 跑满 4h | 是 | **否** — 有效跑 **~2.21h** 后 CamOS 崩溃,测试中断 ⚠️ | + +## 有效段统计(最后完整 summary @ 2026-08-10 23:10) + +| 指标 | 值 | +|------|-----| +| started → updated | 20:57:31 → 23:10:25 | +| uptime | **7974 s(2.21 h)** | +| cycles | **64** | +| cases | 510 | +| frames / valid | **3570 / 3570** | +| green_frames | **0** | +| n1_cases | **0** | +| mix_cases | **1** | +| bugs | **1** | +| switch_fail / roi_fail / crashes | 0 / 0 / 0 | +| lowc / incon | 4 / 4(暗场相位不可判,不记硬故障) | +| build | `#2026081008` | + +### 主要 case 覆盖(by_case 摘要) + +| case | n | green | n1 | mix | +|------|---|-------|----|-----| +| MON_FREERUN | 64 | 0 | 0 | 0 | +| SW_BASE | 64 | 0 | 0 | 0 | +| SW_ROI_RESET | 64 | 0 | 0 | 0 | +| LINE0_BASE | 64 | 0 | 0 | 0 | +| LINE0_ROI_RESET | 63 | 0 | 0 | 0 | +| 其它 ROI 变体 | 多次 | 0 | 0 | 见下 1 次 MIX | + +### 唯一 bug(非绿线、非 hard N-1) + +- **时间**: 2026-08-10 22:24:29 cycle 42 +- **title**: `SW_ROI2_352x328:phase=MIX` +- **含义**: 小 ROI 352×328 下 LO/HI 曝光交替相位判为 MIX(期望 LO/HI/LO/HI,实得 LO/LO/LO/HI);`green=false`,bot 非绿带 +- **means**: ~18.4 / 19.4 / 18.4 / 25.5,span≈7.1 — 更像曝光切换 settle/小 ROI 亮度对比弱,**不是**经典 N-1 整帧滞后,也**不是**底绿 +- 其余 509 cases 无 mix/n1/green + +## 中断原因(非目标故障复现) + +1. **23:10–23:11** 附近 journal:`camos.service: Failed with result 'signal'`;伴随 `imresize_t failed` / RGA / mpp detach 噪声 +2. endurance 进程随 CamOS 死亡;watchdog 自 23:13 起报 dead +3. 板端 `/tmp` 为 **tmpfs**,脚本 `/tmp/imx296_4h_endurance.py` 与输出目录随后不可用;watchdog 盲目 restart 失败刷屏(已停掉) +4. 主机在 23:10 已拉到完整 `summary.json` / `bugs.jsonl`,**以该快照为有效结论** + +## 判定 + +| 维度 | 结论 | +|------|------| +| **底绿回归** | **未复现**(3570 帧 green=0;含 mon→sw、line0、ROI set/reset) | +| **hard N-1** | **未复现**(n1_cases=0) | +| **4h 完整度** | **未跑满**(2.21h / 64 cycle 后基础设施崩溃) | +| **总评** | **目标故障(绿线/N-1)在有效窗口内通过**;完整 4h 需修 CamOS 稳定性或把脚本放到非 tmpfs 后重跑 | + +## 产物路径 + +- 主机:`/tmp/imx296_4h_watchdog_1008/summary.json`、`bugs.jsonl`、`summary.txt`、`watchdog.log` +- 报告本文:`IMX296_4h稳定性复测_20260810.md` +- 绿线排查 MD §10.11 关联 + +## Watchdog 有效采样(摘录) + +| 时间 | cycles | bugs | green | n1 | +|------|--------|------|-------|-----| +| 20:58 | 1 | 0 | 0 | 0 | +| 22:24 前 | … | 0 | 0 | 0 | +| 22:24+ | … | 1(MIX) | 0 | 0 | +| 23:10 最后 | 64 | 1 | 0 | 0 | +| 23:13+ | 进程死 / CamOS signal | — | — | — | + +--- +*结论写入: 2026-08-11 09:03:38* diff --git "a/IMX296_\347\273\277\347\272\277_20260810_ceiling_force\346\216\222\346\237\245\350\256\260\345\275\225.md" "b/IMX296_\347\273\277\347\272\277_20260810_ceiling_force\346\216\222\346\237\245\350\256\260\345\275\225.md" new file mode 100644 index 0000000000000..ac5f7a92c89cc --- /dev/null +++ "b/IMX296_\347\273\277\347\272\277_20260810_ceiling_force\346\216\222\346\237\245\350\256\260\345\275\225.md" @@ -0,0 +1,535 @@ +# IMX296 底部绿线(2026-08-10)— ceiling-force 排查记录 + +> 状态:**#1009 源码已改(未刷板)——C1+C2:gate 仅 warm==0 后递减;IRQ 路径 warm 期 mi_drop 并消耗 warm。见 §12。** +> 板端内核(当前联调仍为):`#2026081008`;树内待刷:`#2026081201` `skip=2 drop=2 warmup=8 pub_gate=6 force_post=70000` +> 历史复现基线:`#2026081003`;CamOS 已弃 discard forced-clear 开闸,warmup=脉冲预算(post=3 回退,见 §11.4) +> 早期复现时 CamOS:warmup 默认 **2**,`residual_discard` forced clear,`post_drop_ms=100` +> 产物:板端 `/tmp/green_capture_run/`(`summary.json`、绿图、`dmesg_full.txt` / `dmesg_ft.txt`) +> 相关:`IMX296_N1相位滞后_诊断与修复记录.md`(N-1 / ceiling-force 背景)、`IMX296_绿线修复_内核修改与验证记录.md`(早期绿线策略) + +--- + +## 1. 现象(用户 + 存图) + +- 触发模式下底部约 **8 行绿带**(经典 NV12 UV 未写完),**概率性**。 +- **不固定** mon→trig 第 1 帧或第 5–6 帧;也可能首帧;常见 **连续 2 帧**。 +- **触发下 ROI set/reset** 后同样概率出现。 +- 用户指名样例(已像素核实): + - `images/2026-08-10/NG/20260810_164309_812_042048.jpg` bot8≈(70,88,40) gdom≈88% + - `images/2026-08-10/NG/20260810_164310_668_456645.jpg` bot8≈(70,88,40) gdom≈90%(连续下一张) + - 同段还有 pure 绿底:`...164301_870...` bot8≈(6,141,5) + +自动化误判过「0 绿」:software 路径/取图缓存与 **line0 真实节拍 + 产品存图** 不一致;**以用户 NG + 带 n1trace 的复现为准**。 + +--- + +## 2. 受控复现(协议一轮,`#2026081003`,n1trace 短开) + +脚本:`/tmp/green_capture_run.py` → 板端 `/tmp/green_capture_run/` + +| 段 | 结果 | +|----|------| +| **A mon→software head16** | **GREEN ×2:sw#3、sw#4 连续** gdom≈84% bot8≈(70,87,41) | +| B mon→line0 | 本轮 `debug/trigger_once` 在 line0 下 tag 不涨,**采样无效**(不能当 line0 已好) | +| C ROI 592 / reset | 本轮 0 绿(未打中) | + +sw 序列: + +``` +sw0–2 暗底、不绿 +sw3–4 GREEN 带(连续) +sw5+ 正常 (92,87,74) +``` + +CamOS 同次切换: + +``` +warmup 2/2, residual_discard=1 forced clear, post_drop_ms=100, mode=software +warmup dt≈0.63s + quarantine 0.1s +``` + +--- + +## 3. n1trace 对齐结论(问题点) + +进 FT 后 arm: + +``` +isp_ft_arm height=1088 skip=2 drop=2 warmup=8 +ceiling_us=70790 force_post=70000 +``` + +### 3.1 已确认的机制 + +1. **V39 + IMX296 FT 上 MI OFFS 长期为 0**(`y_shd=y_st=cb_*=0`) + → early-done **几乎 100% 走 `ceiling-force`**,再 `early complete` + `mi_pub WORK early=1`。 + (与 N-1 文档一致:不能 abandon 到下一拍 MI FE。) + +2. 绿带形态 = **底 ~8 行 UV 不完整**(与 wait_line 终端 8 行历史问题同类)。 + +3. **force→complete 的 post-hold 在绿帧上已经跑满** + - warmup 段 hold ≈ **138ms**(force 70 + ~¾ residual) + - warm 用尽后 ≈ **123ms** + - complete 时 **`warm_left>0`**,**不是**「下一 SOF 打断 stale arm 导致 hold 被砍」。 + +4. **同一条 ceiling-force 路径上:前段发布可绿,后段发布正常** + - 绿帧 hold **更长**(warmup),后段 ok 帧 hold **更短**仍干净。 + - ⇒ **否定「再盲目加长 force_post 就能根治」** 作为主因假设。 + +### 3.2 mi_pub 与用户帧粗对齐(drop=2 耗尽后) + +| mi_pub(约) | warm_left@complete | 用户 sw#(约) | 画质 | +|--------------|--------------------|----------------|------| +| (skip/drop 阶段) | — | 被内核丢掉 | — | +| pub1–3 | 5→3 | sw0–2 | 暗、不绿 | +| **pub4–5** | **2→1** | **sw3–4** | **GREEN** | +| pub6+ | 0 | sw5+ | 正常 | + +cnt=1、2 的 `early complete` 日志侧 **无对应 mi_pub**(与 `mi_drop`/半开会话交错)——发布门槛与 drop 计数**不完全同一条可见路径**,仍属待挖点。 + +### 3.3 一句话问题点 + +> **进 FT 后早期已发布的 ceiling-force 帧,在 post-hold 已满的情况下仍可能带未完成 UV;管道稳定后同一 force 路径不再绿。** +> 产品上再叠加:**CamOS warmup=2 过短**,脏发布会漏到用户可见 tag;ROI 重 arm 会再次打开同一窗口。 + +不是: + +- 单纯 mon→trig「固定第 N 帧」 +- 单纯 hold 从 45ms→70ms 不够再加一点就能完事 +- 与业务 NG 分类误报(像素已是真绿带) + +--- + +## 4. 今日 NG 簇形态(辅助) + +`2026-08-10/NG` classic 绿约 22 张 / 11 簇:多为 **1×PURE 或 2×BAND 连发**,簇间隔随机——与「进 FT / 重 arm 后短窗口」一致。 + +16:43 用户簇与内核: + +``` +16:42:56 再进 FT arm skip=2 drop=2 force=70ms +16:43:01 seq≈87 PURE +16:43:09–10 seq≈90–91 BAND×2 ← 用户两张 +随后恢复正常 +``` + +--- + +## 5. 已做 / 未做的改动(避免继续盲改) + +| 项 | 状态 | +|----|------| +| force_post 45→70、warmup hold 85×8、residual floor 抬高(`#1002/#1003`) | 已刷;**绿线仍在** | +| SKIP_BUF 2↔4 来回 | 只影响前几枪是否出图;**单独不能当根治** | +| 仅 CamOS warmup 2→6 A/B(更早) | 当时矩阵显示不够;**需在长 hold 内核上重验** | +| 本地树 `DLCVCAM_BUILD_VERSION=1004` + 源码 SKIP=4 | **未证明已刷**;板端复现时仍是 **1003 / drop=2** | +| line0 外部真触发 + n1trace | **未完成**(本轮 line0 采样失败) | +| ROI 绿 + n1trace 打中 | **未完成** | + +--- + +## 6. 还差什么(排查清单) + +优先级从高到低: + +1. **[P0] 验证「隔离早期脏发布」是否足够** + - CamOS `SMARTCAM_TRIGGER_WARMUP_FRAMES≥6`(吃掉 sw3–4 一类脏帧) + - 与内核 `drop/skip` 对齐:用户首枪在 quarantine 之后 + - 重跑 `/tmp/green_capture_run.py`:mon→sw 连续多轮,**GREEN_COUNT=0** + +2. **[P0] line0 真路径** + - 外部触发或产品存图,不要只靠 `debug/trigger_once` + - 对时 `trigger-dev seq` ↔ `mi_pub` ↔ NG 文件名 + +3. **[P1] 为何 early force 帧脏、后期同路径干净** + - `mi_pub` 时 `vb_seq` / buffer index 与 `sof` 是否严格一对一 + - FT 入口 dummy×3 / 半开帧是否污染前几块 buffer 的 UV + - cnt=1,2 complete 无 mi_pub 的精确分支(drop vs skip_frame vs 无 buf) + +4. **[P1] ROI 重 arm** + - 高度 592/832/1088 切换时是否重新 `isp_ft_arm` + 短 quarantine + - 与 mon→trig 是否同一类早期脏 pub + +5. **[P2] 若隔离不够:再动完成条件** + - 仅对 `warm_left>0` 的 force 完成 **禁止 pub / 强制 drop**(内核) + - 或 force 路径额外等 MI FE(可能拉回 N-1,需极度谨慎) + - **禁止**无 n1trace 对照的大跳 force_post / drop=10 + +--- + +## 7. 修复方向(最小、可验证) + +### 7.1 产品向(优先做) + +| 层 | 动作 | 理由 | +|----|------|------| +| CamOS | `SMARTCAM_TRIGGER_WARMUP_FRAMES=6`(runtime_settings 或等价) | 复现上脏帧在用户 sw#3–4;warmup=2 盖不住 | +| 内核 | 保持 ceiling-force + 现 hold;`SKIP_BUF/SKIP_EARLY` 与 warmup **对齐且 ≤warmup**(建议 4/4 或维持 2/2 但 warmup≥6) | 半开帧仍丢;用户首枪在 gate 打开后 1:1 | +| 验证 | mon→sw ≥3 轮 head16;ROI set/reset;line0 人工/外部触发 | GREEN=0 且首枪可出图 | + +### 7.2 不做(除非 7.1 失败且有新 n1trace) + +- 再把 `force_post` 加到 100ms+ 当唯一手段 +- `SKIP_BUF≥10`(已知「前 10 枪不出图」) +- 关掉 ceiling-force 回到 abandon(N-1 回归) + +### 7.3 成功标准 + +- mon→software / line0 / ROI reset:用户可见帧 bot8 gdom<40% 且无 pure 绿底 +- 模式切换后 **第 1 次用户触发有图**(warmup 在 gate 内消耗) +- dmesg arm 与 n1trace 抽检仍为 ceiling-force(N-1 不回退) + +--- + +## 8. 时间线摘要 + +| 时间 | 事件 | +|------|------| +| 早前 | N-1 ceiling-force 合入;绿线靠 residual+hold+drop | +| 2026-08-10 | 用户再报随机底绿 + ROI 绿;hold 加长刷 `#1002/#1003` | +| 16:43 | 用户 NG 实锤 BAND×2 + PURE;arm 已是 force=70ms | +| 17:08 | 受控 mon→sw 复现 GREEN sw#3–4 + 完整 n1trace | +| 结论 | 早期 ceiling-force **已发布**帧脏;hold 满仍可绿;后段同路径干净 | + +--- + +## 9. 板端/树状态备忘 + +- 复现内核:`#2026081003` skip=2 drop=2 +- 本地未提交 diff 曾指向 1004 + SKIP=4(**以实机 uname/dmesg arm 为准**) +- n1trace 默认关;抓数时短开,结束后必须 `echo 0 > .../n1trace` +- 备份示例:`/root/boot-backups/boot-before-2026081003.img` + +--- + +## 10. 修复实施与验证日志(2026-08-10 晚) + +> 状态:**#1008 mon→sw 4 轮 TOTAL_GREEN=0(2026-08-10 18:01)** +> 板端:`#2026081008` pub_gate=6(仅 drop+warm 后计数)+ CamOS warmup=12 +> 仍待:ROI / line0;CamOS residual_discard forced clear 仍是产品侧缺口。 + +### 10.1 问题点(不变) + +- FT 进线后 **ceiling-force** 几乎 100%(MI OFFS=0)。 +- **post-hold 已满**(~123–138ms)的 early `mi_pub` 仍可底 8 行绿 UV;同路径稍后干净。 +- 产品缺口:CamOS warmup 过短 + residual_discard **forced clear** 会在 sample 未到时提前开闸。 + +### 10.2 版本轨迹 + +| 构建 | 关键改动 | mon→sw 矩阵 | 备注 | +|------|----------|-------------|------| +| #1003 | force=70 / warm hold / skip=drop=2 | GREEN sw#3–4 | 复现基线 | +| #1005 | `warm_left>0` → mi_drop early WORK | **14/36** 头 0–4 绿 | warm 后仍绿 | +| CamOS warmup 6→8→**12** | runtime_settings + src clamp 16 | 单独不够 | residual_discard 常 forced clear | +| #1006 | `pub_gate=10` 仅 `frame_early` | r0/r2 ok,**r1 全绿 0–4** | r1 仅 `mi_pub IRQ early=0`,gate 未吃 IRQ | +| #1007 | `gate_drop` 不限 early | **15/48** 多轮头 0–4 绿 | gate 与 warm **并行递减**,warm 尽时 gate=0 | +| #1008 | **gate 仅在 `!drop && !warm_drop` 时递减**;`PUB_GATE=6` | **0/48** mon→sw×4 head12 | 见 §10.9;n1trace 确认 gate 在 warm 后 6→0 再 mi_pub | + +### 10.3 n1trace 关键证据 + +**#1005(warm_left only)** +进 FT 后 `mi_drop WORK warm=8→1`,随后 `mi_pub` 且用户 sw0–4 绿 → quarantine 长度不够。 + +**#1006 r1(early-done 未跑起来)** + +``` +isp_ft_arm ... pub_gate=10 +mi_drop IRQ ×2 (drop_left) +随后大量 mi_pub IRQ early=0 ← gate 因要求 frame_early 未扣 +residual_discard=11 forced clear +用户 r1_0..4 GREEN +``` + +**#1007(gate 与 warm 抢计数)** + +``` +mi_drop ... warm=8 gate=9 +... +mi_drop ... warm=0 gate=0 cnt=10 ← 第 10 次 drop 后 gate 已空 +mi_pub pub=1.. → 用户头帧仍绿 +``` + +### 10.4 #1008 设计 + +| 层 | 配置 | +|----|------| +| 内核 `ISP39_IMX296_PUB_GATE_FRAMES` | **6**(覆盖 #1005 头 0–4 +1) | +| `capture_v39` | `gate_drop = ft_diag && gate && !drop && !warm_drop`;IRQ/WORK 皆可 | +| `warm_drop` | 仍仅 `frame_early && warm` | +| CamOS | `SMARTCAM_TRIGGER_WARMUP_FRAMES=12`,interval 150ms;src clamp max 16 | +| 期望 dmesg | `pub_gate 6`;`mi_drop ... gate=6→1` **出现在 warm/drop 之后**;再 `mi_pub` | + +### 10.5 CamOS 已知缺口(未改二进制逻辑) + +- `_warmup_sensor` 脉冲后若 sample 未减 `warmup_discard_cnt`,结束时 **forced clear residual** → 用户闸门可早于内核 quarantine。 +- journal 常见 `triggered 12/12 residual_discard=9..11 forced clear`。 +- 源码树:`/root/smartcam_bs/.../gst_v4l2_grabber.py`;运行时为 **nuitka** `camos` 二进制,靠 `runtime_settings_dlcvcam.json` `startup_env` 注入。 +- 彻底修应用侧:warmup 必须等到 discard 真扣完或与内核 pub 对齐,禁止 forced clear 开真触发(**待做,非本轮内核**)。 + +### 10.6 板端备忘 + +- 备份:`/root/boot-backups/boot-before-202608100{5,6,7}.img` +- 交付镜像:`boot-rk3576-6.1.99-2026081008-01726d6f-dirty.img` +- n1trace 默认关;抓完 `echo 0 > /sys/module/video_rkisp/parameters/n1trace` +- 产物目录:`/tmp/green_verify_1005` `1006` `1007`(及后续 1008) + +### 10.7 成功标准(复述) + +- mon→software ≥3 轮 head12:`TOTAL_GREEN=0`,首枪有图 +- dmesg 仍 ceiling-force(N-1 不回退) +- ROI set/reset + line0 另验 + +### 10.8 下一步 + +1. 刷 `#1008`,CamOS warmup=12,重跑 mon→sw 4 轮矩阵 +2. 若仍绿:对照 n1trace 看 `gate` 是否在 pub 前耗尽;考虑 gate≥8 或修 CamOS forced clear +3. 绿清零后再 ROI / line0 + +--- + +*文档随刷机验证持续追加;勿只改常量不写 n1trace。* + +### 10.9 #1008 验证结果(2026-08-10 18:01–18:03) + +- 内核:`#2026081008` dmesg `pub_gate 6` +- CamOS:`warmup triggered 12/12`(仍常见 `residual_discard=8..12 forced clear`) +- 矩阵:mon→software **4 轮 × head12**,`TOTAL_GREEN **0**`(`/tmp/green_verify_1008`) +- n1trace 形态(正常 early-done 轮): + - `mi_drop` drop×2 + warm 递减时 **gate 保持 6** + - warm=0 后 `gate=5→0` 共 6 次再 `mi_pub` +- 首用户帧偶发 `dt≈2–3s / pulses=3–4`:gate 尚未耗尽时 tag 慢进,属 quarantine 代价,不是绿线 +- **未验**:ROI set/reset、line0 外部真触发、产品存图路径长时间压测 + +### 10.10 仍建议的应用侧收尾 + +1. 禁止 warmup 结束 `forced clear` 开真触发;或 warmup 脉冲等到 discard 真扣完 +2. 将 `SMARTCAM_TRIGGER_WARMUP_FRAMES` 与内核 skip+drop+warm+gate 文档化对齐(当前 12 够用) +3. ROI 边界强制同一套 quarantine(与 mon→FT 同 arm) + +### 10.11 4h 稳定性复测(2026-08-10 20:57 起) + +- 脚本:板端 `/tmp/imx296_4h_endurance.py --hours 4` → `/tmp/imx296_4h_endurance_1008` +- 报告:`IMX296_4h稳定性复测_20260810.md` +- 覆盖:mon/trig×software/line0、ROI set/reset、底绿、N-1 LO/HI +- 内核保持 `#2026081008`,n1trace 关 + +### 10.12 4h 复测结论(有效窗口 ~2.21h) + +- **green=0 / n1=0**,frames=3570,cycles=64;唯一 bug 为小 ROI `352x328` phase=MIX(非绿、非 hard N-1) +- 未跑满 4h:23:11 CamOS signal 退出,`/tmp` 脚本丢失 +- 详见 `IMX296_4h稳定性复测_20260810.md` + + +--- + +## 11. 2026-08-12 复盘:line0 底绿复发 + 文档/实现偏差(CamOS 侧已回退 post=6) + +> 状态:**文档 §10.9「warm 期间 gate 保持 6」在当前 line0/IRQ 主导路径上不成立** +> 板端内核仍为 `#2026081008`(`skip=2 drop=2 warmup=8 pub_gate=6 force_post=70000`) +> CamOS 分支:`fix/roi-trigger-n1-master-rebase`(warmup 已改为脉冲预算 + quarantine;**已回退** post=6/min_appsink=5) +> 产物:板端 `/userdata/imx296_warmup_ab/n1trace_gate_20260812_145230/`、`line0_sw_green_*`、`line0_post6_*`、`line0_post3_revert_*` +> **本轮未改内核代码**,只修订记录与方案。 + +### 11.1 本轮用户侧现象(相对 §10.9 mon→sw green=0) + +| 项 | 观察 | +|----|------| +| mon→line0 头枪 | 早期曾 leading miss 2–4 枪;CamOS 修 PLAYING/prime/post=3 后可稳定 **first_hit=1** | +| apply 墙钟 | 目标 ≤5s;post=3 路径 avg≈4–5s,prime 最坏 ~16 枪时偶发 >5.5s | +| 底绿 / 底绿线 | **line0 多轮抽检仍绿**:`line0_sw_green_20260812_133403` line0_green=22;`135804` 仍 13(且 first_hit 全 1) | +| CamOS 加 post | post=6 + min_appsink=5:green 仍 12,**first_hit 退化** `[4,1,1,5,1,1,3,7]` → **已回退** post=3 / min_appsink=1 | + +结论:在 first_hit 与 ≤5s 约束下,**CamOS 再堆 residual post 消不掉底绿,且伤头枪**。绿帧主因回到内核发布相位。 + +### 11.2 内核契约(源码,#1008 树,与板一致) + +`rkisp.c` 进 FT arm(常数未改): + +| 符号 | 值 | 含义 | +|------|----|------| +| `SKIP_EARLY_FRAMES` | 2 | 头 SOF 不做 early-done | +| `SKIP_BUF_FRAMES` | 2 | `early_done_drop_left` + `stream->skip_frame` | +| `WARMUP_FRAMES` | 8 | `early_done_warmup_left`;warmup 段 post-MI hold 更长 | +| `PUB_GATE_FRAMES` | 6 | `early_done_pub_gate_left` | +| `FORCE_POST_MI_US` | 70000 | ceiling-force 后 hold | +| `WARMUP_POST_MI_US` | 85000 | warm 段 hold | + +`capture_v39.c` `mi_frame_end` 丢帧条件(摘要): + +```text +warm_drop = ft_diag && frame_early && warm>0 +gate_drop = ft_diag && gate>0 && !drop && !warm_drop +if (drop || warm_drop || gate_drop) → mi_drop(requeue,无 vb2_done) +``` + +**关键点(相对 §10.7–10.9 叙述)**: + +1. **`warm_drop` 要求 `frame_early==1`**。 + 当 complete/发布走 **`early=0`(IRQ / MI FE)** 时,即使 `warm_left>0`,**也不会**走 warm_drop。 +2. 此时若 `drop_left==0` 且 `gate>0`,**`gate_drop` 为真** → **gate 在 warm 尚未耗尽时被 IRQ 路径打掉**。 + 这与 §10.9 写的「drop×2 + warm 递减时 **gate 保持 6**;warm=0 后 gate 6→0 再 mi_pub」**只覆盖 early-done WORK 主导轮**,**不覆盖当前 line0 实测的 IRQ 主导轮**。 +3. `#1006` 曾要求「IRQ 也要吃 gate」(防 r1 全绿),`#1007` 要求「gate 不与 warm 并行扣」。 + 现行布尔式把两者捏在一起:**IRQ + warm>0 ⇒ 仍扣 gate**,形成 **#1007 在 IRQ 路径上的回退洞**。 +4. `warm_left` 主要在 `capture.c` early **complete** 路径递减;`mi_drop` 日志里的 `warm=` 是读快照,**IRQ mi_drop 不替代 warm 语义丢帧**。 +5. `mi_pub` 日志**不打印** `warm_left`/`gate`;对齐时需夹在邻近 `early complete` / 上一条 `mi_drop` 之间读。 + +源码注释仍写:`CamOS SMARTCAM_TRIGGER_WARMUP_FRAMES must cover skip_sof+drop+warm+pub_gate`。 +CamOS 已不再以 appsink discard==0 为成功条件,但 **gate 若在 warm 期被 IRQ 耗尽,用户态脉冲再多也盖不住「gate=0 后的 early mi_pub」画质**。 + +### 11.3 板端 n1trace 证据(2026-08-12 14:52,`n1trace_gate_20260812_145230`) + +同一次 `isp_ft_arm ... skip=2 drop=2 warmup=8 pub_gate=6` 后(节选逻辑序): + +```text +mi_drop IRQ left=1→0 warm=8 gate=6 ← drop 耗尽,gate 仍 6 +mi_drop IRQ left=0 warm=8 gate=5 ← drop 已 0,warm 仍 8,gate 开始减(early=0) +mi_drop IRQ warm=7 gate=4 +... +mi_drop IRQ warm=4 gate=0 cnt=8 ← gate 在 warm 仍 4 时归零 +mi_skip_frame skip=2 → skip=1 +early complete warm_left=3 +mi_pub IRQ pub=1 early=0 drop_left=0 ← 首次发布;gate 早已 0,warm 轨迹仍在中后段 +mi_pub pub=2..12 ... +``` + +对照: + +| §10.9 宣称(early 主导) | 本迹(line0/IRQ 主导) | +|--------------------------|------------------------| +| warm 期 gate **保持 6** | warm 期 gate **6→0**(IRQ mi_drop) | +| warm=0 后 gate 再 6→0 | gate 先于 warm 耗尽 | +| 其后 mi_pub 为「gate 后」 | **首 mi_pub 不在 pub_gate 计数内**(gate 已 0),且 complete 侧 warm_left 仍可 >0 | + +与用户 green tag 对齐的含义: + +- 绿帧 **不是**「仍卡在 pub_gate 计数器里没放行」; +- 而是 **gate 被 IRQ 提前烧光后,仍处 FT 前段(warm/ceiling-force 未稳定)的 mi_pub** 进入 appsink/预览; +- 故 CamOS 在「首 appsink 后再 +3/+6 枪」只能偶尔错位相消,**统计上消不干净**,加长 post 还拉高 first_hit/apply。 + +### 11.4 CamOS 侧已做 / 已否(避免文档继续写「加 warmup 帧必绿清」) + +| 动作 | 结果 | +|------|------| +| 去掉 discard==0 / forced-clear 开闸 | 正确方向;与 §10.10.1 一致(已落地思路) | +| 脉冲预算 + PLAYING/appsink 见帧 + quarantine double-flush | first_hit 可稳 1,apply ~4–5s | +| post residual 3→6 + min_appsink 5 | **绿仍在 + 头枪回退** → **回退** | +| 默认(回退后) | `POST_PLAYING=3`,`MIN_APPSINK_SEEN=1`,`TOTAL_PULSE_MAX=22`,interval 100ms | + +**不要**再把「CamOS 再加 post/min_appsink」写成消绿主路径;主路径应回到内核 gate 与 warm/IRQ 的布尔关系。 + +### 11.5 故障点清单(按优先级) + +1. **P0 内核逻辑洞(本迹已坐实)** + `gate_drop` 在 `early=0 && warm>0` 时仍为真 → **pub_gate 与 warm 在 IRQ 路径并行消耗**,违背 #1007 设计意图。 + **建议修复(尚未改代码,需单独评审/刷机)**: + - 方案 C1(推荐):`gate_drop` 增加 **`warm==0`(或 `!warm`)** 条件,使 gate **仅在 drop 耗尽且 warm 耗尽之后** 才递减;IRQ 与 early 共用。 + 伪代码:`gate_drop = ft_diag && gate && !drop && !warm && /* 可选:允许 IRQ */ true` + 同时保留「IRQ 也必须过 gate」:即 warm 完后的 IRQ pub 仍走 gate_drop,而不是 #1006 那样在 warm 期误 pub。 + - 方案 C2:`warm_drop` 改为 **`warm>0` 不依赖 `frame_early`**(warm 期所有 complete 都 mi_drop)。更猛,需防和 skip_frame/drop 双重计数、以及 hold 路径死等。 + - 方案 C3:`mi_pub` 时若 `warm_left>0` **强制 mi_drop**(发布门槛与 complete 计数解耦)。与 C1 可组合。 + - **验证必须**:n1trace 看 `gate` 在 `warm` 归零前是否恒定;首 `mi_pub` 时 `warm_left==0` 且刚经历 gate 6→0;mon→line0 / mon→sw 绿矩阵 + first_hit + apply。 + +2. **P1 观测缺口** + - `mi_pub` 行无 `warm`/`gate` 字段 → 排障易误判「在 gate 内」。建议 n1trace 增补。 + - 自动化 green 曾被 stale preview / 曝光 API 假阳性干扰;应对 **命中帧** 做 bot8 判定(CamOS QA 已部分改正)。 + +3. **P2 文档过时** + - §10.9 / §10.12 的 green=0 **不推广到 line0 2026-08-12 产品路径**。 + - `IMX296_绿线修复_内核修改与验证记录.md` 仍停在 08-06 早期 skip/drop/warmup 数字,**未反映 #1008 pub_gate**;以本文件 §10–§11 与源码为准。 + - CamOS 仓库 plan(`docs/plans/active/2026-08-11_IMX296_warmup...`)写「画质主防护在内核 pub_gate」——**方向对,但须注明 IRQ 洞未修前 line0 仍可能绿**。 + +4. **P3 非根因(已排除作主手段)** + - 单纯再加 `force_post` / CamOS post residual(§3、§11.1 已否定为主因)。 + - 用户态 appsink discard 收齐(多数 pulse 本就无 sample)。 + +### 11.6 与 CamOS 的协作契约(修订) + +| 层 | 应保证 | 不应指望 | +|----|--------|----------| +| 内核 | 进 FT 后 **首帧 mi_pub 时 warm_left==0 且 pub_gate 已按设计耗尽**;IRQ/early 一致 | 用户态多脉冲「盖住」gate 提前烧光后的脏 pub | +| CamOS | mon→FT / ROI 后 XTRIG 预算 + quarantine;**success 后才** line0-stable gate;first_hit=1 与 apply≤5s | 用 post≥6/min_appsink≥5 消绿 | +| 联调 | 短 n1trace:arm → drop → warm → gate → 首 mi_pub 单调 | 只看 CamOS success=1 或只看 endurance green 汇总 | + +### 11.7 建议下一动作(内核树,未执行) + +1. 按 **C1** 改 `capture_v39.c` 布尔条件 + `mi_pub` 日志带上 `warm/gate`。 +2. 本地编译刷 `#20260812xx`,重跑: + - mon→line0 ×8 + mon→sw ×4 绿矩阵 + - 短 n1trace 确认首 mi_pub 相位 + - first_hit / apply 回归(CamOS 保持 post=3) +3. 通过后回写本节为「已修」,并摘一句到 `IMX296_绿线修复_内核修改与验证记录.md` 文首「最后更新」。 + +### 11.8 文档正确性结论(本次 SSH 核对) + +| 文档 | 结论 | +|------|------| +| 本文件 §1–§3 ceiling-force / hold 满仍可绿 | **仍正确** | +| §10.5–10.8 #1005–#1008 迭代故事 | **历史正确** | +| §10.9「warm 期 gate 保持 6」 | **仅 early 主导轮成立;line0/IRQ 主导轮错误 → 以 §11 为准** | +| §10.10 CamOS forced clear | **方向正确**;CamOS 已转向脉冲预算(discard 门槛已弃) | +| §10.12 4h green=0 | **有效窗口结论保留**;**不覆盖 08-12 line0 复发** | +| `IMX296_绿线修复_*.md`(08-06) | **过旧常数**;勿当 #1008 真源 | +| 源码 `rkisp.c` / `capture_v39.c` 注释 | 与 #1008 意图一致,但 **gate_drop 实现与 #1007 文字意图在 IRQ 路径不一致**(§11.2) | + + + +--- + +## 12. 2026-08-12 #1009 内核修复(gate/IRQ warm 洞) + +> 状态:**源码已合入工作区,构建号 `2026081201`,尚未编译刷板验证** +> 依据:§11.3 板端 `n1trace_gate_20260812_145230` + 源码布尔式复核;line0 矩阵 `line0_sw_green_*` / `line0_post6_*` green 仍在且加 CamOS post 无效。 + +### 12.1 根因确认(板端坐实,非猜测) + +板 `root@192.168.1.180` 仍跑 `#2026081008`。`n1trace_gate_20260812_145230/dmesg_n1trace.txt` 进 FT 后: + +```text +isp_ft_arm ... skip=2 drop=2 warmup=8 pub_gate=6 +mi_drop IRQ left=1→0 warm=8 gate=6 ← drop 耗尽 +mi_drop IRQ left=0 warm=8 gate=5 ← warm 仍 8,gate 开始减(early=0) +... +mi_drop IRQ warm=4 gate=0 ← gate 在 warm 中段烧光 +early complete warm_left=3 +mi_pub IRQ pub=1 early=0 ← 首发布:gate 已 0,warm 轨迹仍 >0 +``` + +对照 `#1008` 代码: + +- `warm_drop = ft_diag && frame_early && warm` → **IRQ early=0 永不 warm_drop** +- `gate_drop = ft_diag && gate && !drop && !warm_drop` → **IRQ+warm>0 仍 gate_drop** +- ⇒ 与 §10.9「warm 期 gate 保持 6」只在 early-WORK 主导轮成立;**line0/IRQ 主导轮不成立** + +产品侧:`line0_sw_green_20260812_135804` totals.line0_green=13、first_hit 全 1、apply≈3.5–5s;`line0_post6` green=12 且 first_hit 退化。**再加 CamOS post 不是正解。** + +### 12.2 代码改动(#1009) + +| 文件 | 改动 | +|------|------| +| `capture_v39.c` | `warm_drop = ft_diag && warm`(IRQ+WORK);`gate_drop = ... && !drop && !warm`;IRQ warm 路径 `warm_left--`(early complete 已减则不双扣);`mi_pub` n1trace 增 `warm/gate` | +| `capture.h` / `rkisp.c` | 注释与 #1009 契约对齐;常数不变 skip/drop/warm/gate=2/2/8/6 | +| `DLCVCAM_BUILD_VERSION` | `2026081201` | + +期望 n1trace(刷板后): + +```text +mi_drop ... warm 递减时 gate 恒为 6 +warm→0 后 gate 6→0 +首 mi_pub 时 warm=0 且刚经历 gate 耗尽(或 gate 刚到 0 的下一帧) +``` + +### 12.3 成功标准(刷板后) + +- mon→line0 ×8 + mon→sw ×4:`TOTAL_GREEN=0`,无底串帧 +- first_hit=1(用户触发不被吞);apply 墙钟 **≈5s 或更短**(CamOS 保持 post=3,不靠加 post) +- ROI set/reset 后同样 green=0 + 有图 +- dmesg 仍 ceiling-force(N-1 不回退);短开 n1trace 核对相位后必须 `echo 0` + +### 12.4 未做 + +- 未编译 / 未刷 `#2026081201` +- 未改 CamOS;禁止再把加 post 当消绿主路径 +- 未动 force_post / SKIP 常数(隔离变量) + + +*记录人:CamOS 联调 2026-08-12;构建机 `root@192.168.1.59` 工作区 `fix_imx296_n1_pwm`;板 `180` `#2026081008`。* diff --git "a/IMX296_\347\273\277\347\272\277_20260812_gate_IRQ\344\277\256\345\244\215\344\270\216\351\252\214\350\257\201.md" "b/IMX296_\347\273\277\347\272\277_20260812_gate_IRQ\344\277\256\345\244\215\344\270\216\351\252\214\350\257\201.md" new file mode 100644 index 0000000000000..581fb5f6f4206 --- /dev/null +++ "b/IMX296_\347\273\277\347\272\277_20260812_gate_IRQ\344\277\256\345\244\215\344\270\216\351\252\214\350\257\201.md" @@ -0,0 +1,477 @@ +# IMX296 底绿 gate/IRQ 修复与验证(2026-08-12) + +> 状态:**#1016 `#2026081301`** — 条件 UV 修补 + **多信号底带检测**;切换耗时纳入验收。 +> 板:`root@192.168.1.180` `#2026081301`;工作区 `fix_imx296_n1_pwm`。 +> **禁止**无条件 0x80 / 加长 warmup·gate;目标 mon→trig apply **2–3s**(硬上限 5s)。 + +--- + +## 1. 目标(产品) + +| 项 | 要求 | +|----|------| +| 画质 | mon↔trigger / ROI set/reset 后无底绿、底串帧、底绿线 | +| 触发 | 进触发后用户第 1 枪有图(不吞触发) | +| 时延 | **硬上限约 5s**;目标 **2–3s**(稳定且无绿/不吞枪) | +| 手段 | **内核完成路径正确性**;禁止再靠加 CamOS post residual / 加长 gate 消绿 | + +--- + +## 2. 根因链(已坐实) + +1. V39+IMX296 FT 上 **MI OFFS 长期为 0** → 几乎 100% **ceiling-force** 完成(N-1 修复必需)。 +2. force_post hold 跑满后,**底 ~8 行 UV 仍可未写完** → classic bot8 绿带。 +3. #1008 IRQ 路径曾把 **pub_gate 在 warm 期烧光**(§11)→ #1009 修相位后仍绿。 +4. #1010/#1011/#1012 加长 gate(6→8→12→13)只能**推迟**脏 pub,且顶破 CamOS 脉冲预算 → apply 变慢/挂死。 + **结论:quarantine 不是根治。** + +--- + +## 3. #1013 根治(`#2026081205`) + +### 3.1 内核 + +| 文件 | 改动 | +|------|------| +| `capture_v39.c` | FT early-done 下 MP **pub 前** `rkisp_ft_sanitize_nv12_bot()`:底 8 行 UV 写中性 chroma `0x80` + DMA cache sync | +| `rkisp.c` | quarantine 收缩:`WARMUP=2`、`PUB_GATE=2`(skip/drop 仍 2)— 仅挡半开入口帧 | +| `DLCVCAM_BUILD_VERSION` | `2026081205` | + +原理:绿带来自 **未完成 UV**,不是整帧无效。Y 已由 MI 写好;补齐/中和尾 UV 即可首枪干净,无需 20+ 次 complete 隔离。 + +### 3.2 CamOS + +- **已回撤**板端 `runtime_settings_dlcvcam.json` 里人为加的 + `SMARTCAM_TRIGGER_WARMUP_TOTAL_PULSE_MAX=32` / `WARMUP_FRAMES=12` 等覆盖。 +- 使用代码默认脉冲预算;**不再**为消绿加 post/min_appsink。 + +### 3.3 板验 mon→software(CamOS 默认,localhost 隔离 UI) + +**矩阵 A** `/userdata/imx296_warmup_ab/verify_k1205_sw/` ×8 head12: + +| 指标 | 结果 | +|------|------| +| green_total | **0**(96 帧,green/ 空) | +| first_hit | 全 **1** | +| bot8 抽检 | 中性灰 `(≈97,97,97)`,无 `(75,102,49)` classic 绿带 | +| dmesg arm | `warmup 2 skip 2 drop 2 pub_gate 2` | +| warmup 日志 | prime≈4–5,pulses≈10,warmup dt≈**2.2s** | +| apply | 有效轮 ~2.8–4.2s;R3 曾 CamOS ReadTimeout 90s(非绿) | + +**矩阵 B 时延复测** `/userdata/imx296_warmup_ab/verify_k1205_sw_lat/` ×8: + +| 指标 | 结果 | +|------|------| +| green_total | **0** | +| first_hit | 全 **1** | +| quality_all_pass / strict_all_pass | **true** | +| apply(有效 mon→trig) | **2.79 / 2.83 / 2.88 / 2.84 / 2.86 / 2.88 / 2.80 s** | +| apply_max(有效) | **2.879s**(≤3s 目标内;≤5s 硬上限) | +| 空切 | R2=0.013s(已在 trigger,不计时延) | + +### 3.4 成功标准对照(**已被 §8 #1015 取代**;#1013 产品接缝 FAIL) + +| 项 | 状态 | +|----|------| +| 无底绿 | **PASS** | +| first_hit=1 | **PASS** | +| apply ≤5s | **PASS**(复测有效轮) | +| apply 2–3s | **PASS**(复测有效轮全在 2.8–2.9s) | +| 不加长 warmup / 已回撤 CamOS 覆盖 | **PASS** | + +--- + +## 4. 历史迭代(勿回退) + +| 构建 | 内容 | 结论 | +|------|------|------| +| #1008 | gate 仅 drop+warm 后 | mon→sw 曾 0 绿;line0/IRQ 洞 | +| #1009 `#1201` | warm/gate IRQ 相位 | 相位 OK;head 仍绿×5 | +| #1010 `#1202` | gate=12 | 仍绿 + 脉冲预算爆 | +| #1011 `#1203` | gate=8 | 仍绿;CamOS success 早于脏窗结束 | +| #1012 `#1204` | gate=13 | 方向错误(更长隔离) | +| **#1013 `#1205`** | **UV sanitize + warm/gate=2** | **绿清零 + 切换回到 ~3s** | + +--- + +## 5. 决策树(后续) + +1. **ROI set/reset** 矩阵(同 sanitize 路径,应同样无绿)。 +2. line0:需外部边沿;无源不硬测 soft。 +3. CamOS 偶发 apply 90s:查 `run-mode-switch` 锁 / 连续 PATCH,与绿线无关。 +4. 若要稳定卡在 2–3s:在 **green=0 已成立** 前提下,可评估降低 CamOS `min_total`/prime(默认仍 ~10 脉冲),**不要**再加内核 gate。 +5. sanitize 是尾 UV 中性化:工业场景底 8 行 chroma 会略去饱和;若不可接受,再改为「仅当检测 bot UV 异常才写」或等 MI FE 后再 pub(慎防 N-1)。 + +--- + +## 6. 文件清单 + +| 路径 | 说明 | +|------|------| +| `drivers/.../isp/capture_v39.c` | `rkisp_ft_sanitize_nv12_bot` + pub 前调用 | +| `drivers/.../isp/rkisp.c` | warm=2 gate=2 注释 #1013 | +| `DLCVCAM_BUILD_VERSION` | 2026081205 | +| 板 `/userdata/imx296_warmup_ab/verify_k1205_sw/` | 矩阵结果 | +| 板 `/userdata/imx296_warmup_ab/scripts/verify_k1205_sw.py` | 可复现脚本(勿放 /tmp) | +| 镜像 | `boot-rk3576-6.1.99-2026081205-01726d6f-dirty.img` | + +--- + +*记录:2026-08-12;板 180;#1013 验证 green=0。* + +--- + +## 7. #1205 真实 CamOS 2h endurance(含 line0)— **已中止 FAIL** + +| 项 | 值 | +|----|----| +| 开始 | 2026-08-12 19:56:10 CST | +| 预计结束 | ~21:56 CST(`--hours 2`) | +| 时长 | **2h** | +| 内核 | `#2026081205` warm=2 gate=2 + UV sanitize | +| 脚本 | `/userdata/imx296_warmup_ab/scripts/imx296_day_endurance_1205.py` | +| 输出 | `/userdata/imx296_warmup_ab/endurance_2h_1205/`(**非 /tmp**) | +| 路径 | mon → software(+ROI) → **line0(+ROI)** → mon;line0 用 sysfs XTRIG | +| CamOS | 默认 runtime_settings(无 pulse_max/warmup 覆盖);n1trace=N | +| 主机监控 | `scripts/imx296_2h_watchdog_1205.py` → `IMX296_2h_line0_watch_1205/` | + +### 7.1 成功标准(用户当次) + +- 切换模式(**尤其进 trigger**)与 **ROI set/reset**:无 **N-1** / **绿帧** / **底部串帧** / **底部绿线** +- line0 真路径(sysfs XTRIG)同标准 +- bugs / green/ 目录无绿线类产物 + +### 7.2 中途快照(~20:00,uptime≈4–5min) + +| 指标 | 值 | +|------|-----| +| cycles | 3 | +| cases | ~20 | +| frames/valid | 140/140 | +| **green** | **0**(green/ 空) | +| **n1 / mix** | **0 / 0** | +| bugs / switch_fail / roi_fail | **0 / 0 / 0** | +| recoveries / crashes | 0 / 0 | +| phase | CUR + INCONCLUSIVE(无 N1/MIX) | +| 已覆盖 | MON、SW_BASE、SW_ROI_*、SW_ROI_RESET、LINE0_BASE、LINE0_ROI_*、LINE0_ROI_RESET | + +完跑后以 `summary.json` + `events.jsonl` + `bugs.jsonl` + `green/` 终态回填本节。 + +### 7.3 用户驳回(2026-08-12 ~20:06)— 检测漏报 + sanitize 接缝 + +**用户样例(产品存图 OK)**: +- `images/2026-08-12/OK/20260812_200339_019_111723.jpg` +- `images/2026-08-12/OK/20260812_200536_442_585339.jpg` +- `images/2026-08-12/OK/20260812_200455_830_639063.jpg` + +**像素事实(非 classic G 偏绿)**: +- bot8 RGB ≈ **(18,18,18)** 平坦中性灰;`g_delta=0`,`pure_g=0` +- 紧邻上行 above ≈ **(20,14,28)**(场景偏蓝紫) +- 人眼看到的「底绿线/底带」= **#1013 UV sanitize 中性化造成的色度接缝**(Y 仍在、UV 被写 0x80),不是旧式 (70,88,40) classic 绿带 +- 19:50–20:10 产品 OK **643/643** 张均 `flat_gray/seam`;**classic_or_pure=0** +- endurance `analyze_jpeg` 只判 G>R/B → **系统性漏报**,故 summary green=0 不可信 + +**处置**: +- 已停 2h endurance(PID 76739) +- **#1013 不可当交付**:消 classic 绿的同时引入**每帧可见底灰带/接缝** +- 下一根治方向:禁止无条件整幅尾 UV 刷 0x80;改为「仅异常 UV 才修」或等 MI FE/完整 chroma 再 pub(慎 N-1),并修正检测:flat seam + classic 双判 + +产物:板 `/userdata/imx296_warmup_ab/user_green_recheck_1205/`;主机 `IMX296_user_green_recheck_1205/` + + +### 7.4 中止终态(2026-08-12 20:07,未满 2h) + +| 项 | 值 | +|----|----| +| 进程 | **已停**(用户报产品底绿后 kill;~uptime 11min) | +| cycles/cases/frames | 6 / 45 / 308 valid | +| 脚本 green/n1/mix | 0/0/0(**不可信**,见 §7.3 漏检) | +| green/ 目录 | 0 | +| bugs | **3**(非绿):`SW_ROI2_640x480` TOO_FEW;`switch_fail:LINE0` stopped;`switch_fail:MON_END` stopped | +| switch_fail / roi_fail | 2 / 0 | +| 内核 | `#2026081205` | + +**判定:2h 未完成;#1205 交付 FAIL**(产品底 sanitize 接缝 + 检测假阴性)。勿再跑同一套 green 判据 endurance 当验收。 + +### 7.5 cron/会话复核(2026-08-12 晚,板已升 `#2026081207`) + +| 项 | 值 | +|----|-----| +| 进程 | **无** endurance 进程(已结束) | +| 产物路径 | `/userdata/imx296_warmup_ab/endurance_2h_1205/` 仍在 | +| summary | cycles=**6** cases=45 frames/valid=**308/308** green_frames=**0** n1=**0** mix=0 | +| switch_fail / roi_fail / bugs | **2** / **0** / **3** | +| green/ 目录 | **0** 文件 | +| bugs.jsonl | `SW_ROI2_640x480` TOO_FEW;`switch_fail:LINE0` stopped;`switch_fail:MON_END` stopped | +| 脚本 green=0 | **不可信**(§7.3 旧 G-only 判据;产品样例为 flat seam) | +| mon→trigger / ROI(本 run 内) | SW_BASE/SW_ROI_*/SW_ROI_RESET/LINE0_* 事件均有;**未发现 classic 绿记数**,但 **seam 未检** | +| 后续可信验收 | 见 **§8 #1015 `#2026081207`**:双判 mon→sw + WS ROI **classic=0 seam=0** | + +**终判不变:#1205 2h endurance = 中止 FAIL,不作交付依据。** + + + +--- + + +### 7.6 最终分析(会话收口,数据冻结 20:07) + +**1) 进程** +- PID 文件 `76739`;当前 **无 endurance 进程**(已退出,非跑满 2h)。 +- 运行内核当时:`#2026081205`;复核时板已升 `#2026081207`。 + +**2) 产物(已 scp → 主机 `IMX296_2h_line0_watch_1205/final/`)** +- `summary.json` / `summary.txt` / `events.jsonl` / `bugs.jsonl` / `nohup_outer.log` +- `green/`:**0** 文件 + +**3) 汇总表** + +| 指标 | 值 | 可信度 | +|------|-----|--------| +| 时长 | ~11 min(19:56:10–20:07:02),**未满 2h** | 硬事实 | +| cycles / cases | 6 / 45 | 硬事实 | +| frames / valid | 308 / 308 | 硬事实 | +| green_frames / green/ | **0 / 空** | **脚本 G-only 假阴性**;产品样例为 flat seam | +| n1_cases / mix_cases | **0 / 0** | 事件相位无 hard N1/MIX | +| switch_fail / roi_fail | **2 / 0** | kill 后 stopped | +| bugs | **3** | 见下 | +| recoveries / crashes | 0 / 0 | OK | + +**4) phase_dist(events case,n=45)** + +| phase | count | +|-------|------:| +| CUR | 27 | +| INCONCLUSIVE | 17 | +| TOO_FEW | 1 | +| **N1 / MIX** | **0 / 0** | + +无 hard N-1 / MIX。INCONCLUSIVE 多为 low contrast(MON_FREERUN、部分 ROI reset),**不是** N1。 + +**5) by_case 关键路径** + +| case | n | green | n1 | mix | cur | incon | 备注 | +|------|--:|------:|---:|----:|----:|------:|------| +| SW_BASE | 6 | 0 | 0 | 0 | **6** | 0 | mon→software 全 CUR | +| LINE0_BASE | 5 | 0 | 0 | 0 | **5** | 0 | line0 基线全 CUR(第 6 轮未到) | +| SW_ROI_RESET | 6 | 0 | 0 | 0 | 5 | 1 | ROI 重置无绿记数 | +| LINE0_ROI_RESET | 5 | 0 | 0 | 0 | 1 | 4 | 多 incon(低对比) | +| 各 SW_ROI_* / LINE0_ROI_* | 多次 | 0 | 0 | 0 | 多为 CUR | 部分 incon | 尺寸切换有覆盖 | +| SW_ROI2_640x480 | 1 | 0 | 0 | 0 | 0 | 0 | **TOO_FEW valid=0** → bug | + +**6) bugs 明细** + +1. `SW_ROI2_640x480` phase=TOO_FEW valid=0/6(取流失败,非绿) +2. `switch_fail:LINE0` info=**stopped**(用户/会话中止) +3. `switch_fail:MON_END` info=**stopped** + +**7) 对照用户标准(模式切换尤其进 trigger + ROI set/reset:无 N-1 / 绿帧 / 底串 / 底绿线)** + +| 标准 | 本 2h 脚本结论 | 真实结论 | +|------|----------------|----------| +| 无 hard N-1 | 事件 n1=0、phase 无 N1 | **成立**(短窗内) | +| 无 classic 绿 | green=0 | 脚本未报 classic | +| 无底绿线/接缝 | green=0 | **不成立**:#1013 无条件 UV 0x80 → 产品 bot 平坦灰接缝;旧检测漏报 | +| 跑满 2h 稳定 | — | **不成立**:~11 min 中止 | + +**终判:FAIL(中止 + 检测不可信 + 产品接缝)** +勿将本 run 的 green=0 当作交付。可信短验见 **§8 `#2026081207` 双判** mon→sw / WS ROI。 + + +## 8. #1014/#1015 条件 UV 修补(`#2026081206` → `#2026081207`) + +### 8.1 问题(#1205 交付 FAIL) + +| 项 | 事实 | +|----|------| +| #1013 | FT pub 前 **无条件** `memset` 底 8 行 UV=`0x80` | +| 产品 | bot8 RGB≈(18,18,18) 平坦灰;above 场景色 → **每帧色度接缝** | +| 检测 | endurance 只判 G>R/B → **漏报**;643/643 产品 OK 有接缝 | +| 2h | 中止;脚本 green=0 **不可信** | + +### 8.2 内核 + +| 构建 | 改动 | 板验 | +|------|------|------| +| #1014 `#1206` | 仅当 UV 行相对上行 mean 差 >24 / 近 0/FF 才 `memcpy` 扩展上行 | mon→sw:**seam 仍 9**(暗场 incomplete UV≈0x80,thr 过高) | +| **#1015 `#1207`** | **中性 chroma 占比 >50%** 判坏;mean thr=**6**;坏行扩展 last-good;好行推进 ref | mon→sw + ROI **green=0/seam=0** | + +文件: + +- `capture_v39.c`:`rkisp_ft_uv_line_bad` / `rkisp_ft_sanitize_nv12_bot`(条件修补,禁止无条件 0x80) +- `rkisp.c`:warm=2 gate=2 注释 #1015 +- `DLCVCAM_BUILD_VERSION`:`2026081207` +- 镜像:`boot-rk3576-6.1.99-2026081207-01726d6f-dirty.img` + +### 8.3 检测脚本(同步) + +`scripts/imx296_day_endurance.py` `analyze_jpeg`: + +- **classic**:G 主导 / pure 绿底(原逻辑) +- **seam**:bot8 平坦近灰 + 跳过 bot 上 8 行后的参考带有色 + mean_l1;或 mid 有色 + mid_l1 +- `green = classic OR seam`;明细含 `classic_green` / `seam` + +板端:`/userdata/imx296_warmup_ab/scripts/imx296_day_endurance_1207.py` +矩阵:`verify_k1207_sw.py`、`verify_k1207_roi_ws.py` + +### 8.4 板验 `#2026081207`(双判) + +**mon→software** `/userdata/imx296_warmup_ab/verify_k1207_sw/` ×6 head12: + +| 指标 | 结果 | +|------|------| +| green / classic / seam | **0 / 0 / 0**(72 帧) | +| first_hit | 全 **1** | +| head bot | 场景色 e.g. `(21.9,15.8,30.0)`,**非** (18,18,18) | +| apply 有效 | 2.78–4.31s(R2 CamOS 90s ReadTimeout 与绿无关;空切 0.01s) | +| dmesg arm | warmup 2 skip 2 drop 2 pub_gate 2 | + +**ROI set/reset(WS `rkcam_set_roi`)** `/userdata/imx296_warmup_ab/verify_k1207_roi_ws/`: + +| 步 | dim | green | +|----|-----|-------| +| base full | 1456×1088 | 0 | +| roi 640×480 | 640×480 | 0 | +| roi 800×600 | 800×600 | 0 | +| reset | 1456×1088 | 0 | +| roi 512×384 | 512×384 | 0 | +| reset2 | 1456×1088 | 0 | + +first_hit 全 1;`quality_pass=true`;`dim_changed=true`(ROI 真生效)。 + +### 8.5 对照 + +| 构建 | classic | 接缝 | 检测可信 | 结论 | +|------|---------|------|----------|------| +| #1205 #1013 | 0 | **每帧** | 否 | FAIL | +| #1206 #1014 | 0 | 头枪仍有 | 是 | 条件 thr 不够 | +| **#1207 #1015** | **0** | **0** | **是** | 短矩阵 PASS | + +### 8.6 未做 / 下一步 + +- 长时 endurance(2h line0)尚未用新检测重跑 +- line0 外部边沿短矩阵可选 +- CamOS 偶发 apply 90s 仍与绿线无关 + +--- + +*记录:2026-08-12 晚;板 180;#1015 `#2026081207` mon→sw + ROI 双判 green=0。* + + +--- + +## 9. #1016 + 新检测 `#2026081301`(2026-08-13) + +### 9.1 内核 + +| 项 | 内容 | +|----|------| +| 构建 | `#2026081301` / `boot-rk3576-6.1.99-2026081301-01726d6f-dirty.img` | +| 策略 | 条件 UV 尾修补:mean thr=6;**neu 占比仅当 ref 有 chroma**(`REF_CHROMA_MIN=10`);坏行 `memcpy` last-good;禁止整尾 0x80 | +| quarantine | warm=2 gate=2 skip=2 drop=2(**未加长**) | + +### 9.2 新检查方法(替换旧 G-only) + +`scripts/imx296_bottom_quality.py`(Pillow,板端可用): + +| 信号 | 含义 | +|------|------| +| **classic** | 底带比 above/mid **更** G 主导(避免全局绿场景误报) | +| **seam** | bot 平坦近灰 + 场景有色 + L1(抓 #1013 接缝) | +| **tear** | 行跳变 / 强 L1 不连续 | +| `green/bad` | classic ∨ seam ∨ tear | + +接入:`imx296_day_endurance.py`、`verify_k1301_lat.py`。 +已知 #1013 产品样例:seam=True(回归检测有效)。 + +### 9.3 板验(含切换耗时)`/userdata/imx296_warmup_ab/verify_k1301_lat/` + +**mon→software ×8**(仅统计 real_switch apply≥0.5s): + +| 指标 | 结果 | +|------|------| +| bad / classic / seam / tear | **0 / 0 / 0 / 0** | +| first_hit | 全 **1** | +| apply min/avg/max | **2.832 / 2.869 / 2.902 s** | +| ≤3s / ≤5s | **PASS / PASS** | +| 全部 real apply | `[2.902, 2.880, 2.879, 2.876, 2.845, 2.832, 2.836, 2.900]` | + +**ROI WS set/reset**:base→640→800→reset→512→reset2;尺寸真变;bad=0;first=1。 + +| 总判 | quality_pass | latency_pass_goal(≤3s) | latency_pass_hard(≤5s) | +|------|--------------|------------------------|-------------------------| +| | **true** | **true** | **true** | + +### 9.4 后续缩短耗时(方向,未做) + +当前 ~2.87s 已在 2–3s 目标内。若要再压: + +1. CamOS 侧:warmup 脉冲/prime/cooldown(**先质量再减**) +2. 内核:在 seam/classic=0 前提下评估 warm/gate 1 或略减 force_post(需双判+时延矩阵) +3. 勿再靠加长 quarantine + +板 `/userdata` 仅 1G:旧 verify 帧已清理;产物勿堆满。 + + +--- + +## 10. 真实验收(smartcam_bs 用户场景,`#2026081301`) + +### 10.1 环境 + +| 项 | 内容 | +|----|------| +| 板 | 180 `root@192.168.1.180` | +| 内核 | `#2026081301` / #1016 条件 UV 尾修补 | +| CamOS 启动 | **`/root/smartcam_bs/0_测试运行_camos.sh`**(官方测试脚本,含 frontend build + `python server.py`) | +| runtime | 默认;**无**额外 warmup/gate 覆盖 | +| n1trace | off | +| 检测 | `imx296_bottom_quality`(classic / seam / tear) | +| 脚本 | `scripts/verify_accept_user.py` | +| 产物 | 板 `/userdata/imx296_warmup_ab/verify_accept_user/`;主机 `IMX296_verify_accept_user_1301/` | + +### 10.2 场景矩阵 + +| 场景 | 次数 | 说明 | +|------|------|------| +| mon→software | ×6,每轮 head=12 | PATCH flow_config;测 apply 墙钟 + 底带 + first_hit + tag 唯一 | +| N-1 software | 连续 16 次 soft trigger | 每枪新 `x-frame-tag`;first=1;非冻结 mean | +| mon→line0 | ×4,head=12 | `trigger_mode=line0` + sysfs `4-001a/trigger` | +| N-1 line0 | 连续 16 次 XTRIG | 同上 | +| ROI WS | base→640→800→reset→512→reset2 | `rkcam_set_roi` / `rkcam_reset_roi` | + +### 10.3 结果(ACCEPT **PASS**) + +| 项 | 结果 | +|----|------| +| bad_total / classic / seam / tear | **0 / 0 / 0 / 0** | +| first_hit 全 1 | **true** | +| tag 唯一(无重复发布帧) | **true** | +| n1_pass software + line0 | **true / true**(16/16,miss=0,frozen=false) | +| ROI 尺寸真变 | 1456→640→800→1456→512→1456 | +| apply real n=10 | min **2.791** / avg **2.835** / max **2.866** s | +| ≤3s / ≤5s | **PASS / PASS** | +| `accept_pass` | **true** | + +说明:进 trigger 后 tag 常从 1 重计(prev 监控 tag→1 的 delta 为负属复位,**不**判 N-1);序列内 tag 严格 1,2,3… 递增且每枪新帧。 + +### 10.4 关注点核对 + +| 用户关注 | 结论 | +|----------|------| +| 切到触发后绿帧 | 未检出 | +| 底部绿线(classic) | 0 | +| 底部接缝(#1013 灰缝) | 0 | +| 底部串帧/tear | 0 | +| 第 n 次触发输出第 n-1 帧 | 未出现:first_hit=1、tag 唯一递增、无 miss | +| 模式切换耗时 | ~2.83s,在 2–3s 目标内、硬上限 5s 内 | + +### 10.5 备注 + +- CamOS monitor 侧 `trigger_mode` 规范值为 **`close`**(`none` 会映射为 close)。 +- 验收帧已从板端清理,仅留 `summary.json` + n1 rows,避免 `/userdata` 爆满。 +- 长时 endurance 仍可用新检测另开;本轮为用户场景短矩阵真实验收。 + +--- + +*记录:2026-08-13;板 180;`#2026081301` + smartcam_bs 默认 CamOS;ACCEPT PASS。* diff --git "a/IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" "b/IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" index 1d7556d9f69a8..8602844a8eb58 100644 --- "a/IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" +++ "b/IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" @@ -1,7 +1,8 @@ # IMX296 绿线/串帧修复 — 内核修改与长期验证记录 > 长期记忆与人工复核文件。本文件随每次新修改、新发现持续更新。 -> 最后一次更新:2026-08-06 13:40(内核 procfs 门闩已刷板 `#2026080601`,accept/reject/skip 冒烟通过) +> 最后一次更新:2026-08-12(**#1008 之后 line0 底绿复发与 gate/IRQ 洞**见同目录 `IMX296_绿线_20260810_ceiling_force排查记录.md` **§11**;本文件主体仍为 08-06 早期策略,常数勿当 #1008 真源) +> 此前:2026-08-06 13:40(内核 procfs 门闩已刷板 `#2026080601`,accept/reject/skip 冒烟通过) --- diff --git a/drivers/media/platform/rockchip/isp/capture.c b/drivers/media/platform/rockchip/isp/capture.c index c53578d840da2..a8634a46cdd41 100644 --- a/drivers/media/platform/rockchip/isp/capture.c +++ b/drivers/media/platform/rockchip/isp/capture.c @@ -742,13 +742,15 @@ static u32 rkisp_early_done_post_hold_us(struct rkisp_capture_device *cap_dev, hold = steady; /* - * Ceiling-force means MI offs never verified. Board evidence - * (1456x592 green bot~8): residual+16ms still UV-incomplete. - * Floor at force_post, and add a slice of residual again so short - * ROI cannot under-hold after wait_line. + * Ceiling-force means MI offs never verified. Board evidence: + * - 1456x592 green bot~8 with residual+16ms + * - 1456x1088 mon→FT head frames still green with force=45ms (2026-08-10) + * Floor at force_post; add most of residual again so short ROI and + * full-height mode-switch cannot under-hold after wait_line. Warmup + * frames already prefer warm post; still raise force floor over them. */ if (force_path) { - u32 extra = residual / 2; + u32 extra = residual - (residual / 4); /* ~3/4 residual */ if (force > hold) hold = force; diff --git a/drivers/media/platform/rockchip/isp/capture.h b/drivers/media/platform/rockchip/isp/capture.h index 92ac2ab15e27b..da0b1ba30c782 100644 --- a/drivers/media/platform/rockchip/isp/capture.h +++ b/drivers/media/platform/rockchip/isp/capture.h @@ -377,6 +377,12 @@ struct rkisp_capture_device { u32 early_done_skip_frames; /* Drop next N completed MP buffers (survives stream->skip_frame=0). */ u32 early_done_drop_left; + /* + * Drop next N MP completes after drop+warm drain (IRQ and WORK). + * 2026-08-10 #1005: warm alone still let ~5 green frames after warm=0. + * 2026-08-12 #1009: must not decrement while warm>0 (IRQ hole in #1008). + */ + u32 early_done_pub_gate_left; /* N-1 phase diagnostics (FT only): counts published / dropped MP frames. */ u32 early_done_diag_pub; u32 early_done_diag_drop; diff --git a/drivers/media/platform/rockchip/isp/capture_v39.c b/drivers/media/platform/rockchip/isp/capture_v39.c index e022b385aaa2a..46cbdbce856b5 100644 --- a/drivers/media/platform/rockchip/isp/capture_v39.c +++ b/drivers/media/platform/rockchip/isp/capture_v39.c @@ -2,6 +2,8 @@ /* Copyright (c) 2023 Rockchip Electronics Co., Ltd. */ #include +#include +#include #include #include #include @@ -1033,6 +1035,222 @@ static int isp_frame_end(struct rkisp_stream *stream, u32 irq) * is processing and we should set up buffer for next-next frame, * otherwise it will overflow. */ + +/* + * V39+IMX296 FT: MI OFFS stay 0 → every frame is ceiling-force. Last NV12 + * chroma rows can still be incomplete after force_post hold. + * + * #1013 unconditional 0x80 → permanent flat-grey seam (#1205 FAIL). + * #1014/#1015: conditional repair. #1016: neutral-fraction only when the + * reference line is *not* neutral (scene has chroma); always honor mean + * jump / stuck / classic-G. Extend last good UV line. Gate stays tiny. + */ +#define RKISP_FT_NV12_UV_TAIL_ROWS 8U +/* |meanU-refU| / |meanV-refV| — dark colored scenes need a low bar. */ +#define RKISP_FT_UV_LINE_MEAN_THR 6U +/* Sample counts as near-neutral chroma (buffer-clear / incomplete). */ +#define RKISP_FT_UV_NEU_LO 0x78U +#define RKISP_FT_UV_NEU_HI 0x88U +/* Bad if this fraction of UV pairs is near-neutral *and* ref has chroma. */ +#define RKISP_FT_UV_NEU_FRAC_NUM 1U +#define RKISP_FT_UV_NEU_FRAC_DEN 2U +/* ref |u-0x80| or |v-0x80| above this ⇒ scene chroma present. */ +#define RKISP_FT_UV_REF_CHROMA_MIN 10U +#define RKISP_FT_UV_NEAR_FRAC_NUM 3U +#define RKISP_FT_UV_NEAR_FRAC_DEN 4U + +static bool rkisp_ft_uv_line_bad(const u8 *line, u32 bpl, u8 ref_u, u8 ref_v) +{ + u32 i, n = bpl / 2; + u64 su = 0, sv = 0; + u32 near0 = 0, nearff = 0, neu = 0; + u32 mu, mv, du, dv, ref_du, ref_dv; + u8 u, v; + bool ref_has_chroma; + + if (n < 4) + return false; + + for (i = 0; i < n; i++) { + u = line[i * 2]; + v = line[i * 2 + 1]; + su += u; + sv += v; + if (u < 8 && v < 8) + near0++; + if (u > 248 && v > 248) + nearff++; + if (u >= RKISP_FT_UV_NEU_LO && u <= RKISP_FT_UV_NEU_HI && + v >= RKISP_FT_UV_NEU_LO && v <= RKISP_FT_UV_NEU_HI) + neu++; + } + mu = (u32)(su / n); + mv = (u32)(sv / n); + du = mu > ref_u ? mu - ref_u : ref_u - mu; + dv = mv > ref_v ? mv - ref_v : ref_v - mv; + ref_du = ref_u > 0x80 ? ref_u - 0x80 : 0x80 - ref_u; + ref_dv = ref_v > 0x80 ? ref_v - 0x80 : 0x80 - ref_v; + ref_has_chroma = (ref_du >= RKISP_FT_UV_REF_CHROMA_MIN || + ref_dv >= RKISP_FT_UV_REF_CHROMA_MIN); + + /* Unwritten / stuck blocks. */ + if (near0 * RKISP_FT_UV_NEAR_FRAC_DEN > n * RKISP_FT_UV_NEAR_FRAC_NUM) + return true; + if (nearff * RKISP_FT_UV_NEAR_FRAC_DEN > n * RKISP_FT_UV_NEAR_FRAC_NUM) + return true; + /* + * Incomplete FT UV often stays buffer-clear ~0x80 while Y is done. + * Only treat high neutral fraction as bad when the line above has + * real scene chroma — avoids rewriting naturally neutral bottoms. + */ + if (ref_has_chroma && + neu * RKISP_FT_UV_NEU_FRAC_DEN > n * RKISP_FT_UV_NEU_FRAC_NUM) + return true; + /* Mean jump vs last good line (primary for dark colored scenes). */ + if (du > RKISP_FT_UV_LINE_MEAN_THR || dv > RKISP_FT_UV_LINE_MEAN_THR) + return true; + /* Classic G band: both U/V depressed vs neutral while ref is higher. */ + if (mu + 20 < 0x80 && mv + 20 < 0x80 && + (ref_u > mu + 10 || ref_v > mv + 10)) + return true; + + return false; +} + +static void rkisp_ft_uv_line_mean(const u8 *line, u32 bpl, u8 *out_u, u8 *out_v) +{ + u32 i, n = bpl / 2; + u64 su = 0, sv = 0; + + if (n == 0) { + *out_u = 0x80; + *out_v = 0x80; + return; + } + for (i = 0; i < n; i++) { + su += line[i * 2]; + sv += line[i * 2 + 1]; + } + *out_u = (u8)(su / n); + *out_v = (u8)(sv / n); +} + +static void rkisp_ft_sanitize_nv12_bot(struct rkisp_stream *stream, + struct rkisp_buffer *buf) +{ + struct rkisp_device *dev = stream->ispdev; + struct v4l2_pix_format_mplane *pixm = &stream->out_fmt; + struct vb2_buffer *vb; + struct device *dma_dev; + struct sg_table *sgt; + u8 *base, *uv, *tail, *ref, *line; + u8 ref_u, ref_v; + u32 fourcc, bpl, h, y_size, uv_size, tail_rows, tail_bytes; + u32 uv_off, uv_rows, ref_off, i; + u32 sync_off, sync_len; + int uv_plane; + bool repaired = false; + + if (!buf || stream->id != RKISP_STREAM_MP) + return; + if (!dev->cap_dev.is_done_early) + return; + if (!dev->hw_dev) + return; + + fourcc = pixm->pixelformat; + if (fourcc != V4L2_PIX_FMT_NV12 && fourcc != V4L2_PIX_FMT_NV12M && + fourcc != V4L2_PIX_FMT_NV21 && fourcc != V4L2_PIX_FMT_NV21M) + return; + + base = buf->vaddr[0]; + if (!base) + base = vb2_plane_vaddr(&buf->vb.vb2_buf, 0); + if (!base) + return; + + bpl = pixm->plane_fmt[0].bytesperline; + h = pixm->height; + if (!bpl || h < 4) + return; + + y_size = bpl * h; + tail_rows = RKISP_FT_NV12_UV_TAIL_ROWS; + uv_rows = h / 2; + if (tail_rows > uv_rows) + tail_rows = uv_rows; + if (tail_rows >= uv_rows || tail_rows == 0) + return; + tail_bytes = bpl * tail_rows; + if (!tail_bytes) + return; + + if (pixm->num_planes >= 2) { + uv = buf->vaddr[1]; + if (!uv) + uv = vb2_plane_vaddr(&buf->vb.vb2_buf, 1); + uv_size = pixm->plane_fmt[1].sizeimage; + uv_plane = 1; + uv_off = 0; + } else { + uv = base + y_size; + uv_size = y_size / 2; + uv_plane = 0; + uv_off = y_size; + } + if (!uv || uv_size < bpl * (tail_rows + 1)) + return; + if (tail_bytes + bpl > uv_size) + return; + + ref_off = uv_size - tail_bytes - bpl; + tail = uv + (uv_size - tail_bytes); + ref = uv + ref_off; + sync_off = ref_off; + sync_len = tail_bytes + bpl; + vb = &buf->vb.vb2_buf; + dma_dev = dev->hw_dev->dev; + + if (dev->hw_dev->is_dma_sg_ops) { + sgt = vb2_dma_sg_plane_desc(vb, uv_plane); + if (sgt) + dma_sync_sgtable_for_cpu(dma_dev, sgt, DMA_BIDIRECTIONAL); + } else { + dma_addr_t da = vb2_dma_contig_plane_dma_addr(vb, uv_plane); + + if (da) + dma_sync_single_for_cpu(dma_dev, da + uv_off + sync_off, + sync_len, DMA_BIDIRECTIONAL); + } + + rkisp_ft_uv_line_mean(ref, bpl, &ref_u, &ref_v); + + for (i = 0; i < tail_rows; i++) { + line = tail + i * bpl; + if (!rkisp_ft_uv_line_bad(line, bpl, ref_u, ref_v)) { + /* Advance reference through good tail lines. */ + ref = line; + rkisp_ft_uv_line_mean(ref, bpl, &ref_u, &ref_v); + continue; + } + memcpy(line, ref, bpl); + repaired = true; + } + + if (dev->hw_dev->is_dma_sg_ops) { + sgt = vb2_dma_sg_plane_desc(vb, uv_plane); + if (sgt) + dma_sync_sgtable_for_device(dma_dev, sgt, DMA_BIDIRECTIONAL); + } else { + dma_addr_t da = vb2_dma_contig_plane_dma_addr(vb, uv_plane); + + if (da) + dma_sync_single_for_device(dma_dev, da + uv_off + sync_off, + sync_len, DMA_BIDIRECTIONAL); + } + (void)repaired; +} + static int mi_frame_end(struct rkisp_stream *stream, u32 state) { struct rkisp_device *dev = stream->ispdev; @@ -1091,17 +1309,58 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) if (stream->id == RKISP_STREAM_MP) { u32 drop = READ_ONCE(dev->cap_dev.early_done_drop_left); - - if (drop) { - WRITE_ONCE(dev->cap_dev.early_done_drop_left, drop - 1); + /* + * 2026-08-12 #1009 (fix #1008 IRQ hole, ceiling-force doc §11): + * mon→FT is ~100% ceiling-force. Quarantine order must be + * drop → warm → pub_gate on BOTH early WORK and MI FE IRQ. + * + * #1008 bug: warm_drop required frame_early, so IRQ (early=0) + * with warm>0 fell into gate_drop and burned pub_gate while + * warm was still high; first mi_pub then landed mid-warm with + * gate already 0 (line0 green). CamOS post residual cannot fix. + * + * #1009: + * - warm_drop: any MP complete while warm>0 (IRQ+WORK) + * - gate_drop: only after drop==0 && warm==0 (still covers IRQ) + * CamOS SMARTCAM_TRIGGER_WARMUP must cover skip+drop+warm+gate. + */ + u32 warm = READ_ONCE(dev->cap_dev.early_done_warmup_left); + u32 gate = READ_ONCE(dev->cap_dev.early_done_pub_gate_left); + /* Warm quarantine is path-agnostic (IRQ and early WORK). */ + bool warm_drop = ft_diag && warm; + /* + * Gate starts only after drop+warm are exhausted. IRQ pubs + * after warm still consume gate (#1006 r1); they must NOT + * steal gate during warm (#1007 intent, restored for IRQ). + */ + bool gate_drop = ft_diag && gate && !drop && !warm; + + if (drop || warm_drop || gate_drop) { + if (drop) + WRITE_ONCE(dev->cap_dev.early_done_drop_left, + drop - 1); + /* + * Consume warm on the publish path when early-done did + * not already decrement it (IRQ / non-early completes). + * early complete already did warm-- before mi_frame_end; + * do not double-count those. + */ + if (warm_drop && !stream->frame_early) + WRITE_ONCE(dev->cap_dev.early_done_warmup_left, + warm - 1); + if (gate_drop) + WRITE_ONCE(dev->cap_dev.early_done_pub_gate_left, + gate - 1); if (ft_diag) { WRITE_ONCE(dev->cap_dev.early_done_diag_drop, READ_ONCE(dev->cap_dev.early_done_diag_drop) + 1); rkisp_n1trace_info(&dev->v4l2_dev, - "n1trace mi_drop state=%s left=%u cnt=%u early=%d sof=%llu t_ns=%llu\n", + "n1trace mi_drop state=%s left=%u warm=%u gate=%u cnt=%u early=%d sof=%llu t_ns=%llu\n", state == FRAME_WORK ? "WORK" : state == FRAME_IRQ ? "IRQ" : "OTH", - drop - 1, + READ_ONCE(dev->cap_dev.early_done_drop_left), + READ_ONCE(dev->cap_dev.early_done_warmup_left), + READ_ONCE(dev->cap_dev.early_done_pub_gate_left), READ_ONCE(dev->cap_dev.early_done_diag_drop), stream->frame_early, sof_ns, ktime_get_ns()); @@ -1177,14 +1436,21 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) WRITE_ONCE(dev->cap_dev.early_done_diag_pub, READ_ONCE(dev->cap_dev.early_done_diag_pub) + 1); rkisp_n1trace_info(&dev->v4l2_dev, - "n1trace mi_pub state=%s vb_seq=%u early=%d drop_left=%u pub=%u sof=%llu t_ns=%llu\n", + "n1trace mi_pub state=%s vb_seq=%u early=%d drop_left=%u warm=%u gate=%u pub=%u sof=%llu t_ns=%llu\n", state == FRAME_WORK ? "WORK" : state == FRAME_IRQ ? "IRQ" : "OTH", seq, stream->frame_early, READ_ONCE(dev->cap_dev.early_done_drop_left), + READ_ONCE(dev->cap_dev.early_done_warmup_left), + READ_ONCE(dev->cap_dev.early_done_pub_gate_left), READ_ONCE(dev->cap_dev.early_done_diag_pub), sof_ns, ns); } + /* + * #1016: conditional UV-tail repair (ref-chroma-gated neu + mean). + * #1013 0x80 seam FAIL; #1014/#1015 thr/neu iterations. + */ + rkisp_ft_sanitize_nv12_bot(stream, buf); 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); diff --git a/drivers/media/platform/rockchip/isp/rkisp.c b/drivers/media/platform/rockchip/isp/rkisp.c index 0cdf3093b0e62..67fe04b92e127 100644 --- a/drivers/media/platform/rockchip/isp/rkisp.c +++ b/drivers/media/platform/rockchip/isp/rkisp.c @@ -69,30 +69,53 @@ * residual and still applies force_post_mi hold so N-1 stays aligned * without publishing at bare wait_line. ROI stream-on re-seeds skip/drop. */ -#define ISP39_IMX296_TAIL_WAIT_US_DEFAULT 35000U +#define ISP39_IMX296_TAIL_WAIT_US_DEFAULT 45000U #define ISP39_IMX296_TAIL_WAIT_US_MAX 100000U #define ISP39_IMX296_LINE_US_MIN 80U #define ISP39_IMX296_LINE_US_MAX 250U #define ISP39_IMX296_EARLY_DONE_POLL_US 50U -#define ISP39_IMX296_POST_MI_US 20000U +#define ISP39_IMX296_POST_MI_US 25000U /* * Ceiling-force path floor: MI OFFS stay 0 so we never see live full. - * Green is held by residual + force_post, NOT by dropping user shots. - * Product 3–5 fps: CamOS already warms 6 frames before opening the - * external gate; kernel must not burn another ~10 user triggers. + * 2026-08-10 #1005: warm_left drop alone is NOT enough — first ~5 + * mi_pub after warm_left==0 still classic green. Quarantine is + * PUB_GATE_FRAMES MP pubs after drop+warm (see capture_v39 #1009). + * CamOS SMARTCAM_TRIGGER_WARMUP must cover skip_sof + skip_buf + * + warm + pub_gate completes so user 1:1 starts after gate. */ -#define ISP39_IMX296_FORCE_POST_MI_US 45000U -/* First FT frames after free-run/ROI stream-on (longer hold only). */ -#define ISP39_IMX296_WARMUP_POST_MI_US 55000U -#define ISP39_IMX296_WARMUP_FRAMES 6U +#define ISP39_IMX296_FORCE_POST_MI_US 70000U +/* First FT frames after free-run/ROI stream-on (longer hold). */ +#define ISP39_IMX296_WARMUP_POST_MI_US 85000U +#define ISP39_IMX296_WARMUP_FRAMES 2U /* SOFs that skip early-done (MI FE only) after free-run/ROI->FT. */ #define ISP39_IMX296_SKIP_EARLY_FRAMES 2U /* - * Drop only the first completed MP buffers after FT stream-on (half-open). - * Keep tiny: CamOS residual_discard/warmup covers mode-switch quarantine. - * Large drop was the "trigger but no photo" for the first 10–12 shots. + * Extra half-open drops at FT stream-on (IRQ path / residual). + * Keep tiny — green quarantine is early_done_pub_gate_left. */ #define ISP39_IMX296_SKIP_BUF_FRAMES 2U +/* + * Post drop+warm MP pub quarantine (IRQ and WORK). #1007/#1009: gate + * must NOT decrement while warm>0 (IRQ path burned gate in #1008). + * #1009 board (#1201): phase fixed but mon→sw still bot8 green on + * user head 1–5 after gate=6 (same #1005 residual). #1010 gate=12 + * still green and blew CamOS default pulse max 22 → apply hang. + * #1011 gate=8: phase OK, but align n1trace shows first mi_pub at + * warm=0/gate=0 still followed by user head 1–5 classic bot8 green + * (CamOS success at ~20 pulses / appsink_seen≈4 discards only pub1–4; + * user head1=pub5 still dirty). Dirty window ≈5 pubs after quarantine. + * #1012: gate=13 → skip2+drop2+warm8+gate13=25 completes. CamOS + * SMARTCAM_TRIGGER_WARMUP_TOTAL_PULSE_MAX must be ≥28 (default 22 + * is too small). Do not rely on CamOS post residual to erase green. + */ +/* + * #1013–#1016: UV-tail repair on FT pub (capture_v39) replaces long gate. + * #1016: neu-frac only if ref has chroma; mean thr=6; extend last-good. + * Keep tiny drop/warm/gate only for half-open FT entry buffers. + * Budget skip2+drop2+warm2+gate2 ≈ 8 completes → CamOS default pulses OK, + * mon→FT apply target 2–3s without raising TOTAL_PULSE_MAX. + */ +#define ISP39_IMX296_PUB_GATE_FRAMES 2U static unsigned int rkisp_imx296_tail_wait_us = ISP39_IMX296_TAIL_WAIT_US_DEFAULT; module_param_named(imx296_tail_wait_us, rkisp_imx296_tail_wait_us, uint, 0644); @@ -141,10 +164,11 @@ static u32 rkisp_imx296_residual_ceiling_us(u32 width, u32 height, * tail. Observed green band is ~OUT_LINE tail (8–10 rows) of bad UV * when force completes too early — use ~half-frame chroma time. */ - tail_us = (remain + ISP39_OUT_LINE_COUNTER_TAIL * 6) * line_us + - (height / 2) * (line_us / 2) + 5000; - if (tail_us < 30000) - tail_us = 30000; + tail_us = (remain + ISP39_OUT_LINE_COUNTER_TAIL * 8) * line_us + + (height / 2) * (line_us / 2) + 8000; + /* Full-height mon→FT green still needs a solid residual floor. */ + if (tail_us < 40000) + tail_us = 40000; param = rkisp_imx296_tail_wait_us; if (param < tail_us) @@ -2501,6 +2525,7 @@ static int rkisp_isp_start(struct rkisp_device *dev) dev->cap_dev.early_done_warmup_left = 0; dev->cap_dev.early_done_skip_frames = 0; dev->cap_dev.early_done_drop_left = 0; + dev->cap_dev.early_done_pub_gate_left = 0; dev->cap_dev.early_done_diag_pub = 0; dev->cap_dev.early_done_diag_drop = 0; dev->cap_dev.early_done_diag_skip_sof = 0; @@ -2535,10 +2560,11 @@ static int rkisp_isp_start(struct rkisp_device *dev) * stream->skip_frame being cleared in rkisp_start(). */ dev->cap_dev.early_done_drop_left = ISP39_IMX296_SKIP_BUF_FRAMES; + dev->cap_dev.early_done_pub_gate_left = ISP39_IMX296_PUB_GATE_FRAMES; if (mp->streaming) mp->skip_frame = ISP39_IMX296_SKIP_BUF_FRAMES; v4l2_info(&dev->v4l2_dev, - "IMX296 fast trigger: early buffer done at line %u/%u terminal %u, MI-drain ceiling %u us post-MI %u/%u force %u us (warmup %u) skip %u drop %u poll %u us\n", + "IMX296 fast trigger: early buffer done at line %u/%u terminal %u, MI-drain ceiling %u us post-MI %u/%u force %u us (warmup %u) skip %u drop %u pub_gate %u poll %u us\n", dev->cap_dev.wait_line, height, dev->cap_dev.early_done_terminal_line, dev->cap_dev.early_done_delay_us, @@ -2548,13 +2574,15 @@ static int rkisp_isp_start(struct rkisp_device *dev) dev->cap_dev.early_done_warmup_left, dev->cap_dev.early_done_skip_frames, ISP39_IMX296_SKIP_BUF_FRAMES, + ISP39_IMX296_PUB_GATE_FRAMES, dev->cap_dev.early_done_poll_us); rkisp_n1trace_info(&dev->v4l2_dev, - "n1trace isp_ft_arm height=%u wait_line=%u skip=%u drop=%u warmup=%u ceiling_us=%u force_post=%u t_ns=%llu\n", + "n1trace isp_ft_arm height=%u wait_line=%u skip=%u drop=%u warmup=%u pub_gate=%u ceiling_us=%u force_post=%u t_ns=%llu\n", height, dev->cap_dev.wait_line, dev->cap_dev.early_done_skip_frames, ISP39_IMX296_SKIP_BUF_FRAMES, dev->cap_dev.early_done_warmup_left, + ISP39_IMX296_PUB_GATE_FRAMES, dev->cap_dev.early_done_delay_us, dev->cap_dev.early_done_force_post_mi_us, ktime_get_ns()); diff --git a/scripts/imx296_bottom_quality.py b/scripts/imx296_bottom_quality.py new file mode 100644 index 0000000000000..3ec2c32cd2eab --- /dev/null +++ b/scripts/imx296_bottom_quality.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +"""IMX296 bottom-band quality check (classic green / chroma seam / tear). + +Designed to catch: + - classic G-dominant green band (incomplete UV green cast) + - #1013-style flat neutral-grey seam (UV forced 0x80) + - bottom tear / row discontinuity (abrupt band vs scene) + +Pure stdlib + Pillow (no numpy required) so it runs on board CamOS images. +""" +from __future__ import annotations + +from io import BytesIO +from typing import Any + +try: + from PIL import Image +except ImportError: # pragma: no cover + Image = None # type: ignore + +BOT_ROWS = 8 +# Skip transition / UV-footprint zone above bot when sampling reference. +REF_SKIP = 8 +REF_ROWS = 8 + +# classic green +G_DELTA = 20.0 +G_ABS = 80.0 +LAST_G_DELTA = 25.0 + +# seam: flat achromatic bot vs colored scene +GRAY_CH_MAX = 7.0 +FLAT_STD_MAX = 5.5 +SEAM_L1_MIN = 3.0 +SEAM_SAT_MIN = 5.5 +SEAM_LUMA_MAX = 90.0 + +# tear: abrupt discontinuity even if not grey +TEAR_L1_MIN = 10.0 +TEAR_ROW_JUMP = 12.0 # mean |row_i - row_{i-1}| across RGB +TEAR_BOT_STD_MAX = 18.0 # tear band itself not wildly textured noise + + +def _mean_std(vals: list[float]) -> tuple[float, float]: + n = len(vals) or 1 + m = sum(vals) / n + if n < 2: + return m, 0.0 + var = sum((v - m) ** 2 for v in vals) / n + return m, var ** 0.5 + + +def _band_stats(pix, w: int, y0: int, y1: int, x_step: int = 2) -> dict[str, float]: + rs: list[float] = [] + gs: list[float] = [] + bs: list[float] = [] + y0 = max(0, y0) + y1 = max(y0, y1) + for y in range(y0, y1): + for x in range(0, w, x_step): + r, g, b = pix[x, y][:3] + rs.append(float(r)) + gs.append(float(g)) + bs.append(float(b)) + if not rs: + return {"r": 0.0, "g": 0.0, "b": 0.0, "std": 0.0, "sat": 0.0, "luma": 0.0} + mr, sr = _mean_std(rs) + mg, sg = _mean_std(gs) + mb, sb = _mean_std(bs) + sat = max(abs(mr - mg), abs(mg - mb), abs(mr - mb)) + luma = 0.299 * mr + 0.587 * mg + 0.114 * mb + return { + "r": mr, + "g": mg, + "b": mb, + "std": max(sr, sg, sb), + "sat": sat, + "luma": luma, + } + + +def _row_mean(pix, w: int, y: int, x_step: int = 4) -> tuple[float, float, float]: + rs, gs, bs = [], [], [] + for x in range(0, w, x_step): + r, g, b = pix[x, y][:3] + rs.append(float(r)) + gs.append(float(g)) + bs.append(float(b)) + n = len(rs) or 1 + return sum(rs) / n, sum(gs) / n, sum(bs) / n + + +def analyze_image(im: "Image.Image") -> dict[str, Any]: + """Analyze bottom quality of an RGB PIL image.""" + im = im.convert("RGB") + w, h = im.size + pix = im.load() + n = min(BOT_ROWS, h) + + bot = _band_stats(pix, w, h - n, h) + skip = min(REF_SKIP, max(0, h - 2 * n)) + a1 = h - n - skip + a0 = max(0, a1 - REF_ROWS) + above = _band_stats(pix, w, a0, a1) if a1 > a0 else bot + mid = _band_stats(pix, w, h // 2, h // 2 + 1) + + br, bg, bb = bot["r"], bot["g"], bot["b"] + ar, ag, ab = above["r"], above["g"], above["b"] + mr, mg, mb = mid["r"], mid["g"], mid["b"] + + # --- classic green (bottom band vs scene; ignore global green cast) --- + classic = False + reasons: list[str] = [] + mean_l1 = (abs(br - ar) + abs(bg - ag) + abs(bb - ab)) / 3.0 + mid_l1 = (abs(br - mr) + abs(bg - mg) + abs(bb - mb)) / 3.0 + bot_g_ex = bg - max(br, bb) + above_g_ex = ag - max(ar, ab) + mid_g_ex = mg - max(mr, mb) + g_dom = bg > br + G_DELTA and bg > bb + G_DELTA + # True incomplete-UV stripe: bot more G-dominant than above/mid. + if g_dom and bot_g_ex >= above_g_ex + 12 and bot_g_ex >= mid_g_ex + 8: + classic = True + reasons.append("g_delta_band") + if ( + bg > G_ABS + and br < 55 + and bb < 55 + and (mean_l1 >= 3.0 or bot_g_ex >= above_g_ex + 15) + ): + classic = True + reasons.append("pure_g") + lr, lg, lb = _row_mean(pix, w, h - 1) + if ( + lg > lr + LAST_G_DELTA + and lg > lb + LAST_G_DELTA + and (lg - max(lr, lb)) >= (ag - max(ar, ab)) + 10 + ): + classic = True + reasons.append("last_row_g") + + # --- seam: flat grey bot vs colored scene --- + bot_gray = ( + abs(br - bg) <= GRAY_CH_MAX + and abs(bg - bb) <= GRAY_CH_MAX + and abs(br - bb) <= GRAY_CH_MAX + ) + bot_flat = bot["std"] <= FLAT_STD_MAX + scene_sat = max(above["sat"], mid["sat"]) + seam = False + if ( + bot_flat + and bot_gray + and scene_sat >= SEAM_SAT_MIN + and mean_l1 >= SEAM_L1_MIN + and bot["luma"] <= SEAM_LUMA_MAX + ): + seam = True + reasons.append("seam_above") + if ( + not seam + and bot_flat + and bot_gray + and mid["sat"] >= SEAM_SAT_MIN + and mid_l1 >= SEAM_L1_MIN + and bot["luma"] <= SEAM_LUMA_MAX + ): + seam = True + reasons.append("seam_mid") + + # --- tear: abrupt discontinuity (not necessarily grey) --- + # 1) band L1 vs above + # 2) max adjacent-row jump inside last 16 rows + max_jump = 0.0 + y_start = max(0, h - max(16, 2 * n)) + prev = _row_mean(pix, w, y_start) + for y in range(y_start + 1, h): + cur = _row_mean(pix, w, y) + jump = (abs(cur[0] - prev[0]) + abs(cur[1] - prev[1]) + abs(cur[2] - prev[2])) / 3.0 + if jump > max_jump: + max_jump = jump + prev = cur + + tear = False + if ( + mean_l1 >= TEAR_L1_MIN + and bot["std"] <= TEAR_BOT_STD_MAX + and scene_sat >= 4.0 + and max_jump >= TEAR_ROW_JUMP + ): + # avoid flagging classic green twice; still record tear if not classic-only + tear = True + reasons.append("tear_jump") + # strong L1 discontinuity without needing jump (sanitize hard edge) + if ( + not tear + and mean_l1 >= TEAR_L1_MIN + 4 + and bot_flat + and scene_sat >= SEAM_SAT_MIN + and not classic + ): + tear = True + reasons.append("tear_l1") + + bad = bool(classic or seam or tear) + # severity score 0..100 for ranking dumps + score = 0.0 + if classic: + score += 40 + min(40.0, max(0.0, bg - max(br, bb))) + if seam: + score += 35 + min(25.0, mean_l1 * 3) + if tear: + score += 25 + min(20.0, max_jump) + score = min(100.0, score) + + # center luma for phase (endurance compatibility) + ch = max(8, int(h * 0.25)) + cw = max(8, int(w * 0.25)) + y0 = (h - ch) // 2 + x0 = (w - cw) // 2 + center = _band_stats(pix, w, y0, y0 + ch, x_step=4) + # approximate center luma already in center['luma'] but band_stats uses full width step; + # recompute tighter: + cs = [] + for y in range(y0, y0 + ch): + for x in range(x0, x0 + cw, 4): + r, g, b = pix[x, y][:3] + cs.append(0.299 * r + 0.587 * g + 0.114 * b) + mean = sum(cs) / len(cs) if cs else center["luma"] + + return { + "dim": f"{w}x{h}", + "mean": float(mean), + "br": round(br, 1), + "bg": round(bg, 1), + "bb": round(bb, 1), + "mr": round(mr, 1), + "mg": round(mg, 1), + "mb": round(mb, 1), + "ar": round(ar, 1), + "ag": round(ag, 1), + "ab": round(ab, 1), + "bot_std": round(bot["std"], 2), + "above_sat": round(above["sat"], 2), + "mid_sat": round(mid["sat"], 2), + "mean_l1": round(mean_l1, 2), + "mid_l1": round(mid_l1, 2), + "max_row_jump": round(max_jump, 2), + "classic_green": bool(classic), + "seam": bool(seam), + "tear": bool(tear), + "flat_gray": bool(bot_flat and bot_gray), + "reasons": reasons, + "score": round(score, 1), + # endurance / legacy + "green": bad, + "bad": bad, + } + + +def analyze_jpeg(data: bytes) -> dict[str, Any]: + if Image is None: + raise RuntimeError("Pillow required") + return analyze_image(Image.open(BytesIO(data))) + + +def analyze_path(path: str) -> dict[str, Any]: + if Image is None: + raise RuntimeError("Pillow required") + return analyze_image(Image.open(path)) diff --git a/scripts/imx296_day_endurance.py b/scripts/imx296_day_endurance.py new file mode 100644 index 0000000000000..7e4a233231a2f --- /dev/null +++ b/scripts/imx296_day_endurance.py @@ -0,0 +1,1507 @@ +#!/usr/bin/env python3 +"""IMX296 24h endurance via CamOS frontend/backend path (not grabber direct). + +Runs on board against local CamOS (server.py + preview/WS): + login cookie → PATCH /api/flow_config (monitor/trigger × close/software/line0) + → WS rkcam_set_roi / rkcam_reset_roi / rkcam_set_exposure_time / rkcam_trigger_once + → GET /preview/frame + X-Frame-Tag + → LO/HI 曝光交替判 N-1;JPEG 底行判绿线 + +Requires CamOS already up (prefer SMARTCAM_NO_VITE_DEV=1 python server.py, +or systemctl start camos). Do NOT recover via 0_测试运行_camos.sh (rebuilds frontend). +Does not own /dev/video11 directly. + +Outputs under OUT_DIR (default /tmp/imx296_day_endurance): + console.log, events.jsonl, summary.json, summary.txt, bugs.jsonl, green/* +""" +from __future__ import annotations + +import argparse +import asyncio +import http.cookiejar +import json +import os +import random +import signal +import subprocess +import sys +import time +import traceback +import urllib.error +import urllib.request +from datetime import datetime, timezone +from io import BytesIO +from pathlib import Path + +import numpy as np + +try: + import websockets +except ImportError: + print("FATAL: websockets missing", file=sys.stderr) + raise SystemExit(2) + +try: + from PIL import Image +except ImportError: + print("FATAL: Pillow missing", file=sys.stderr) + raise SystemExit(2) + +BASE = os.environ.get("CAMOS_BASE", "http://127.0.0.1:8000") +WS_URL = os.environ.get("CAMOS_WS", "ws://127.0.0.1:8000/ws") +USER = os.environ.get("CAMOS_USER", "admin") +PASS = os.environ.get("CAMOS_PASS", "dlcv2026") + +RUN_MODE_SYSFS = Path("/sys/bus/i2c/devices/4-001a/run_mode") +TRIGGER_SYSFS = Path("/sys/bus/i2c/devices/4-001a/trigger") +PULSE_SYSFS = Path("/sys/bus/i2c/devices/4-001a/trigger_pulse_us") + +LO_US = 100.0 +HI_US = 16000.0 +CENTER_FRAC = 0.25 +BOT_ROWS = 8 +GREEN_UV_THR = 8.0 # unused for JPEG path; kept for summary compatibility +GREEN_G_DELTA = 20.0 +GREEN_G_ABS = 80.0 +# Bottom quality: classic / seam / tear — see scripts/imx296_bottom_quality.py +# 暗场 JPEG 下 span 常 <2;低于此阈值的相位结果标 INCONCLUSIVE,不记 n1/mix bug +PHASE_SPAN_MIN = 2.0 +PHASE_N1_SPAN_MIN = 5.0 # 仅 span 达标才记 hard N1 +# 0.5h 轮 MIX 主因:曝光切换后 1~2 枪仍是旧亮度 / 单帧 outlier 把 mid 拉飞 +PHASE_DROP_HEAD = 2 # 有效样本前再丢 head(切换残留) +PHASE_OUTLIER_MAD_K = 4.0 # 组内 |x-med| > k*MAD(或 >0.45*组间span)剔除 +EXP_SETTLE_S = 0.45 # set_exposure 后等待 +EXP_DISCARD_EACH = 1 # 每次改曝光后先丢枪再采样(trigger 路径) +RUN_MODE_COOLDOWN_S = 5.5 # CamOS RUN_MODE_SWITCH_COOLDOWN_S = 5.0 +PATCH_FLOW_TIMEOUT_S = 55.0 +ROI_SETTLE_S = 4.0 +MON_SETTLE_S = 3.5 +TRIG_SETTLE_S = 4.5 +DISCARD_AFTER_SWITCH = 3 +# ROI 最小高度:240 边界在 HW crop/ISP 上曾导致 restart 后 streaming=0 +ROI_MIN_W = 320 +ROI_MIN_H = 256 + +SENSOR_W = 1456 +SENSOR_H = 1088 +ROIS = [ + (0, 0, 640, 480), + (200, 200, 640, 480), + (100, 100, 800, 600), + (50, 80, 1024, 768), + (0, 0, 1200, 560), + (300, 200, 640, 360), + (0, 100, 1456, 400), + (400, 100, 512, 384), + (0, 0, 1456, 1088), + (120, 60, 960, 540), + (80, 200, 720, 400), + (16, 16, 1280, 720), + (256, 128, 800, 480), + (0, 0, 400, 400), + (500, 300, 640, 480), +] + +STOP = False + + +def align_down(v: int, a: int = 2) -> int: + return max(0, (int(v) // a) * a) + + +def random_roi() -> tuple[int, int, int, int]: + if random.random() < 0.55: + x, y, w, h = random.choice(ROIS) + else: + w = align_down(random.randint(320, SENSOR_W), 16) or 320 + h = align_down(random.randint(240, SENSOR_H), 8) or 240 + x = align_down(random.randint(0, max(0, SENSOR_W - w)), 2) + y = align_down(random.randint(0, max(0, SENSOR_H - h)), 2) + w = min(align_down(w, 2) or 320, SENSOR_W) + h = min(align_down(h, 2) or 240, SENSOR_H) + x = min(align_down(x, 2), max(0, SENSOR_W - w)) + y = min(align_down(y, 2), max(0, SENSOR_H - h)) + if w < ROI_MIN_W: + w = ROI_MIN_W + if h < ROI_MIN_H: + h = ROI_MIN_H + # 对齐到 16x8,降低 subdev/VB2 对齐失败概率 + w = max(ROI_MIN_W, align_down(w, 16) or ROI_MIN_W) + h = max(ROI_MIN_H, align_down(h, 8) or ROI_MIN_H) + if x + w > SENSOR_W: + x = max(0, SENSOR_W - w) + if y + h > SENSOR_H: + y = max(0, SENSOR_H - h) + return int(x), int(y), int(w), int(h) + + +def _on_sig(_s, _f): + global STOP + STOP = True + + +signal.signal(signal.SIGTERM, _on_sig) +signal.signal(signal.SIGINT, _on_sig) + + +def now_local() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def now_iso() -> str: + return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds") + + +def run_mode_text() -> str: + try: + return RUN_MODE_SYSFS.read_text(encoding="utf-8", errors="ignore").replace("\n", " | ") + except Exception as e: + return f"ERR {e}" + + +def sysfs_pulse(us: int | None = None) -> None: + if us is not None: + try: + PULSE_SYSFS.write_text(f"{int(us)}\n", encoding="utf-8") + except Exception: + pass + try: + TRIGGER_SYSFS.write_text("1\n", encoding="utf-8") + except Exception: + pass + + +class Sink: + def __init__(self, out: Path): + self.out = out + self.out.mkdir(parents=True, exist_ok=True) + (self.out / "green").mkdir(exist_ok=True) + self.console = self.out / "console.log" + self.events = self.out / "events.jsonl" + self.bugs = self.out / "bugs.jsonl" + self.summary_json = self.out / "summary.json" + self.summary_txt = self.out / "summary.txt" + self.stats = { + "started_at": now_iso(), + "updated_at": now_iso(), + "path": "camos", + "cycles": 0, + "cases": 0, + "frames": 0, + "valid_frames": 0, + "green_frames": 0, + "n1_cases": 0, + "mix_cases": 0, + "low_contrast_cases": 0, + "inconclusive_cases": 0, + "too_few_cases": 0, + "no_frame": 0, + "switch_fail": 0, + "switch_warn": 0, + "roi_fail": 0, + "recoveries": 0, + "crashes": 0, + "bugs": 0, + "by_case": {}, + "last_bug": None, + "kernel": None, + "build": None, + } + + def log(self, msg: str, alert: bool = False): + line = f"[{now_local()}] {'ALERT ' if alert else ''}{msg}" + # 仅 print;由 nohup 重定向写入 console.log,避免双重 write + print(line, flush=True) + + def event(self, kind: str, **kw): + ev = {"ts": now_iso(), "kind": kind, **kw} + with self.events.open("a", encoding="utf-8") as f: + f.write(json.dumps(ev, ensure_ascii=False) + "\n") + return ev + + def bug(self, title: str, **kw): + self.stats["bugs"] += 1 + b = {"ts": now_iso(), "title": title, "cycle": self.stats["cycles"], **kw} + self.stats["last_bug"] = b + with self.bugs.open("a", encoding="utf-8") as f: + f.write(json.dumps(b, ensure_ascii=False) + "\n") + self.log(f"BUG {title} {json.dumps(kw, ensure_ascii=False)[:300]}", alert=True) + self.event("bug", title=title, **kw) + return b + + def touch_summary(self): + self.stats["updated_at"] = now_iso() + try: + self.stats["uptime_s"] = int( + time.time() - datetime.fromisoformat(self.stats["started_at"]).timestamp() + ) + except Exception: + self.stats["uptime_s"] = 0 + self.summary_json.write_text( + json.dumps(self.stats, indent=2, ensure_ascii=False), encoding="utf-8" + ) + s = self.stats + lines = [ + f"updated={s['updated_at']}", + f"started={s['started_at']}", + f"uptime_s={s.get('uptime_s', 0)}", + f"path={s.get('path')}", + f"cycles={s['cycles']} cases={s['cases']} frames={s['frames']} valid={s['valid_frames']}", + f"green={s['green_frames']} n1={s['n1_cases']} mix={s['mix_cases']} " + f"lowc={s['low_contrast_cases']} incon={s.get('inconclusive_cases', 0)} " + f"toofew={s['too_few_cases']} noframe={s['no_frame']}", + f"switch_fail={s['switch_fail']} switch_warn={s.get('switch_warn', 0)} " + f"roi_fail={s['roi_fail']} recoveries={s['recoveries']} " + f"crashes={s['crashes']} bugs={s['bugs']}", + f"build={s.get('build')} kernel={s.get('kernel')}", + f"last_bug={json.dumps(s.get('last_bug'), ensure_ascii=False)[:240] if s.get('last_bug') else None}", + f"by_case={json.dumps(s.get('by_case'), ensure_ascii=False)}", + f"run_mode={run_mode_text()}", + ] + self.summary_txt.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def _mad(vals: list[float]) -> float: + if not vals: + return 0.0 + med = float(np.median(np.asarray(vals, dtype=np.float64))) + return float(np.median(np.abs(np.asarray(vals, dtype=np.float64) - med))) + + +def filter_phase_samples( + means: list[float], expects: list[str] +) -> tuple[list[float], list[str], dict]: + """Drop head samples + within-expect outliers that inflate MIX false positives.""" + meta: dict = {"in_n": len(means), "dropped_head": 0, "dropped_outlier": 0} + mm = list(means) + ee = list(expects) + head = min(PHASE_DROP_HEAD, max(0, len(mm) - 3)) + if head: + mm = mm[head:] + ee = ee[head:] + meta["dropped_head"] = head + if len(mm) < 2: + return mm, ee, meta + + lo_vals = [m for m, e in zip(mm, ee) if e == "LO"] + hi_vals = [m for m, e in zip(mm, ee) if e == "HI"] + if lo_vals and hi_vals: + lo_med = float(np.median(lo_vals)) + hi_med = float(np.median(hi_vals)) + gap = abs(hi_med - lo_med) + thr_gap = max(3.0, 0.45 * gap) if gap > 0 else 1e9 + else: + lo_med = hi_med = 0.0 + thr_gap = 1e9 + + keep_m: list[float] = [] + keep_e: list[str] = [] + for m, e in zip(mm, ee): + group = lo_vals if e == "LO" else hi_vals + med = float(np.median(group)) if group else float(m) + mad = _mad(group) if len(group) >= 3 else 0.0 + thr = max(thr_gap, PHASE_OUTLIER_MAD_K * mad) if mad > 1e-6 else thr_gap + if abs(float(m) - med) > thr: + meta["dropped_outlier"] += 1 + continue + keep_m.append(float(m)) + keep_e.append(e) + # 剔太狠则回退到仅 drop-head 的序列,避免 TOO_FEW + if len(keep_m) < 3: + return mm, ee, {**meta, "outlier_reverted": True} + return keep_m, keep_e, meta + + +def classify_phase(means: list[float], expects: list[str]) -> dict: + if len(means) < 2: + return { + "verdict": "TOO_FEW", + "span": 0.0, + "labels": [], + "expects": expects, + "means": means, + } + arr = np.asarray(means, dtype=np.float64) + # 用 expect 分组中位数做阈值,避免单帧 outlier 把 min/max mid 拉飞 + lo_vals = [float(m) for m, e in zip(means, expects) if e == "LO"] + hi_vals = [float(m) for m, e in zip(means, expects) if e == "HI"] + if lo_vals and hi_vals: + lo_ref = float(np.median(lo_vals)) + hi_ref = float(np.median(hi_vals)) + if hi_ref < lo_ref: + lo_ref, hi_ref = hi_ref, lo_ref + mid = 0.5 * (lo_ref + hi_ref) + span = float(hi_ref - lo_ref) + # 仍报告全序列极值 span,便于看 outlier + span_ext = float(arr.max() - arr.min()) + else: + lo_m, hi_m = float(arr.min()), float(arr.max()) + span = hi_m - lo_m + span_ext = span + mid = 0.5 * (lo_m + hi_m) + labels = ["HI" if m >= mid else "LO" for m in means] + # 先按 LO/HI 标签与 expect 对齐判相位;暗场 JPEG 下 span 可能 <1 但仍交替正确 + cur = sum(g == e for g, e in zip(labels, expects)) + n1 = 0 + for i in range(1, len(labels)): + if labels[i] == expects[i - 1] and labels[i] != expects[i]: + n1 += 1 + # 允许 1 枪噪声:匹配率够高仍算 CUR(收 MIX 假阳) + if cur == len(labels) or (len(labels) >= 5 and cur >= len(labels) - 1): + verdict = "CUR" + elif span < PHASE_SPAN_MIN and span_ext < PHASE_SPAN_MIN: + verdict = "INCONCLUSIVE" + elif n1 >= max(2, (len(labels) - 1) // 2): + verdict = "N1" + else: + verdict = "MIX" + return { + "verdict": verdict, + "span": round(span_ext if span_ext > span else span, 3), + "span_ref": round(span, 3), + "mid": round(mid, 3), + "labels": labels, + "expects": expects, + "means": [round(m, 3) for m in means], + "cur_matches": cur, + "n1_hits": sum( + 1 + for i in range(1, len(labels)) + if labels[i] == expects[i - 1] and labels[i] != expects[i] + ), + } + + +def analyze_jpeg(data: bytes) -> dict: + """Multi-signal bottom quality (classic green / chroma seam / tear). + + Implementation lives in imx296_bottom_quality.py (Pillow-only). Falls back + to an inlined import path next to this file for board deploys. + """ + try: + from imx296_bottom_quality import analyze_jpeg as _aq + return _aq(data) + except Exception: + pass + import importlib.util + from pathlib import Path as _P + + for cand in ( + _P(__file__).resolve().parent / "imx296_bottom_quality.py", + _P("/userdata/imx296_warmup_ab/scripts/imx296_bottom_quality.py"), + ): + if cand.is_file(): + spec = importlib.util.spec_from_file_location("imx296_bottom_quality", cand) + mod = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(mod) + return mod.analyze_jpeg(data) + raise RuntimeError("imx296_bottom_quality.py not found") + + +class CamOSClient: + def __init__(self, sink: Sink): + self.sink = sink + self.cj = http.cookiejar.CookieJar() + self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cj)) + self.ws = None + self._last_run_mode_switch = 0.0 + self._current_flow = {"run_mode": None, "trigger_mode": None} + + @property + def cookie_header(self) -> str: + return "; ".join(f"{c.name}={c.value}" for c in self.cj) + + def login(self) -> bool: + req = urllib.request.Request( + f"{BASE}/api/auth/login", + data=json.dumps({"username": USER, "password": PASS}).encode(), + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with self.opener.open(req, timeout=10) as r: + body = r.read() + ok = b'"success":true' in body or b'"username"' in body + if ok: + self.sink.log(f"login ok cookies={len(list(self.cj))}") + return ok + except Exception as e: + self.sink.log(f"login fail {e}", alert=True) + return False + + def get_flow(self) -> dict: + req = urllib.request.Request( + f"{BASE}/api/flow_config", + headers={"Cookie": self.cookie_header}, + ) + with self.opener.open(req, timeout=8) as r: + return json.loads(r.read().decode()) + + def _cooldown_wait(self, target_run: str): + cur = self._current_flow.get("run_mode") + if cur is None or cur == target_run: + return + elapsed = time.monotonic() - self._last_run_mode_switch + if elapsed < RUN_MODE_COOLDOWN_S: + time.sleep(RUN_MODE_COOLDOWN_S - elapsed + 0.15) + + def _refresh_flow_cache(self) -> dict: + try: + cfg = self.get_flow() + self._current_flow = { + "run_mode": cfg.get("run_mode"), + "trigger_mode": cfg.get("trigger_mode"), + } + return cfg + except Exception: + return {} + + def preview_healthy(self, retries: int = 4, gap_s: float = 0.35) -> tuple[bool, str]: + """True only if preview returns JPEG body (login-alone is not enough). + + line0/FT 下 ROI restart 后常见短暂 204/空 body,streaming 仍为 1;多探几次再判死。 + """ + last = "no_attempt" + for i in range(max(1, retries)): + st, tag, data = self._get_preview_raw() + if st == 200 and data and len(data) > 800: + return True, f"ok tag={tag} sz={len(data)} try={i}" + last = f"st={st} tag={tag} sz={len(data) if data else 0}" + time.sleep(gap_s) + return False, last + + def stream_and_preview_ok( + self, *, poke_line0: bool = False, retries: int = 4 + ) -> tuple[bool, str]: + rm = run_mode_text() + if "streaming=1" not in rm: + return False, f"no_stream rm={rm}" + # line0 需要外部脉冲才刷新 preview;健康检查前轻推几枪避免假死 + if poke_line0 or "master_fast_trigger" in rm: + try: + for _ in range(2): + sysfs_pulse() + time.sleep(0.08) + except Exception: + pass + ok, info = self.preview_healthy(retries=retries) + if not ok: + # 再等 ROI encoder settle 一次 + time.sleep(0.6) + try: + if poke_line0 or "master_fast_trigger" in rm: + sysfs_pulse() + except Exception: + pass + ok, info = self.preview_healthy(retries=3) + if not ok: + return False, f"preview_bad {info} rm={rm}" + return True, f"{info} | {rm}" + + def patch_flow(self, run_mode: str, trigger_mode: str, retries: int = 12) -> tuple[bool, str]: + """PATCH /api/flow_config; respect cooldown; short-circuit only if healthy.""" + cur_rm = self._current_flow.get("run_mode") + cur_tm = self._current_flow.get("trigger_mode") + if cur_rm is None or cur_tm is None: + self._refresh_flow_cache() + cur_rm = self._current_flow.get("run_mode") + cur_tm = self._current_flow.get("trigger_mode") + if cur_rm == run_mode and cur_tm == trigger_mode: + healthy, hinfo = self.stream_and_preview_ok() + if healthy: + return True, f"already {run_mode}/{trigger_mode}" + # 死流时禁止短路径,强制再走硬件/后续 recover + self.sink.log( + f"patch short-circuit blocked (unhealthy): {hinfo}", alert=True + ) + + self._cooldown_wait(run_mode) + last_err = "" + for attempt in range(retries): + if STOP: + return False, "stopped" + payload = {"run_mode": run_mode, "trigger_mode": trigger_mode} + req = urllib.request.Request( + f"{BASE}/api/flow_config", + data=json.dumps(payload).encode(), + headers={ + "Content-Type": "application/json", + "Cookie": self.cookie_header, + }, + method="PATCH", + ) + t0 = time.monotonic() + try: + with self.opener.open(req, timeout=PATCH_FLOW_TIMEOUT_S) as r: + body = r.read() + patch_ms = int((time.monotonic() - t0) * 1000) + data = json.loads(body.decode()) + if data.get("ok"): + cfg = data.get("config") or {} + prev = self._current_flow.get("run_mode") + self._current_flow = { + "run_mode": cfg.get("run_mode", run_mode), + "trigger_mode": cfg.get("trigger_mode", trigger_mode), + } + if prev != self._current_flow["run_mode"]: + self._last_run_mode_switch = time.monotonic() + return True, ( + f"ok patch_ms={patch_ms} " + + json.dumps(cfg, ensure_ascii=False)[:180] + ) + last_err = str(data.get("error") or body[:200]) + if "冷却" in last_err or "cooldown" in last_err.lower(): + time.sleep(RUN_MODE_COOLDOWN_S) + continue + except urllib.error.HTTPError as e: + patch_ms = int((time.monotonic() - t0) * 1000) + err_body = "" + try: + err_body = e.read().decode(errors="ignore")[:200] + except Exception: + pass + last_err = f"HTTP {e.code} patch_ms={patch_ms} {err_body}" + if e.code in (401, 403): + self.login() + time.sleep(1.0) + except Exception as e: + patch_ms = int((time.monotonic() - t0) * 1000) + msg = str(e) + kind = "error" + if "timed out" in msg.lower() or "timeout" in msg.lower(): + kind = "client_timeout" + cfg = self._refresh_flow_cache() + if ( + cfg.get("run_mode") == run_mode + and cfg.get("trigger_mode") == trigger_mode + ): + return True, f"timeout_but_cfg_ok patch_ms={patch_ms}" + last_err = f"{kind} patch_ms={patch_ms} {msg}" + time.sleep(1.0) + return False, last_err + + def wait_stream( + self, + timeout: float = 18.0, + need_ft: bool = False, + need_free: bool = False, + strict_mode: bool = False, + ) -> tuple[bool, str, dict]: + """Wait streaming=1. Default soft: mode mismatch still ok (meta.mode_mismatch).""" + t0 = time.time() + last_rm = "" + saw_stream = False + while time.time() - t0 < timeout and not STOP: + rm = run_mode_text() + last_rm = rm + streaming = "streaming=1" in rm + if streaming: + saw_stream = True + mode_ok = True + if need_ft: + mode_ok = mode_ok and "active=master_fast_trigger" in rm + if need_free: + mode_ok = mode_ok and "active=free_run" in rm + if streaming and (mode_ok or not strict_mode): + return True, rm, { + "wait_ms": int((time.time() - t0) * 1000), + "mode_mismatch": bool(streaming and not mode_ok), + "streaming": True, + } + time.sleep(0.25) + meta = { + "wait_ms": int((time.time() - t0) * 1000), + "mode_mismatch": True, + "streaming": saw_stream or ("streaming=1" in last_rm), + } + if not strict_mode and "streaming=1" in last_rm: + meta["mode_mismatch"] = ( + (need_ft and "active=master_fast_trigger" not in last_rm) + or (need_free and "active=free_run" not in last_rm) + ) + return True, last_rm, meta + return False, last_rm or run_mode_text(), meta + + async def open_ws(self): + if self.ws is not None: + try: + await self.ws.close() + except Exception: + pass + self.ws = None + # websockets lib: pass cookie header + self.ws = await websockets.connect( + WS_URL, + additional_headers={"Cookie": self.cookie_header}, + open_timeout=8, + ping_interval=20, + ping_timeout=20, + max_size=4 * 1024 * 1024, + ) + return self.ws + + async def ensure_ws(self): + if self.ws is None: + return await self.open_ws() + try: + # cheap liveness + if getattr(self.ws, "closed", False): + return await self.open_ws() + except Exception: + return await self.open_ws() + return self.ws + + async def ws_cmd(self, cmd: str, value=None, drain: int = 3): + ws = await self.ensure_ws() + msg = {"cmd": cmd} + if value is not None: + msg["value"] = value + try: + await ws.send(json.dumps(msg)) + except Exception: + ws = await self.open_ws() + await ws.send(json.dumps(msg)) + # drain a few status pushes so buffer doesn't grow forever + for _ in range(drain): + try: + await asyncio.wait_for(ws.recv(), timeout=0.25) + except Exception: + break + return ws + + def _get_preview_raw(self): + req = urllib.request.Request( + f"{BASE}/preview/frame?t={time.time_ns()}", + headers={"Cookie": self.cookie_header}, + ) + try: + resp = self.opener.open(req, timeout=6) + tag = resp.headers.get("X-Frame-Tag") + data = resp.read() + return resp.status, tag, data + except urllib.error.HTTPError as e: + if e.code in (401, 403): + self.login() + return e.code, None, b"" + except Exception as e: + return str(e), None, b"" + + def peek_tag(self): + st, tag, data = self._get_preview_raw() + if st == 200 and data and len(data) > 800: + return tag + return tag + + async def fire_trigger(self, mode: str): + """software: WS+debug; line0: sysfs XTRIG only.""" + m = str(mode or "").strip().lower() + if m == "line0": + sysfs_pulse() + return + try: + await self.ws_cmd("rkcam_trigger_once", drain=1) + except Exception: + pass + try: + req = urllib.request.Request( + f"{BASE}/debug/trigger_once", + data=b"{}", + headers={ + "Content-Type": "application/json", + "Cookie": self.cookie_header, + }, + method="POST", + ) + self.opener.open(req, timeout=3).read() + except Exception: + pass + + async def set_exposure(self, us: float): + try: + await self.ws_cmd("rkcam_set_exposure_time", float(us), drain=1) + except Exception: + try: + await self.ws_cmd("set_exposure_us", float(us), drain=1) + except Exception: + pass + + async def set_gain(self, gain_db: float = 0.0): + """固定模拟增益。判 N-1 前应置 0,避免默认高增益把 LO/HI 对比压扁。""" + try: + await self.ws_cmd("rkcam_set_gain", float(gain_db), drain=1) + except Exception: + try: + await self.ws_cmd("set_gain", float(gain_db), drain=1) + except Exception: + pass + + async def set_roi(self, x: int, y: int, w: int, h: int): + if w >= SENSOR_W and h >= SENSOR_H and x == 0 and y == 0: + await self.ws_cmd("rkcam_reset_roi") + else: + await self.ws_cmd( + "rkcam_set_roi", {"x": int(x), "y": int(y), "w": int(w), "h": int(h)} + ) + + async def reset_roi(self): + await self.ws_cmd("rkcam_reset_roi") + + async def grab_after_trigger( + self, mode: str, timeout_s: float = 3.0, poll_s: float = 0.05 + ) -> tuple[bytes | None, str | None, bool, dict]: + """Return (data, tag, is_new, meta).""" + before = self.peek_tag() + await self.fire_trigger(mode) + t0 = time.time() + last_tag = before + last_data = b"" + last_st: object = None + miss_reason = "no_response" + while time.time() - t0 < timeout_s and not STOP: + st, tag, data = self._get_preview_raw() + last_st = st + if st == 200 and data and len(data) > 800: + last_tag = tag + last_data = data + if before is None or tag is None: + if time.time() - t0 > 0.12: + return data, tag, True, {"reason": "ok", "before": before} + elif str(tag) != str(before): + return data, tag, True, { + "reason": "ok", + "before": before, + "tag": tag, + } + else: + miss_reason = "old_tag" + elif st != 200: + miss_reason = f"http_{st}" + elif not data or len(data) <= 800: + miss_reason = "empty_body" + await asyncio.sleep(poll_s) + await self.fire_trigger(mode) + t0 = time.time() + while time.time() - t0 < timeout_s and not STOP: + st, tag, data = self._get_preview_raw() + last_st = st + if st == 200 and data and len(data) > 800: + last_tag = tag + last_data = data + if before is None or (tag is not None and str(tag) != str(before)): + return data, tag, True, { + "reason": "ok_retry", + "before": before, + "tag": tag, + } + miss_reason = "old_tag" + elif st != 200: + miss_reason = f"http_{st}" + else: + miss_reason = "empty_body" + await asyncio.sleep(poll_s) + meta = { + "reason": miss_reason, + "before": before, + "tag": last_tag, + "http_st": last_st, + } + if last_data and len(last_data) > 800: + return last_data, last_tag, False, meta + return None, last_tag, False, meta + + async def grab_free_run(self, timeout_s: float = 2.0) -> tuple[bytes | None, str | None]: + before = self.peek_tag() + t0 = time.time() + while time.time() - t0 < timeout_s and not STOP: + st, tag, data = self._get_preview_raw() + if st == 200 and data and len(data) > 800: + if before is None or tag is None or str(tag) != str(before): + return data, tag + await asyncio.sleep(0.05) + st, tag, data = self._get_preview_raw() + if st == 200 and data and len(data) > 800: + return data, tag + return None, tag + + async def close(self): + if self.ws is not None: + try: + await self.ws.close() + except Exception: + pass + self.ws = None + + +async def switch_to( + client: CamOSClient, sink: Sink, run_mode: str, trigger_mode: str, label: str +) -> bool: + ok, info = client.patch_flow(run_mode, trigger_mode) + sink.event("switch", label=label, ok=ok, info=info, rm=run_mode_text()) + if not ok: + sink.stats["switch_fail"] += 1 + sink.bug(f"switch_fail:{label}", info=info, rm=run_mode_text()) + return False + settle = MON_SETTLE_S if run_mode == "monitor" else TRIG_SETTLE_S + await asyncio.sleep(settle) + need_ft = run_mode == "trigger" + need_free = run_mode in ("monitor", "manual") + ok_s, rm, meta = client.wait_stream( + 18.0, need_ft=need_ft, need_free=need_free, strict_mode=False + ) + if not ok_s: + ok_s2, rm2, meta2 = client.wait_stream( + 10.0, need_ft=False, need_free=False, strict_mode=False + ) + if not ok_s2: + sink.stats["switch_fail"] += 1 + sink.bug( + f"stream_fail:{label}", + rm=rm2 or rm, + wait_ms=meta2.get("wait_ms"), + info=info, + ) + return False + rm, meta = rm2, meta2 + if meta.get("mode_mismatch"): + sink.stats["switch_warn"] += 1 + sink.event( + "switch_warn", + label=label, + rm=rm, + wait_ms=meta.get("wait_ms"), + need_ft=need_ft, + need_free=need_free, + ) + sink.log(f"switch {label} WARN mode_mismatch rm={rm}", alert=True) + else: + sink.log(f"switch {label} ok wait_ms={meta.get('wait_ms')} rm={rm}") + try: + await client.ensure_ws() + except Exception as e: + sink.log(f"ws after switch: {e}") + return True + +async def run_alt_exp_case( + sink: Sink, + client: CamOSClient, + name: str, + mode: str, + n: int = 8, + free_run: bool = False, +) -> dict: + means: list[float] = [] + expects: list[str] = [] + greens: list[bool] = [] + details: list[dict] = [] + # 相位判定前固定增益=0:默认用户集常为 ~12dB,暗场下 LO/HI 均值挤在一起会误判 MIX + await client.set_gain(0.0) + await asyncio.sleep(0.2) + # ROI/模式切换后多 discard,减轻首帧相位污染 + if not free_run: + for _ in range(DISCARD_AFTER_SWITCH): + try: + await client.grab_after_trigger(mode, timeout_s=2.8) + except Exception: + pass + for i in range(n): + if STOP: + break + exp = LO_US if (i % 2 == 0) else HI_US + expects.append("LO" if exp == LO_US else "HI") + await client.set_exposure(exp) + await asyncio.sleep(EXP_SETTLE_S) + grab_meta: dict = {} + if free_run: + # free_run:多等一帧时间再取,减少曝光未生效 + await asyncio.sleep(0.08) + data, tag = await client.grab_free_run(timeout_s=2.0) + is_new = data is not None + else: + # 每次改曝光后先丢 EXP_DISCARD_EACH 枪,再采统计样本 + for _ in range(EXP_DISCARD_EACH): + try: + await client.grab_after_trigger(mode, timeout_s=2.5) + except Exception: + pass + data, tag, is_new, grab_meta = await client.grab_after_trigger( + mode, timeout_s=3.5 + ) + sink.stats["frames"] += 1 + if not data or not is_new: + means.append(float("nan")) + greens.append(False) + sink.stats["no_frame"] += 1 + details.append( + { + "i": i, + "err": "no_frame", + "tag": tag, + "is_new": is_new, + "miss": grab_meta, + } + ) + # 连续 503 / 无流:熔断本 case,避免空转 + reason = str((grab_meta or {}).get("reason") or "") + if reason.startswith("http_503") or reason.startswith("http_5"): + consec = sum( + 1 + for d in details[-3:] + if str((d.get("miss") or {}).get("reason") or "").startswith("http_5") + ) + if consec >= 2: + sink.log( + f"ABORT {name} consecutive preview 5xx, break case", + alert=True, + ) + break + continue + try: + a = analyze_jpeg(data) + means.append(float(a["mean"])) + greens.append(bool(a["green"])) + sink.stats["valid_frames"] += 1 + if a["green"]: + sink.stats["green_frames"] += 1 + gpath = sink.out / "green" / f"c{sink.stats['cycles']:04d}_{name}_{i:02d}.jpg" + gpath.write_bytes(data) + gpath.with_suffix(".json").write_text( + json.dumps( + {"case": name, "i": i, "tag": tag, **a, "rm": run_mode_text()}, + indent=2, + ensure_ascii=False, + ), + encoding="utf-8", + ) + details.append( + { + "i": i, + "exp": exp, + "mean": round(float(a["mean"]), 3), + "green": a["green"], + "classic_green": a.get("classic_green", False), + "seam": a.get("seam", False), + "tear": a.get("tear", False), + "score": a.get("score", 0), + "reasons": a.get("reasons", []), + "tag": tag, + "dim": a["dim"], + "bot": [a["br"], a["bg"], a["bb"]], + } + ) + except Exception as e: + means.append(float("nan")) + greens.append(False) + details.append({"i": i, "err": str(e), "tag": tag}) + + mm, ee = [], [] + for m, e in zip(means, expects): + if m == m: # not nan + mm.append(m) + ee.append(e) + filt_m, filt_e, filt_meta = filter_phase_samples(mm, ee) + if len(filt_m) >= 2: + phase = classify_phase(filt_m, filt_e) + else: + phase = classify_phase(mm, ee) + phase_all = classify_phase(mm, ee) if len(mm) >= 2 else phase + phase = dict(phase) + phase["filter"] = filt_meta + + result = { + "name": name, + "phase_drop_first": phase, + "phase_all": phase_all, + "green_frames": int(sum(1 for x in greens if x)), + "total": len(means), + "valid": len(mm), + "details": details, + "run_mode": run_mode_text(), + "phase_filter": filt_meta, + } + + sink.stats["cases"] += 1 + bc = sink.stats["by_case"].setdefault( + name, + { + "n": 0, + "green": 0, + "n1": 0, + "mix": 0, + "cur": 0, + "lowc": 0, + "incon": 0, + "toofew": 0, + "fail": 0, + }, + ) + bc["n"] += 1 + v = phase["verdict"] + span = float(phase.get("span") or 0.0) + if v == "CUR": + bc["cur"] += 1 + elif v == "N1": + pass # hard count after span gate + elif v == "MIX": + bc["mix"] += 1 + sink.stats["mix_cases"] += 1 + elif v in ("LOW_CONTRAST", "INCONCLUSIVE"): + bc["incon"] = bc.get("incon", 0) + 1 + bc["lowc"] += 1 + sink.stats["inconclusive_cases"] += 1 + sink.stats["low_contrast_cases"] += 1 + v = "INCONCLUSIVE" + elif v == "TOO_FEW": + bc["toofew"] += 1 + sink.stats["too_few_cases"] += 1 + gcnt = result["green_frames"] + bc["green"] += gcnt + + bad = False + reasons = [] + is_free = "FREERUN" in name or free_run + if v == "N1": + if span >= PHASE_N1_SPAN_MIN and not is_free: + bad = True + reasons.append("phase=N1") + bc["n1"] += 1 + sink.stats["n1_cases"] += 1 + else: + v = "INCONCLUSIVE" + bc["incon"] = bc.get("incon", 0) + 1 + sink.stats["inconclusive_cases"] += 1 + sink.log( + f"INCONCLUSIVE {name} weak_N1 span={span} (need>={PHASE_N1_SPAN_MIN})" + ) + elif v == "TOO_FEW": + bad = True + reasons.append(f"phase={v}") + elif v == "MIX" and not is_free: + if span >= PHASE_SPAN_MIN: + bad = True + reasons.append(f"phase={v}") + else: + bc["mix"] = max(0, bc.get("mix", 0) - 1) + sink.stats["mix_cases"] = max(0, sink.stats["mix_cases"] - 1) + v = "INCONCLUSIVE" + bc["incon"] = bc.get("incon", 0) + 1 + sink.stats["inconclusive_cases"] += 1 + elif v == "INCONCLUSIVE": + pass + if gcnt > 0: + bad = True + reasons.append(f"green={gcnt}") + if result["valid"] < max(2, n // 2): + bad = True + reasons.append(f"valid={result['valid']}/{n}") + + result["phase_final"] = v + result["span"] = span + if bad: + bc["fail"] += 1 + sink.bug( + f"{name}:{','.join(reasons)}", + case=name, + phase=v, + phase_all=phase_all.get("verdict"), + green=gcnt, + labels=phase.get("labels"), + expects=phase.get("expects"), + means=phase.get("means"), + span=phase.get("span"), + run_mode=result["run_mode"], + sample_details=details[:4], + ) + else: + sink.log( + f"OK {name} phase={v} green={gcnt} valid={result['valid']}/{n} " + f"span={phase.get('span')} rm={result['run_mode'][:60]}" + ) + sink.event("case", **{k: result[k] for k in result if k != "details"}, details_n=len(details)) + sink.touch_summary() + return result + + + +def _kill_camos_devs() -> None: + """Stop all CamOS server paths (dev + systemd unit process).""" + subprocess.call( + ["systemctl", "stop", "camos"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + subprocess.call( + ["pkill", "-9", "-f", "python server.py"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + # packaged entry may appear as .../camos/camos + subprocess.call( + ["pkill", "-9", "-f", "site-packages/camos/camos"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + subprocess.call( + ["pkill", "-9", "-f", "[v]ite"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + time.sleep(1.0) + # release video node if still held + subprocess.call( + ["fuser", "-k", "/dev/video11"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + time.sleep(0.5) + + +def _start_camos_light(sink: Sink) -> None: + """Always single-instance: NO_VITE python server.py from /root/smartcam_bs. + + Do NOT systemctl start camos while a dev tree is the test target — it races + the existing server and leaves streaming=0 with login still 200. + """ + try: + RUN_MODE_SYSFS.write_text("free_run\n", encoding="utf-8") + except Exception: + pass + + _kill_camos_devs() + time.sleep(1) + logf = open("/tmp/camos_boot_endurance.log", "a") + env = os.environ.copy() + env["SMARTCAM_NO_VITE_DEV"] = "1" + env["PYTHONUNBUFFERED"] = "1" + subprocess.Popen( + ["python", "server.py", "--host", "0.0.0.0", "--port", "8000", "80"], + cwd="/root/smartcam_bs", + stdout=logf, + stderr=subprocess.STDOUT, + start_new_session=True, + env=env, + ) + sink.log("camos start via python server.py (NO_VITE_DEV=1, single instance)") + + +async def ensure_camos_up(sink: Sink, client: CamOSClient) -> bool: + """Login + preview body required. Restart single instance if unhealthy.""" + if client.login(): + ok, info = client.preview_healthy() + if ok: + # also prefer streaming=1 but preview body is the hard gate + sink.log(f"camos up preview {info}") + return True + sink.log(f"camos login ok but preview unhealthy: {info}", alert=True) + else: + sink.log("camos login failed", alert=True) + + sink.log("camos not healthy, light restart (single instance)", alert=True) + sink.stats["recoveries"] += 1 + _start_camos_light(sink) + for i in range(90): + if STOP: + return False + time.sleep(2) + if not client.login(): + continue + ok, info = client.preview_healthy() + if ok: + # clear stale flow cache after restart + client._current_flow = {"run_mode": None, "trigger_mode": None} + try: + client._refresh_flow_cache() + except Exception: + pass + try: + await client.open_ws() + except Exception: + pass + sink.log(f"camos restarted ok i={i} {info}") + return True + sink.bug("camos_restart_fail") + return False + + +async def one_cycle(sink: Sink, client: CamOSClient, cycle: int): + sink.stats["cycles"] = cycle + sink.log(f"===== cycle {cycle} begin path=camos =====") + + # 1) monitor / free_run smoke (few frames, no hard N-1) + if not await switch_to(client, sink, "monitor", "close", "MON"): + await client_recover(sink, client) + if not await switch_to(client, sink, "monitor", "close", "MON_retry"): + return + await run_alt_exp_case(sink, client, "MON_FREERUN", mode="close", n=4, free_run=True) + + # 2) software trigger full + ROI set/reset + if not await switch_to(client, sink, "trigger", "software", "SW"): + await client_recover(sink, client) + if not await switch_to(client, sink, "trigger", "software", "SW_retry"): + return + await run_alt_exp_case(sink, client, "SW_BASE", mode="software", n=8) + + async def _roi_then_case(label_prefix: str, xx, yy, ww, hh, mode: str, n: int) -> bool: + """set ROI, require stream+preview healthy, then run case. False => need recover.""" + try: + await client.set_roi(xx, yy, ww, hh) + await asyncio.sleep(ROI_SETTLE_S) + sink.event("roi_set", x=xx, y=yy, w=ww, h=hh, rm=run_mode_text()) + except Exception as e: + sink.stats["roi_fail"] += 1 + sink.bug("roi_set_fail", err=str(e), roi=[xx, yy, ww, hh]) + return False + # software 也给 ROI 后 encoder 一点缓冲,避免瞬时 204 假死 + healthy, hinfo = client.stream_and_preview_ok(retries=5) + if not healthy: + sink.stats["roi_fail"] += 1 + sink.bug( + f"roi_dead_after_set:{label_prefix}", + info=hinfo, + roi=[xx, yy, ww, hh], + rm=run_mode_text(), + ) + return False + result = await run_alt_exp_case( + sink, client, f"{label_prefix}_{ww}x{hh}", mode=mode, n=n + ) + # case itself may have hit 503 storm + if result.get("valid", 0) == 0 and result.get("total", 0) > 0: + healthy2, hinfo2 = client.stream_and_preview_ok(retries=5) + if not healthy2: + sink.bug( + f"roi_dead_after_case:{label_prefix}", + info=hinfo2, + roi=[xx, yy, ww, hh], + rm=run_mode_text(), + ) + return False + return True + + x, y, w, h = random_roi() + if not await _roi_then_case("SW_ROI", x, y, w, h, "software", 8): + await client_recover(sink, client) + if not await switch_to(client, sink, "trigger", "software", "SW_after_roi_fail"): + return + else: + try: + await client.reset_roi() + await asyncio.sleep(ROI_SETTLE_S) + sink.event("roi_reset", rm=run_mode_text()) + healthy, hinfo = client.stream_and_preview_ok() + if not healthy: + sink.bug("roi_dead_after_reset", info=hinfo, rm=run_mode_text()) + await client_recover(sink, client) + if not await switch_to( + client, sink, "trigger", "software", "SW_after_reset_fail" + ): + return + else: + await run_alt_exp_case(sink, client, "SW_ROI_RESET", mode="software", n=8) + except Exception as e: + sink.stats["roi_fail"] += 1 + sink.bug("roi_reset_fail_sw", err=str(e)) + + # second random ROI in software + x2, y2, w2, h2 = random_roi() + if not await _roi_then_case("SW_ROI2", x2, y2, w2, h2, "software", 6): + await client_recover(sink, client) + if not await switch_to(client, sink, "trigger", "software", "SW_after_roi2_fail"): + return + else: + try: + await client.reset_roi() + await asyncio.sleep(ROI_SETTLE_S) + healthy, hinfo = client.stream_and_preview_ok() + if not healthy: + sink.bug("roi2_dead_after_reset", info=hinfo, rm=run_mode_text()) + await client_recover(sink, client) + except Exception as e: + sink.stats["roi_fail"] += 1 + sink.bug("roi2_fail_sw", err=str(e)) + + # 3) flip software → line0 (same run_mode=trigger, only trigger_mode change — no cooldown) + ok, info = client.patch_flow("trigger", "line0") + sink.event("switch", label="LINE0", ok=ok, info=info, rm=run_mode_text()) + if ok: + await asyncio.sleep(TRIG_SETTLE_S) + ok_l0, rm_l0, meta_l0 = client.wait_stream( + 14.0, need_ft=True, strict_mode=False + ) + sink.event( + "switch_wait", + label="LINE0", + ok=ok_l0, + rm=rm_l0, + wait_ms=meta_l0.get("wait_ms"), + mode_mismatch=meta_l0.get("mode_mismatch"), + ) + await run_alt_exp_case(sink, client, "LINE0_BASE", mode="line0", n=8) + x3, y3, w3, h3 = random_roi() + try: + await client.set_roi(x3, y3, w3, h3) + await asyncio.sleep(ROI_SETTLE_S) + # line0:先 XTRIG 唤醒 preview,再健康检查(避免 204 假死触发 recover) + try: + for _ in range(3): + sysfs_pulse() + await asyncio.sleep(0.12) + except Exception: + pass + healthy, hinfo = client.stream_and_preview_ok(poke_line0=True, retries=6) + if not healthy: + sink.bug("roi_dead_line0_set", info=hinfo, roi=[x3, y3, w3, h3]) + await client_recover(sink, client) + else: + await run_alt_exp_case( + sink, client, f"LINE0_ROI_{w3}x{h3}", mode="line0", n=8 + ) + await client.reset_roi() + await asyncio.sleep(ROI_SETTLE_S) + try: + for _ in range(3): + sysfs_pulse() + await asyncio.sleep(0.12) + except Exception: + pass + healthy2, hinfo2 = client.stream_and_preview_ok( + poke_line0=True, retries=6 + ) + if not healthy2: + sink.bug("roi_dead_line0_reset", info=hinfo2) + await client_recover(sink, client) + else: + await run_alt_exp_case( + sink, client, "LINE0_ROI_RESET", mode="line0", n=6 + ) + except Exception as e: + sink.stats["roi_fail"] += 1 + sink.bug("roi_fail_line0", err=str(e)) + else: + sink.stats["switch_fail"] += 1 + sink.bug("switch_fail:LINE0", info=info) + + # 4) back to monitor + await switch_to(client, sink, "monitor", "close", "MON_END") + sink.touch_summary() + sink.log( + f"===== cycle {cycle} end bugs={sink.stats['bugs']} " + f"green={sink.stats['green_frames']} n1={sink.stats['n1_cases']} =====" + ) + + +async def client_recover(sink: Sink, client: CamOSClient): + sink.stats["recoveries"] += 1 + sink.log("recover begin", alert=True) + # soft path first + try: + # force patch even if cache says monitor (dead stream blocks short-circuit) + client._current_flow = {"run_mode": None, "trigger_mode": None} + client.patch_flow("monitor", "close") + await asyncio.sleep(2) + await client.ensure_ws() + await client.reset_roi() + await asyncio.sleep(ROI_SETTLE_S) + except Exception as e: + sink.log(f"recover soft {e}") + ok_s, rm, _meta = client.wait_stream(8.0, strict_mode=False) + ok_p, pinfo = client.preview_healthy() + if ok_s and ok_p: + sink.log(f"recover soft ok rm={rm} {pinfo}") + sink.event("recover", kind_soft=True, rm=rm) + return + sink.log( + f"recover soft failed stream={ok_s} preview={ok_p}/{pinfo} -> hard restart", + alert=True, + ) + await ensure_camos_up(sink, client) + try: + client._current_flow = {"run_mode": None, "trigger_mode": None} + client.patch_flow("monitor", "close") + await asyncio.sleep(2) + await client.open_ws() + await client.reset_roi() + except Exception as e: + sink.log(f"recover hard followup {e}") + sink.event("recover", kind_soft=False, rm=run_mode_text()) + + +async def amain(hours: float, out: Path) -> int: + sink = Sink(out) + try: + uname = subprocess.check_output(["uname", "-a"], text=True).strip() + except Exception: + uname = "?" + sink.stats["kernel"] = uname + # build id from uname #YYYYMMDDNN + for part in uname.split(): + if part.startswith("#20"): + sink.stats["build"] = part + break + sink.log(f"start CamOS endurance hours={hours} out={out} kernel={uname}") + sink.touch_summary() + + client = CamOSClient(sink) + if not await ensure_camos_up(sink, client): + sink.log("FATAL camos not up", alert=True) + sink.touch_summary() + return 2 + try: + await client.open_ws() + except Exception as e: + sink.log(f"ws open fail {e}, continue with HTTP-only + retry") + + # seed flow state + try: + cfg = client.get_flow() + client._current_flow = { + "run_mode": cfg.get("run_mode"), + "trigger_mode": cfg.get("trigger_mode"), + } + sink.log(f"flow_config={json.dumps(cfg, ensure_ascii=False)[:240]}") + except Exception as e: + sink.log(f"get_flow {e}") + + t_end = time.time() + hours * 3600.0 + cycle = 0 + while time.time() < t_end and not STOP: + cycle += 1 + try: + await one_cycle(sink, client, cycle) + except Exception as e: + sink.stats["crashes"] += 1 + sink.bug("cycle_crash", err=str(e), tb=traceback.format_exc()[-800:]) + sink.touch_summary() + try: + await client_recover(sink, client) + except Exception: + pass + await asyncio.sleep(2) + # brief pause between cycles + for _ in range(10): + if STOP or time.time() >= t_end: + break + await asyncio.sleep(0.5) + + # leave camera in monitor/free_run + try: + client.patch_flow("monitor", "close") + except Exception: + pass + await client.close() + sink.log(f"done cycles={cycle} bugs={sink.stats['bugs']}") + sink.touch_summary() + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--hours", type=float, default=24.0) + ap.add_argument("--out", default="/tmp/imx296_day_endurance") + args = ap.parse_args() + out = Path(args.out) + return asyncio.run(amain(args.hours, out)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/imx296_day_watchdog.py b/scripts/imx296_day_watchdog.py new file mode 100644 index 0000000000000..9312284cb17e3 --- /dev/null +++ b/scripts/imx296_day_watchdog.py @@ -0,0 +1,426 @@ +#!/usr/bin/env python3 +"""Host-side watchdog for board IMX296 day endurance. + +SSH to root@192.168.1.180, pull summary/bugs, append local markdown report, +restart board test if dead (optional). +""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from datetime import datetime +from pathlib import Path + +HOST = "root@192.168.1.180" +BOARD_OUT = "/tmp/imx296_day_endurance" +BOARD_SCRIPT = "/tmp/imx296_day_endurance.py" +SSH_BASE = [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=8", + "-o", + "BatchMode=yes", + "-o", + "ServerAliveInterval=5", + "-o", + "ServerAliveCountMax=2", + HOST, +] + + +def ts() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def ssh(script: str, timeout: int = 60) -> str: + try: + r = subprocess.run( + SSH_BASE + ["bash", "-s"], + input=script, + text=True, + capture_output=True, + timeout=timeout, + ) + out = r.stdout or "" + if r.stderr: + out += "\n" + r.stderr + return out + except subprocess.TimeoutExpired: + return "SSH_TIMEOUT" + except Exception as e: + return f"SSH_ERR:{e}" + + +def scp_pull(remote: str, local: Path, timeout: int = 45) -> bool: + local.parent.mkdir(parents=True, exist_ok=True) + try: + r = subprocess.run( + [ + "scp", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=8", + "-o", + "BatchMode=yes", + f"{HOST}:{remote}", + str(local), + ], + capture_output=True, + text=True, + timeout=timeout, + ) + return r.returncode == 0 and local.is_file() + except Exception: + return False + + +def _parse_summary_txt(txt: str) -> dict: + """Parse key=value lines from summary.txt into a compact dict.""" + s: dict = {} + for ln in (txt or "").splitlines(): + if "=" not in ln: + continue + k, v = ln.split("=", 1) + k, v = k.strip(), v.strip() + if k in ( + "cycles", + "bugs", + "green_frames", + "n1_cases", + "mix_cases", + "low_contrast_cases", + "too_few_cases", + "no_frame", + "switch_fail", + "roi_fail", + "recoveries", + "crashes", + "cases", + "frames", + "valid_frames", + "uptime_s", + ): + # lines may pack multiple keys; handle single-key first + try: + s[k] = int(re.sub(r"[^\d-]", "", v.split()[0]) or 0) + except Exception: + pass + elif k in ("started_at", "updated_at", "build", "kernel", "last_bug", "run_mode", "by_case"): + s[k] = v + elif k == "green": + # green=0 n1=0 mix=0 ... packed line from summary.txt + for part in ln.replace(",", " ").split(): + if "=" in part: + pk, pv = part.split("=", 1) + try: + if pk == "green": + s["green_frames"] = int(pv) + elif pk == "n1": + s["n1_cases"] = int(pv) + elif pk == "mix": + s["mix_cases"] = int(pv) + elif pk == "lowc": + s["low_contrast_cases"] = int(pv) + elif pk == "toofew": + s["too_few_cases"] = int(pv) + elif pk == "noframe": + s["no_frame"] = int(pv) + elif pk in ( + "cycles", + "cases", + "frames", + "bugs", + "switch_fail", + "roi_fail", + "recoveries", + "crashes", + ): + s[pk if pk != "frames" else "frames"] = int(pv) + elif pk == "valid": + s["valid_frames"] = int(pv) + except Exception: + pass + elif ln.startswith("cycles=") or " cycles=" in f" {ln}": + for part in ln.replace(",", " ").split(): + if "=" in part: + pk, pv = part.split("=", 1) + try: + if pk == "cycles": + s["cycles"] = int(pv) + elif pk == "cases": + s["cases"] = int(pv) + elif pk == "frames": + s["frames"] = int(pv) + elif pk == "valid": + s["valid_frames"] = int(pv) + except Exception: + pass + elif ln.startswith("switch_fail=") or ln.startswith("green="): + for part in ln.replace(",", " ").split(): + if "=" in part: + pk, pv = part.split("=", 1) + try: + mapping = { + "switch_fail": "switch_fail", + "roi_fail": "roi_fail", + "recoveries": "recoveries", + "crashes": "crashes", + "bugs": "bugs", + "green": "green_frames", + "n1": "n1_cases", + "mix": "mix_cases", + "lowc": "low_contrast_cases", + "toofew": "too_few_cases", + "noframe": "no_frame", + } + if pk in mapping: + s[mapping[pk]] = int(pv) + except Exception: + pass + return s + + +def collect() -> dict: + out = ssh( + f""" +set +e +pid=$(pgrep -f '{BOARD_SCRIPT}' | head -1) +echo PID=$pid +if [ -f {BOARD_OUT}/summary.txt ]; then + echo '---SUMMARY---' + cat {BOARD_OUT}/summary.txt + echo '---END_SUMMARY---' +fi +bugs_n=$(wc -l < {BOARD_OUT}/bugs.jsonl 2>/dev/null || echo 0) +echo BUGS_N=$bugs_n +if [ -f {BOARD_OUT}/bugs.jsonl ]; then + echo '---BUGS_TAIL---' + tail -n 8 {BOARD_OUT}/bugs.jsonl + echo '---END_BUGS---' +fi +echo '---RMODE---' +cat /sys/bus/i2c/devices/4-001a/run_mode 2>/dev/null | tr '\\n' ' ' +echo +echo '---UNAME---' +uname -a +echo '---DMESG---' +dmesg 2>/dev/null | grep -E 'SError|Oops|BUG:|n1trace early ceiling-force|ignored XTRIG' | tail -n 8 +echo '---END---' +""", + timeout=50, + ) + d: dict = {"_raw": out, "ok": "SSH_" not in out[:20]} + m = re.search(r"^PID=(.*)$", out, re.M) + d["pid"] = (m.group(1) if m else "").strip() + m = re.search(r"^BUGS_N=(.*)$", out, re.M) + d["bugs_n"] = int(re.sub(r"\D", "", (m.group(1) if m else "0")) or 0) + sm = re.search(r"---SUMMARY---\n(.*?)\n---END_SUMMARY---", out, re.S) + d["summary_txt"] = sm.group(1).strip() if sm else "" + # Prefer scp'd summary.json (avoids SSH stream mangling large JSON) + d["summary"] = None + tmp_json = Path("/tmp/imx296_day_watchdog/_pull_summary.json") + if scp_pull(f"{BOARD_OUT}/summary.json", tmp_json, timeout=30): + try: + d["summary"] = json.loads(tmp_json.read_text(encoding="utf-8")) + except Exception: + d["summary"] = None + if d["summary"] is None and d["summary_txt"]: + d["summary"] = _parse_summary_txt(d["summary_txt"]) + bm = re.search(r"---BUGS_TAIL---\n(.*?)\n---END_BUGS---", out, re.S) + d["bugs_tail"] = bm.group(1).strip() if bm else "" + rm = re.search(r"---RMODE---\n(.*)", out) + d["run_mode"] = rm.group(1).strip() if rm else "" + um = re.search(r"---UNAME---\n(.*)", out) + d["uname"] = um.group(1).strip() if um else "" + dm = re.search(r"---DMESG---\n(.*?)\n---END---", out, re.S) + d["dmesg"] = dm.group(1).strip() if dm else "" + return d + + +def restart_board_test() -> str: + return ssh( + f""" +set +e +pkill -f '{BOARD_SCRIPT}' 2>/dev/null || true +sleep 1 +# free camera +echo free_run > /sys/bus/i2c/devices/4-001a/run_mode 2>/dev/null || true +mkdir -p {BOARD_OUT} +# keep previous bugs +if [ -f {BOARD_OUT}/bugs.jsonl ]; then + cp {BOARD_OUT}/bugs.jsonl {BOARD_OUT}/bugs.jsonl.bak.$(date +%s) 2>/dev/null || true +fi +nohup /root/dlcv/bin/python3 -u {BOARD_SCRIPT} --hours 24 --out {BOARD_OUT} \ + >>{BOARD_OUT}/console.log 2>&1 & +sleep 2 +pgrep -af '{BOARD_SCRIPT}' | head -3 +test -f {BOARD_OUT}/summary.txt && head -5 {BOARD_OUT}/summary.txt +""", + timeout=40, + ) + + +def ensure_report(path: Path, meta: dict): + if path.is_file(): + return + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + f"""# IMX296 24h 稳定性连续测试记录 + +- **开始时间**: {ts()} +- **板卡**: `{HOST}` +- **内核**: `{meta.get('uname', 'unknown')}` +- **测试脚本**: `{BOARD_SCRIPT}` → 输出 `{BOARD_OUT}` +- **主机 watchdog**: `scripts/imx296_day_watchdog.py` +- **关注点**: 模式切换(close/software/line0)、ROI set/reset、底部绿线/串帧、N-1 相位 + +## 汇总 + +| 时间 | 状态 | cycles | bugs | green | n1 | mix | 备注 | +|------|------|--------|------|-------|----|-----|------| +""", + encoding="utf-8", + ) + + +def append_report(path: Path, d: dict, note: str = ""): + s = d.get("summary") or {} + cycles = s.get("cycles", "?") + bugs = s.get("bugs", d.get("bugs_n", "?")) + green = s.get("green_frames", "?") + n1 = s.get("n1_cases", "?") + mix = s.get("mix_cases", "?") + pid = d.get("pid") or "-" + alive = "ALIVE" if pid and pid not in ("",) else "DEAD" + if not d.get("ok"): + alive = "SSH_FAIL" + row = ( + f"| {ts()} | {alive} pid={pid} | {cycles} | {bugs} | {green} | {n1} | {mix} | " + f"{note.replace('|', '/')} |\n" + ) + with path.open("a", encoding="utf-8") as f: + f.write(row) + if d.get("bugs_tail"): + f.write(f"\n### {ts()} bugs tail\n\n```\n{d['bugs_tail'][:2000]}\n```\n\n") + if note.startswith("ALERT") or alive != "ALIVE": + f.write(f"\n### {ts()} detail\n\n") + f.write(f"- run_mode: `{d.get('run_mode', '')}`\n") + if d.get("summary_txt"): + f.write(f"\n```\n{d['summary_txt'][:1500]}\n```\n\n") + if d.get("dmesg"): + f.write(f"\n```\n{d['dmesg'][:1500]}\n```\n\n") + + +def write_snapshot(snap_dir: Path, d: dict): + snap_dir.mkdir(parents=True, exist_ok=True) + (snap_dir / "last_collect.json").write_text( + json.dumps( + { + "ts": ts(), + "pid": d.get("pid"), + "bugs_n": d.get("bugs_n"), + "summary": d.get("summary"), + "run_mode": d.get("run_mode"), + "uname": d.get("uname"), + "bugs_tail": d.get("bugs_tail"), + "dmesg": d.get("dmesg"), + }, + indent=2, + ensure_ascii=False, + ), + encoding="utf-8", + ) + if d.get("summary_txt"): + (snap_dir / "summary.txt").write_text(d["summary_txt"] + "\n", encoding="utf-8") + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument( + "--report", + default=str( + Path(__file__).resolve().parents[1] + / "IMX296_24h稳定性测试_20260807.md" + ), + ) + ap.add_argument("--state-dir", default="/tmp/imx296_day_watchdog") + ap.add_argument("--interval", type=int, default=120) + ap.add_argument("--once", action="store_true") + ap.add_argument("--restart-if-dead", action="store_true", default=True) + ap.add_argument("--no-restart", action="store_true") + args = ap.parse_args() + + report = Path(args.report) + state = Path(args.state_dir) + state.mkdir(parents=True, exist_ok=True) + logf = state / "watchdog.log" + + def wlog(msg: str): + line = f"[{ts()}] {msg}" + print(line, flush=True) + with logf.open("a", encoding="utf-8") as f: + f.write(line + "\n") + + last_bugs = -1 + dead_streak = 0 + + while True: + d = collect() + write_snapshot(state, d) + s = d.get("summary") or {} + bugs = int(s.get("bugs", d.get("bugs_n") or 0) or 0) + pid = (d.get("pid") or "").strip() + alive = bool(pid) + + ensure_report(report, d) + note = "" + if not d.get("ok"): + note = "ALERT SSH problem" + dead_streak += 1 + elif not alive: + note = "ALERT test process dead" + dead_streak += 1 + else: + dead_streak = 0 + if last_bugs >= 0 and bugs > last_bugs: + note = f"ALERT new bugs +{bugs - last_bugs}" + else: + note = "ok" + + append_report(report, d, note=note) + wlog( + f"alive={alive} pid={pid or '-'} bugs={bugs} cycles={s.get('cycles')} " + f"green={s.get('green_frames')} n1={s.get('n1_cases')} note={note}" + ) + + # pull bugs file periodically + scp_pull(f"{BOARD_OUT}/bugs.jsonl", state / "bugs.jsonl") + scp_pull(f"{BOARD_OUT}/summary.json", state / "summary.json") + + if ( + (not args.no_restart) + and args.restart_if_dead + and dead_streak >= 2 + and d.get("ok") + ): + wlog("restarting board endurance") + out = restart_board_test() + wlog(out[:500].replace("\n", " | ")) + append_report(report, d, note="ALERT restarted board test") + dead_streak = 0 + + last_bugs = bugs + if args.once: + break + time.sleep(max(30, args.interval)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_accept_user.py b/scripts/verify_accept_user.py new file mode 100644 index 0000000000000..63fb5727127fa --- /dev/null +++ b/scripts/verify_accept_user.py @@ -0,0 +1,644 @@ +#!/usr/bin/env python3 +"""Real-user acceptance on CamOS (smartcam_bs defaults). + +Scenarios: + - mon → software trigger (apply latency + quality + first_hit) + - mon → line0 / XTRIG (sysfs) quality + first_hit + - software ROI set/reset via WS + - N-1 / phase: successive triggers must each produce a NEW frame tag; + optional center-luma monotonicity under controlled light (soft check) + - multi-signal bottom quality: classic / seam / tear + +Does not override CamOS warmup/gate. Kernel #2026081301 + imx296_bottom_quality. +""" +from __future__ import annotations + +import asyncio +import importlib.util +import json +import os +import statistics +import sys +import time +from pathlib import Path + +import requests +import websockets + +BASE = os.environ.get("CAMOS_BASE", "http://127.0.0.1:8000") +WS_URL = os.environ.get("CAMOS_WS", "ws://127.0.0.1:8000/ws") +OUT = Path(os.environ.get("VERIFY_OUT", "/userdata/imx296_warmup_ab/verify_accept_user")) +SW_ROUNDS = int(os.environ.get("VERIFY_SW_ROUNDS", "6")) +LINE0_ROUNDS = int(os.environ.get("VERIFY_LINE0_ROUNDS", "4")) +HEAD = int(os.environ.get("VERIFY_HEAD", "12")) +N1_SEQ = int(os.environ.get("VERIFY_N1_SEQ", "16")) +ROI_HEAD = int(os.environ.get("VERIFY_ROI_HEAD", "8")) +COOLDOWN = float(os.environ.get("VERIFY_COOLDOWN", "5.5")) +USER, PASS = "admin", "dlcv2026" +XTRIG = Path(os.environ.get("XTRIG_PATH", "/sys/bus/i2c/devices/4-001a/trigger")) +N1TRACE = Path("/sys/module/video_rkisp/parameters/n1trace") + +_det = None +for cand in ( + Path(__file__).resolve().parent / "imx296_bottom_quality.py", + Path("/userdata/imx296_warmup_ab/scripts/imx296_bottom_quality.py"), +): + if cand.is_file(): + spec = importlib.util.spec_from_file_location("imx296_bottom_quality", cand) + _det = importlib.util.module_from_spec(spec) + assert spec.loader + spec.loader.exec_module(_det) + break +if _det is None: + sys.exit("imx296_bottom_quality.py missing") + +if OUT.exists(): + import shutil + + shutil.rmtree(OUT) +OUT.mkdir(parents=True) +(OUT / "frames").mkdir() +(OUT / "bad").mkdir() +(OUT / "n1").mkdir() + +s = requests.Session() + + +def login(): + s.post(BASE + "/api/auth/login", json={"username": USER, "password": PASS}, timeout=10) + return "; ".join(f"{c.name}={c.value}" for c in s.cookies) + + +cookie = login() + + +def n1trace(on: bool): + try: + N1TRACE.write_text("Y\n" if on else "N\n") + except Exception as e: + print("n1trace", e, flush=True) + + +def patch(body, timeout=90): + t0 = time.time() + try: + r = s.patch(BASE + "/api/flow_config", json=body, timeout=timeout) + js = r.json() if r.content else {} + ok = r.status_code < 400 + if isinstance(js, dict): + if "success" in js: + ok = bool(js["success"]) + elif "ok" in js: + ok = bool(js["ok"]) + return time.time() - t0, js, None, ok + except Exception as e: + return time.time() - t0, {}, str(e), False + + +def peek_tag(): + try: + pr = s.get(BASE + "/preview/frame", params={"t": time.time_ns()}, timeout=8) + return int(pr.headers.get("x-frame-tag") or pr.headers.get("X-Frame-Tag") or 0) + except Exception: + return 0 + + +def fire(mode: str): + try: + if mode == "line0": + XTRIG.write_text("1\n") + else: + s.post(BASE + "/debug/trigger_once", timeout=8) + return True + except Exception as e: + print(f" fire_{mode}_err={e}", flush=True) + return False + + +def wait_new_frame(last_tag: int, timeout_s: float = 2.0): + end = time.time() + timeout_s + while time.time() < end: + try: + pr = s.get(BASE + "/preview/frame", params={"t": time.time_ns()}, timeout=8) + except Exception: + time.sleep(0.03) + continue + h = pr.headers.get("x-frame-tag") or pr.headers.get("X-Frame-Tag") or "" + try: + tn = int(h) if str(h).isdigit() else -1 + except Exception: + tn = -1 + if pr.status_code == 200 and pr.content and tn != last_tag and tn >= 0: + return tn, pr.content, None + time.sleep(0.03) + return last_tag, b"", "timeout" + + +def shoot(label: str, n: int, mode: str = "software"): + last = peek_tag() + first = None + stats = { + "got": 0, + "miss": 0, + "bad": 0, + "classic": 0, + "seam": 0, + "tear": 0, + "idx": [], + "bots": [], + "dims": [], + "tags": [], + "means": [], + "dup_tag": 0, + "tag_non_increasing": 0, + } + prev_tag = last + for i in range(n): + fire(mode) + tn, data, err = wait_new_frame(last, 2.0) + if err or not data: + stats["miss"] += 1 + time.sleep(0.08) + continue + if first is None: + first = i + 1 + if tn == prev_tag: + stats["dup_tag"] += 1 + if tn < prev_tag and prev_tag > 0: + stats["tag_non_increasing"] += 1 + last = tn + prev_tag = tn + stats["got"] += 1 + stats["tags"].append(tn) + (OUT / "frames" / f"{label}_s{i+1:02d}.jpg").write_bytes(data) + a = _det.analyze_jpeg(data) + stats["dims"].append(a.get("dim")) + stats["means"].append(a.get("mean")) + if len(stats["bots"]) < 3: + stats["bots"].append({"i": i + 1, "tag": tn, **a}) + if a.get("green") or a.get("bad"): + stats["bad"] += 1 + stats["classic"] += int(bool(a.get("classic_green"))) + stats["seam"] += int(bool(a.get("seam"))) + stats["tear"] += int(bool(a.get("tear"))) + stats["idx"].append(i + 1) + (OUT / "bad" / f"{label}_s{i+1:02d}.jpg").write_bytes(data) + (OUT / "bad" / f"{label}_s{i+1:02d}.json").write_text( + json.dumps({"tag": tn, **a}, ensure_ascii=False, indent=2), encoding="utf-8" + ) + time.sleep(0.1) + stats["first_hit"] = first + # unique tags == got → no N-1 reuse of same published frame + stats["unique_tags"] = len(set(stats["tags"])) + stats["tag_unique_ok"] = stats["unique_tags"] == stats["got"] and stats["got"] > 0 + return stats + + +def n1_sequence(label: str, n: int, mode: str = "software"): + """Strict 1-trigger → 1-new-tag sequence; detect stalls / repeats (N-1 symptom).""" + last = peek_tag() + rows = [] + miss = 0 + bad = 0 + classic = seam = tear = 0 + tags = [] + for i in range(n): + t_fire = time.time() + fire(mode) + tn, data, err = wait_new_frame(last, 2.2) + dt = time.time() - t_fire + if err or not data: + miss += 1 + rows.append({"i": i + 1, "miss": True, "dt": round(dt, 3)}) + time.sleep(0.12) + continue + a = _det.analyze_jpeg(data) + row = { + "i": i + 1, + "tag": tn, + "prev": last, + "delta_tag": tn - last if last else None, + "dt": round(dt, 3), + "mean": a.get("mean"), + "dim": a.get("dim"), + "bad": bool(a.get("bad")), + "classic": bool(a.get("classic_green")), + "seam": bool(a.get("seam")), + "tear": bool(a.get("tear")), + "reasons": a.get("reasons"), + } + rows.append(row) + tags.append(tn) + (OUT / "n1" / f"{label}_{i+1:02d}.jpg").write_bytes(data) + if a.get("bad"): + bad += 1 + classic += int(bool(a.get("classic_green"))) + seam += int(bool(a.get("seam"))) + tear += int(bool(a.get("tear"))) + (OUT / "bad" / f"{label}_n1_{i+1:02d}.jpg").write_bytes(data) + last = tn + time.sleep(0.15) + # N-1 failure modes: + # - miss on first trigger but hit on second with stale content (hard without scene change) + # - repeated tags + # - first_hit > 1 (first fire produced no new frame) + first_hit = next((r["i"] for r in rows if not r.get("miss")), None) + unique = len(set(tags)) + # delta_tag should be >0 for each hit; if always 0 impossible since wait_new_frame + deltas = [r["delta_tag"] for r in rows if r.get("delta_tag") is not None] + # soft: mean should not be identical for all (frozen frame) if n large + means = [r["mean"] for r in rows if r.get("mean") is not None] + frozen = False + if len(means) >= 6: + # if all means within 0.05 → suspicious freeze + if max(means) - min(means) < 0.05: + frozen = True + return { + "label": label, + "mode": mode, + "n": n, + "got": len(tags), + "miss": miss, + "first_hit": first_hit, + "bad": bad, + "classic": classic, + "seam": seam, + "tear": tear, + "unique_tags": unique, + "tag_unique_ok": unique == len(tags) and len(tags) > 0, + "first_is_1": first_hit == 1, + "frozen_mean": frozen, + "delta_min": min(deltas) if deltas else None, + "delta_max": max(deltas) if deltas else None, + "rows": rows, + # pass: every fire gets new tag, first=1, no quality bad, not frozen + "n1_pass": ( + miss == 0 + and first_hit == 1 + and unique == len(tags) + and len(tags) == n + and bad == 0 + and not frozen + ), + } + + +async def ws_cmd(ws, cmd, value=None): + msg = {"cmd": cmd} + if value is not None: + msg["value"] = value + await ws.send(json.dumps(msg)) + for _ in range(3): + try: + await asyncio.wait_for(ws.recv(), timeout=0.25) + except Exception: + break + + +def lat_stats(vals): + vals = [v for v in vals if v is not None] + if not vals: + return {"n": 0} + return { + "n": len(vals), + "min": round(min(vals), 3), + "max": round(max(vals), 3), + "avg": round(sum(vals) / len(vals), 3), + "p50": round(statistics.median(vals), 3), + "all": [round(v, 3) for v in vals], + } + + +async def main(): + n1trace(False) + summary = { + "kernel": Path("/proc/version").read_text().strip(), + "detector": "imx296_bottom_quality", + "camos": "smartcam_bs/0_测试运行_camos.sh defaults", + "sw": [], + "line0": [], + "n1": [], + "roi": [], + "targets": {"apply_hard_s": 5.0, "apply_goal_s": [2.0, 3.0]}, + } + + def to_mon(): + # CamOS uses trigger_mode=close for monitor + return patch({"run_mode": "monitor", "trigger_mode": "close"}) + + dt, _, err, ok = to_mon() + print(f"init mon dt={dt:.3f} ok={ok} err={err}", flush=True) + time.sleep(COOLDOWN) + + real_applies = [] + + # --- mon → software --- + for r in range(SW_ROUNDS): + print(f"=== SW R{r} mon->software ===", flush=True) + to_mon() + time.sleep(COOLDOWN) + dt, body, err, ok = patch({"run_mode": "trigger", "trigger_mode": "software"}, timeout=90) + real = bool(ok) and dt >= 0.5 and err is None + if real: + real_applies.append(dt) + print(f" apply={dt:.3f}s ok={ok} real={real} err={err}", flush=True) + time.sleep(0.35) + sh = shoot(f"sw_r{r}", HEAD, "software") + print( + f" first={sh['first_hit']} got={sh['got']} miss={sh['miss']} bad={sh['bad']} " + f"c/s/t={sh['classic']}/{sh['seam']}/{sh['tear']} " + f"uniq={sh['unique_tags']}/{sh['got']} idx={sh['idx']}", + flush=True, + ) + if sh["bots"]: + b = sh["bots"][0] + print( + f" head bot=({b.get('br')},{b.get('bg')},{b.get('bb')}) " + f"above=({b.get('ar')},{b.get('ag')},{b.get('ab')}) reasons={b.get('reasons')}", + flush=True, + ) + summary["sw"].append( + { + "r": r, + "apply_s": round(dt, 3), + "apply_ok": ok, + "real_switch": real, + "apply_err": err, + **{ + k: sh[k] + for k in ( + "first_hit", + "got", + "miss", + "bad", + "classic", + "seam", + "tear", + "idx", + "unique_tags", + "tag_unique_ok", + "dup_tag", + ) + }, + "bots_head": sh["bots"][:2], + "tags_head": sh["tags"][:6], + } + ) + dt2, _, err2, _ = to_mon() + print(f" back mon dt={dt2:.3f} err={err2}", flush=True) + time.sleep(0.3) + + # --- N-1 sequence under software (after fresh enter) --- + print("=== N1 SEQ software ===", flush=True) + to_mon() + time.sleep(COOLDOWN) + dt, _, err, ok = patch({"run_mode": "trigger", "trigger_mode": "software"}) + print(f" enter sw apply={dt:.3f} ok={ok}", flush=True) + time.sleep(0.5) + n1s = n1_sequence("n1_sw", N1_SEQ, "software") + print( + f" n1_sw got={n1s['got']}/{n1s['n']} miss={n1s['miss']} first={n1s['first_hit']} " + f"bad={n1s['bad']} uniq_ok={n1s['tag_unique_ok']} frozen={n1s['frozen_mean']} " + f"pass={n1s['n1_pass']}", + flush=True, + ) + summary["n1"].append({k: v for k, v in n1s.items() if k != "rows"}) + (OUT / "n1_sw_rows.json").write_text( + json.dumps(n1s["rows"], ensure_ascii=False, indent=2), encoding="utf-8" + ) + to_mon() + time.sleep(COOLDOWN) + + # --- mon → line0 --- + for r in range(LINE0_ROUNDS): + print(f"=== LINE0 R{r} mon->line0 ===", flush=True) + to_mon() + time.sleep(COOLDOWN) + # hardware / external line0 mode if supported + dt, body, err, ok = patch( + {"run_mode": "trigger", "trigger_mode": "line0"}, timeout=90 + ) + if not ok: + # fallback common aliases + for alt in ( + {"run_mode": "trigger", "trigger_mode": "hardware"}, + {"run_mode": "trigger", "trigger_mode": "external"}, + {"run_mode": "trigger", "trigger_mode": "xtrig"}, + ): + dt, body, err, ok = patch(alt, timeout=90) + if ok: + print(f" alt_mode={alt} apply={dt:.3f}", flush=True) + break + real = bool(ok) and dt >= 0.5 and err is None + if real: + real_applies.append(dt) + print(f" apply={dt:.3f}s ok={ok} real={real} err={err} body_keys={list(body)[:6] if isinstance(body, dict) else body}", flush=True) + time.sleep(0.4) + # fire via sysfs XTRIG regardless of CamOS mode name + sh = shoot(f"l0_r{r}", HEAD, "line0") + print( + f" first={sh['first_hit']} got={sh['got']} miss={sh['miss']} bad={sh['bad']} " + f"c/s/t={sh['classic']}/{sh['seam']}/{sh['tear']} " + f"uniq={sh['unique_tags']}/{sh['got']} idx={sh['idx']}", + flush=True, + ) + summary["line0"].append( + { + "r": r, + "apply_s": round(dt, 3), + "apply_ok": ok, + "real_switch": real, + "apply_err": err, + **{ + k: sh[k] + for k in ( + "first_hit", + "got", + "miss", + "bad", + "classic", + "seam", + "tear", + "idx", + "unique_tags", + "tag_unique_ok", + ) + }, + "bots_head": sh["bots"][:2], + } + ) + to_mon() + time.sleep(0.3) + + # N-1 under line0 + print("=== N1 SEQ line0 ===", flush=True) + to_mon() + time.sleep(COOLDOWN) + dt, _, err, ok = patch({"run_mode": "trigger", "trigger_mode": "line0"}) + if not ok: + for alt in ( + {"run_mode": "trigger", "trigger_mode": "hardware"}, + {"run_mode": "trigger", "trigger_mode": "external"}, + ): + dt, _, err, ok = patch(alt) + if ok: + break + print(f" enter l0 apply={dt:.3f} ok={ok}", flush=True) + time.sleep(0.5) + n1l = n1_sequence("n1_l0", N1_SEQ, "line0") + print( + f" n1_l0 got={n1l['got']}/{n1l['n']} miss={n1l['miss']} first={n1l['first_hit']} " + f"bad={n1l['bad']} uniq_ok={n1l['tag_unique_ok']} frozen={n1l['frozen_mean']} " + f"pass={n1l['n1_pass']}", + flush=True, + ) + summary["n1"].append({k: v for k, v in n1l.items() if k != "rows"}) + (OUT / "n1_l0_rows.json").write_text( + json.dumps(n1l["rows"], ensure_ascii=False, indent=2), encoding="utf-8" + ) + + # --- ROI under software --- + print("=== ROI WS ===", flush=True) + to_mon() + time.sleep(COOLDOWN) + dt, _, err, ok = patch({"run_mode": "trigger", "trigger_mode": "software"}) + print(f"enter sw for ROI apply={dt:.3f} ok={ok} err={err}", flush=True) + time.sleep(COOLDOWN) + cookie2 = login() + ws = await websockets.connect( + WS_URL, + additional_headers={"Cookie": cookie2}, + open_timeout=8, + ping_interval=20, + max_size=4 * 1024 * 1024, + ) + steps = [ + ("base", None), + ("roi640", {"x": 100, "y": 100, "w": 640, "h": 480}), + ("roi800", {"x": 200, "y": 150, "w": 800, "h": 600}), + ("reset", "reset"), + ("roi512", {"x": 50, "y": 80, "w": 512, "h": 384}), + ("reset2", "reset"), + ] + for name, roi in steps: + t0 = time.time() + try: + if roi == "reset": + await ws_cmd(ws, "rkcam_reset_roi") + elif isinstance(roi, dict): + await ws_cmd(ws, "rkcam_set_roi", roi) + except Exception as e: + print(f" {name} ws_err={e}", flush=True) + roi_dt = time.time() - t0 + time.sleep(4.0) + try: + sh = shoot(f"roi_{name}", ROI_HEAD, "software") + except Exception as e: + print(f" {name} shoot_err={e}", flush=True) + login() + summary["roi"].append({"name": name, "error": str(e), "bad": -1}) + continue + print( + f" {name} roi_cmd={roi_dt:.3f}s dim={sh['dims'][:1]} first={sh['first_hit']} " + f"got={sh['got']} bad={sh['bad']} c/s/t={sh['classic']}/{sh['seam']}/{sh['tear']} " + f"uniq_ok={sh['tag_unique_ok']}", + flush=True, + ) + summary["roi"].append( + { + "name": name, + "roi": roi, + "roi_cmd_s": round(roi_dt, 3), + **{ + k: sh[k] + for k in ( + "first_hit", + "got", + "miss", + "bad", + "classic", + "seam", + "tear", + "idx", + "dims", + "unique_tags", + "tag_unique_ok", + ) + }, + "bots_head": sh["bots"][:2], + } + ) + await ws.close() + to_mon() + + def _sum_bad(rows): + return sum(x.get("bad") or 0 for x in rows if (x.get("bad") or 0) >= 0) + + bad_sw = _sum_bad(summary["sw"]) + bad_l0 = _sum_bad(summary["line0"]) + bad_roi = _sum_bad(summary["roi"]) + bad_n1 = sum(x.get("bad") or 0 for x in summary["n1"]) + all_blocks = summary["sw"] + summary["line0"] + summary["roi"] + firsts = [x.get("first_hit") for x in all_blocks if x.get("first_hit") is not None] + firsts += [x.get("first_hit") for x in summary["n1"] if x.get("first_hit") is not None] + uniq_oks = [ + x.get("tag_unique_ok") + for x in all_blocks + summary["n1"] + if "tag_unique_ok" in x + ] + n1_pass_all = all(x.get("n1_pass") for x in summary["n1"]) if summary["n1"] else False + lat = lat_stats(real_applies) + within_3 = all(v <= 3.0 for v in real_applies) if real_applies else False + within_5 = all(v <= 5.0 for v in real_applies) if real_applies else False + quality_ok = (bad_sw + bad_l0 + bad_roi + bad_n1) == 0 + first_ok = all(f == 1 for f in firsts) if firsts else False + tag_ok = all(uniq_oks) if uniq_oks else False + + summary["totals"] = { + "bad_sw": bad_sw, + "bad_line0": bad_l0, + "bad_roi": bad_roi, + "bad_n1": bad_n1, + "bad_total": bad_sw + bad_l0 + bad_roi + bad_n1, + "classic": sum( + (x.get("classic") or 0) for x in all_blocks + summary["n1"] + ), + "seam": sum((x.get("seam") or 0) for x in all_blocks + summary["n1"]), + "tear": sum((x.get("tear") or 0) for x in all_blocks + summary["n1"]), + "first_all_1": first_ok, + "tag_unique_all": tag_ok, + "n1_pass_all": n1_pass_all, + "apply_latency": lat, + "apply_all_le_3s": within_3, + "apply_all_le_5s": within_5, + "quality_pass": quality_ok and first_ok, + "n1_phase_pass": n1_pass_all and tag_ok, + "latency_pass_hard": within_5 and bool(real_applies), + "latency_pass_goal": within_3 and bool(real_applies), + "dim_changed": any( + (x.get("dims") or [None])[0] not in (None, "1456x1088") + for x in summary["roi"] + if str(x.get("name", "")).startswith("roi") + and not str(x.get("name")).startswith("reset") + ), + "accept_pass": ( + quality_ok + and first_ok + and tag_ok + and n1_pass_all + and within_5 + and bool(real_applies) + ), + } + (OUT / "summary.json").write_text( + json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8" + ) + print("BEGIN_SUMMARY_JSON", flush=True) + print(json.dumps(summary["totals"], ensure_ascii=False), flush=True) + print("END_SUMMARY_JSON", flush=True) + print("OUT", OUT, flush=True) + print("ACCEPT", "PASS" if summary["totals"]["accept_pass"] else "FAIL", flush=True) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/verify_k1301_lat.py b/scripts/verify_k1301_lat.py new file mode 100644 index 0000000000000..1a073bea02632 --- /dev/null +++ b/scripts/verify_k1301_lat.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +"""#1301 latency + multi-signal quality matrix. + +Measures mon→software apply wall-clock (real switches only: apply_s>=0.5). +Hard limit 5s target 2–3s. Does not lengthen warmup/gate. +""" +from __future__ import annotations + +import asyncio +import importlib.util +import json +import os +import statistics +import sys +import time +from pathlib import Path + +import requests +import websockets + +BASE = os.environ.get("CAMOS_BASE", "http://127.0.0.1:8000") +WS_URL = os.environ.get("CAMOS_WS", "ws://127.0.0.1:8000/ws") +OUT = Path(os.environ.get("VERIFY_OUT", "/userdata/imx296_warmup_ab/verify_k1301_lat")) +SW_ROUNDS = int(os.environ.get("VERIFY_SW_ROUNDS", "8")) +HEAD = int(os.environ.get("VERIFY_HEAD", "10")) +ROI_HEAD = int(os.environ.get("VERIFY_ROI_HEAD", "8")) +COOLDOWN = float(os.environ.get("VERIFY_COOLDOWN", "5.5")) +USER, PASS = "admin", "dlcv2026" + +_det = None +for cand in ( + Path(__file__).resolve().parent / "imx296_bottom_quality.py", + Path("/userdata/imx296_warmup_ab/scripts/imx296_bottom_quality.py"), +): + if cand.is_file(): + spec = importlib.util.spec_from_file_location("imx296_bottom_quality", cand) + _det = importlib.util.module_from_spec(spec) + assert spec.loader + spec.loader.exec_module(_det) + break +if _det is None: + sys.exit("imx296_bottom_quality.py missing") + +if OUT.exists(): + import shutil + + shutil.rmtree(OUT) +OUT.mkdir(parents=True) +(OUT / "frames").mkdir() +(OUT / "bad").mkdir() + +s = requests.Session() + + +def login(): + s.post(BASE + "/api/auth/login", json={"username": USER, "password": PASS}, timeout=10) + return "; ".join(f"{c.name}={c.value}" for c in s.cookies) + + +cookie = login() + + +def patch(body, timeout=90): + t0 = time.time() + try: + r = s.patch(BASE + "/api/flow_config", json=body, timeout=timeout) + js = r.json() if r.content else {} + ok = r.status_code < 400 + if isinstance(js, dict): + if "success" in js: + ok = bool(js["success"]) + elif "ok" in js: + ok = bool(js["ok"]) + return time.time() - t0, js, None, ok + except Exception as e: + return time.time() - t0, {}, str(e), False + + +def peek_tag(): + try: + pr = s.get(BASE + "/preview/frame", params={"t": time.time_ns()}, timeout=8) + return int(pr.headers.get("x-frame-tag") or pr.headers.get("X-Frame-Tag") or 0) + except Exception: + return 0 + + +def shoot(label, n, mode="software"): + last = peek_tag() + first = None + stats = { + "got": 0, + "bad": 0, + "classic": 0, + "seam": 0, + "tear": 0, + "idx": [], + "bots": [], + "dims": [], + } + for i in range(n): + try: + if mode == "line0": + Path("/sys/bus/i2c/devices/4-001a/trigger").write_text("1\n") + else: + s.post(BASE + "/debug/trigger_once", timeout=8) + except Exception: + pass + end = time.time() + 2.0 + hit = False + data = b"" + while time.time() < end: + try: + pr = s.get(BASE + "/preview/frame", params={"t": time.time_ns()}, timeout=8) + except Exception: + time.sleep(0.04) + continue + h = pr.headers.get("x-frame-tag") or pr.headers.get("X-Frame-Tag") or "" + try: + tn = int(h) if str(h).isdigit() else -1 + except Exception: + tn = -1 + if pr.status_code == 200 and pr.content and tn != last and tn >= 0: + hit = True + last = tn + data = pr.content + break + time.sleep(0.03) + if first is None and hit: + first = i + 1 + if not hit: + continue + stats["got"] += 1 + (OUT / "frames" / f"{label}_s{i+1:02d}.jpg").write_bytes(data) + a = _det.analyze_jpeg(data) + stats["dims"].append(a.get("dim")) + if len(stats["bots"]) < 3: + stats["bots"].append({"i": i + 1, **a}) + if a.get("green") or a.get("bad"): + stats["bad"] += 1 + stats["classic"] += int(bool(a.get("classic_green"))) + stats["seam"] += int(bool(a.get("seam"))) + stats["tear"] += int(bool(a.get("tear"))) + stats["idx"].append(i + 1) + (OUT / "bad" / f"{label}_s{i+1:02d}.jpg").write_bytes(data) + (OUT / "bad" / f"{label}_s{i+1:02d}.json").write_text( + json.dumps(a, ensure_ascii=False, indent=2), encoding="utf-8" + ) + time.sleep(0.1) + stats["first_hit"] = first + return stats + + +async def ws_cmd(ws, cmd, value=None): + msg = {"cmd": cmd} + if value is not None: + msg["value"] = value + await ws.send(json.dumps(msg)) + for _ in range(3): + try: + await asyncio.wait_for(ws.recv(), timeout=0.25) + except Exception: + break + + +def lat_stats(vals): + vals = [v for v in vals if v is not None] + if not vals: + return {"n": 0} + return { + "n": len(vals), + "min": round(min(vals), 3), + "max": round(max(vals), 3), + "avg": round(sum(vals) / len(vals), 3), + "p50": round(statistics.median(vals), 3), + "all": [round(v, 3) for v in vals], + } + + +async def main(): + summary = { + "kernel": Path("/proc/version").read_text().strip(), + "detector": "imx296_bottom_quality", + "sw": [], + "roi": [], + "targets": {"apply_hard_s": 5.0, "apply_goal_s": [2.0, 3.0]}, + } + + # ensure mon + dt, _, err, ok = patch({"run_mode": "monitor", "trigger_mode": "none"}) + print(f"init mon dt={dt:.3f} ok={ok} err={err}", flush=True) + time.sleep(COOLDOWN) + + real_applies = [] + for r in range(SW_ROUNDS): + print(f"=== SW R{r} mon->software ===", flush=True) + # force mon first if previous left us in trigger + patch({"run_mode": "monitor", "trigger_mode": "none"}) + time.sleep(COOLDOWN) + dt, body, err, ok = patch({"run_mode": "trigger", "trigger_mode": "software"}, timeout=90) + real = bool(ok) and dt >= 0.5 and err is None + if real: + real_applies.append(dt) + print( + f" apply={dt:.3f}s ok={ok} real_switch={real} err={err}", + flush=True, + ) + time.sleep(0.35) + sh = shoot(f"sw_r{r}", HEAD) + print( + f" first={sh['first_hit']} got={sh['got']} bad={sh['bad']} " + f"c/s/t={sh['classic']}/{sh['seam']}/{sh['tear']} idx={sh['idx']}", + flush=True, + ) + if sh["bots"]: + b = sh["bots"][0] + print( + f" head bot=({b.get('br')},{b.get('bg')},{b.get('bb')}) " + f"above=({b.get('ar')},{b.get('ag')},{b.get('ab')}) " + f"reasons={b.get('reasons')}", + flush=True, + ) + summary["sw"].append( + { + "r": r, + "apply_s": round(dt, 3), + "apply_ok": ok, + "real_switch": real, + "apply_err": err, + **{k: sh[k] for k in ("first_hit", "got", "bad", "classic", "seam", "tear", "idx")}, + "bots_head": sh["bots"][:2], + } + ) + dt2, _, err2, _ = patch({"run_mode": "monitor", "trigger_mode": "none"}) + print(f" back mon dt={dt2:.3f} err={err2}", flush=True) + time.sleep(0.3) + + # ROI under software + print("=== ROI WS ===", flush=True) + dt, _, err, ok = patch({"run_mode": "trigger", "trigger_mode": "software"}) + print(f"enter sw for ROI apply={dt:.3f} ok={ok} err={err}", flush=True) + time.sleep(COOLDOWN) + cookie2 = login() + ws = await websockets.connect( + WS_URL, + additional_headers={"Cookie": cookie2}, + open_timeout=8, + ping_interval=20, + max_size=4 * 1024 * 1024, + ) + steps = [ + ("base", None), + ("roi640", {"x": 100, "y": 100, "w": 640, "h": 480}), + ("roi800", {"x": 200, "y": 150, "w": 800, "h": 600}), + ("reset", "reset"), + ("roi512", {"x": 50, "y": 80, "w": 512, "h": 384}), + ("reset2", "reset"), + ] + for name, roi in steps: + t0 = time.time() + try: + if roi == "reset": + await ws_cmd(ws, "rkcam_reset_roi") + elif isinstance(roi, dict): + await ws_cmd(ws, "rkcam_set_roi", roi) + except Exception as e: + print(f" {name} ws_err={e}", flush=True) + roi_dt = time.time() - t0 + time.sleep(4.0) + try: + sh = shoot(f"roi_{name}", ROI_HEAD) + except Exception as e: + print(f" {name} shoot_err={e}", flush=True) + login() + summary["roi"].append({"name": name, "error": str(e), "bad": -1}) + continue + print( + f" {name} roi_cmd={roi_dt:.3f}s dim={sh['dims'][:1]} first={sh['first_hit']} " + f"got={sh['got']} bad={sh['bad']} c/s/t={sh['classic']}/{sh['seam']}/{sh['tear']}", + flush=True, + ) + summary["roi"].append( + { + "name": name, + "roi": roi, + "roi_cmd_s": round(roi_dt, 3), + **{k: sh[k] for k in ("first_hit", "got", "bad", "classic", "seam", "tear", "idx", "dims")}, + "bots_head": sh["bots"][:2], + } + ) + await ws.close() + patch({"run_mode": "monitor", "trigger_mode": "none"}) + + bad_sw = sum(x.get("bad") or 0 for x in summary["sw"] if (x.get("bad") or 0) >= 0) + bad_roi = sum(x.get("bad") or 0 for x in summary["roi"] if (x.get("bad") or 0) >= 0) + firsts = [ + x.get("first_hit") + for x in summary["sw"] + summary["roi"] + if x.get("first_hit") is not None + ] + lat = lat_stats(real_applies) + within_3 = all(v <= 3.0 for v in real_applies) if real_applies else False + within_5 = all(v <= 5.0 for v in real_applies) if real_applies else False + summary["totals"] = { + "bad_sw": bad_sw, + "bad_roi": bad_roi, + "bad_total": bad_sw + bad_roi, + "classic": sum(x.get("classic") or 0 for x in summary["sw"] + summary["roi"]), + "seam": sum(x.get("seam") or 0 for x in summary["sw"] + summary["roi"]), + "tear": sum(x.get("tear") or 0 for x in summary["sw"] + summary["roi"]), + "first_all_1": all(f == 1 for f in firsts) if firsts else False, + "apply_latency": lat, + "apply_all_le_3s": within_3, + "apply_all_le_5s": within_5, + "quality_pass": (bad_sw + bad_roi) == 0 + and (all(f == 1 for f in firsts) if firsts else False), + "latency_pass_hard": within_5 and bool(real_applies), + "latency_pass_goal": within_3 and bool(real_applies), + "dim_changed": any( + (x.get("dims") or [None])[0] not in (None, "1456x1088") + for x in summary["roi"] + if str(x.get("name", "")).startswith("roi") and not str(x.get("name")).startswith("reset") + ), + } + (OUT / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8") + print("BEGIN_SUMMARY_JSON", flush=True) + print(json.dumps(summary["totals"], ensure_ascii=False), flush=True) + print("END_SUMMARY_JSON", flush=True) + print("OUT", OUT, flush=True) + + +if __name__ == "__main__": + asyncio.run(main()) From 6910f2dff3e0f47ac4fb61da4a3e469154a0bd58 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Thu, 13 Aug 2026 10:52:53 +0800 Subject: [PATCH 18/21] =?UTF-8?q?chore:=20=E5=BF=BD=E7=95=A5=20dist/=20?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=E4=BA=A4=E4=BB=98=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本地打包的 boot 镜像与校验文件不进版本库。 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8da8fc0c46027..6ce4619da8748 100644 --- a/.gitignore +++ b/.gitignore @@ -193,3 +193,4 @@ trigger /IMX296_2h_*/ /IMX296_user_*/ /IMX296_build_*.log +/dist/ From 12abd2728fdfe7296d5b2bbf3723ed990ad0b4a2 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Thu, 20 Aug 2026 13:26:22 +0800 Subject: [PATCH 19/21] =?UTF-8?q?fix(imx296/isp):=20#1017=20=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=20Y=20=E5=B0=BE=E4=BF=AE=E8=A1=A5=E5=B9=B6=E4=BA=A4?= =?UTF-8?q?=E4=BB=98=20#2026082001?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FT ceiling-force 后曝光/PWM 大跳时底 8 行 Y 仍可能是上一档曝光, 表现为底部亮度串帧(84,gain=240,1500→15000)。在 rkisp_ft_sanitize_nv12_bot 对 Y 尾做条件 last-good 扩展,坏行过半 则整带复制上一行;UV 路径保持 #1016。未加长 warmup/gate。 影响:V39 MP NV12/NV21 early-done 发布前;构建号 2026082001。 --- DLCVCAM_BUILD_VERSION | 2 +- .../media/platform/rockchip/isp/capture_v39.c | 224 ++++++++++++++---- 2 files changed, 174 insertions(+), 52 deletions(-) diff --git a/DLCVCAM_BUILD_VERSION b/DLCVCAM_BUILD_VERSION index 3bb86e818f0e5..e4ef0dde81f6e 100644 --- a/DLCVCAM_BUILD_VERSION +++ b/DLCVCAM_BUILD_VERSION @@ -1 +1 @@ -2026081301 +2026082001 diff --git a/drivers/media/platform/rockchip/isp/capture_v39.c b/drivers/media/platform/rockchip/isp/capture_v39.c index 46cbdbce856b5..29bbb29b3aa7f 100644 --- a/drivers/media/platform/rockchip/isp/capture_v39.c +++ b/drivers/media/platform/rockchip/isp/capture_v39.c @@ -1038,14 +1038,24 @@ static int isp_frame_end(struct rkisp_stream *stream, u32 irq) /* * V39+IMX296 FT: MI OFFS stay 0 → every frame is ceiling-force. Last NV12 - * chroma rows can still be incomplete after force_post hold. + * chroma rows can still be incomplete after force_post hold. After a large + * exposure / PWM-pulse jump the last ~8 Y rows can still be the previous + * exposure (board 84, gain=240, 1500→15000): a luma tear, not classic UV. * * #1013 unconditional 0x80 → permanent flat-grey seam (#1205 FAIL). * #1014/#1015: conditional repair. #1016: neutral-fraction only when the * reference line is *not* neutral (scene has chroma); always honor mean - * jump / stuck / classic-G. Extend last good UV line. Gate stays tiny. + * jump / stuck / classic-G. Extend last good UV line. + * #1017: same idea for Y — only rewrite the last 8 luma rows when the + * whole-line mean jumps; majority-bad copies the entire band from the + * line above so a 1-row blend cannot poison the reference. Gate stays tiny. */ #define RKISP_FT_NV12_UV_TAIL_ROWS 8U +#define RKISP_FT_NV12_Y_TAIL_ROWS 8U +/* Whole-line |meanY-refY|. Userspace tear uses 12; splice samples were 14–74. */ +#define RKISP_FT_Y_LINE_MEAN_THR 12U +/* If this many tail Y rows jump vs the pre-tail line, treat as a splice band. */ +#define RKISP_FT_Y_BAND_BAD_MIN 4U /* |meanU-refU| / |meanV-refV| — dark colored scenes need a low bar. */ #define RKISP_FT_UV_LINE_MEAN_THR 6U /* Sample counts as near-neutral chroma (buffer-clear / incomplete). */ @@ -1135,6 +1145,25 @@ static void rkisp_ft_uv_line_mean(const u8 *line, u32 bpl, u8 *out_u, u8 *out_v) *out_v = (u8)(sv / n); } +static u32 rkisp_ft_y_line_mean(const u8 *line, u32 bpl) +{ + u32 i; + u64 s = 0; + + if (!bpl) + return 0; + for (i = 0; i < bpl; i++) + s += line[i]; + return (u32)(s / bpl); +} + +static bool rkisp_ft_y_mean_jump(u32 mean, u32 ref) +{ + u32 d = mean > ref ? mean - ref : ref - mean; + + return d > RKISP_FT_Y_LINE_MEAN_THR; +} + static void rkisp_ft_sanitize_nv12_bot(struct rkisp_stream *stream, struct rkisp_buffer *buf) { @@ -1143,12 +1172,15 @@ static void rkisp_ft_sanitize_nv12_bot(struct rkisp_stream *stream, struct vb2_buffer *vb; struct device *dma_dev; struct sg_table *sgt; - u8 *base, *uv, *tail, *ref, *line; + u8 *base, *uv = NULL, *tail = NULL, *ref = NULL, *line; + u8 *y_tail, *y_ref, *y_line; u8 ref_u, ref_v; u32 fourcc, bpl, h, y_size, uv_size, tail_rows, tail_bytes; - u32 uv_off, uv_rows, ref_off, i; + u32 y_tail_rows, y_sync_off, y_sync_len, y_init, y_refm, y_mean, y_bad; + u32 uv_off, uv_rows, ref_off = 0, i; u32 sync_off, sync_len; int uv_plane; + bool have_uv = false; bool repaired = false; if (!buf || stream->id != RKISP_STREAM_MP) @@ -1175,78 +1207,168 @@ static void rkisp_ft_sanitize_nv12_bot(struct rkisp_stream *stream, return; y_size = bpl * h; + y_tail_rows = RKISP_FT_NV12_Y_TAIL_ROWS; + if (y_tail_rows + 1 > h) + y_tail_rows = h > 1 ? h - 1 : 0; + y_tail = NULL; + y_ref = NULL; + y_sync_off = 0; + y_sync_len = 0; + if (y_tail_rows) { + y_sync_off = (h - y_tail_rows - 1) * bpl; + y_sync_len = (y_tail_rows + 1) * bpl; + if (y_sync_off + y_sync_len <= y_size) { + y_ref = base + y_sync_off; + y_tail = y_ref + bpl; + } else { + y_tail_rows = 0; + } + } + tail_rows = RKISP_FT_NV12_UV_TAIL_ROWS; uv_rows = h / 2; if (tail_rows > uv_rows) tail_rows = uv_rows; - if (tail_rows >= uv_rows || tail_rows == 0) - return; tail_bytes = bpl * tail_rows; - if (!tail_bytes) - return; - - if (pixm->num_planes >= 2) { - uv = buf->vaddr[1]; - if (!uv) - uv = vb2_plane_vaddr(&buf->vb.vb2_buf, 1); - uv_size = pixm->plane_fmt[1].sizeimage; - uv_plane = 1; - uv_off = 0; - } else { - uv = base + y_size; - uv_size = y_size / 2; - uv_plane = 0; - uv_off = y_size; + uv = NULL; + uv_size = 0; + uv_plane = 0; + uv_off = 0; + sync_off = 0; + sync_len = 0; + if (tail_rows && tail_rows < uv_rows && tail_bytes) { + if (pixm->num_planes >= 2) { + uv = buf->vaddr[1]; + if (!uv) + uv = vb2_plane_vaddr(&buf->vb.vb2_buf, 1); + uv_size = pixm->plane_fmt[1].sizeimage; + uv_plane = 1; + uv_off = 0; + } else { + uv = base + y_size; + uv_size = y_size / 2; + uv_plane = 0; + uv_off = y_size; + } + if (uv && uv_size >= bpl * (tail_rows + 1) && + tail_bytes + bpl <= uv_size) { + ref_off = uv_size - tail_bytes - bpl; + tail = uv + (uv_size - tail_bytes); + ref = uv + ref_off; + sync_off = ref_off; + sync_len = tail_bytes + bpl; + have_uv = true; + } } - if (!uv || uv_size < bpl * (tail_rows + 1)) - return; - if (tail_bytes + bpl > uv_size) + if (!y_tail_rows && !have_uv) return; - ref_off = uv_size - tail_bytes - bpl; - tail = uv + (uv_size - tail_bytes); - ref = uv + ref_off; - sync_off = ref_off; - sync_len = tail_bytes + bpl; vb = &buf->vb.vb2_buf; dma_dev = dev->hw_dev->dev; if (dev->hw_dev->is_dma_sg_ops) { - sgt = vb2_dma_sg_plane_desc(vb, uv_plane); + sgt = vb2_dma_sg_plane_desc(vb, 0); if (sgt) dma_sync_sgtable_for_cpu(dma_dev, sgt, DMA_BIDIRECTIONAL); + if (have_uv && uv_plane != 0) { + sgt = vb2_dma_sg_plane_desc(vb, uv_plane); + if (sgt) + dma_sync_sgtable_for_cpu(dma_dev, sgt, + DMA_BIDIRECTIONAL); + } } else { - dma_addr_t da = vb2_dma_contig_plane_dma_addr(vb, uv_plane); + dma_addr_t da0 = vb2_dma_contig_plane_dma_addr(vb, 0); + + if (da0 && y_sync_len) + dma_sync_single_for_cpu(dma_dev, da0 + y_sync_off, + y_sync_len, DMA_BIDIRECTIONAL); + if (have_uv) { + dma_addr_t da = vb2_dma_contig_plane_dma_addr(vb, + uv_plane); + + if (da) + dma_sync_single_for_cpu(dma_dev, + da + uv_off + sync_off, + sync_len, + DMA_BIDIRECTIONAL); + } + } - if (da) - dma_sync_single_for_cpu(dma_dev, da + uv_off + sync_off, - sync_len, DMA_BIDIRECTIONAL); + if (y_tail && y_ref && y_tail_rows) { + y_init = rkisp_ft_y_line_mean(y_ref, bpl); + y_bad = 0; + for (i = 0; i < y_tail_rows; i++) { + y_mean = rkisp_ft_y_line_mean(y_tail + i * bpl, bpl); + if (rkisp_ft_y_mean_jump(y_mean, y_init)) + y_bad++; + } + if (y_bad >= RKISP_FT_Y_BAND_BAD_MIN) { + for (i = 0; i < y_tail_rows; i++) + memcpy(y_tail + i * bpl, y_ref, bpl); + repaired = true; + } else if (y_bad) { + u8 *y_good = y_ref; + + y_refm = y_init; + for (i = 0; i < y_tail_rows; i++) { + y_line = y_tail + i * bpl; + y_mean = rkisp_ft_y_line_mean(y_line, bpl); + if (rkisp_ft_y_mean_jump(y_mean, y_refm) || + rkisp_ft_y_mean_jump(y_mean, y_init)) { + memcpy(y_line, y_good, bpl); + repaired = true; + continue; + } + y_good = y_line; + y_refm = y_mean; + } + } } - rkisp_ft_uv_line_mean(ref, bpl, &ref_u, &ref_v); + if (have_uv) { + rkisp_ft_uv_line_mean(ref, bpl, &ref_u, &ref_v); - for (i = 0; i < tail_rows; i++) { - line = tail + i * bpl; - if (!rkisp_ft_uv_line_bad(line, bpl, ref_u, ref_v)) { - /* Advance reference through good tail lines. */ - ref = line; - rkisp_ft_uv_line_mean(ref, bpl, &ref_u, &ref_v); - continue; + for (i = 0; i < tail_rows; i++) { + line = tail + i * bpl; + if (!rkisp_ft_uv_line_bad(line, bpl, ref_u, ref_v)) { + /* Advance reference through good tail lines. */ + ref = line; + rkisp_ft_uv_line_mean(ref, bpl, &ref_u, &ref_v); + continue; + } + memcpy(line, ref, bpl); + repaired = true; } - memcpy(line, ref, bpl); - repaired = true; } if (dev->hw_dev->is_dma_sg_ops) { - sgt = vb2_dma_sg_plane_desc(vb, uv_plane); + sgt = vb2_dma_sg_plane_desc(vb, 0); if (sgt) - dma_sync_sgtable_for_device(dma_dev, sgt, DMA_BIDIRECTIONAL); + dma_sync_sgtable_for_device(dma_dev, sgt, + DMA_BIDIRECTIONAL); + if (have_uv && uv_plane != 0) { + sgt = vb2_dma_sg_plane_desc(vb, uv_plane); + if (sgt) + dma_sync_sgtable_for_device(dma_dev, sgt, + DMA_BIDIRECTIONAL); + } } else { - dma_addr_t da = vb2_dma_contig_plane_dma_addr(vb, uv_plane); - - if (da) - dma_sync_single_for_device(dma_dev, da + uv_off + sync_off, - sync_len, DMA_BIDIRECTIONAL); + dma_addr_t da0 = vb2_dma_contig_plane_dma_addr(vb, 0); + + if (da0 && y_sync_len) + dma_sync_single_for_device(dma_dev, da0 + y_sync_off, + y_sync_len, + DMA_BIDIRECTIONAL); + if (have_uv) { + dma_addr_t da = vb2_dma_contig_plane_dma_addr(vb, + uv_plane); + + if (da) + dma_sync_single_for_device(dma_dev, + da + uv_off + sync_off, + sync_len, + DMA_BIDIRECTIONAL); + } } (void)repaired; } From 9047c5a788420a7df226109e43c65a8c528910f8 Mon Sep 17 00:00:00 2001 From: ymxc152 <1764578602@qq.com> Date: Wed, 26 Aug 2026 14:29:21 +0800 Subject: [PATCH 20/21] =?UTF-8?q?docs(imx296):=20=E7=94=A8=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E5=85=A8=E6=99=AF=E6=9B=BF=E6=8D=A2=E8=BF=87=E6=97=B6?= =?UTF-8?q?=E8=BF=87=E7=A8=8B=E7=A8=BF=EF=BC=8C=E9=AA=8C=E6=94=B6=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E9=BB=98=E8=AE=A4=20/ws/cmd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 24h/4h、0810 ceiling-force 排查、早期绿线修复记录(口径已过时) - 加入 IMX296_问题全景_给后续Agent_20260820.md(#2026082001 / #1017) - 验收脚本默认 CamOS 控制面 /ws/cmd;账号改走 CAMOS_USER/CAMOS_PASS - 新增 scripts/imx296_148_extra.py(Y 跳 / idle line0 / pulse / n1trace) - watchdog 报告默认改到 /tmp,不再依赖已删的 24h md --- ...0\247\346\265\213\350\257\225_20260807.md" | 281 --------- ...0\247\345\244\215\346\265\213_20260810.md" | 89 --- ...22\346\237\245\350\256\260\345\275\225.md" | 535 ------------------ ...14\350\257\201\350\256\260\345\275\225.md" | 214 ------- ...\345\220\216\347\273\255Agent_20260820.md" | 125 ++++ scripts/imx296_148_extra.py | 465 +++++++++++++++ scripts/imx296_day_endurance.py | 2 +- scripts/imx296_day_watchdog.py | 5 +- scripts/verify_accept_user.py | 8 +- scripts/verify_k1301_lat.py | 6 +- 10 files changed, 601 insertions(+), 1129 deletions(-) delete mode 100644 "IMX296_24h\347\250\263\345\256\232\346\200\247\346\265\213\350\257\225_20260807.md" delete mode 100644 "IMX296_4h\347\250\263\345\256\232\346\200\247\345\244\215\346\265\213_20260810.md" delete mode 100644 "IMX296_\347\273\277\347\272\277_20260810_ceiling_force\346\216\222\346\237\245\350\256\260\345\275\225.md" delete mode 100644 "IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" create mode 100644 "IMX296_\351\227\256\351\242\230\345\205\250\346\231\257_\347\273\231\345\220\216\347\273\255Agent_20260820.md" create mode 100644 scripts/imx296_148_extra.py diff --git "a/IMX296_24h\347\250\263\345\256\232\346\200\247\346\265\213\350\257\225_20260807.md" "b/IMX296_24h\347\250\263\345\256\232\346\200\247\346\265\213\350\257\225_20260807.md" deleted file mode 100644 index 05603de8ab321..0000000000000 --- "a/IMX296_24h\347\250\263\345\256\232\346\200\247\346\265\213\350\257\225_20260807.md" +++ /dev/null @@ -1,281 +0,0 @@ -# IMX296 24h 稳定性测试 — 总结报告 - -- **板卡**: `root@192.168.1.180` -- **内核**: `6.1.99-rk3576 #2026080701` / commit `8950d5ff`(分支 `fix/imx296-fast-trigger-n1-pwm-period`) -- **关注点**: 模式切换、ROI set/reset、触发、底部绿线、N-1 相位 -- **报告写入**: 2026-08-08(监控已停;板端 endurance 进程当时仍可能存活) -- **原始数据**: 板端 `/tmp/imx296_day_endurance/`(`summary.*` / `bugs.jsonl` / `events.jsonl` / `console.log`) -- **主机快照**: `/tmp/imx296_day_watchdog/` - ---- - -## 1. 测试怎么跑的 - -### 1.1 两阶段路径(重要) - -| 阶段 | 时间(约) | 路径 | 说明 | -|------|------------|------|------| -| A. Grabber 直连 | 08-07 18:14 → 约 22:40 | `GstV4L2Grabber` 占 `/dev/video11` | **错误路径**。未走 CamOS;后与手动 CamOS 抢设备,TOO_FEW/no_frame 爆炸,结果交叉污染 | -| B. CamOS 前后端 | 08-07 22:52 → 监控截止约 08-08 00:57 | `0_测试运行_camos.sh` + API/WS | **正确路径**。login → PATCH `/api/flow_config` → WS `rkcam_*` → `/preview/frame` + `X-Frame-Tag` | - -**下文结论以阶段 B(CamOS)为准。** 阶段 A 仅作对照说明,不计入 CamOS 轮次统计。 - -### 1.2 CamOS 轮次内容(每 cycle) - -1. `monitor/close`(free_run)抽几帧 -2. `trigger/software`:BASE LO/HI 相位 + 随机 ROI set/reset + 再 ROI -3. 同 run_mode 下切 `line0`:BASE + ROI set/reset -4. 回到 `monitor/close` -5. 判据:增益固定 0 → LO/HI 曝光交替 → `phase`;JPEG 底行 → 绿线;等新 `X-Frame-Tag` - -脚本:worktree `scripts/imx296_day_endurance.py` → 板端 `/tmp/imx296_day_endurance.py` -输出:`/tmp/imx296_day_endurance/` -CamOS 轮次起始 pid:`188060`,`--hours 24` - -### 1.3 测法注意(已写入脚本) - -- **判 N-1 前 `rkcam_set_gain(0)`**:默认用户集增益常 ~12 dB,暗场下会把 LO/HI 对比压扁,假 MIX/LOW_CONTRAST。 -- 曝光:LO≈100 µs / HI≈16000 µs;ROI/模式切换后丢弃 1 枪再采样。 -- 绿线:JPEG RGB 底行启发式(非 NV12 底 48 行;与 grabber 期算法不同)。 - ---- - -## 2. CamOS 阶段终态结果 - -**窗口**: 2026-08-07 22:52:25 → 约 2026-08-08 00:57(uptime ≈ 7519 s,**约 2.1 h**;计划 24h,监控提前结束) - -| 指标 | 数值 | 解读 | -|------|------|------| -| cycles | 39 | 后半程切换失败拖慢 | -| cases | 271 | | -| frames / valid | 1894 / 1788 | valid 率 ≈ **94.4%** | -| **green** | **0** | **绿线未复现** | -| **n1** | **1** | 仅 1 条,且对比度极低(见下) | -| mix | 24 | 多为 ROI/暗场相关 | -| lowc | 2 | 曝光几乎无差分 | -| too_few | 15 | 整段采不够有效帧 | -| no_frame | 106 | 单次触发未等到新 tag | -| switch_fail | **17** | **后半程主矛盾** | -| recoveries | 12 | | -| crashes | 0 | 测试进程未崩 | -| bugs 总计 | **57** | 见分类 | - -### 2.1 Bug 分类(bugs.jsonl 标题归类) - -| 类别 | 约计数 | 代表标题 | -|------|--------|----------| -| 相位 MIX | ~22 | `SW_ROI_*:phase=MIX`、`SW_ROI_RESET:phase=MIX`、`LINE0_ROI_*:phase=MIX` | -| 切换/流失败 | ~17 | `switch_fail:MON` / `MON_retry` / `MON_END` / `LINE0`;`stream_fail:MON*` | -| TOO_FEW | ~15 | ROI 后 / LINE0_BASE / LINE0_ROI_RESET `valid=0/6` 或 `0/8` | -| LOW_CONTRAST | 2 | `SW_BASE:phase=LOW_CONTRAST` | -| N1 | 1 | `SW_BASE:phase=N1` @ cycle 26 | -| green | 0 | — | - -### 2.2 对照「内核绿线 / N-1 修复」目标 - -| 目标 | CamOS 轮结果 | 判断 | -|------|----------------|------| -| 底部绿线 | 0 | **本轮未复现,正面** | -| 明确 N-1 回归 | 1 条边缘样本 | **不能当铁证**(见 §3.2) | -| 长稳切换 + ROI + 触发 | switch_fail / TOO_FEW / no_frame 突出 | **可用性主风险在 CamOS 路径与取帧,不在绿线** | - -### 2.3 与阶段 A(Grabber)对照(污染轮,仅参考) - -Grabber 直连曾到:cycles≈22,bugs≈307,**n1=7**,green=0,但 **noframe≈1945、valid 卡在 530**——典型 **与 CamOS 抢 `/dev/video11` 后流死亡**,N1/MIX 统计不可信。 -该阶段已归档:`/tmp/imx296_day_endurance_archive/grabber_*.tgz`。 - ---- - -## 3. 发现的问题与原因分析 - -### 3.1 【主问题 A】模式切换失败堆积(后半程主导) - -**现象** - -- `switch_fail` / `stream_fail` 合计约 17 次。 -- 约 00:00 后大量 `switch_fail:MON` / `MON_retry`,cycle 几乎耗在切换重试上,summary 一度长时间不刷新。 -- 日志里常见:`info=timed out`;有时 sysfs 已是 `active=free_run` 且 `streaming=1`,脚本仍判失败;也有 `streaming=0` 窗口。 - -**可能原因(按可能性)** - -1. **脚本等待条件过严 / 与 CamOS 语义不一致** - - `wait_stream(need_free=True)` 要求 `active=free_run`;`need_ft=True` 要求 `master_fast_trigger`。 - - CamOS `PATCH /api/flow_config` 成功 ≠ 立刻满足脚本假设;冷却 `RUN_MODE_SWITCH_COOLDOWN_S=5s`、硬件 apply 耗时、ROI 后 pipeline 重建都会拉长窗口。 - - 已 free_run + streaming=1 仍 timeout → 更像 **判定逻辑/超时预算** 问题,而非 sensor 彻底挂死。 - -2. **CamOS 切换路径偶发卡住或恢复不完整** - - `apply_flow_config_patch` 串行 + 硬件 `set_trigger_mode`;失败会 rollback,但前端/脚本若连续狂切,易叠加 cooldown 拒绝与 recover 重启。 - - recover 会 `pkill server.py` + 再跑 `0_测试运行_camos.sh`(含 **vite build**),一次恢复可达数十秒~数分钟,期间全是 fail。 - -3. **preview/流在 monitor 边界短暂 streaming=0** - - 切换瞬间 grabber 停-起,脚本把「短暂无流」记成 stream_fail。 - -**影响**: 不是绿线/N-1,但会 **严重稀释 24h 有效 cycle 数**,并把 bugs 数抬高。 - -**建议改法** - -| 层级 | 动作 | -|------|------| -| 测试脚本 | 放宽 `wait_stream`:monitor 只需 `streaming=1`(不必死磕 free_run 字符串);拉长 settle/timeout;cooldown 显式 sleep ≥5.5s 且对「已是目标模式」短路径成功 | -| 测试脚本 | recover **禁止**动辄整站 `0_测试运行_camos.sh`(会 rebuild 前端);改为仅 `patch monitor` + `rkcam_reset_roi` + 重连 WS;真死再用轻量 `python server.py` 拉起 | -| CamOS | 给 flow 切换增加「硬件已达目标 + streaming」的统一 ready 查询;超时错误区分 cooldown / hardware fail / stream down | -| 运维 | 长稳前确认单实例 server.py;避免测试中手动再开 CamOS | - ---- - -### 3.2 【主问题 B】触发后无新帧 → no_frame / TOO_FEW - -**现象** - -- `no_frame=106`,`too_few=15`(整 case valid 不足一半)。 -- 多发于:**ROI set/reset 之后**、`LINE0_BASE` / `LINE0_ROI_RESET`。 -- 机制:`fire_trigger`(WS `rkcam_trigger_once` + `/debug/trigger_once` + sysfs XTRIG)后轮询 `/preview/frame`,要求 **新的 `X-Frame-Tag`**。 - -**可能原因** - -1. **ROI 切换后 pipeline/encoder 重建窗口** - - CamOS ROI 会停流重建;脚本 settle 2.5s 可能不够,或首几枪 tag 不涨。 -2. **LINE0 与 software 触发路径不一致** - - line0 更依赖外部/XTRIG;仅 WS soft trigger 可能无效;sysfs 脉冲时序与 sensor 窗口不对齐会丢帧。 -3. **preview JPEG 路径与「原始触发帧」不同步** - - 测的是编码后 preview,不是 raw NV12;tag 更新策略若合并/跳帧,会表现为「触发了但 tag 不变」。 -4. **暗场 + 极短曝光** 不直接导致 no_frame,但会与相位问题叠加。 - -**建议改法** - -| 层级 | 动作 | -|------|------| -| 测试脚本 | ROI 后:更长 settle(≥ ROI 实测 latency,历史 CamOS ~3.7s)+ 多 discard 帧;LINE0 明确只靠 XTRIG 或只靠已验证的 trigger API | -| 测试脚本 | tag 不变时区分「HTTP 失败 / 旧 tag / 空 body」,写入 bug 字段,便于分流 | -| CamOS | ROI 完成后 ready 信号;trigger_once 与 preview tag 原子绑定(每成功触发必新 tag 或明确 error) | -| 内核/驱动 | 若 ready 后仍系统性丢触发,再查 FT/XTRIG;**先排除 CamOS/脚本** | - ---- - -### 3.3 【次要】相位 MIX / LOW_CONTRAST(多与测法 + ROI) - -**现象** - -- MIX ≈22,LOW_CONTRAST=2;集中 SW/LINE0 的 ROI 与 RESET。 -- 多数 means 贴在 ~18.3–19.5,**span 常 <1**,暗场 JPEG 对比极差。 - -**原因** - -1. **场景本身极暗**:中心 luma 几乎贴地,LO/HI 差分小,标签抖动 → MIX。 -2. **曝光/增益生效滞后**:WS 异步队列;settle 不足时 HI 帧仍像 LO。 -3. **ROI 后首帧/尺寸变化** 污染序列(已有 1 枪 discard,可能不够)。 -4. **与真 N-1 的区分**:真 N-1 在 **span 足够大** 时 labels 整体错一位;本轮多数 MIX **不具备** 这种清晰对比。 - -**建议改法** - -- 相位用例加可控照明或提高 HI 上限 / 拉长 HI,目标 span≫5。 -- 或对 span<阈值的 case **降级为 inconclusive**,不记 bug(与 green/N1 硬故障分开)。 -- 保持 gain=0;曝光 set 后 wait 可调到 1 帧时间以上。 - ---- - -### 3.4 【N-1】唯一样本 — 证据弱 - -- **唯一**: `SW_BASE:phase=N1` @ **cycle 26**(2026-08-07 23:36:16) -- means ≈ `[19.024, 18.39, 18.39, 18.391, 18.39, 18.39, 18.39]`,labels 几乎全 LO,span≈0.63 -- **解读**: 更像「只有一点略亮、其余无对比」被算法打成 N1,**不是** grabber 早期那种 span~4、整段错位的经典 N-1。 -- **结论**: **不能单独作为内核 N-1 回归失败的证据**;需在高对比条件下复测。 - ---- - -### 3.5 【绿线】本轮未复现 - -- CamOS 轮 `green_frames=0`,bugs 无 green 类。 -- 注意:本轮绿线判据是 **JPEG 底行 RGB**,与早期 NV12 底 48 行不同;若只关心 ISP raw 底行,建议另加 raw/NV12 探针或内核侧 n1trace/绿线专用 case。 -- 结合阶段 A 在污染前也长期 green=0,**倾向:当前 `#2026080701` 上绿线不是主复发项**。 - ---- - -## 4. 根因分层结论(给后续排期) - -``` - ┌─────────────────────────┐ - 本轮主噪声 ──► │ CamOS 切换超时/等待过严 │ → 改脚本判定 + CamOS ready API - │ ROI/触发后 preview 无新帧 │ → settle/tag 绑定 + LINE0 触发路径 - └─────────────────────────┘ - ┌─────────────────────────┐ - 测法噪声 ──► │ 暗场 JPEG LO/HI 对比不足 │ → 照明/曝光/span 门闩 - └─────────────────────────┘ - ┌─────────────────────────┐ - 内核目标 ──► │ 绿线:未复现 │ → 保持观察,可加 NV12 探针 - │ N-1:1 条弱样本 │ → 高对比专用复现后再定 - └─────────────────────────┘ -``` - -**一句话**: -当前 `#2026080701` 在 **CamOS 真实路径** 上,**绿线没出来、N-1 没有可靠复现**;真正拖垮长稳的是 **模式切换失败** 和 **ROI/触发后等不到 preview 新帧**,应优先从 **CamOS + 测试脚本** 修,而不是先再刷一版内核。 - ---- - -## 5. 建议的解决顺序(可执行) - -### P0 — 让 24h 跑得动、统计可信 - -1. 改 endurance:`wait_stream` 语义、cooldown、禁止 recover 触发前端 build。 -2. ROI 后 settle ≥ 实测 latency;LINE0 触发路径单一化并打日志。 -3. span 过低的相位结果标 `INCONCLUSIVE`,不计入 n1/mix bug。 -4. 确认板上始终 **单** `python server.py`,测试期间无人手开第二套 CamOS。 - -### P1 — 验证内核修复是否仍站得住 - -5. 固定高对比场景(灯/白墙 + gain0 + LO/HI span 目标)跑 software/line0/ROI 各 N 次。 -6. 可选:并行短时 NV12/raw 底行绿线探针,与 JPEG 解耦。 -7. 只有 P0 稳定后,再开真正 24h,并以 green/n1(高对比)为门禁。 - -### P2 — CamOS 产品侧 - -8. flow 切换 ready 状态机;ROI/trigger 与 `X-Frame-Tag` 契约。 -9. 避免测试/恢复路径走完整 `vite build`。 - ---- - -## 6. 过程时间线(精简) - -| 时间 | 事件 | -|------|------| -| 08-07 18:14 | Grabber 版 24h 启动(路径错误) | -| 18:14–22:xx | Grabber:早期有 N1;中后期抢设备 → no_frame/TOO_FEW 主导 | -| ~22:40 | 停 Grabber,归档;CamOS 单实例拉起 | -| **22:52** | **CamOS endurance 启动** pid=188060;冒烟 SW/LINE0 多为 CUR,green=0 | -| 22:52–23:30 | 正常推进;出现 MIX/TOO_FEW;**23:36 唯一 N1(弱)** | -| ~00:00 后 | **switch_fail:MON\*** 连发,有效 case 增速下降 | -| 08-08 ~00:57 | 监控停止时快照:cycles=39 bugs=57 green=0 n1=1 | -| 08-08 | 主机 cron / watchdog 已停;本总结写入 | - ---- - -## 7. 附件与命令 - -```bash -# 板端结果 -ssh root@192.168.1.180 'cat /tmp/imx296_day_endurance/summary.txt; wc -l /tmp/imx296_day_endurance/bugs.jsonl' - -# 归档(示例) -ssh root@192.168.1.180 'tar czf /tmp/imx296_day_endurance_archive/camos_$(date +%Y%m%d_%H%M%S).tgz -C /tmp imx296_day_endurance' - -# 停板端测试(若仍需要) -ssh root@192.168.1.180 'pkill -f /tmp/imx296_day_endurance.py' -``` - -- 脚本: `scripts/imx296_day_endurance.py`(CamOS)、`scripts/imx296_day_watchdog.py`(已停) -- 内核记录入口: 记忆/既有 `IMX296_绿线修复_*`、`IMX296_N1相位滞后_*` -- 构建号: DLCVCAM `#2026080701` - ---- - -## 8. 最终判定(给决策) - -| 问题 | 是否本轮确认 | 优先级 | 主责方向 | -|------|--------------|--------|----------| -| 底部绿线复发 | **否** | 观察 | 内核已像稳住;可选 NV12 补测 | -| N-1 回归 | **未确认**(1 弱样本) | 中(要高对比复测) | 测法 + 必要时内核 | -| 模式切换超时 | **是** | **P0** | 测试脚本 + CamOS | -| ROI/触发失帧 | **是** | **P0** | CamOS preview/trigger + 脚本 settle | -| 暗场相位 MIX | **是(噪声为主)** | P1 | 测法门闩 | - -**总体**: CamOS 路径约 2.1h 样本支持「绿线/硬 N-1 不是当前头号问题」;**先修切换与取帧,再重开干净 24h。** diff --git "a/IMX296_4h\347\250\263\345\256\232\346\200\247\345\244\215\346\265\213_20260810.md" "b/IMX296_4h\347\250\263\345\256\232\346\200\247\345\244\215\346\265\213_20260810.md" deleted file mode 100644 index 6fe930a96b9dc..0000000000000 --- "a/IMX296_4h\347\250\263\345\256\232\346\200\247\345\244\215\346\265\213_20260810.md" +++ /dev/null @@ -1,89 +0,0 @@ -# IMX296 4h 稳定性复测(#2026081008 绿线 / N-1 / ROI) - -- **开始时间**: 2026-08-10 20:57 (CST) -- **计划时长**: 4 小时(约至 2026-08-11 00:57) -- **板卡**: `root@192.168.1.180` -- **内核**: `#2026081008` pub_gate=6(drop+warm 后计数)+ CamOS `SMARTCAM_TRIGGER_WARMUP_FRAMES=12` -- **脚本**: 板端 `/tmp/imx296_4h_endurance.py` → `/tmp/imx296_4h_endurance_1008/` -- **主机产物**: `/tmp/imx296_4h_watchdog_1008/summary.json`(板端 `/tmp` 为 tmpfs,重启/崩溃后脚本与目录曾丢失) -- **关注故障**: 底绿、N-1、模式切换/ROI、首枪无图 - -## 成功标准 - -| 项 | 标准 | 结果 | -|----|------|------| -| classic 绿线 `green_frames` | **0** | **0** ✅ | -| hard N-1 `n1_cases` | **0** | **0** ✅ | -| 切换/ROI 硬失败 | 0 | switch_fail=0 roi_fail=0 ✅ | -| 跑满 4h | 是 | **否** — 有效跑 **~2.21h** 后 CamOS 崩溃,测试中断 ⚠️ | - -## 有效段统计(最后完整 summary @ 2026-08-10 23:10) - -| 指标 | 值 | -|------|-----| -| started → updated | 20:57:31 → 23:10:25 | -| uptime | **7974 s(2.21 h)** | -| cycles | **64** | -| cases | 510 | -| frames / valid | **3570 / 3570** | -| green_frames | **0** | -| n1_cases | **0** | -| mix_cases | **1** | -| bugs | **1** | -| switch_fail / roi_fail / crashes | 0 / 0 / 0 | -| lowc / incon | 4 / 4(暗场相位不可判,不记硬故障) | -| build | `#2026081008` | - -### 主要 case 覆盖(by_case 摘要) - -| case | n | green | n1 | mix | -|------|---|-------|----|-----| -| MON_FREERUN | 64 | 0 | 0 | 0 | -| SW_BASE | 64 | 0 | 0 | 0 | -| SW_ROI_RESET | 64 | 0 | 0 | 0 | -| LINE0_BASE | 64 | 0 | 0 | 0 | -| LINE0_ROI_RESET | 63 | 0 | 0 | 0 | -| 其它 ROI 变体 | 多次 | 0 | 0 | 见下 1 次 MIX | - -### 唯一 bug(非绿线、非 hard N-1) - -- **时间**: 2026-08-10 22:24:29 cycle 42 -- **title**: `SW_ROI2_352x328:phase=MIX` -- **含义**: 小 ROI 352×328 下 LO/HI 曝光交替相位判为 MIX(期望 LO/HI/LO/HI,实得 LO/LO/LO/HI);`green=false`,bot 非绿带 -- **means**: ~18.4 / 19.4 / 18.4 / 25.5,span≈7.1 — 更像曝光切换 settle/小 ROI 亮度对比弱,**不是**经典 N-1 整帧滞后,也**不是**底绿 -- 其余 509 cases 无 mix/n1/green - -## 中断原因(非目标故障复现) - -1. **23:10–23:11** 附近 journal:`camos.service: Failed with result 'signal'`;伴随 `imresize_t failed` / RGA / mpp detach 噪声 -2. endurance 进程随 CamOS 死亡;watchdog 自 23:13 起报 dead -3. 板端 `/tmp` 为 **tmpfs**,脚本 `/tmp/imx296_4h_endurance.py` 与输出目录随后不可用;watchdog 盲目 restart 失败刷屏(已停掉) -4. 主机在 23:10 已拉到完整 `summary.json` / `bugs.jsonl`,**以该快照为有效结论** - -## 判定 - -| 维度 | 结论 | -|------|------| -| **底绿回归** | **未复现**(3570 帧 green=0;含 mon→sw、line0、ROI set/reset) | -| **hard N-1** | **未复现**(n1_cases=0) | -| **4h 完整度** | **未跑满**(2.21h / 64 cycle 后基础设施崩溃) | -| **总评** | **目标故障(绿线/N-1)在有效窗口内通过**;完整 4h 需修 CamOS 稳定性或把脚本放到非 tmpfs 后重跑 | - -## 产物路径 - -- 主机:`/tmp/imx296_4h_watchdog_1008/summary.json`、`bugs.jsonl`、`summary.txt`、`watchdog.log` -- 报告本文:`IMX296_4h稳定性复测_20260810.md` -- 绿线排查 MD §10.11 关联 - -## Watchdog 有效采样(摘录) - -| 时间 | cycles | bugs | green | n1 | -|------|--------|------|-------|-----| -| 20:58 | 1 | 0 | 0 | 0 | -| 22:24 前 | … | 0 | 0 | 0 | -| 22:24+ | … | 1(MIX) | 0 | 0 | -| 23:10 最后 | 64 | 1 | 0 | 0 | -| 23:13+ | 进程死 / CamOS signal | — | — | — | - ---- -*结论写入: 2026-08-11 09:03:38* diff --git "a/IMX296_\347\273\277\347\272\277_20260810_ceiling_force\346\216\222\346\237\245\350\256\260\345\275\225.md" "b/IMX296_\347\273\277\347\272\277_20260810_ceiling_force\346\216\222\346\237\245\350\256\260\345\275\225.md" deleted file mode 100644 index ac5f7a92c89cc..0000000000000 --- "a/IMX296_\347\273\277\347\272\277_20260810_ceiling_force\346\216\222\346\237\245\350\256\260\345\275\225.md" +++ /dev/null @@ -1,535 +0,0 @@ -# IMX296 底部绿线(2026-08-10)— ceiling-force 排查记录 - -> 状态:**#1009 源码已改(未刷板)——C1+C2:gate 仅 warm==0 后递减;IRQ 路径 warm 期 mi_drop 并消耗 warm。见 §12。** -> 板端内核(当前联调仍为):`#2026081008`;树内待刷:`#2026081201` `skip=2 drop=2 warmup=8 pub_gate=6 force_post=70000` -> 历史复现基线:`#2026081003`;CamOS 已弃 discard forced-clear 开闸,warmup=脉冲预算(post=3 回退,见 §11.4) -> 早期复现时 CamOS:warmup 默认 **2**,`residual_discard` forced clear,`post_drop_ms=100` -> 产物:板端 `/tmp/green_capture_run/`(`summary.json`、绿图、`dmesg_full.txt` / `dmesg_ft.txt`) -> 相关:`IMX296_N1相位滞后_诊断与修复记录.md`(N-1 / ceiling-force 背景)、`IMX296_绿线修复_内核修改与验证记录.md`(早期绿线策略) - ---- - -## 1. 现象(用户 + 存图) - -- 触发模式下底部约 **8 行绿带**(经典 NV12 UV 未写完),**概率性**。 -- **不固定** mon→trig 第 1 帧或第 5–6 帧;也可能首帧;常见 **连续 2 帧**。 -- **触发下 ROI set/reset** 后同样概率出现。 -- 用户指名样例(已像素核实): - - `images/2026-08-10/NG/20260810_164309_812_042048.jpg` bot8≈(70,88,40) gdom≈88% - - `images/2026-08-10/NG/20260810_164310_668_456645.jpg` bot8≈(70,88,40) gdom≈90%(连续下一张) - - 同段还有 pure 绿底:`...164301_870...` bot8≈(6,141,5) - -自动化误判过「0 绿」:software 路径/取图缓存与 **line0 真实节拍 + 产品存图** 不一致;**以用户 NG + 带 n1trace 的复现为准**。 - ---- - -## 2. 受控复现(协议一轮,`#2026081003`,n1trace 短开) - -脚本:`/tmp/green_capture_run.py` → 板端 `/tmp/green_capture_run/` - -| 段 | 结果 | -|----|------| -| **A mon→software head16** | **GREEN ×2:sw#3、sw#4 连续** gdom≈84% bot8≈(70,87,41) | -| B mon→line0 | 本轮 `debug/trigger_once` 在 line0 下 tag 不涨,**采样无效**(不能当 line0 已好) | -| C ROI 592 / reset | 本轮 0 绿(未打中) | - -sw 序列: - -``` -sw0–2 暗底、不绿 -sw3–4 GREEN 带(连续) -sw5+ 正常 (92,87,74) -``` - -CamOS 同次切换: - -``` -warmup 2/2, residual_discard=1 forced clear, post_drop_ms=100, mode=software -warmup dt≈0.63s + quarantine 0.1s -``` - ---- - -## 3. n1trace 对齐结论(问题点) - -进 FT 后 arm: - -``` -isp_ft_arm height=1088 skip=2 drop=2 warmup=8 -ceiling_us=70790 force_post=70000 -``` - -### 3.1 已确认的机制 - -1. **V39 + IMX296 FT 上 MI OFFS 长期为 0**(`y_shd=y_st=cb_*=0`) - → early-done **几乎 100% 走 `ceiling-force`**,再 `early complete` + `mi_pub WORK early=1`。 - (与 N-1 文档一致:不能 abandon 到下一拍 MI FE。) - -2. 绿带形态 = **底 ~8 行 UV 不完整**(与 wait_line 终端 8 行历史问题同类)。 - -3. **force→complete 的 post-hold 在绿帧上已经跑满** - - warmup 段 hold ≈ **138ms**(force 70 + ~¾ residual) - - warm 用尽后 ≈ **123ms** - - complete 时 **`warm_left>0`**,**不是**「下一 SOF 打断 stale arm 导致 hold 被砍」。 - -4. **同一条 ceiling-force 路径上:前段发布可绿,后段发布正常** - - 绿帧 hold **更长**(warmup),后段 ok 帧 hold **更短**仍干净。 - - ⇒ **否定「再盲目加长 force_post 就能根治」** 作为主因假设。 - -### 3.2 mi_pub 与用户帧粗对齐(drop=2 耗尽后) - -| mi_pub(约) | warm_left@complete | 用户 sw#(约) | 画质 | -|--------------|--------------------|----------------|------| -| (skip/drop 阶段) | — | 被内核丢掉 | — | -| pub1–3 | 5→3 | sw0–2 | 暗、不绿 | -| **pub4–5** | **2→1** | **sw3–4** | **GREEN** | -| pub6+ | 0 | sw5+ | 正常 | - -cnt=1、2 的 `early complete` 日志侧 **无对应 mi_pub**(与 `mi_drop`/半开会话交错)——发布门槛与 drop 计数**不完全同一条可见路径**,仍属待挖点。 - -### 3.3 一句话问题点 - -> **进 FT 后早期已发布的 ceiling-force 帧,在 post-hold 已满的情况下仍可能带未完成 UV;管道稳定后同一 force 路径不再绿。** -> 产品上再叠加:**CamOS warmup=2 过短**,脏发布会漏到用户可见 tag;ROI 重 arm 会再次打开同一窗口。 - -不是: - -- 单纯 mon→trig「固定第 N 帧」 -- 单纯 hold 从 45ms→70ms 不够再加一点就能完事 -- 与业务 NG 分类误报(像素已是真绿带) - ---- - -## 4. 今日 NG 簇形态(辅助) - -`2026-08-10/NG` classic 绿约 22 张 / 11 簇:多为 **1×PURE 或 2×BAND 连发**,簇间隔随机——与「进 FT / 重 arm 后短窗口」一致。 - -16:43 用户簇与内核: - -``` -16:42:56 再进 FT arm skip=2 drop=2 force=70ms -16:43:01 seq≈87 PURE -16:43:09–10 seq≈90–91 BAND×2 ← 用户两张 -随后恢复正常 -``` - ---- - -## 5. 已做 / 未做的改动(避免继续盲改) - -| 项 | 状态 | -|----|------| -| force_post 45→70、warmup hold 85×8、residual floor 抬高(`#1002/#1003`) | 已刷;**绿线仍在** | -| SKIP_BUF 2↔4 来回 | 只影响前几枪是否出图;**单独不能当根治** | -| 仅 CamOS warmup 2→6 A/B(更早) | 当时矩阵显示不够;**需在长 hold 内核上重验** | -| 本地树 `DLCVCAM_BUILD_VERSION=1004` + 源码 SKIP=4 | **未证明已刷**;板端复现时仍是 **1003 / drop=2** | -| line0 外部真触发 + n1trace | **未完成**(本轮 line0 采样失败) | -| ROI 绿 + n1trace 打中 | **未完成** | - ---- - -## 6. 还差什么(排查清单) - -优先级从高到低: - -1. **[P0] 验证「隔离早期脏发布」是否足够** - - CamOS `SMARTCAM_TRIGGER_WARMUP_FRAMES≥6`(吃掉 sw3–4 一类脏帧) - - 与内核 `drop/skip` 对齐:用户首枪在 quarantine 之后 - - 重跑 `/tmp/green_capture_run.py`:mon→sw 连续多轮,**GREEN_COUNT=0** - -2. **[P0] line0 真路径** - - 外部触发或产品存图,不要只靠 `debug/trigger_once` - - 对时 `trigger-dev seq` ↔ `mi_pub` ↔ NG 文件名 - -3. **[P1] 为何 early force 帧脏、后期同路径干净** - - `mi_pub` 时 `vb_seq` / buffer index 与 `sof` 是否严格一对一 - - FT 入口 dummy×3 / 半开帧是否污染前几块 buffer 的 UV - - cnt=1,2 complete 无 mi_pub 的精确分支(drop vs skip_frame vs 无 buf) - -4. **[P1] ROI 重 arm** - - 高度 592/832/1088 切换时是否重新 `isp_ft_arm` + 短 quarantine - - 与 mon→trig 是否同一类早期脏 pub - -5. **[P2] 若隔离不够:再动完成条件** - - 仅对 `warm_left>0` 的 force 完成 **禁止 pub / 强制 drop**(内核) - - 或 force 路径额外等 MI FE(可能拉回 N-1,需极度谨慎) - - **禁止**无 n1trace 对照的大跳 force_post / drop=10 - ---- - -## 7. 修复方向(最小、可验证) - -### 7.1 产品向(优先做) - -| 层 | 动作 | 理由 | -|----|------|------| -| CamOS | `SMARTCAM_TRIGGER_WARMUP_FRAMES=6`(runtime_settings 或等价) | 复现上脏帧在用户 sw#3–4;warmup=2 盖不住 | -| 内核 | 保持 ceiling-force + 现 hold;`SKIP_BUF/SKIP_EARLY` 与 warmup **对齐且 ≤warmup**(建议 4/4 或维持 2/2 但 warmup≥6) | 半开帧仍丢;用户首枪在 gate 打开后 1:1 | -| 验证 | mon→sw ≥3 轮 head16;ROI set/reset;line0 人工/外部触发 | GREEN=0 且首枪可出图 | - -### 7.2 不做(除非 7.1 失败且有新 n1trace) - -- 再把 `force_post` 加到 100ms+ 当唯一手段 -- `SKIP_BUF≥10`(已知「前 10 枪不出图」) -- 关掉 ceiling-force 回到 abandon(N-1 回归) - -### 7.3 成功标准 - -- mon→software / line0 / ROI reset:用户可见帧 bot8 gdom<40% 且无 pure 绿底 -- 模式切换后 **第 1 次用户触发有图**(warmup 在 gate 内消耗) -- dmesg arm 与 n1trace 抽检仍为 ceiling-force(N-1 不回退) - ---- - -## 8. 时间线摘要 - -| 时间 | 事件 | -|------|------| -| 早前 | N-1 ceiling-force 合入;绿线靠 residual+hold+drop | -| 2026-08-10 | 用户再报随机底绿 + ROI 绿;hold 加长刷 `#1002/#1003` | -| 16:43 | 用户 NG 实锤 BAND×2 + PURE;arm 已是 force=70ms | -| 17:08 | 受控 mon→sw 复现 GREEN sw#3–4 + 完整 n1trace | -| 结论 | 早期 ceiling-force **已发布**帧脏;hold 满仍可绿;后段同路径干净 | - ---- - -## 9. 板端/树状态备忘 - -- 复现内核:`#2026081003` skip=2 drop=2 -- 本地未提交 diff 曾指向 1004 + SKIP=4(**以实机 uname/dmesg arm 为准**) -- n1trace 默认关;抓数时短开,结束后必须 `echo 0 > .../n1trace` -- 备份示例:`/root/boot-backups/boot-before-2026081003.img` - ---- - -## 10. 修复实施与验证日志(2026-08-10 晚) - -> 状态:**#1008 mon→sw 4 轮 TOTAL_GREEN=0(2026-08-10 18:01)** -> 板端:`#2026081008` pub_gate=6(仅 drop+warm 后计数)+ CamOS warmup=12 -> 仍待:ROI / line0;CamOS residual_discard forced clear 仍是产品侧缺口。 - -### 10.1 问题点(不变) - -- FT 进线后 **ceiling-force** 几乎 100%(MI OFFS=0)。 -- **post-hold 已满**(~123–138ms)的 early `mi_pub` 仍可底 8 行绿 UV;同路径稍后干净。 -- 产品缺口:CamOS warmup 过短 + residual_discard **forced clear** 会在 sample 未到时提前开闸。 - -### 10.2 版本轨迹 - -| 构建 | 关键改动 | mon→sw 矩阵 | 备注 | -|------|----------|-------------|------| -| #1003 | force=70 / warm hold / skip=drop=2 | GREEN sw#3–4 | 复现基线 | -| #1005 | `warm_left>0` → mi_drop early WORK | **14/36** 头 0–4 绿 | warm 后仍绿 | -| CamOS warmup 6→8→**12** | runtime_settings + src clamp 16 | 单独不够 | residual_discard 常 forced clear | -| #1006 | `pub_gate=10` 仅 `frame_early` | r0/r2 ok,**r1 全绿 0–4** | r1 仅 `mi_pub IRQ early=0`,gate 未吃 IRQ | -| #1007 | `gate_drop` 不限 early | **15/48** 多轮头 0–4 绿 | gate 与 warm **并行递减**,warm 尽时 gate=0 | -| #1008 | **gate 仅在 `!drop && !warm_drop` 时递减**;`PUB_GATE=6` | **0/48** mon→sw×4 head12 | 见 §10.9;n1trace 确认 gate 在 warm 后 6→0 再 mi_pub | - -### 10.3 n1trace 关键证据 - -**#1005(warm_left only)** -进 FT 后 `mi_drop WORK warm=8→1`,随后 `mi_pub` 且用户 sw0–4 绿 → quarantine 长度不够。 - -**#1006 r1(early-done 未跑起来)** - -``` -isp_ft_arm ... pub_gate=10 -mi_drop IRQ ×2 (drop_left) -随后大量 mi_pub IRQ early=0 ← gate 因要求 frame_early 未扣 -residual_discard=11 forced clear -用户 r1_0..4 GREEN -``` - -**#1007(gate 与 warm 抢计数)** - -``` -mi_drop ... warm=8 gate=9 -... -mi_drop ... warm=0 gate=0 cnt=10 ← 第 10 次 drop 后 gate 已空 -mi_pub pub=1.. → 用户头帧仍绿 -``` - -### 10.4 #1008 设计 - -| 层 | 配置 | -|----|------| -| 内核 `ISP39_IMX296_PUB_GATE_FRAMES` | **6**(覆盖 #1005 头 0–4 +1) | -| `capture_v39` | `gate_drop = ft_diag && gate && !drop && !warm_drop`;IRQ/WORK 皆可 | -| `warm_drop` | 仍仅 `frame_early && warm` | -| CamOS | `SMARTCAM_TRIGGER_WARMUP_FRAMES=12`,interval 150ms;src clamp max 16 | -| 期望 dmesg | `pub_gate 6`;`mi_drop ... gate=6→1` **出现在 warm/drop 之后**;再 `mi_pub` | - -### 10.5 CamOS 已知缺口(未改二进制逻辑) - -- `_warmup_sensor` 脉冲后若 sample 未减 `warmup_discard_cnt`,结束时 **forced clear residual** → 用户闸门可早于内核 quarantine。 -- journal 常见 `triggered 12/12 residual_discard=9..11 forced clear`。 -- 源码树:`/root/smartcam_bs/.../gst_v4l2_grabber.py`;运行时为 **nuitka** `camos` 二进制,靠 `runtime_settings_dlcvcam.json` `startup_env` 注入。 -- 彻底修应用侧:warmup 必须等到 discard 真扣完或与内核 pub 对齐,禁止 forced clear 开真触发(**待做,非本轮内核**)。 - -### 10.6 板端备忘 - -- 备份:`/root/boot-backups/boot-before-202608100{5,6,7}.img` -- 交付镜像:`boot-rk3576-6.1.99-2026081008-01726d6f-dirty.img` -- n1trace 默认关;抓完 `echo 0 > /sys/module/video_rkisp/parameters/n1trace` -- 产物目录:`/tmp/green_verify_1005` `1006` `1007`(及后续 1008) - -### 10.7 成功标准(复述) - -- mon→software ≥3 轮 head12:`TOTAL_GREEN=0`,首枪有图 -- dmesg 仍 ceiling-force(N-1 不回退) -- ROI set/reset + line0 另验 - -### 10.8 下一步 - -1. 刷 `#1008`,CamOS warmup=12,重跑 mon→sw 4 轮矩阵 -2. 若仍绿:对照 n1trace 看 `gate` 是否在 pub 前耗尽;考虑 gate≥8 或修 CamOS forced clear -3. 绿清零后再 ROI / line0 - ---- - -*文档随刷机验证持续追加;勿只改常量不写 n1trace。* - -### 10.9 #1008 验证结果(2026-08-10 18:01–18:03) - -- 内核:`#2026081008` dmesg `pub_gate 6` -- CamOS:`warmup triggered 12/12`(仍常见 `residual_discard=8..12 forced clear`) -- 矩阵:mon→software **4 轮 × head12**,`TOTAL_GREEN **0**`(`/tmp/green_verify_1008`) -- n1trace 形态(正常 early-done 轮): - - `mi_drop` drop×2 + warm 递减时 **gate 保持 6** - - warm=0 后 `gate=5→0` 共 6 次再 `mi_pub` -- 首用户帧偶发 `dt≈2–3s / pulses=3–4`:gate 尚未耗尽时 tag 慢进,属 quarantine 代价,不是绿线 -- **未验**:ROI set/reset、line0 外部真触发、产品存图路径长时间压测 - -### 10.10 仍建议的应用侧收尾 - -1. 禁止 warmup 结束 `forced clear` 开真触发;或 warmup 脉冲等到 discard 真扣完 -2. 将 `SMARTCAM_TRIGGER_WARMUP_FRAMES` 与内核 skip+drop+warm+gate 文档化对齐(当前 12 够用) -3. ROI 边界强制同一套 quarantine(与 mon→FT 同 arm) - -### 10.11 4h 稳定性复测(2026-08-10 20:57 起) - -- 脚本:板端 `/tmp/imx296_4h_endurance.py --hours 4` → `/tmp/imx296_4h_endurance_1008` -- 报告:`IMX296_4h稳定性复测_20260810.md` -- 覆盖:mon/trig×software/line0、ROI set/reset、底绿、N-1 LO/HI -- 内核保持 `#2026081008`,n1trace 关 - -### 10.12 4h 复测结论(有效窗口 ~2.21h) - -- **green=0 / n1=0**,frames=3570,cycles=64;唯一 bug 为小 ROI `352x328` phase=MIX(非绿、非 hard N-1) -- 未跑满 4h:23:11 CamOS signal 退出,`/tmp` 脚本丢失 -- 详见 `IMX296_4h稳定性复测_20260810.md` - - ---- - -## 11. 2026-08-12 复盘:line0 底绿复发 + 文档/实现偏差(CamOS 侧已回退 post=6) - -> 状态:**文档 §10.9「warm 期间 gate 保持 6」在当前 line0/IRQ 主导路径上不成立** -> 板端内核仍为 `#2026081008`(`skip=2 drop=2 warmup=8 pub_gate=6 force_post=70000`) -> CamOS 分支:`fix/roi-trigger-n1-master-rebase`(warmup 已改为脉冲预算 + quarantine;**已回退** post=6/min_appsink=5) -> 产物:板端 `/userdata/imx296_warmup_ab/n1trace_gate_20260812_145230/`、`line0_sw_green_*`、`line0_post6_*`、`line0_post3_revert_*` -> **本轮未改内核代码**,只修订记录与方案。 - -### 11.1 本轮用户侧现象(相对 §10.9 mon→sw green=0) - -| 项 | 观察 | -|----|------| -| mon→line0 头枪 | 早期曾 leading miss 2–4 枪;CamOS 修 PLAYING/prime/post=3 后可稳定 **first_hit=1** | -| apply 墙钟 | 目标 ≤5s;post=3 路径 avg≈4–5s,prime 最坏 ~16 枪时偶发 >5.5s | -| 底绿 / 底绿线 | **line0 多轮抽检仍绿**:`line0_sw_green_20260812_133403` line0_green=22;`135804` 仍 13(且 first_hit 全 1) | -| CamOS 加 post | post=6 + min_appsink=5:green 仍 12,**first_hit 退化** `[4,1,1,5,1,1,3,7]` → **已回退** post=3 / min_appsink=1 | - -结论:在 first_hit 与 ≤5s 约束下,**CamOS 再堆 residual post 消不掉底绿,且伤头枪**。绿帧主因回到内核发布相位。 - -### 11.2 内核契约(源码,#1008 树,与板一致) - -`rkisp.c` 进 FT arm(常数未改): - -| 符号 | 值 | 含义 | -|------|----|------| -| `SKIP_EARLY_FRAMES` | 2 | 头 SOF 不做 early-done | -| `SKIP_BUF_FRAMES` | 2 | `early_done_drop_left` + `stream->skip_frame` | -| `WARMUP_FRAMES` | 8 | `early_done_warmup_left`;warmup 段 post-MI hold 更长 | -| `PUB_GATE_FRAMES` | 6 | `early_done_pub_gate_left` | -| `FORCE_POST_MI_US` | 70000 | ceiling-force 后 hold | -| `WARMUP_POST_MI_US` | 85000 | warm 段 hold | - -`capture_v39.c` `mi_frame_end` 丢帧条件(摘要): - -```text -warm_drop = ft_diag && frame_early && warm>0 -gate_drop = ft_diag && gate>0 && !drop && !warm_drop -if (drop || warm_drop || gate_drop) → mi_drop(requeue,无 vb2_done) -``` - -**关键点(相对 §10.7–10.9 叙述)**: - -1. **`warm_drop` 要求 `frame_early==1`**。 - 当 complete/发布走 **`early=0`(IRQ / MI FE)** 时,即使 `warm_left>0`,**也不会**走 warm_drop。 -2. 此时若 `drop_left==0` 且 `gate>0`,**`gate_drop` 为真** → **gate 在 warm 尚未耗尽时被 IRQ 路径打掉**。 - 这与 §10.9 写的「drop×2 + warm 递减时 **gate 保持 6**;warm=0 后 gate 6→0 再 mi_pub」**只覆盖 early-done WORK 主导轮**,**不覆盖当前 line0 实测的 IRQ 主导轮**。 -3. `#1006` 曾要求「IRQ 也要吃 gate」(防 r1 全绿),`#1007` 要求「gate 不与 warm 并行扣」。 - 现行布尔式把两者捏在一起:**IRQ + warm>0 ⇒ 仍扣 gate**,形成 **#1007 在 IRQ 路径上的回退洞**。 -4. `warm_left` 主要在 `capture.c` early **complete** 路径递减;`mi_drop` 日志里的 `warm=` 是读快照,**IRQ mi_drop 不替代 warm 语义丢帧**。 -5. `mi_pub` 日志**不打印** `warm_left`/`gate`;对齐时需夹在邻近 `early complete` / 上一条 `mi_drop` 之间读。 - -源码注释仍写:`CamOS SMARTCAM_TRIGGER_WARMUP_FRAMES must cover skip_sof+drop+warm+pub_gate`。 -CamOS 已不再以 appsink discard==0 为成功条件,但 **gate 若在 warm 期被 IRQ 耗尽,用户态脉冲再多也盖不住「gate=0 后的 early mi_pub」画质**。 - -### 11.3 板端 n1trace 证据(2026-08-12 14:52,`n1trace_gate_20260812_145230`) - -同一次 `isp_ft_arm ... skip=2 drop=2 warmup=8 pub_gate=6` 后(节选逻辑序): - -```text -mi_drop IRQ left=1→0 warm=8 gate=6 ← drop 耗尽,gate 仍 6 -mi_drop IRQ left=0 warm=8 gate=5 ← drop 已 0,warm 仍 8,gate 开始减(early=0) -mi_drop IRQ warm=7 gate=4 -... -mi_drop IRQ warm=4 gate=0 cnt=8 ← gate 在 warm 仍 4 时归零 -mi_skip_frame skip=2 → skip=1 -early complete warm_left=3 -mi_pub IRQ pub=1 early=0 drop_left=0 ← 首次发布;gate 早已 0,warm 轨迹仍在中后段 -mi_pub pub=2..12 ... -``` - -对照: - -| §10.9 宣称(early 主导) | 本迹(line0/IRQ 主导) | -|--------------------------|------------------------| -| warm 期 gate **保持 6** | warm 期 gate **6→0**(IRQ mi_drop) | -| warm=0 后 gate 再 6→0 | gate 先于 warm 耗尽 | -| 其后 mi_pub 为「gate 后」 | **首 mi_pub 不在 pub_gate 计数内**(gate 已 0),且 complete 侧 warm_left 仍可 >0 | - -与用户 green tag 对齐的含义: - -- 绿帧 **不是**「仍卡在 pub_gate 计数器里没放行」; -- 而是 **gate 被 IRQ 提前烧光后,仍处 FT 前段(warm/ceiling-force 未稳定)的 mi_pub** 进入 appsink/预览; -- 故 CamOS 在「首 appsink 后再 +3/+6 枪」只能偶尔错位相消,**统计上消不干净**,加长 post 还拉高 first_hit/apply。 - -### 11.4 CamOS 侧已做 / 已否(避免文档继续写「加 warmup 帧必绿清」) - -| 动作 | 结果 | -|------|------| -| 去掉 discard==0 / forced-clear 开闸 | 正确方向;与 §10.10.1 一致(已落地思路) | -| 脉冲预算 + PLAYING/appsink 见帧 + quarantine double-flush | first_hit 可稳 1,apply ~4–5s | -| post residual 3→6 + min_appsink 5 | **绿仍在 + 头枪回退** → **回退** | -| 默认(回退后) | `POST_PLAYING=3`,`MIN_APPSINK_SEEN=1`,`TOTAL_PULSE_MAX=22`,interval 100ms | - -**不要**再把「CamOS 再加 post/min_appsink」写成消绿主路径;主路径应回到内核 gate 与 warm/IRQ 的布尔关系。 - -### 11.5 故障点清单(按优先级) - -1. **P0 内核逻辑洞(本迹已坐实)** - `gate_drop` 在 `early=0 && warm>0` 时仍为真 → **pub_gate 与 warm 在 IRQ 路径并行消耗**,违背 #1007 设计意图。 - **建议修复(尚未改代码,需单独评审/刷机)**: - - 方案 C1(推荐):`gate_drop` 增加 **`warm==0`(或 `!warm`)** 条件,使 gate **仅在 drop 耗尽且 warm 耗尽之后** 才递减;IRQ 与 early 共用。 - 伪代码:`gate_drop = ft_diag && gate && !drop && !warm && /* 可选:允许 IRQ */ true` - 同时保留「IRQ 也必须过 gate」:即 warm 完后的 IRQ pub 仍走 gate_drop,而不是 #1006 那样在 warm 期误 pub。 - - 方案 C2:`warm_drop` 改为 **`warm>0` 不依赖 `frame_early`**(warm 期所有 complete 都 mi_drop)。更猛,需防和 skip_frame/drop 双重计数、以及 hold 路径死等。 - - 方案 C3:`mi_pub` 时若 `warm_left>0` **强制 mi_drop**(发布门槛与 complete 计数解耦)。与 C1 可组合。 - - **验证必须**:n1trace 看 `gate` 在 `warm` 归零前是否恒定;首 `mi_pub` 时 `warm_left==0` 且刚经历 gate 6→0;mon→line0 / mon→sw 绿矩阵 + first_hit + apply。 - -2. **P1 观测缺口** - - `mi_pub` 行无 `warm`/`gate` 字段 → 排障易误判「在 gate 内」。建议 n1trace 增补。 - - 自动化 green 曾被 stale preview / 曝光 API 假阳性干扰;应对 **命中帧** 做 bot8 判定(CamOS QA 已部分改正)。 - -3. **P2 文档过时** - - §10.9 / §10.12 的 green=0 **不推广到 line0 2026-08-12 产品路径**。 - - `IMX296_绿线修复_内核修改与验证记录.md` 仍停在 08-06 早期 skip/drop/warmup 数字,**未反映 #1008 pub_gate**;以本文件 §10–§11 与源码为准。 - - CamOS 仓库 plan(`docs/plans/active/2026-08-11_IMX296_warmup...`)写「画质主防护在内核 pub_gate」——**方向对,但须注明 IRQ 洞未修前 line0 仍可能绿**。 - -4. **P3 非根因(已排除作主手段)** - - 单纯再加 `force_post` / CamOS post residual(§3、§11.1 已否定为主因)。 - - 用户态 appsink discard 收齐(多数 pulse 本就无 sample)。 - -### 11.6 与 CamOS 的协作契约(修订) - -| 层 | 应保证 | 不应指望 | -|----|--------|----------| -| 内核 | 进 FT 后 **首帧 mi_pub 时 warm_left==0 且 pub_gate 已按设计耗尽**;IRQ/early 一致 | 用户态多脉冲「盖住」gate 提前烧光后的脏 pub | -| CamOS | mon→FT / ROI 后 XTRIG 预算 + quarantine;**success 后才** line0-stable gate;first_hit=1 与 apply≤5s | 用 post≥6/min_appsink≥5 消绿 | -| 联调 | 短 n1trace:arm → drop → warm → gate → 首 mi_pub 单调 | 只看 CamOS success=1 或只看 endurance green 汇总 | - -### 11.7 建议下一动作(内核树,未执行) - -1. 按 **C1** 改 `capture_v39.c` 布尔条件 + `mi_pub` 日志带上 `warm/gate`。 -2. 本地编译刷 `#20260812xx`,重跑: - - mon→line0 ×8 + mon→sw ×4 绿矩阵 - - 短 n1trace 确认首 mi_pub 相位 - - first_hit / apply 回归(CamOS 保持 post=3) -3. 通过后回写本节为「已修」,并摘一句到 `IMX296_绿线修复_内核修改与验证记录.md` 文首「最后更新」。 - -### 11.8 文档正确性结论(本次 SSH 核对) - -| 文档 | 结论 | -|------|------| -| 本文件 §1–§3 ceiling-force / hold 满仍可绿 | **仍正确** | -| §10.5–10.8 #1005–#1008 迭代故事 | **历史正确** | -| §10.9「warm 期 gate 保持 6」 | **仅 early 主导轮成立;line0/IRQ 主导轮错误 → 以 §11 为准** | -| §10.10 CamOS forced clear | **方向正确**;CamOS 已转向脉冲预算(discard 门槛已弃) | -| §10.12 4h green=0 | **有效窗口结论保留**;**不覆盖 08-12 line0 复发** | -| `IMX296_绿线修复_*.md`(08-06) | **过旧常数**;勿当 #1008 真源 | -| 源码 `rkisp.c` / `capture_v39.c` 注释 | 与 #1008 意图一致,但 **gate_drop 实现与 #1007 文字意图在 IRQ 路径不一致**(§11.2) | - - - ---- - -## 12. 2026-08-12 #1009 内核修复(gate/IRQ warm 洞) - -> 状态:**源码已合入工作区,构建号 `2026081201`,尚未编译刷板验证** -> 依据:§11.3 板端 `n1trace_gate_20260812_145230` + 源码布尔式复核;line0 矩阵 `line0_sw_green_*` / `line0_post6_*` green 仍在且加 CamOS post 无效。 - -### 12.1 根因确认(板端坐实,非猜测) - -板 `root@192.168.1.180` 仍跑 `#2026081008`。`n1trace_gate_20260812_145230/dmesg_n1trace.txt` 进 FT 后: - -```text -isp_ft_arm ... skip=2 drop=2 warmup=8 pub_gate=6 -mi_drop IRQ left=1→0 warm=8 gate=6 ← drop 耗尽 -mi_drop IRQ left=0 warm=8 gate=5 ← warm 仍 8,gate 开始减(early=0) -... -mi_drop IRQ warm=4 gate=0 ← gate 在 warm 中段烧光 -early complete warm_left=3 -mi_pub IRQ pub=1 early=0 ← 首发布:gate 已 0,warm 轨迹仍 >0 -``` - -对照 `#1008` 代码: - -- `warm_drop = ft_diag && frame_early && warm` → **IRQ early=0 永不 warm_drop** -- `gate_drop = ft_diag && gate && !drop && !warm_drop` → **IRQ+warm>0 仍 gate_drop** -- ⇒ 与 §10.9「warm 期 gate 保持 6」只在 early-WORK 主导轮成立;**line0/IRQ 主导轮不成立** - -产品侧:`line0_sw_green_20260812_135804` totals.line0_green=13、first_hit 全 1、apply≈3.5–5s;`line0_post6` green=12 且 first_hit 退化。**再加 CamOS post 不是正解。** - -### 12.2 代码改动(#1009) - -| 文件 | 改动 | -|------|------| -| `capture_v39.c` | `warm_drop = ft_diag && warm`(IRQ+WORK);`gate_drop = ... && !drop && !warm`;IRQ warm 路径 `warm_left--`(early complete 已减则不双扣);`mi_pub` n1trace 增 `warm/gate` | -| `capture.h` / `rkisp.c` | 注释与 #1009 契约对齐;常数不变 skip/drop/warm/gate=2/2/8/6 | -| `DLCVCAM_BUILD_VERSION` | `2026081201` | - -期望 n1trace(刷板后): - -```text -mi_drop ... warm 递减时 gate 恒为 6 -warm→0 后 gate 6→0 -首 mi_pub 时 warm=0 且刚经历 gate 耗尽(或 gate 刚到 0 的下一帧) -``` - -### 12.3 成功标准(刷板后) - -- mon→line0 ×8 + mon→sw ×4:`TOTAL_GREEN=0`,无底串帧 -- first_hit=1(用户触发不被吞);apply 墙钟 **≈5s 或更短**(CamOS 保持 post=3,不靠加 post) -- ROI set/reset 后同样 green=0 + 有图 -- dmesg 仍 ceiling-force(N-1 不回退);短开 n1trace 核对相位后必须 `echo 0` - -### 12.4 未做 - -- 未编译 / 未刷 `#2026081201` -- 未改 CamOS;禁止再把加 post 当消绿主路径 -- 未动 force_post / SKIP 常数(隔离变量) - - -*记录人:CamOS 联调 2026-08-12;构建机 `root@192.168.1.59` 工作区 `fix_imx296_n1_pwm`;板 `180` `#2026081008`。* diff --git "a/IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" "b/IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" deleted file mode 100644 index 8602844a8eb58..0000000000000 --- "a/IMX296_\347\273\277\347\272\277\344\277\256\345\244\215_\345\206\205\346\240\270\344\277\256\346\224\271\344\270\216\351\252\214\350\257\201\350\256\260\345\275\225.md" +++ /dev/null @@ -1,214 +0,0 @@ -# IMX296 绿线/串帧修复 — 内核修改与长期验证记录 - -> 长期记忆与人工复核文件。本文件随每次新修改、新发现持续更新。 -> 最后一次更新:2026-08-12(**#1008 之后 line0 底绿复发与 gate/IRQ 洞**见同目录 `IMX296_绿线_20260810_ceiling_force排查记录.md` **§11**;本文件主体仍为 08-06 早期策略,常数勿当 #1008 真源) -> 此前:2026-08-06 13:40(内核 procfs 门闩已刷板 `#2026080601`,accept/reject/skip 冒烟通过) - ---- - -## 0. 问题背景 - -IMX296 在 **trigger 模式(master_fast_trigger)** 下,尤其是 free-run → trigger 切换、ROI 修改/重置后的最初几帧,出现 **绿线(底部残帧/串帧)**。根因不在 sensor 曝光本身,而在 ISP 通路帧完成与 buffer 交接的竞态。 - -测试痛点:板端无硬件触发源时通过 sysfs `run_mode` + WebSocket 软触发驱动。真正有意义的 bug 几乎都出现在 **模式切换 / ROI 修改** 时。 - ---- - -## 1. 根因分析(结论) - -IMX296 FT(fast trigger / XTRIG)下 MIPI 帧「保持打开」直到下一次触发到来,造成: - -1. **ISP V39 提前完成(early-done)竞态**:`wait_line` 在 height−8−1 处触发 `ISP3X_OUT_FRM_HALF`,但此时 **MI 的 Y/CB 写尚未完成**,尤其 FT 帧被 XTRIG 提前关闭时,chroma(CB)最后几行可能还没落盘。 -2. **V39 的 ISP_OUT_LINE 提前饱和**:约 8 行提前饱和,不能作为完成依据。 -3. **MI Y/CB_OFFS_CNT_SHD 可能残留上一帧 final 值**:FT 的 SOF 到来时 MI 的 SHD 寄存器还持有上一帧的写计数,导致「读到 full」就立即放行的假象。 -4. **进入 FT 后第一帧**:上一轮 free-run 的打开帧 + 首个 XTRIG,invalid 帧关闭与首个 valid 帧交接。 -5. **`rkisp_start` 里 `stream->skip_frame` 会被清零**,所以「丢掉头 N 帧」不能靠它,需要独立的持久计数器。 - ---- - -## 2. 内核修改文件清单 - -| 文件 | 修改点 | 作用 | -|---|---|---| -| `drivers/media/platform/rockchip/isp/capture.h` | 新增 early-done 状态字段与 API | 结构体字段:`early_done_delay_us/timer/pending/armed_sof_ns/poll_us/terminal_line/deadline_ns_lo/hi/mi_y_size/mi_cb_size/mi_saw_y/mi_saw_cb/mi_ready/post_mi_us/warmup_left/warmup_post_mi_us/skip_frames/drop_left`。API:`rkisp_stream_buf_done_early_drain`、`rkisp_early_done_clear_state` | -| `drivers/media/platform/rockchip/isp/capture.c` | MI 写计数缓存、`rkisp_early_done_mi_finished()`、定时器轮询后 hold | 读取 MI Y/CB_OFFS_CNT_SHD 得到 `mi_y_size/mi_cb_size`;判定「live ramp(saw_y/saw_cb 低于目标值)+ full」才 ready;ready 后按 `post_mi_us` 再 hold 若干 µs 让 chroma 尾行落盘;超 residual 上限只记日志,不强制 `vb2_done` | -| `drivers/media/platform/rockchip/isp/capture_v39.c` | `mi_frame_end` 里 early-done 前丢弃帧 | 若 `early_done_drop_left > 0`:递减、requeue buffer、goto end(不触发 vb2_done),可跨 `stream->skip_frame` 清零存活 | -| `drivers/media/platform/rockchip/isp/rkisp.c` | 0518 策略常量与 FT 启动逻辑 | TAIL=8、residual 上限默认 15000(max 40000)、line_us 80–250、poll 50µs、POST_MI=6000µs、WARMUP_POST_MI=16000µs、WARMUP_FRAMES=4、SKIP_EARLY_FRAMES=3、SKIP_BUF_FRAMES=3 | -| `drivers/media/i2c/imx296.c` | 进入 FT 的双 dummy 触发 | `imx296_dummy_trigger_locked()` 两次(日志 `enter-fast-trigger` / `enter-fast-trigger-2`),关闭进入 FT 后残留的 invalid 打开帧 | - ---- - -## 3. Early-done 完成策略(capture.c 核心逻辑) - -``` -FT SOF → - early_done_skip_frames > 0 ? 不做 early-done(走真实 MI FE IRQ),递减 - : 在 wait_line(OUT_FRM_HALF) 处 arm early_done 定时器 -定时器每 poll(50µs) 轮询一次: - MI Y_OFFS_CNT_SHD / CB_OFFS_CNT_SHD 与 programmed size 比较 - saw_y/saw_cb 记录「这一臂里见过 < size 的值」→ 证明不是残留 SHD - 只有 (saw_y && saw_cb && Y_full && CB_full) → mi_ready=1 - mi_ready 后计时 post_mi_us(FT 用 6000,warmup 用 16000)再完成 vb2_done -residual 超上限:仅 dev_dbg 告警,绝不强制 vb2_done(避免残帧出流) -``` - -设计要点: -- **不信任单次 SHD 读值**,必须看到 live ramp(从低于 size 增长到 size)才认为本帧 MI 真写完。 -- **不做「超时强制 done」**。残帧宁可丢(被 drop_left / skip 掉),也不能带着未写完的 chroma 出流形成绿线。 -- **post-MI hold** 让最后几行 NV12 色度落盘后再上报帧完成。 -- warmup(进入 FT 后前 `WARMUP_FRAMES=4` 帧)用更保守的 `WARMUP_POST_MI=16000µs`。 -- `SKIP_EARLY_FRAMES=3`:前 3 个 SOF 完全交给 MI FE IRQ。 -- `SKIP_BUF_FRAMES=3`:`stream->skip_frame` 额外丢 3 个 FT 头帧(free-run→FT 后首个 XTRIG 帧最脏)。 -- `early_done_drop_left`(capture_v39)提供**跨 start 清零存活的丢弃**。 - ---- - -## 4. Free-run 路径处理 - -free-run 下 **不 arm early-done**(`rkisp_early_done_clear_state` 清理状态),完全走真实 MI FE IRQ 完成,保持出厂路径不变,避免误伤 free-run 稳定性。 - ---- - -## 5. 测试方法论(board: root@192.168.1.180) - -- **CamOS**:`/root/smartcam_bs/0_测试运行_camos.sh`,软触发走 sysfs + WS。 -- **连续采集脚本**:`/root/roi_endurance_live.py`(持久副本;`/tmp/roi_endurance_live.py` 是运行副本)。 - - 每个 sample:软触发 → 等 `X-Frame-Tag` 变化(**绝不用 hold-last 预览当新帧**)→ 存 `frames/`。 - - free-run 与 trigger 两模式都连续存图。 - - 事件集:`random_roi`(随机尺寸+随机偏移+重置全幅)、`roi_thrash`、`mode_flip`(free↔trigger↔monitor)、`min_settle`。 -- **绿线判定**:每帧底部区域 RGB,任一通道异常偏高记为 green;绿帧存 `green/`。 -- **MISS 判定**:触发后未见新 X-Frame-Tag。 -- **skip_free**:ROI 变更后 free-run 的前 4 帧丢弃(`skip_free_left=4`),因实测 free tag=1(变更后首帧)确为脏帧。 - -### 关键运行参数(board 端) -``` -sysfs run_mode: /sys/bus/i2c/devices/4-001a/run_mode -free_run / master_fast_trigger / monitor -capture node: /dev/video11 (rkisp_mainpath) -API: http://127.0.0.1:8000/api/auth/login {"username":"admin","password":"dlcv2026"} → {"success":true} -``` - ---- - -## 6. 长期验证进展(更新记录) - -### 6.1 内核构建 `#2026080518`(6.1.99-rk3576) -- 首次承载本修复全量策略(MI live+full、post-MI hold、skip/drop、dummy×2)。 -- 已刷入板端 flash(多次重启后仍生效,验证通过)。 - -### 6.2 长时间 endurance 结果(多段) -| 时间段 | free 存图 | trig 存图 | green | miss | roi 次数 | mode 次数 | 结论 | -|---|---|---|---|---|---|---|---| -| 首段 ~18:00–20:30(含 3 次板端重启) | 累计增长 | 累计增长 | **0** | 0 | 数十 | 上百 | 稳定,重启为网络/系统问题非 bug | -| 20:30–00:40 段(单进程 4535) | ~20.7k | ~8.2k | **0** | 58(集中在 00:11 一次 burst 超时卡死窗口,非绿线) | 212 | 309 | 稳定;miss 为 API/burst 瞬时超时 | -| 00:40 板端重启后新周期(10h,监控至 09:13 结束) | 跨 4 次重启(00:40/05:02/06:54/08:52)多段累计 free≈1.2 万+、trig≈5 千+(每段被重启清零重新累计) | 同左 | **0** | 0(各恢复后段) | 数百(含随机偏移) | 数百 | 全段 green=0;重启为 systemd 看门狗 30s 触发(非绿线) | - -**核心结论:green=0 已跨多段、多 ROI 组合、多模式切换、多板端重启保持。** 偶发 MISS 均为 API 软触发/网络瞬时故障导致的自愈事件,**与绿线无关**。 - -### 6.3 典型自愈事件(记录留档) -- **20:11 MISS 风暴**:ROI `1456x400_0_474` 后触发 MISS(46 次),free 帧恢复正常。手动复位:kill endurance + CamOS → 重启 → 恢复。 -- **00:11 burst 超时**:`event_reset_roi → shoot_burst` 抛 `TimeoutError`,seq 卡死 25437,watchdog 判 stall → 重启 CamOS;endurance 自愈 recover,miss=58 冻结不再增长。 -- **00:40 板端重启**:内核保留(flash 验证),CamOS 自启,endurance 需手动重启(见 §7 命令)。 -- **04:32:50 例行恢复**:api=False → 04:33:31 自愈 api=True,green=0,seq=24858。00:11 后第 5 次同类 API 瞬时故障(00:11/01:36/02:45/03:03/04:32),全部自愈,miss 计数自 02:45 后冻结在 158 不再增长。均为软触发/API 链路瞬时问题,与绿线无关。 -- **04:36:18 例行恢复**:api=False(seq 冻结 24947)→ 04:36:59 自愈 api=True,green=0,seq=25582(追帧正常)。第 6 次同类事件,~40s 自愈。**观察:04:32–04:36 窗口内两次 API 瞬时故障,软触发/API 链路抖动频率略有上升**,但 green 恒 0、seq 持续推进,未影响帧质量(详见 §8 待办)。 -- **04:38:21 例行恢复**:api=False → 04:39:02 自愈 api=True,green=0,seq=25659。第 7 次同类事件(04:32/04:36/04:38 三连),全部 ~40s 内自愈,小时报确认板端 streaming=1 正常。**结论:API 链路抖动加剧但自愈极快,帧质量不受影响;10h 周期内暂不需介入,周期结束后建议单独排查 CamOS 软触发链路(§8)。** -- **04:46:35 例行恢复**:api=False → 04:47:17 自愈 api=True,green=0,seq=26588,~42s。第 8 次事件,04:32 起约 15 分钟内 4 次抖动(~1次/5min),全部自愈,watchdog 全程正确 defer 不双杀。**green 恒 0、seq 每次恢复后推进,帧质量不受影响。** -- **04:50:01 例行恢复**:api=False(seq 一度冻结 26588 达 ~5min)→ 04:50:43 自愈 api=True,seq=26644(恢复推进)。第 9 次事件,04:32–04:50 约 18 分钟内 5 次抖动,全部 ~40s 自愈,watchdog 按 streak 逻辑 defer、未误判。**链路抖动持续但帧质量保持 green=0。** -- **05:02:30 板端重启(第 2 次)**:/tmp 整体清空 → endurance/camos 全灭,watchdog 判定 endurance dead 并自动重启。手动恢复:CamOS 拉起(i=18 OK)、endurance PID 4279、END_TS 保留原 10h 窗口(1785984201,至 08-06 10:43)、left=5.66h。**dmesg/journal 无 panic/oops/watchdog 记录**,重启原因不明;两次重启(00:40、05:02)均发生在 API 抖动之后,疑为板端外部复位(硬件看门狗/供电/外部指令),与绿线无关(green 全程 0)。**待办:查板端复位来源(§8)。** -- **05:27 小时报新发现(ROI→recover 触发条件定位)**:重启后(05:03 起)7 次 recover 全部在 `roi set_*` 后 ~15s 触发(05:15/05:19/05:22/05:25 均如此)。即 endurance 在 ROI 变更后的健康检查(等新 X-Frame-Tag)超时,判定异常 → 重启 CamOS。恢复后 seq 持续推进、green=0、miss=0。**结论更新:API 抖动主因是 ROI 变更后的软触发链路超时自愈,非随机故障;与绿线无关,但每次 ROI 变更耗 ~60-90s 恢复窗口。** -- **06:54:32 板端重启(第 3 次,根因确认)**:/tmp 清空 → 恢复流程同前(endurance PID 4090、left=3.8h、END 保留)。**诊断结论**:`/etc/systemd/system.conf.d/10-dlcvcam-watchdog.conf` 配置 `RuntimeWatchdogSec=30s`,systemd(PID 1)持有 /dev/watchdog0 每 30s 喂硬件看门狗(2ace0000)。**三次重启(00:40/05:02/06:54)均发生在重度流切换/API 抖动之后,机制为:系统阻塞 >30s(软锁/D-state 风暴)→ 硬件复位**。上次启动日志在流状态正常后无 panic 结束,符合看门狗复位。重启间隔缩短(4.4h→1.9h)与 recover 频率上升同步。**与绿线无关(green 全程 0)。** -- **08:52:10 板端重启(第 4 次)**:/tmp 清空 → 恢复(endurance PID 4361、left=1.84h、END 保留至 10:43)。重启间隔稳定在 ~2h(00:40/05:02/06:54/08:52)。**看门狗 30s 机制下,重度流切换期间系统 >30s 阻塞反复触发硬件复位;每次重启清零已积累数据,但 green=0 在所有跨重启周期保持。** - ---- - -## 7. 运维命令速查 - -### 板端 -```bash -# 重启 endurance(10 小时) -OUT=/tmp/roi_endurance_live; mkdir -p $OUT/frames $OUT/green -END_TS=$(( $(date +%s) + 36000 )); echo $END_TS > $OUT/end_ts -cp -f /root/roi_endurance_live.py /tmp/roi_endurance_live.py -setsid nohup env ENDURANCE_OUT=$OUT python3 /tmp/roi_endurance_live.py --hours 10 >> $OUT/console.log 2>&1 < /dev/null & - -# 看状态 -cat /tmp/roi_endurance_live/summary.txt -tail -n 20 /tmp/roi_endurance_live/console.log -``` - -### 本机 watchdog(dev 机) -```bash -# 常驻:/tmp/endurance_watchdog.py (PID 385900),每 40s 采集 -# 输出:/tmp/endurance_watchdog_state/status.json -cat /tmp/endurance_watchdog_state/status.json -``` -Watchdog 行为:API 优先健康判定;endurance 自愈窗口(recover)不抢杀;seq stall>360s → CamOS 重启;endurance 死 → 重启(若未过 end_ts)。 - -### 内核 -```bash -# 编译 & 刷板(见 DLCVCAM内核编译与更新操作手册.md) -# 本仓库最新 fix 提交:416f32d22c72 fix(imx296): close FT frame and harden V39 early-done drain -``` - ---- - -## 8. 后续关注点(待办/风险) - -- [x] 00:40 重启后新 10h 周期跑完,复核绿色 0 与 roi/mode 覆盖率(09:13 监控结束判定 green=0 可交付)。 -- [ ] 若再次出现 free tag=1 绿线,考虑把 skip_free 的丢弃逻辑前移到内核(而非测试脚本)。 -- [x] **板端重启根因复核(2026-08-06 10:10,纠正先前“仅 30s 看门狗误复位”判断)**: - - 配置属实:`/etc/systemd/system.conf.d/10-dlcvcam-watchdog.conf` → `RuntimeWatchdogSec=30s`,`systemd` PID1 持有 `/dev/watchdog0`(`2ace0000.watchdog`)。设计意图是 hard hang → 停 pet → SoC 复位 → U-Boot boot-try。 - - **pstore 实锤不是“单纯 >30s 软锁”**:`/var/lib/systemd/pstore/dmesg-ramoops-0` 捕获 - `Kernel panic - not syncing: Asynchronous SError Interrupt`, - `Comm: python`,`pc: vfree` / `lr: rkisp_proc_write+0xf8`(`drivers/.../isp/procfs.c` 写路径末尾 `vfree(buf)`;SError 多为此前 `writel`/`rkisp_write` 非法访问 ISP 寄存器的异步报错)。 - 同一签名在 `dmesg.txt`(build `#2026080504`)也出现过 → 可复现,非偶发。 - - **崩溃前序(重度流切换 thrash)**:反复 `stream on/off` → `MIPI_CSI2 ERR2` → 大量 `CIF_ISP_PIC_SIZE_ERROR` → `start pipeline failed -22` → 再 `run_mode=master_fast_trigger`(streaming=0 仍打触发)→ userspace 写 rkisp procfs:`mode=0x100`、`400=0x6197` → SError panic。另有 `rkisp_stream_stop id:0 timeout`。 - - **重启形态分类**: - - boot -2/-3/-4/-5:journal 无 `System is rebooting`,末尾停在流错误/PIC_SIZE → **硬死/panic 后复位**(看门狗是收尸,不是首因)。 - - boot -1(09:40):有 `systemd-logind: System is rebooting` → **软件 reboot**(endurance 周期尾/人工/脚本),与 panic 不同。 - - **处置建议**(改配置仍需用户确认): - 1. **优先修根因**:CamOS/python 经 `/proc` 写 ISP 寄存器的时机与 ISP power/clock/pipeline 状态;以及 ROI/mode thrash 下 `PIC_SIZE_ERROR` / `pipeline failed -22` / `stream_stop timeout`。 - 2. **RuntimeWatchdogSec=30s 可临时放宽到 2–5min 作辅助**,但 **放宽 alone 挡不住 SError panic**;panic 路径仍会复位。仅当确认存在“无 panic 的长阻塞”时放宽才有独立收益。 - 3. 保留 pstore/ramoops;下次复位后优先 `cp /var/lib/systemd/pstore/dmesg-ramoops-*` 归档。 -- [ ] **ROI→recover 触发条件已定位**:endurance 在 ROI 变更后等新 X-Frame-Tag 超时(~15s)即重启 CamOS。建议排查 endurance 的 ROI 变更后健康检查超时阈值/触发逻辑(与绿线无关,但影响测试效率,每次 ROI 变更耗 ~60-90s)。 -- [ ] MISS 风暴根因在 API/WS 软触发链路,与绿线无关,但值得单独跟进稳定性(可选)。 -- [ ] 内核层面可将 warmup 帧数/skip 数做成可调参数,便于回归调参。 -- [x] **rkisp_proc_write SError 写者定位(2026-08-06)**:CamOS `smartcam_backend/camera/rkcam/_runtime/gst_v4l2_controls.py` → `V4L2ControlsRoiManager._write_wb_register_from_ratio()`(默认 `SMARTCAM_WB_REGISTER_ENABLE=1`)。写入序列: - 1. `mode=0x100`(`RKISP_PROCFS_FIL_AIQ`) - 2. `400=0x6197`(`ISP3X_ISP_CTRL0`,开 AWB 等) - 3. `538/53c/...` AWB gain(`ISP3X_ISP_AWB_GAIN*_G/RB`) - 调用链:`set_balance_ratio` / load user set;路径 `/proc/rkisp-vir0`。**无 streaming 门闩**,ROI/mode thrash teardown 窗口仍会写 → SError。板端已确认 **V4L2 无 red/blue balance**,不能靠关 WB-reg 代替(会丢手动白平衡功能)。 -- [x] **最小功能保留修复(板端 CamOS,2026-08-06 10:57 已落地并重启验证)**:同文件加 streaming/ISP 门闩,**不关** `SMARTCAM_WB_REGISTER_ENABLE`: - - 默认 `SMARTCAM_WB_REGISTER_REQUIRE_STREAMING=1` - - 写 MMIO 前要求:非 `camera_offline`,且 `run_mode streaming=1` 或 `/proc/rkisp*` 显示 `Isp online/offline`(内核 `ISP_START`);否则 skip 并打 `[wb-reg] skip MMIO (...)` - - `set_balance_ratio` **先写入 state 目标 ratio**,MMIO 失败也不丢用户设置(ROI/mode 恢复后 grabber restore 会再写) - - 备份:`gst_v4l2_controls.py.bak.20260806_serror_gate` - - 验证:streaming=1 时 write=True(启动加载用户集2 `r=1383 g=1024 b=2000` 成功);`camera_offline` / `streaming=0` / `isp_not_start` 时 write=False;`/preview/frame` JPEG 正常;未关 WB 功能。 -- [x] **ROI/mode thrash 门闩验证(2026-08-06 10:59:55–11:21:38,~22 min)**: - - 输出:`/tmp/roi_endurance_wb_gate`,`ENDURANCE_OUT=... python3 ... --hours 0.33` - - **结果**:`free=772 trig=565 green=0 miss=0 roi=17 mode=29 recover=8 camos_rst=4 err=0`,存图 1337 帧,`green/` 空 - - **无板端重启**:`boot_same=y`,uptime 连续,dmesg 无 SError/panic,pstore 未刷新新 panic(仍为 08:51/09:41 旧档) - - 事件覆盖:`random_roi×8`、`roi_thrash×2`、`mode_flip×2`、`reset_roi×2`、`min_settle×1` - - **WB 功能保留**:streaming 窗口内多次 `[wb-reg] write ... ratio r=1383 g=1024 b=2000`(`camos_server_wb_gate.log` 14 条 + camos_restart 后 `camos_boot.log` 7 条);门闩代码仍在板端 - - **残留(非 panic)**:ROI 后 endurance 健康检查仍触发 recover/camos_rst(与绿线/SError 无关,测试效率问题) -- [x] **内核门闩补丁已刷板(2026-08-06 13:37,`#2026080601`)**:`drivers/media/platform/rockchip/isp/procfs.c` - - 镜像:`boot-rk3576-6.1.99-2026080601-416f32d2-dirty.img`(sha256 `33eff39a…b516244`),`dd` → `/dev/disk/by-partlabel/boot` - - 含绿线策略工作区变更 + procfs 门闩(ISP_START + RPM get/put + reg `<0x6000` 且 4 字节对齐 + 写长度 cap) - - **冒烟**: - 1. streaming=1:`mode=0x100` / `400=0x6197` / `538`/`53c` → rc=0,dmesg 正常 `rkisp_proc_write`,**无 skip** - 2. 坏寄存器:`6000=0x1`、`401=0x1` → rc≠0,dmesg `reject bad reg` - 3. CamOS 停、streaming=0:`400=…`/`538=…` → `-ENODEV`,dmesg `skip MMIO (isp_state=0x10001 …)`(**mode= 仍可写**,只拦 FIL_AIQ/FIL_SW MMIO) - 4. CamOS 重启后:gain/pulse 可调(gain=55、pulse≈8986)、preview JPEG OK、CamOS `[wb-reg] write` 仍成功;dmesg 无 SError/panic - - 与 CamOS streaming 门闩双层防护;watchdog 未改 - ---- - -## 9. 结论 - -当前内核策略在长期混合负载(free+trigger、随机 ROI 含偏移、模式频繁切换、板端多次重启)下 **green=0 稳定保持**。修复核心:**MI 真实完成(live ramp + full)+ post-MI hold + 早期 SOF skip + 持久 buffer drop + 进入 FT dummy 触发**。测试脚本侧以 skip_free 丢弃 ROI 变更后脏 free 帧为辅。 - -**判定标准**:单段 ≥ 数小时、覆盖多种 ROI(含随机偏移)与模式切换、green=0 且 free/trig 连续存图正常,即可判定修复稳定可交付。**2026-08-06 09:13 监控结束:00:43 起 10h 周期累计 ~8.5h(含 4 次板端重启、数十次 CamOS 重启、数百 ROI/mode 切换),全程 green=0,判定修复稳定。** - -**SError / WB-reg(2026-08-06)**:硬复位主因更正为 thrash 窗口 CamOS 经 `/proc/rkisp-vir0` 写 ISP 寄存器触发 Asynchronous SError。**双层修复已上板**:① CamOS streaming/ISP 门闩(不关 WB-reg,thrash ~22min 无复位);② 内核 `procfs.c` 门闩 build `#2026080601` 已刷 180,accept/reject/skip 冒烟通过。`RuntimeWatchdogSec` 未改。 - -**遗留问题**(均与绿线无关,见 §8):① ROI 变更后 endurance 健康检查超时自愈;② 可选更长 thrash 提高 no-SError 置信度;③ `#2026080601` 为 dirty 树构建(含未提交绿线/procfs 工作区改动),合入前需正式 commit。 diff --git "a/IMX296_\351\227\256\351\242\230\345\205\250\346\231\257_\347\273\231\345\220\216\347\273\255Agent_20260820.md" "b/IMX296_\351\227\256\351\242\230\345\205\250\346\231\257_\347\273\231\345\220\216\347\273\255Agent_20260820.md" new file mode 100644 index 0000000000000..473ea5a35af4a --- /dev/null +++ "b/IMX296_\351\227\256\351\242\230\345\205\250\346\231\257_\347\273\231\345\220\216\347\273\255Agent_20260820.md" @@ -0,0 +1,125 @@ +# IMX296 / V39 FT 问题全景(给后续 Agent) + +> 修订:2026-08-24(补 148 / 测法陷阱;勿当 08-20 冻结稿) +> 内核:`fix/imx296-fast-trigger-n1-pwm-period` @ `12abd272` / `#2026082001` +> 交付:`boot-rk3576-6.1.99-2026082001-12abd272.img` +> sha256:`46bc9873845bc4f5caf539cc745b6d7b4294fdf0c02440c4bc8ca71fee372b85` +> 板:84=`192.168.1.84` DLCVCAM-3585;180=`192.168.1.180` DLCVCAM-49e7;**148=`192.168.1.148` DLCVCAM-5f95** +> 硬件:sensor `4-001a`,capture `/dev/video11`,曝光/增益常 `/dev/v4l-subdev3` +> CamOS:**不在本仓**,三板不是同一份 grabber。84/180 常见 `/root/smartcam_bs` + `/root/CamOS`;**148 cwd `/data/dlcv/CamOS`**(与 `/root/dlcv/CamOS` 可同 inode)。HTTP `:8000`。 +> 机理附录:`IMX296_N1相位滞后_诊断与修复记录.md`、`IMX296_绿线_20260812_gate_IRQ修复与验证.md`。刷机见 `DLCVCAM内核编译与更新操作手册.md`。早期 24h/4h 与 0810 ceiling-force 过程稿已从树中移除,口径以本文为准。 + +--- + +## 0. 硅上事实(所有底带问题的同一根) + +FT:一拍 XTRIG **开** MIPI 帧,**下一拍**才关。V39 上 `MI Y/CB_OFFS_CNT_SHD` 开帧期间常整帧为 0。abandon 等 MI FE → **稳定 N-1**。因此每帧 **ceiling-force**:`wait_line ≈ height−8`,hold 后再 `vb2_done`。底 **~8 行**经常没写完 → UV 绿、灰缝、Y 亮度串帧,都在这条缝上。 + +内核 quarantine(`rkisp.c`,**禁止加长**):skip/drop/warm/gate **全 2**(约 8 complete;CamOS dummy×3 先烧掉一部分)。`TAIL_WAIT=45000`,`POST_MI=25000`,`FORCE_POST=70000`,`WARMUP_POST=85000`。 + +FT 下 `V4L2_CID_EXPOSURE` **no-op**;曝光 = PWM `trigger_pulse_us`(CamOS:`pulse ≈ exposure_us − 14`)。`GAINDLY=0x08` 当次生效,无 REGHOLD 切枪。 + +**禁止(已用失败实验打过):** 加长 warm/gate/CamOS post residual 消绿;无条件 UV `0x80`;无条件刷底 8 行 Y;伪造 `appsink_seen`;把 ROI warmup `min` 打回 1;为消绿再减 dummy×3;commit 带 `Co-Authored-By: Claude`。 + +画质:`scripts/imx296_bottom_quality.py`(classic / seam / tear)。**不要只看 green=0。** +n1trace 默认关;诊断:`echo Y > /sys/module/video_rkisp/parameters/n1trace`。 + +刷机:`python3 scripts/dlcvcam_verify_bootimg.py verify --require-sidecar` → +`dd if= of=/dev/disk/by-partlabel/boot bs=4M conv=fsync` → reboot → `uname -v`。 +84 重启后 **systemd `camos` 是 disabled**,需手动:`cd /root/smartcam_bs && python server.py --host 0.0.0.0 --port 8000`。 +148 **不要**跑 `0_测试运行_camos.sh`(会停服务、重建前端);用已有 `python server.py --host 0.0.0.0`。控制走 **`/ws/cmd`**(cookie `access_token`),`/ws` 只读,没有 REST `set_exposure`。 + +--- + +## 1. 看到 NG 先分类 + +| 样子 | 家族 | 层 | 不该当 | +|------|------|----|--------| +| 底 8 行 G 明显高于 R/B,主体正常 | classic UV 绿带 | 内核 sanitize UV | 整帧脏、WB | +| 底 8 行平灰、主体有色 | #1013 无条件 0x80 | 勿把该逻辑加回 | classic 绿 | +| 底 8 行亮度差一截,缝在 `h-8` | Y 串帧 / 曝光跳 | 内核 sanitize Y | UV 绿 | +| **整幅**橄榄,常 `(~73,98,45)` | 脏缓冲或 WB 未写 ISP | **CamOS** | 底缝 sanitize | +| 第 N 枪是第 N−1 枪曝光 | N-1 相位 | 内核 ceiling-force | 底带 | +| 无触发帧数 +1(常整幅绿) | line0 late complete | CamOS irq 闸 | 底 UV | +| 真触发被吞 `drop consumed` | post-arm drop | CamOS | 加长 gate | +| preview 503 / `streaming=0` | ROI media-ctl / pool | CamOS | 内核绿线 | +| `/preview/frame` 的 `X-Frame-Tag` 在 apply 期间 +N | **hold-last 预览 tag ≠ 入模 seq** | CamOS 编码层 | 当 first_hit / 无触发 +1 | + +点名(84,修前):`/root/CamOS/images/2026-08-20/NG/20260820_112555_813_632100.jpg` +body~46,bot8~120,dY≈+74,跳在 `h-8` → **Y 串帧**。 + +--- + +## 2. 缺陷(按还要不要动手) + +### 内核已修,勿回退 + +**N-1** — 复现:gain=0,曝光 min↔max 交替,每改一枪看中心 luma 是否跟**当前** pulse。修:`capture.c` ceiling-force;`imx296.c` 进 FT dummy×3(800µs);free_run 忽略 XTRIG。`#2026080601` 起。附录 N-1 文。 + +**入模脏帧 / IRQ 烧 gate** — #1008 曾在 warm 期把 `pub_gate` 烧光。#1009:drop→warm→gate 在 WORK **和** IRQ 都遵守;warm>0 时 gate 不减。**不能**靠把 gate 加到 8/12/13 消绿。附录 0812 文。 + +**底 UV 绿带 #1016** — ceiling-force 后 chroma 尾未写完。`rkisp_ft_sanitize_nv12_bot` **条件**修 UV(stuck、ref 有 chroma 时的中性占比、mean 跳>6、classic G),last-good memcpy。`821edce1` / `#2026081301`。180 ACCEPT bad=0,apply avg 2.835s。无条件 0x80(#1013)已用 endurance #1205 FAIL 否决,不要加回。 + +**底 Y 串帧 #1017(当前内核工作)** — 84、gain=240、1500↔15000 头几帧底亮/暗一截。180 同内核可没有。PWM 大跳后 Y 尾仍是上一档或未写完;#1016 只修 UV。同一 sanitize:Y 底 8 行相对 pre-tail mean 跳 **>12** 且坏行 **≥4** → 整带 memcpy 上一行。`12abd272` / `#2026082001`。84 一轮:1500→15000 max \|dY\|=2.23(修前≈74);反向 0.67;tear=0。 +副作用:坏带被上一行纹理盖住,不是真读出。未做:soak、真 GPIO line0、ROI 跳、180 回归。 +不要和更早的「短曝后 PWM period 未恢复」(`4d2a190`)搞混——那是 period,不是 Y 尾。 + +**其它已合入、本地图不展开:** ROI 后交付当前 FT 帧(`3eef7dac`);procfs MMIO 门闩防 teardown SError(`1a89f1c`);n1trace 默认关(`01726d6`)。 + +### CamOS(本仓改不到;84≠180) + +整幅绿 = 脏缓冲或 AWB 默认,sanitize 只动底 8 行。改 grabber 必须重启 `server.py`。下列长期在 **180 工作区**,memory 写未 commit/未 push。 + +| 问题 | 复现 | 现行修 | 坑 | +|------|------|--------|----| +| 冷启动 line0 首枪 +2/绿 | 持久化 line0 重启 CamOS | close→FT `hard_reset` | 曾 `drop=1`,会吞真枪 | +| 无触发仍 +1 整幅绿 | 稳态 mon→line0 不打 GPIO | 开闸后要 `trigger-dev irq` 相对基线增量 | `echo 1 > 4-001a/trigger` **不增 irq** | +| 真 GPIO 首枪被吞 | `drop consumed=1` | **`POST_ARM_USER_DROP` 默认 0** | 贴边残帧只丢不扣额度 | +| 仅冷启动首枪整幅橄榄、第二枪正常 | `20260814_141955` (72.5,98,45) | WB **pre-warmup + pre-gate** 各写一次 | 禁止再用 drop 吞首枪 | +| ROI preview 503 | HW ROI sensor `set_crop` | 全幅/ROI 优先 **media-ctl ISP pad crop** | 板 commit 曾 `9b39efd`,是否 push 未知 | +| mon→trig 墙钟 | `set_trigger_mode` 到 apply done | 180 fast warmup avg **2.26s**;默认 CamOS ~2.8s | ≤2.0s 是新产品要求;**不要减内核 skip/drop/warm/gate** | +| 触发态 ROI 墙钟 | 拆 pipeline | R8 avg 2.51s,6/6≤3s | 1s 物理不够;2s 中位可冲但计划未实施。量 `roi restart done total=`,不要 `roi_cmd_s`。勿 `min=1`/伪造 appsink | +| **148 line0 入模无 GPIO 仍 +1/+2** | 产品线是 `echo 1 > 4-001a/trigger`(**不增** trigger-dev irq) | `_line0_arming` 盖 warmup→snapshot;`REQUIRE_IRQ=1` 时 `irq_ok is not True` 且无 i2c mtime 则丢(堵住 None fail-open);appsink 信号 hold 到二次 irq snapshot(`hold_signals=1` → `resumed reason=line0-armed`) | 未加长 gate、未默认 `POST_ARM_USER_DROP=1`。预览 tag 仍可 +N。CamOS `fix/imx296-switch-latency-clean` 曾 ahead 未 push。备份 `gst_v4l2_grabber.py.bak.20260821_095918` | + +**测法(曾把「0 绿 / 0 N-1」测成假 PASS):** + +- 用 **CamOS** 路径,不要 GstV4L2Grabber 直连 `/dev/video11`(会和 CamOS 抢设备)。 +- 等 **入模/存图 seq** 或明确的新帧,不要把 `/preview/frame` hold-last `X-Frame-Tag` 当用户枪。 +- 判 N-1 先把增益打到 **0**,否则暗场 LO/HI 被压扁变假 MIX。 +- 板无硬件触发时:software 用 `/debug/trigger_once`;**产品 line0 用 i2c echo**;验收 first_hit 不要只用 GPIO irq(148 产品路径 echo 不增 irq)。 +- `rkisp_start` 会清 `stream->skip_frame`,丢头帧不能只靠它(用 `early_done_drop_left` 等跨 start 计数)。 +- 早期文档写「residual 超时绝不 force complete」——那是 **#1008 前** 策略,已被 ceiling-force 取代;以 N-1 附录 + 现行 `rkisp.c` 为准。 + +--- + +## 3. 代码入口 + +| 路径 | 作用 | +|------|------| +| `drivers/media/platform/rockchip/isp/capture.c` | ceiling-force / post hold | +| `drivers/media/platform/rockchip/isp/rkisp.c` | skip/drop/warm/gate、hold us、n1trace | +| `drivers/media/platform/rockchip/isp/capture_v39.c` | `rkisp_ft_sanitize_nv12_bot`(#1016 UV + #1017 Y),`mi_frame_end` pub 前 | +| `drivers/media/i2c/imx296.c` | FT / PWM / dummy×3 / free_run 忽略 XTRIG | +| `scripts/imx296_bottom_quality.py` | classic / seam / tear | +| `scripts/verify_accept_user.py` | mon→sw、line0、N-1 tag、ROI;默认 `/ws/cmd` | +| `scripts/verify_k1301_lat.py` | apply 墙钟;默认 `/ws/cmd` | +| `scripts/imx296_day_endurance.py` | 长稳;默认 `/ws/cmd` | +| `scripts/imx296_148_extra.py` | §4 E/F/I/J 补验(Y 跳、idle line0、pulse、n1trace) | +| `build_bootimg.sh` | 命名 `boot-rk3576-6.1.99--.img`;未跟踪 md **不算** dirty | + +板:`smartcam_backend/camera/rkcam/_runtime/gst_v4l2_grabber.py`。 + +--- + +## 4. 验收(不加长 gate) + +1. `uname -v` = `#2026082001`。 +2. **Y**:84,FT,gain=240,1500↔15000,头 8 帧 \|dY\|≪10、`h-8` 无跳、tear=0。 +3. **UV**:mon→software ×6 head12,classic=0、seam=0。 +4. **N-1**:16 枪 unique tag;可选 LO/HI 中心 luma 跟当前 pulse。 +5. **first_hit**:mon→sw=1。line0:产品路径用 **i2c echo**(148);有真实 GPIO 的板另验 irq 增量。不要把预览 tag 当入模。 +6. **冷启动 / 入模 line0**:无触发时用户 seq 不涨;真首枪 +1 非整幅绿;允许 `drop skipped (irq-quota)`,禁止无故 `drop consumed` 真枪。 +7. **T_apply**:稳态 2–3s,硬上限 5s。 +8. **ROI**:尺寸真变、preview 非 503、first_hit=1。 + +84 上 #1017 只跑过 **software + 一来一回曝光跳**。CamOS 冷启动/irq/WB/ROI:**180 与 148 代码不同步**,不要假设 84 已有任一侧的 grabber。 diff --git a/scripts/imx296_148_extra.py b/scripts/imx296_148_extra.py new file mode 100644 index 0000000000000..a776dbaf0cad5 --- /dev/null +++ b/scripts/imx296_148_extra.py @@ -0,0 +1,465 @@ +#!/usr/bin/env python3 +"""Extra checks beyond verify_accept_user.py (originally 148). + +Covers panorama §4 / defect E (Y tear at gain=240 1500↔15000), +F idle line0 (no spurious +1), J n1trace default, I pulse restore, +and whole-frame olive (WB/dirty) on head frames. + +CamOS 控制面默认 /ws/cmd;账号用 CAMOS_USER / CAMOS_PASS。 +""" +from __future__ import annotations + +import asyncio +import importlib.util +import json +import os +import sys +import time +from pathlib import Path + +import requests +import websockets + +BASE = os.environ.get("CAMOS_BASE", "http://127.0.0.1:8000") +WS_URL = os.environ.get("CAMOS_WS", "ws://127.0.0.1:8000/ws/cmd") +OUT = Path(os.environ.get("VERIFY_OUT", "/userdata/imx296_accept_148/extra")) +USER = os.environ.get("CAMOS_USER", "admin") +PASS = os.environ.get("CAMOS_PASS", "dlcv2026") +PULSE = Path("/sys/bus/i2c/devices/4-001a/trigger_pulse_us") +RUN_MODE = Path("/sys/bus/i2c/devices/4-001a/run_mode") +N1TRACE = Path("/sys/module/video_rkisp/parameters/n1trace") +TAIL = Path("/sys/module/video_rkisp/parameters/imx296_tail_wait_us") +STATS = Path("/sys/devices/platform/trigger-dev/stats") +UNAME = Path("/proc/version") + +det = None +for cand in ( + Path(__file__).resolve().parent / "imx296_bottom_quality.py", + Path("/userdata/imx296_accept_148/imx296_bottom_quality.py"), + Path("/userdata/imx296_warmup_ab/scripts/imx296_bottom_quality.py"), +): + if cand.is_file(): + spec = importlib.util.spec_from_file_location("imx296_bottom_quality", cand) + det = importlib.util.module_from_spec(spec) + assert spec.loader + spec.loader.exec_module(det) + break +if det is None: + sys.exit("imx296_bottom_quality.py missing") + +OUT.mkdir(parents=True, exist_ok=True) +(OUT / "frames").mkdir(exist_ok=True) + +s = requests.Session() + + +def login(): + s.post(BASE + "/api/auth/login", json={"username": USER, "password": PASS}, timeout=10) + return "; ".join(f"{c.name}={c.value}" for c in s.cookies) + + +cookie = login() + + +def patch(body, timeout=90): + t0 = time.time() + r = s.patch(BASE + "/api/flow_config", json=body, timeout=timeout) + js = r.json() if r.content else {} + ok = r.status_code < 400 + if isinstance(js, dict) and "success" in js: + ok = bool(js["success"]) + return time.time() - t0, js, ok + + +def peek_tag(): + pr = s.get(BASE + "/preview/frame", params={"t": time.time_ns()}, timeout=8) + return int(pr.headers.get("X-Frame-Tag") or pr.headers.get("x-frame-tag") or 0) + + +def wait_new(last, timeout_s=2.2): + end = time.time() + timeout_s + while time.time() < end: + pr = s.get(BASE + "/preview/frame", params={"t": time.time_ns()}, timeout=8) + tn = int(pr.headers.get("X-Frame-Tag") or pr.headers.get("x-frame-tag") or 0) + if pr.status_code == 200 and pr.content and tn != last: + return tn, pr.content + time.sleep(0.03) + return last, b"" + + +def fire_sw(): + s.post(BASE + "/debug/trigger_once", timeout=8) + + +def fire_i2c(): + Path("/sys/bus/i2c/devices/4-001a/trigger").write_text("1\n") + + +def luma_bot_above(a: dict) -> float: + bot = 0.299 * a["br"] + 0.587 * a["bg"] + 0.114 * a["bb"] + above = 0.299 * a["ar"] + 0.587 * a["ag"] + 0.114 * a["ab"] + return bot - above + + +def olive_whole(a: dict) -> bool: + # panorama: whole-frame olive mean often (~73,98,45) + mr, mg, mb = a["mr"], a["mg"], a["mb"] + g_ex = mg - max(mr, mb) + return g_ex >= 18 and 50 <= mg <= 130 and mr < 95 and mb < 70 + + +def rkcam(): + return s.get(BASE + "/debug/status", timeout=10).json()["rkcam"] + + +def parse_stats(): + txt = STATS.read_text() + out = {} + for tok in txt.replace("\n", " ").split(): + if "=" in tok: + k, _, v = tok.partition("=") + try: + out[k] = int(v) + except Exception: + out[k] = v + return out + + +async def ws_cmd(ws, cmd, value=None): + msg = {"cmd": cmd} + if value is not None: + msg["value"] = value + await ws.send(json.dumps(msg)) + for _ in range(4): + try: + await asyncio.wait_for(ws.recv(), timeout=0.3) + except Exception: + break + + +async def set_exp_gain(ws, exp=None, gain=None): + if gain is not None: + await ws_cmd(ws, "rkcam_set_gain", float(gain)) + if exp is not None: + await ws_cmd(ws, "rkcam_set_exposure_time", float(exp)) + # FT 曝光走 PWM pulse;CamOS 应写 trigger_pulse_us。再直写一份兜底。 + try: + pulse = max(1, int(round(float(exp))) - 14) + PULSE.write_text(f"{pulse}\n") + except Exception as e: + print("pulse_sysfs_err", e, flush=True) + # wait until rkcam reports (gain always; exp may stay cached) + deadline = time.time() + 2.5 + while time.time() < deadline: + st = rkcam() + g_ok = gain is None or abs(float(st.get("gain") or -1) - float(gain)) < 0.6 + p_ok = True + if exp is not None and PULSE.exists(): + try: + p_ok = abs(int(PULSE.read_text().strip()) - (int(round(float(exp))) - 14)) <= 2 + except Exception: + p_ok = False + if g_ok and p_ok: + break + time.sleep(0.08) + st = rkcam() + print( + f" set_exp_gain exp={exp} gain={gain} -> rkcam exp={st.get('exposure_us')} " + f"gain={st.get('gain')} pulse={PULSE.read_text().strip()}", + flush=True, + ) + + +def shoot_n(label, n, fire, last=None): + if last is None: + last = peek_tag() + rows = [] + first = None + for i in range(n): + fire() + tn, data = wait_new(last, 2.2) + if not data: + rows.append({"i": i + 1, "miss": True}) + continue + if first is None: + first = i + 1 + a = det.analyze_jpeg(data) + dy = luma_bot_above(a) + rec = { + "i": i + 1, + "tag": tn, + "miss": False, + "dim": a["dim"], + "mean": round(a["mean"], 2), + "dY": round(dy, 2), + "max_row_jump": a["max_row_jump"], + "classic": a["classic_green"], + "seam": a["seam"], + "tear": a["tear"], + "bad": a["bad"], + "olive": olive_whole(a), + "bot": [a["br"], a["bg"], a["bb"]], + "above": [a["ar"], a["ag"], a["ab"]], + "mid": [a["mr"], a["mg"], a["mb"]], + "reasons": a["reasons"], + } + (OUT / "frames" / f"{label}_{i+1:02d}.jpg").write_bytes(data) + rows.append(rec) + last = tn + time.sleep(0.1) + return {"first_hit": first, "rows": rows, "last": last} + + +async def main(): + summary = { + "host": Path("/etc/hostname").read_text().strip() if Path("/etc/hostname").exists() else "", + "kernel": UNAME.read_text().strip(), + "uname_v": os.uname().version, + "n1trace": N1TRACE.read_text().strip() if N1TRACE.exists() else None, + "tail_wait_us": TAIL.read_text().strip() if TAIL.exists() else None, + "run_mode_before": RUN_MODE.read_text().strip() if RUN_MODE.exists() else None, + "pulse_before": PULSE.read_text().strip() if PULSE.exists() else None, + "stats_before": parse_stats(), + "rkcam_before": { + k: rkcam().get(k) + for k in ( + "exposure_us", + "gain", + "trigger_mode", + "frame_w", + "frame_h", + "roi", + "fps", + "frame_tag", + "wb_ratio", + "last_error", + ) + }, + } + print("KERNEL", summary["uname_v"], flush=True) + print("n1trace", summary["n1trace"], "tail", summary["tail_wait_us"], flush=True) + + orig = dict(summary["rkcam_before"]) + orig_flow = s.get(BASE + "/api/flow_config", timeout=10).json() + + ws = await websockets.connect( + WS_URL, + additional_headers={"Cookie": cookie}, + open_timeout=8, + ping_interval=20, + max_size=4 * 1024 * 1024, + ) + + # --- enter software --- + print("=== enter software ===", flush=True) + dt, body, ok = patch({"run_mode": "trigger", "trigger_mode": "software"}) + print(f"apply_sw={dt:.3f} ok={ok}", flush=True) + summary["apply_sw_s"] = round(dt, 3) + summary["apply_sw_ok"] = ok + time.sleep(0.5) + await set_exp_gain(ws, gain=240) + time.sleep(0.3) + print("rkcam after gain240", rkcam().get("gain"), rkcam().get("exposure_us"), flush=True) + + # 1500 → 15000 + print("=== E 1500 -> 15000 gain=240 ===", flush=True) + await set_exp_gain(ws, exp=1500, gain=240) + time.sleep(0.2) + # discard 2 at low + shoot_n("e_lo_disc", 2, fire_sw) + pulse_lo = PULSE.read_text().strip() + await set_exp_gain(ws, exp=15000, gain=240) + time.sleep(0.15) + pulse_hi = PULSE.read_text().strip() + hi = shoot_n("e_lo2hi", 8, fire_sw) + dys = [r["dY"] for r in hi["rows"] if not r.get("miss")] + tears = sum(1 for r in hi["rows"] if r.get("tear")) + bads = sum(1 for r in hi["rows"] if r.get("bad")) + olives = sum(1 for r in hi["rows"] if r.get("olive")) + max_abs_dy = max((abs(x) for x in dys), default=None) + print( + f" lo2hi first={hi['first_hit']} tear={tears} bad={bads} olive={olives} " + f"max|dY|={max_abs_dy} pulse {pulse_lo}->{pulse_hi}", + flush=True, + ) + summary["E_lo2hi"] = { + "pulse_lo": pulse_lo, + "pulse_hi": pulse_hi, + "first_hit": hi["first_hit"], + "tear": tears, + "bad": bads, + "olive": olives, + "max_abs_dY": max_abs_dy, + "dYs": dys, + "rows": hi["rows"], + "pass": tears == 0 and bads == 0 and olives == 0 and (max_abs_dy is not None and max_abs_dy < 10), + } + + # 15000 → 1500 + print("=== E 15000 -> 1500 gain=240 ===", flush=True) + await set_exp_gain(ws, exp=15000, gain=240) + shoot_n("e_hi_disc", 2, fire_sw) + await set_exp_gain(ws, exp=1500, gain=240) + time.sleep(0.15) + lo = shoot_n("e_hi2lo", 8, fire_sw) + dys2 = [r["dY"] for r in lo["rows"] if not r.get("miss")] + tears2 = sum(1 for r in lo["rows"] if r.get("tear")) + bads2 = sum(1 for r in lo["rows"] if r.get("bad")) + olives2 = sum(1 for r in lo["rows"] if r.get("olive")) + max_abs_dy2 = max((abs(x) for x in dys2), default=None) + pulse_after = PULSE.read_text().strip() + print( + f" hi2lo first={lo['first_hit']} tear={tears2} bad={bads2} olive={olives2} " + f"max|dY|={max_abs_dy2} pulse_after={pulse_after}", + flush=True, + ) + summary["E_hi2lo"] = { + "pulse_after": pulse_after, + "first_hit": lo["first_hit"], + "tear": tears2, + "bad": bads2, + "olive": olives2, + "max_abs_dY": max_abs_dy2, + "dYs": dys2, + "rows": lo["rows"], + "pass": tears2 == 0 and bads2 == 0 and olives2 == 0 and (max_abs_dy2 is not None and max_abs_dy2 < 10), + } + + # I: after short exp, pulse should track current (not stuck at short) + await set_exp_gain(ws, exp=15000, gain=0) + time.sleep(0.5) + pulse_restore = PULSE.read_text().strip() + try: + pv = int(pulse_restore) + except Exception: + pv = -1 + summary["I_pulse_restore"] = { + "pulse": pulse_restore, + "pass": pv > 10000, # 15000-14 ≈ 14986 + } + print(f"I pulse after 15000={pulse_restore} pass={summary['I_pulse_restore']['pass']}", flush=True) + + # restore milder gain for remaining + await set_exp_gain(ws, exp=orig.get("exposure_us") or 3000, gain=0) + + # --- F: mon → line0 idle, no fire, tag must not +1 --- + print("=== F idle line0 no-fire ===", flush=True) + dtm, _, okm = patch({"run_mode": "monitor", "trigger_mode": "close"}) + time.sleep(5.5) + tag0 = peek_tag() + stats0 = parse_stats() + rk0 = { + k: rkcam().get(k) + for k in ("frame_tag", "frame_seq", "native_frame_seq", "preview_seq") + } + dtl, _, okl = patch({"run_mode": "trigger", "trigger_mode": "line0"}) + time.sleep(2.5) + tag1 = peek_tag() + stats1 = parse_stats() + rk1 = { + k: rkcam().get(k) + for k in ("frame_tag", "frame_seq", "native_frame_seq", "preview_seq") + } + idle_delta = (tag1 or 0) - (tag0 or 0) + irq_delta = (stats1.get("irq") or 0) - (stats0.get("irq") or 0) + # /preview/frame 的 X-Frame-Tag 来自 encoded hold-last-frame(切换后仍显示 + # 最后一张 monitor JPEG 的 raw_seq)。用户路径以 raw/frame_seq 为准: + # quarantine 后应为 0,且无 echo 时不得再 put。 + user_seq = int(rk1.get("frame_seq") or rk1.get("frame_tag") or 0) + print( + f" apply_l0={dtl:.3f} ok={okl} preview_tag {tag0}->{tag1} delta={idle_delta} " + f"irq_delta={irq_delta} user_seq={user_seq} rk {rk0}->{rk1}", + flush=True, + ) + # 再等 2s:warmup 可能刚好吃掉 1 个 tag;稳态不应继续涨 + time.sleep(2.0) + tag2 = peek_tag() + late_delta = (tag2 or 0) - (tag1 or 0) + rk2 = { + k: rkcam().get(k) + for k in ("frame_tag", "frame_seq", "native_frame_seq", "preview_seq") + } + user_seq2 = int(rk2.get("frame_seq") or rk2.get("frame_tag") or 0) + print( + f" idle+2s preview_tag {tag1}->{tag2} late_delta={late_delta} " + f"user_seq {user_seq}->{user_seq2}", + flush=True, + ) + summary["F_idle_line0"] = { + "apply_s": round(dtl, 3), + "apply_ok": okl, + "tag0": tag0, + "tag1": tag1, + "tag2": tag2, + "tag_delta": idle_delta, + "late_delta": late_delta, + "irq_delta": irq_delta, + "rk0": rk0, + "rk1": rk1, + "rk2": rk2, + "user_seq": user_seq, + "user_seq2": user_seq2, + "stats0": stats0, + "stats1": stats1, + # preview tag 在 apply 期间可能被 monitor hold 编码推高,不等于入模。 + "enter_spurious": user_seq != 0 and irq_delta == 0, + "pass": okl and late_delta == 0 and user_seq == 0 and user_seq2 == 0, + } + + # first i2c fire after idle (mtime fallback, not true GPIO) + last = peek_tag() + i2c = shoot_n("f_i2c", 6, fire_i2c, last=last) + stats2 = parse_stats() + olives_i = sum(1 for r in i2c["rows"] if r.get("olive")) + bads_i = sum(1 for r in i2c["rows"] if r.get("bad")) + print( + f" i2c first={i2c['first_hit']} got={sum(1 for r in i2c['rows'] if not r.get('miss'))} " + f"bad={bads_i} olive={olives_i} irq {stats1.get('irq')}->{stats2.get('irq')}", + flush=True, + ) + summary["F_i2c_line0"] = { + "first_hit": i2c["first_hit"], + "got": sum(1 for r in i2c["rows"] if not r.get("miss")), + "bad": bads_i, + "olive": olives_i, + "irq_before": stats1.get("irq"), + "irq_after": stats2.get("irq"), + "note": "i2c echo does not increment trigger-dev irq; mtime fallback", + "rows": i2c["rows"], + "pass": i2c["first_hit"] == 1 and bads_i == 0 and olives_i == 0, + } + + # J + summary["J_n1trace_off"] = { + "value": N1TRACE.read_text().strip(), + "pass": N1TRACE.read_text().strip() in ("N", "0", "n"), + } + + # restore monitor + original exp/gain + print("=== restore ===", flush=True) + patch({"run_mode": orig_flow.get("run_mode", "monitor"), "trigger_mode": orig_flow.get("trigger_mode", "close")}) + await set_exp_gain(ws, exp=orig.get("exposure_us"), gain=orig.get("gain")) + await ws.close() + + summary["E_pass"] = bool(summary["E_lo2hi"]["pass"] and summary["E_hi2lo"]["pass"]) + summary["F_idle_pass"] = bool(summary["F_idle_line0"]["pass"]) + summary["extra_pass"] = bool( + summary["E_pass"] + and summary["F_idle_pass"] + and summary["I_pulse_restore"]["pass"] + and summary["J_n1trace_off"]["pass"] + ) + (OUT / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8") + print("BEGIN_EXTRA_JSON", flush=True) + slim = {k: summary[k] for k in summary if k not in ("E_lo2hi", "E_hi2lo", "F_i2c_line0", "F_idle_line0")} + slim["E_lo2hi"] = {k: summary["E_lo2hi"][k] for k in summary["E_lo2hi"] if k != "rows"} + slim["E_hi2lo"] = {k: summary["E_hi2lo"][k] for k in summary["E_hi2lo"] if k != "rows"} + slim["F_idle_line0"] = {k: summary["F_idle_line0"][k] for k in summary["F_idle_line0"] if k not in ("stats0", "stats1")} + slim["F_i2c_line0"] = {k: summary["F_i2c_line0"][k] for k in summary["F_i2c_line0"] if k != "rows"} + print(json.dumps(slim, ensure_ascii=False), flush=True) + print("END_EXTRA_JSON", flush=True) + print("EXTRA", "PASS" if summary["extra_pass"] else "FAIL", flush=True) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/imx296_day_endurance.py b/scripts/imx296_day_endurance.py index 7e4a233231a2f..9b145db5497cc 100644 --- a/scripts/imx296_day_endurance.py +++ b/scripts/imx296_day_endurance.py @@ -48,7 +48,7 @@ raise SystemExit(2) BASE = os.environ.get("CAMOS_BASE", "http://127.0.0.1:8000") -WS_URL = os.environ.get("CAMOS_WS", "ws://127.0.0.1:8000/ws") +WS_URL = os.environ.get("CAMOS_WS", "ws://127.0.0.1:8000/ws/cmd") USER = os.environ.get("CAMOS_USER", "admin") PASS = os.environ.get("CAMOS_PASS", "dlcv2026") diff --git a/scripts/imx296_day_watchdog.py b/scripts/imx296_day_watchdog.py index 9312284cb17e3..7408092b0ee84 100644 --- a/scripts/imx296_day_watchdog.py +++ b/scripts/imx296_day_watchdog.py @@ -344,10 +344,7 @@ def main() -> int: ap = argparse.ArgumentParser() ap.add_argument( "--report", - default=str( - Path(__file__).resolve().parents[1] - / "IMX296_24h稳定性测试_20260807.md" - ), + default=str(Path("/tmp/imx296_day_watchdog/report.md")), ) ap.add_argument("--state-dir", default="/tmp/imx296_day_watchdog") ap.add_argument("--interval", type=int, default=120) diff --git a/scripts/verify_accept_user.py b/scripts/verify_accept_user.py index 63fb5727127fa..fa530437ece3b 100644 --- a/scripts/verify_accept_user.py +++ b/scripts/verify_accept_user.py @@ -9,7 +9,8 @@ optional center-luma monotonicity under controlled light (soft check) - multi-signal bottom quality: classic / seam / tear -Does not override CamOS warmup/gate. Kernel #2026081301 + imx296_bottom_quality. +Does not override CamOS warmup/gate. Kernel #2026082001 + imx296_bottom_quality. +CamOS 控制面默认 /ws/cmd(/ws 只读);可用 CAMOS_WS 覆盖。 """ from __future__ import annotations @@ -26,7 +27,7 @@ import websockets BASE = os.environ.get("CAMOS_BASE", "http://127.0.0.1:8000") -WS_URL = os.environ.get("CAMOS_WS", "ws://127.0.0.1:8000/ws") +WS_URL = os.environ.get("CAMOS_WS", "ws://127.0.0.1:8000/ws/cmd") OUT = Path(os.environ.get("VERIFY_OUT", "/userdata/imx296_warmup_ab/verify_accept_user")) SW_ROUNDS = int(os.environ.get("VERIFY_SW_ROUNDS", "6")) LINE0_ROUNDS = int(os.environ.get("VERIFY_LINE0_ROUNDS", "4")) @@ -34,7 +35,8 @@ N1_SEQ = int(os.environ.get("VERIFY_N1_SEQ", "16")) ROI_HEAD = int(os.environ.get("VERIFY_ROI_HEAD", "8")) COOLDOWN = float(os.environ.get("VERIFY_COOLDOWN", "5.5")) -USER, PASS = "admin", "dlcv2026" +USER = os.environ.get("CAMOS_USER", "admin") +PASS = os.environ.get("CAMOS_PASS", "dlcv2026") XTRIG = Path(os.environ.get("XTRIG_PATH", "/sys/bus/i2c/devices/4-001a/trigger")) N1TRACE = Path("/sys/module/video_rkisp/parameters/n1trace") diff --git a/scripts/verify_k1301_lat.py b/scripts/verify_k1301_lat.py index 1a073bea02632..44e979ed8a0a5 100644 --- a/scripts/verify_k1301_lat.py +++ b/scripts/verify_k1301_lat.py @@ -3,6 +3,7 @@ Measures mon→software apply wall-clock (real switches only: apply_s>=0.5). Hard limit 5s target 2–3s. Does not lengthen warmup/gate. +CamOS 控制面默认 /ws/cmd(/ws 只读);可用 CAMOS_WS 覆盖。 """ from __future__ import annotations @@ -19,13 +20,14 @@ import websockets BASE = os.environ.get("CAMOS_BASE", "http://127.0.0.1:8000") -WS_URL = os.environ.get("CAMOS_WS", "ws://127.0.0.1:8000/ws") +WS_URL = os.environ.get("CAMOS_WS", "ws://127.0.0.1:8000/ws/cmd") OUT = Path(os.environ.get("VERIFY_OUT", "/userdata/imx296_warmup_ab/verify_k1301_lat")) SW_ROUNDS = int(os.environ.get("VERIFY_SW_ROUNDS", "8")) HEAD = int(os.environ.get("VERIFY_HEAD", "10")) ROI_HEAD = int(os.environ.get("VERIFY_ROI_HEAD", "8")) COOLDOWN = float(os.environ.get("VERIFY_COOLDOWN", "5.5")) -USER, PASS = "admin", "dlcv2026" +USER = os.environ.get("CAMOS_USER", "admin") +PASS = os.environ.get("CAMOS_PASS", "dlcv2026") _det = None for cand in ( From a917e7713b514a1d3f9e6f72ac68c5447eaa20fc Mon Sep 17 00:00:00 2001 From: hamletzhang Date: Thu, 3 Sep 2026 17:18:09 +0800 Subject: [PATCH 21/21] =?UTF-8?q?fix(rkisp):=20=E7=A7=BB=E9=99=A4=20IMX296?= =?UTF-8?q?=20FT=20=E5=B0=BE=E8=A1=8C=20sanitize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DLCVCAM_BUILD_VERSION | 2 +- .../media/platform/rockchip/isp/capture_v39.c | 344 ------------------ 2 files changed, 1 insertion(+), 345 deletions(-) diff --git a/DLCVCAM_BUILD_VERSION b/DLCVCAM_BUILD_VERSION index e4ef0dde81f6e..c487fed3a6e1c 100644 --- a/DLCVCAM_BUILD_VERSION +++ b/DLCVCAM_BUILD_VERSION @@ -1 +1 @@ -2026082001 +2026090301 diff --git a/drivers/media/platform/rockchip/isp/capture_v39.c b/drivers/media/platform/rockchip/isp/capture_v39.c index 29bbb29b3aa7f..e756a0ea813c6 100644 --- a/drivers/media/platform/rockchip/isp/capture_v39.c +++ b/drivers/media/platform/rockchip/isp/capture_v39.c @@ -2,8 +2,6 @@ /* Copyright (c) 2023 Rockchip Electronics Co., Ltd. */ #include -#include -#include #include #include #include @@ -1036,343 +1034,6 @@ static int isp_frame_end(struct rkisp_stream *stream, u32 irq) * otherwise it will overflow. */ -/* - * V39+IMX296 FT: MI OFFS stay 0 → every frame is ceiling-force. Last NV12 - * chroma rows can still be incomplete after force_post hold. After a large - * exposure / PWM-pulse jump the last ~8 Y rows can still be the previous - * exposure (board 84, gain=240, 1500→15000): a luma tear, not classic UV. - * - * #1013 unconditional 0x80 → permanent flat-grey seam (#1205 FAIL). - * #1014/#1015: conditional repair. #1016: neutral-fraction only when the - * reference line is *not* neutral (scene has chroma); always honor mean - * jump / stuck / classic-G. Extend last good UV line. - * #1017: same idea for Y — only rewrite the last 8 luma rows when the - * whole-line mean jumps; majority-bad copies the entire band from the - * line above so a 1-row blend cannot poison the reference. Gate stays tiny. - */ -#define RKISP_FT_NV12_UV_TAIL_ROWS 8U -#define RKISP_FT_NV12_Y_TAIL_ROWS 8U -/* Whole-line |meanY-refY|. Userspace tear uses 12; splice samples were 14–74. */ -#define RKISP_FT_Y_LINE_MEAN_THR 12U -/* If this many tail Y rows jump vs the pre-tail line, treat as a splice band. */ -#define RKISP_FT_Y_BAND_BAD_MIN 4U -/* |meanU-refU| / |meanV-refV| — dark colored scenes need a low bar. */ -#define RKISP_FT_UV_LINE_MEAN_THR 6U -/* Sample counts as near-neutral chroma (buffer-clear / incomplete). */ -#define RKISP_FT_UV_NEU_LO 0x78U -#define RKISP_FT_UV_NEU_HI 0x88U -/* Bad if this fraction of UV pairs is near-neutral *and* ref has chroma. */ -#define RKISP_FT_UV_NEU_FRAC_NUM 1U -#define RKISP_FT_UV_NEU_FRAC_DEN 2U -/* ref |u-0x80| or |v-0x80| above this ⇒ scene chroma present. */ -#define RKISP_FT_UV_REF_CHROMA_MIN 10U -#define RKISP_FT_UV_NEAR_FRAC_NUM 3U -#define RKISP_FT_UV_NEAR_FRAC_DEN 4U - -static bool rkisp_ft_uv_line_bad(const u8 *line, u32 bpl, u8 ref_u, u8 ref_v) -{ - u32 i, n = bpl / 2; - u64 su = 0, sv = 0; - u32 near0 = 0, nearff = 0, neu = 0; - u32 mu, mv, du, dv, ref_du, ref_dv; - u8 u, v; - bool ref_has_chroma; - - if (n < 4) - return false; - - for (i = 0; i < n; i++) { - u = line[i * 2]; - v = line[i * 2 + 1]; - su += u; - sv += v; - if (u < 8 && v < 8) - near0++; - if (u > 248 && v > 248) - nearff++; - if (u >= RKISP_FT_UV_NEU_LO && u <= RKISP_FT_UV_NEU_HI && - v >= RKISP_FT_UV_NEU_LO && v <= RKISP_FT_UV_NEU_HI) - neu++; - } - mu = (u32)(su / n); - mv = (u32)(sv / n); - du = mu > ref_u ? mu - ref_u : ref_u - mu; - dv = mv > ref_v ? mv - ref_v : ref_v - mv; - ref_du = ref_u > 0x80 ? ref_u - 0x80 : 0x80 - ref_u; - ref_dv = ref_v > 0x80 ? ref_v - 0x80 : 0x80 - ref_v; - ref_has_chroma = (ref_du >= RKISP_FT_UV_REF_CHROMA_MIN || - ref_dv >= RKISP_FT_UV_REF_CHROMA_MIN); - - /* Unwritten / stuck blocks. */ - if (near0 * RKISP_FT_UV_NEAR_FRAC_DEN > n * RKISP_FT_UV_NEAR_FRAC_NUM) - return true; - if (nearff * RKISP_FT_UV_NEAR_FRAC_DEN > n * RKISP_FT_UV_NEAR_FRAC_NUM) - return true; - /* - * Incomplete FT UV often stays buffer-clear ~0x80 while Y is done. - * Only treat high neutral fraction as bad when the line above has - * real scene chroma — avoids rewriting naturally neutral bottoms. - */ - if (ref_has_chroma && - neu * RKISP_FT_UV_NEU_FRAC_DEN > n * RKISP_FT_UV_NEU_FRAC_NUM) - return true; - /* Mean jump vs last good line (primary for dark colored scenes). */ - if (du > RKISP_FT_UV_LINE_MEAN_THR || dv > RKISP_FT_UV_LINE_MEAN_THR) - return true; - /* Classic G band: both U/V depressed vs neutral while ref is higher. */ - if (mu + 20 < 0x80 && mv + 20 < 0x80 && - (ref_u > mu + 10 || ref_v > mv + 10)) - return true; - - return false; -} - -static void rkisp_ft_uv_line_mean(const u8 *line, u32 bpl, u8 *out_u, u8 *out_v) -{ - u32 i, n = bpl / 2; - u64 su = 0, sv = 0; - - if (n == 0) { - *out_u = 0x80; - *out_v = 0x80; - return; - } - for (i = 0; i < n; i++) { - su += line[i * 2]; - sv += line[i * 2 + 1]; - } - *out_u = (u8)(su / n); - *out_v = (u8)(sv / n); -} - -static u32 rkisp_ft_y_line_mean(const u8 *line, u32 bpl) -{ - u32 i; - u64 s = 0; - - if (!bpl) - return 0; - for (i = 0; i < bpl; i++) - s += line[i]; - return (u32)(s / bpl); -} - -static bool rkisp_ft_y_mean_jump(u32 mean, u32 ref) -{ - u32 d = mean > ref ? mean - ref : ref - mean; - - return d > RKISP_FT_Y_LINE_MEAN_THR; -} - -static void rkisp_ft_sanitize_nv12_bot(struct rkisp_stream *stream, - struct rkisp_buffer *buf) -{ - struct rkisp_device *dev = stream->ispdev; - struct v4l2_pix_format_mplane *pixm = &stream->out_fmt; - struct vb2_buffer *vb; - struct device *dma_dev; - struct sg_table *sgt; - u8 *base, *uv = NULL, *tail = NULL, *ref = NULL, *line; - u8 *y_tail, *y_ref, *y_line; - u8 ref_u, ref_v; - u32 fourcc, bpl, h, y_size, uv_size, tail_rows, tail_bytes; - u32 y_tail_rows, y_sync_off, y_sync_len, y_init, y_refm, y_mean, y_bad; - u32 uv_off, uv_rows, ref_off = 0, i; - u32 sync_off, sync_len; - int uv_plane; - bool have_uv = false; - bool repaired = false; - - if (!buf || stream->id != RKISP_STREAM_MP) - return; - if (!dev->cap_dev.is_done_early) - return; - if (!dev->hw_dev) - return; - - fourcc = pixm->pixelformat; - if (fourcc != V4L2_PIX_FMT_NV12 && fourcc != V4L2_PIX_FMT_NV12M && - fourcc != V4L2_PIX_FMT_NV21 && fourcc != V4L2_PIX_FMT_NV21M) - return; - - base = buf->vaddr[0]; - if (!base) - base = vb2_plane_vaddr(&buf->vb.vb2_buf, 0); - if (!base) - return; - - bpl = pixm->plane_fmt[0].bytesperline; - h = pixm->height; - if (!bpl || h < 4) - return; - - y_size = bpl * h; - y_tail_rows = RKISP_FT_NV12_Y_TAIL_ROWS; - if (y_tail_rows + 1 > h) - y_tail_rows = h > 1 ? h - 1 : 0; - y_tail = NULL; - y_ref = NULL; - y_sync_off = 0; - y_sync_len = 0; - if (y_tail_rows) { - y_sync_off = (h - y_tail_rows - 1) * bpl; - y_sync_len = (y_tail_rows + 1) * bpl; - if (y_sync_off + y_sync_len <= y_size) { - y_ref = base + y_sync_off; - y_tail = y_ref + bpl; - } else { - y_tail_rows = 0; - } - } - - tail_rows = RKISP_FT_NV12_UV_TAIL_ROWS; - uv_rows = h / 2; - if (tail_rows > uv_rows) - tail_rows = uv_rows; - tail_bytes = bpl * tail_rows; - uv = NULL; - uv_size = 0; - uv_plane = 0; - uv_off = 0; - sync_off = 0; - sync_len = 0; - if (tail_rows && tail_rows < uv_rows && tail_bytes) { - if (pixm->num_planes >= 2) { - uv = buf->vaddr[1]; - if (!uv) - uv = vb2_plane_vaddr(&buf->vb.vb2_buf, 1); - uv_size = pixm->plane_fmt[1].sizeimage; - uv_plane = 1; - uv_off = 0; - } else { - uv = base + y_size; - uv_size = y_size / 2; - uv_plane = 0; - uv_off = y_size; - } - if (uv && uv_size >= bpl * (tail_rows + 1) && - tail_bytes + bpl <= uv_size) { - ref_off = uv_size - tail_bytes - bpl; - tail = uv + (uv_size - tail_bytes); - ref = uv + ref_off; - sync_off = ref_off; - sync_len = tail_bytes + bpl; - have_uv = true; - } - } - if (!y_tail_rows && !have_uv) - return; - - vb = &buf->vb.vb2_buf; - dma_dev = dev->hw_dev->dev; - - if (dev->hw_dev->is_dma_sg_ops) { - sgt = vb2_dma_sg_plane_desc(vb, 0); - if (sgt) - dma_sync_sgtable_for_cpu(dma_dev, sgt, DMA_BIDIRECTIONAL); - if (have_uv && uv_plane != 0) { - sgt = vb2_dma_sg_plane_desc(vb, uv_plane); - if (sgt) - dma_sync_sgtable_for_cpu(dma_dev, sgt, - DMA_BIDIRECTIONAL); - } - } else { - dma_addr_t da0 = vb2_dma_contig_plane_dma_addr(vb, 0); - - if (da0 && y_sync_len) - dma_sync_single_for_cpu(dma_dev, da0 + y_sync_off, - y_sync_len, DMA_BIDIRECTIONAL); - if (have_uv) { - dma_addr_t da = vb2_dma_contig_plane_dma_addr(vb, - uv_plane); - - if (da) - dma_sync_single_for_cpu(dma_dev, - da + uv_off + sync_off, - sync_len, - DMA_BIDIRECTIONAL); - } - } - - if (y_tail && y_ref && y_tail_rows) { - y_init = rkisp_ft_y_line_mean(y_ref, bpl); - y_bad = 0; - for (i = 0; i < y_tail_rows; i++) { - y_mean = rkisp_ft_y_line_mean(y_tail + i * bpl, bpl); - if (rkisp_ft_y_mean_jump(y_mean, y_init)) - y_bad++; - } - if (y_bad >= RKISP_FT_Y_BAND_BAD_MIN) { - for (i = 0; i < y_tail_rows; i++) - memcpy(y_tail + i * bpl, y_ref, bpl); - repaired = true; - } else if (y_bad) { - u8 *y_good = y_ref; - - y_refm = y_init; - for (i = 0; i < y_tail_rows; i++) { - y_line = y_tail + i * bpl; - y_mean = rkisp_ft_y_line_mean(y_line, bpl); - if (rkisp_ft_y_mean_jump(y_mean, y_refm) || - rkisp_ft_y_mean_jump(y_mean, y_init)) { - memcpy(y_line, y_good, bpl); - repaired = true; - continue; - } - y_good = y_line; - y_refm = y_mean; - } - } - } - - if (have_uv) { - rkisp_ft_uv_line_mean(ref, bpl, &ref_u, &ref_v); - - for (i = 0; i < tail_rows; i++) { - line = tail + i * bpl; - if (!rkisp_ft_uv_line_bad(line, bpl, ref_u, ref_v)) { - /* Advance reference through good tail lines. */ - ref = line; - rkisp_ft_uv_line_mean(ref, bpl, &ref_u, &ref_v); - continue; - } - memcpy(line, ref, bpl); - repaired = true; - } - } - - if (dev->hw_dev->is_dma_sg_ops) { - sgt = vb2_dma_sg_plane_desc(vb, 0); - if (sgt) - dma_sync_sgtable_for_device(dma_dev, sgt, - DMA_BIDIRECTIONAL); - if (have_uv && uv_plane != 0) { - sgt = vb2_dma_sg_plane_desc(vb, uv_plane); - if (sgt) - dma_sync_sgtable_for_device(dma_dev, sgt, - DMA_BIDIRECTIONAL); - } - } else { - dma_addr_t da0 = vb2_dma_contig_plane_dma_addr(vb, 0); - - if (da0 && y_sync_len) - dma_sync_single_for_device(dma_dev, da0 + y_sync_off, - y_sync_len, - DMA_BIDIRECTIONAL); - if (have_uv) { - dma_addr_t da = vb2_dma_contig_plane_dma_addr(vb, - uv_plane); - - if (da) - dma_sync_single_for_device(dma_dev, - da + uv_off + sync_off, - sync_len, - DMA_BIDIRECTIONAL); - } - } - (void)repaired; -} - static int mi_frame_end(struct rkisp_stream *stream, u32 state) { struct rkisp_device *dev = stream->ispdev; @@ -1568,11 +1229,6 @@ static int mi_frame_end(struct rkisp_stream *stream, u32 state) READ_ONCE(dev->cap_dev.early_done_diag_pub), sof_ns, ns); } - /* - * #1016: conditional UV-tail repair (ref-chroma-gated neu + mean). - * #1013 0x80 seam FAIL; #1014/#1015 thr/neu iterations. - */ - rkisp_ft_sanitize_nv12_bot(stream, buf); 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);