From b19c2b5f88f141e58044e5d1012f867d46f74bf3 Mon Sep 17 00:00:00 2001
From: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
Date: Thu, 21 Sep 2023 18:18:53 +0300
Subject: [PATCH] media: rp1: csi2: Track CSI-2 errors

Track the errors from the CSI-2 receiver: overflows and discards. These
are recorded in a table which can be read by the userspace via debugfs.

As tracking the errors may cause much more interrupt load, the tracking
needs to be enabled with a module parameter.

Note that the recording is not perfect: we only record the last
discarded DT for each discard type, instead of recording all of them.
This means that e.g. if the device is discarding two unmatched DTs, the
debugfs file only shows the last one recorded. Recording all of them
would need a more sophisticated recording system to avoid the need of a
very large table, or dynamic allocation.

Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
---
 .../media/platform/raspberrypi/rp1_cfe/csi2.c | 123 ++++++++++++++++++
 .../media/platform/raspberrypi/rp1_cfe/csi2.h |  16 +++
 2 files changed, 139 insertions(+)

--- a/drivers/media/platform/raspberrypi/rp1_cfe/csi2.c
+++ b/drivers/media/platform/raspberrypi/rp1_cfe/csi2.c
@@ -16,6 +16,10 @@
 #include "csi2.h"
 #include "cfe.h"
 
+static bool csi2_track_errors;
+module_param_named(track_csi2_errors, csi2_track_errors, bool, 0);
+MODULE_PARM_DESC(track_csi2_errors, "track csi-2 errors");
+
 #define csi2_dbg_verbose(fmt, arg...)                             \
 	do {                                                      \
 		if (cfe_debug_verbose)                            \
@@ -32,9 +36,28 @@
 #define CSI2_DISCARDS_INACTIVE	0x00c
 #define CSI2_DISCARDS_UNMATCHED	0x010
 #define CSI2_DISCARDS_LEN_LIMIT	0x014
+
+#define CSI2_DISCARDS_AMOUNT_SHIFT	0
+#define CSI2_DISCARDS_AMOUNT_MASK	GENMASK(23, 0)
+#define CSI2_DISCARDS_DT_SHIFT		24
+#define CSI2_DISCARDS_DT_MASK		GENMASK(29, 24)
+#define CSI2_DISCARDS_VC_SHIFT		30
+#define CSI2_DISCARDS_VC_MASK		GENMASK(31, 30)
+
 #define CSI2_LLEV_PANICS	0x018
 #define CSI2_ULEV_PANICS	0x01c
 #define CSI2_IRQ_MASK		0x020
+#define CSI2_IRQ_MASK_IRQ_OVERFLOW		BIT(0)
+#define CSI2_IRQ_MASK_IRQ_DISCARD_OVERFLOW	BIT(1)
+#define CSI2_IRQ_MASK_IRQ_DISCARD_LENGTH_LIMIT	BIT(2)
+#define CSI2_IRQ_MASK_IRQ_DISCARD_UNMATCHED	BIT(3)
+#define CSI2_IRQ_MASK_IRQ_DISCARD_INACTIVE	BIT(4)
+#define CSI2_IRQ_MASK_IRQ_ALL                                              \
+	(CSI2_IRQ_MASK_IRQ_OVERFLOW | CSI2_IRQ_MASK_IRQ_DISCARD_OVERFLOW | \
+	 CSI2_IRQ_MASK_IRQ_DISCARD_LENGTH_LIMIT |                          \
+	 CSI2_IRQ_MASK_IRQ_DISCARD_UNMATCHED |                             \
+	 CSI2_IRQ_MASK_IRQ_DISCARD_INACTIVE)
+
 #define CSI2_CTRL		0x024
 #define CSI2_CH_CTRL(x)		((x) * 0x40 + 0x28)
 #define CSI2_CH_ADDR0(x)	((x) * 0x40 + 0x2c)
@@ -149,6 +172,92 @@ static int csi2_regs_show(struct seq_fil
 
 DEFINE_SHOW_ATTRIBUTE(csi2_regs);
 
+static int csi2_errors_show(struct seq_file *s, void *data)
+{
+	struct csi2_device *csi2 = s->private;
+	unsigned long flags;
+	u32 discards_table[DISCARDS_TABLE_NUM_VCS][DISCARDS_TABLE_NUM_ENTRIES];
+	u32 discards_dt_table[DISCARDS_TABLE_NUM_ENTRIES];
+	u32 overflows;
+
+	spin_lock_irqsave(&csi2->errors_lock, flags);
+
+	memcpy(discards_table, csi2->discards_table, sizeof(discards_table));
+	memcpy(discards_dt_table, csi2->discards_dt_table,
+	       sizeof(discards_dt_table));
+	overflows = csi2->overflows;
+
+	csi2->overflows = 0;
+	memset(csi2->discards_table, 0, sizeof(discards_table));
+	memset(csi2->discards_dt_table, 0, sizeof(discards_dt_table));
+
+	spin_unlock_irqrestore(&csi2->errors_lock, flags);
+
+	seq_printf(s, "Overflows %u\n", overflows);
+	seq_puts(s, "Discards:\n");
+	seq_puts(s, "VC            OVLF        LEN  UNMATCHED   INACTIVE\n");
+
+	for (unsigned int vc = 0; vc < DISCARDS_TABLE_NUM_VCS; ++vc) {
+		seq_printf(s, "%u       %10u %10u %10u %10u\n", vc,
+			   discards_table[vc][DISCARDS_TABLE_OVERFLOW],
+			   discards_table[vc][DISCARDS_TABLE_LENGTH_LIMIT],
+			   discards_table[vc][DISCARDS_TABLE_UNMATCHED],
+			   discards_table[vc][DISCARDS_TABLE_INACTIVE]);
+	}
+
+	seq_printf(s, "Last DT %10u %10u %10u %10u\n",
+		   discards_dt_table[DISCARDS_TABLE_OVERFLOW],
+		   discards_dt_table[DISCARDS_TABLE_LENGTH_LIMIT],
+		   discards_dt_table[DISCARDS_TABLE_UNMATCHED],
+		   discards_dt_table[DISCARDS_TABLE_INACTIVE]);
+
+	return 0;
+}
+
+DEFINE_SHOW_ATTRIBUTE(csi2_errors);
+
+static void csi2_isr_handle_errors(struct csi2_device *csi2, u32 status)
+{
+	spin_lock(&csi2->errors_lock);
+
+	if (status & IRQ_OVERFLOW)
+		csi2->overflows++;
+
+	for (unsigned int i = 0; i < DISCARDS_TABLE_NUM_ENTRIES; ++i) {
+		static const u32 discard_bits[] = {
+			IRQ_DISCARD_OVERFLOW,
+			IRQ_DISCARD_LEN_LIMIT,
+			IRQ_DISCARD_UNMATCHED,
+			IRQ_DISCARD_INACTIVE,
+		};
+		static const u8 discard_regs[] = {
+			CSI2_DISCARDS_OVERFLOW,
+			CSI2_DISCARDS_LEN_LIMIT,
+			CSI2_DISCARDS_UNMATCHED,
+			CSI2_DISCARDS_INACTIVE,
+		};
+		u32 amount;
+		u8 dt, vc;
+		u32 v;
+
+		if (!(status & discard_bits[i]))
+			continue;
+
+		v = csi2_reg_read(csi2, discard_regs[i]);
+		csi2_reg_write(csi2, discard_regs[i], 0);
+
+		amount = (v & CSI2_DISCARDS_AMOUNT_MASK) >>
+			 CSI2_DISCARDS_AMOUNT_SHIFT;
+		dt = (v & CSI2_DISCARDS_DT_MASK) >> CSI2_DISCARDS_DT_SHIFT;
+		vc = (v & CSI2_DISCARDS_VC_MASK) >> CSI2_DISCARDS_VC_SHIFT;
+
+		csi2->discards_table[vc][i] += amount;
+		csi2->discards_dt_table[i] = dt;
+	}
+
+	spin_unlock(&csi2->errors_lock);
+}
+
 void csi2_isr(struct csi2_device *csi2, bool *sof, bool *eof, bool *lci)
 {
 	unsigned int i;
@@ -183,6 +292,9 @@ void csi2_isr(struct csi2_device *csi2,
 		eof[i] = !!(status & IRQ_FE_ACK(i));
 		lci[i] = !!(status & IRQ_LE_ACK(i));
 	}
+
+	if (csi2_track_errors)
+		csi2_isr_handle_errors(csi2, status);
 }
 
 void csi2_set_buffer(struct csi2_device *csi2, unsigned int channel,
@@ -277,6 +389,9 @@ void csi2_stop_channel(struct csi2_devic
 
 void csi2_open_rx(struct csi2_device *csi2)
 {
+	csi2_reg_write(csi2, CSI2_IRQ_MASK,
+		       csi2_track_errors ? CSI2_IRQ_MASK_IRQ_ALL : 0);
+
 	dphy_start(&csi2->dphy);
 
 	csi2_reg_write(csi2, CSI2_CTRL,
@@ -286,6 +401,8 @@ void csi2_open_rx(struct csi2_device *cs
 void csi2_close_rx(struct csi2_device *csi2)
 {
 	dphy_stop(&csi2->dphy);
+
+	csi2_reg_write(csi2, CSI2_IRQ_MASK, 0);
 }
 
 static struct csi2_device *to_csi2_device(struct v4l2_subdev *subdev)
@@ -398,11 +515,17 @@ int csi2_init(struct csi2_device *csi2,
 {
 	unsigned int i, ret;
 
+	spin_lock_init(&csi2->errors_lock);
+
 	csi2->dphy.dev = csi2->v4l2_dev->dev;
 	dphy_probe(&csi2->dphy);
 
 	debugfs_create_file("csi2_regs", 0444, debugfs, csi2, &csi2_regs_fops);
 
+	if (csi2_track_errors)
+		debugfs_create_file("csi2_errors", 0444, debugfs, csi2,
+				    &csi2_errors_fops);
+
 	for (i = 0; i < CSI2_NUM_CHANNELS * 2; i++)
 		csi2->pad[i].flags = i < CSI2_NUM_CHANNELS ?
 				     MEDIA_PAD_FL_SINK : MEDIA_PAD_FL_SOURCE;
--- a/drivers/media/platform/raspberrypi/rp1_cfe/csi2.h
+++ b/drivers/media/platform/raspberrypi/rp1_cfe/csi2.h
@@ -17,6 +17,8 @@
 
 #define CSI2_NUM_CHANNELS 4
 
+#define DISCARDS_TABLE_NUM_VCS 4
+
 enum csi2_mode {
 	CSI2_MODE_NORMAL,
 	CSI2_MODE_REMAP,
@@ -37,6 +39,14 @@ struct csi2_cfg {
 	u32 buffer_size;
 };
 
+enum discards_table_index {
+	DISCARDS_TABLE_OVERFLOW = 0,
+	DISCARDS_TABLE_LENGTH_LIMIT,
+	DISCARDS_TABLE_UNMATCHED,
+	DISCARDS_TABLE_INACTIVE,
+	DISCARDS_TABLE_NUM_ENTRIES,
+};
+
 struct csi2_device {
 	/* Parent V4l2 device */
 	struct v4l2_device *v4l2_dev;
@@ -53,6 +63,12 @@ struct csi2_device {
 
 	struct media_pad pad[CSI2_NUM_CHANNELS * 2];
 	struct v4l2_subdev sd;
+
+	/* lock for csi2 errors counters */
+	spinlock_t errors_lock;
+	u32 overflows;
+	u32 discards_table[DISCARDS_TABLE_NUM_VCS][DISCARDS_TABLE_NUM_ENTRIES];
+	u32 discards_dt_table[DISCARDS_TABLE_NUM_ENTRIES];
 };
 
 void csi2_isr(struct csi2_device *csi2, bool *sof, bool *eof, bool *lci);
