diff -uprN -X a/Documentation/dontdiff a/drivers/tty/serial/8250/8250.h b/drivers/tty/serial/8250/8250.h
--- a/drivers/tty/serial/8250/8250.h	2022-11-16 02:04:15.000000000 -0700
+++ b/drivers/tty/serial/8250/8250.h	2023-08-21 16:15:51.429933400 -0600
@@ -403,3 +403,6 @@ static inline int serial_index(struct ua
  {
  	return port->minor - 64;
  }
+
+extern void uart_debug_create(struct uart_port *port);
+extern void uart_debug_destroy(struct uart_port *port);
diff -uprN -X a/Documentation/dontdiff a/drivers/tty/serial/8250/8250_core.c b/drivers/tty/serial/8250/8250_core.c
--- a/drivers/tty/serial/8250/8250_core.c	2022-11-16 02:04:15.000000000 -0700
+++ b/drivers/tty/serial/8250/8250_core.c	2023-08-21 16:16:47.502740700 -0600
@@ -1094,6 +1094,7 @@ int serial8250_register_8250_port(const
  			if (ret)
  				goto err;
  
+			uart_debug_create(&uart->port);
  			ret = uart->port.line;
  		} else {
  			dev_info(uart->port.dev,
@@ -1151,6 +1152,7 @@ void serial8250_unregister_port(int line
  		spin_unlock_irqrestore(&uart->port.lock, flags);
  	}
  
+	uart_debug_destroy(&uart->port);
  	uart_remove_one_port(&serial8250_reg, &uart->port);
  	if (serial8250_isa_devs) {
  		uart->port.flags &= ~UPF_BOOT_AUTOCONF;
diff -uprN -X a/Documentation/dontdiff a/drivers/tty/serial/8250/8250_debug.c b/drivers/tty/serial/8250/8250_debug.c
--- a/drivers/tty/serial/8250/8250_debug.c	1969-12-31 17:00:00.000000000 -0700
+++ b/drivers/tty/serial/8250/8250_debug.c	2023-08-22 13:19:12.234454600 -0600
@@ -0,0 +1,519 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ *  This module creates a debugfs interface that can be used to hook the low level serial
+ *  I/O routines and generate timestamped traces of UART register reads and writes.  This
+ *  feature provides an accurate and reliable "serial port sniffer" that can be used for
+ *  debugging problems with the serial port.
+ *
+ *  Each serial port ("ttyS0", "ttyS1", etc.) will have its own directory in the root of
+ *  the debug filesystem.  Inside each directory will be the following pseudo-files:
+ *
+ *  trace_all    - toggles tracing of all registers vs. just the RX/TX registers
+ *  trace_buffer - for consuming the trace data
+ *  trace_enable - enables/disables tracing
+ *  trace_size   - configures the size of the trace buffer
+ *
+ *  When tracing is disabled there will be zero impact to the operation of the UART
+ *  driver.  When tracing is enabled a small cost will be incurred after every register
+ *  access to write an entry to the trace buffer in RAM.  The buffer size is limited
+ *  only by the amount of virtual memory available to the kernel.  This is 128 MiB by
+ *  default but it can be increased using vmalloc=xxx on the kernel command line.  The
+ *  memory will be freed after tracing is disabled and all data in the buffer has been
+ *  consumed.
+ *
+ *  Example session:
+ *
+ *  > mount -t debugfs debugfs /sys/kernel/debug
+ *  > echo '1' > /sys/kernel/debug/ttyS1/trace_all
+ *  > echo '1' > /sys/kernel/debug/ttyS1/trace_enable
+ *
+ *  (perform UART operation)
+ *
+ *  > cat /sys/kernel/debug/ttyS1/trace_buffer
+ *
+ *  00:03:04.822166  IER <- 07
+ *  00:03:04.822175  IIR -> C2
+ *  00:03:04.822177  LSR -> 60
+ *  00:03:04.822178  MSR -> 00
+ *  00:03:04.822180  THR <- C9
+ *  00:03:04.822184  IER <- 05
+ *  00:03:04.822185  IIR -> C1
+ *  00:03:05.062943  IIR -> CC
+ *  00:03:05.062945  LSR -> 61
+ *  00:03:05.062948  RBR -> C9
+ *  00:03:05.062950  LSR -> 61
+ *  00:03:05.062952  RBR -> ED
+ *  00:03:05.062953  LSR -> 61
+ *  00:03:05.062954  RBR -> 00
+ *  00:03:05.062955  LSR -> 60
+ *  00:03:05.062965  MSR -> 00
+ *  00:03:05.062967  IIR -> C1
+ */
+
+#include <linux/cpufreq.h>
+#include <linux/debugfs.h>
+#include <linux/fs.h>
+#include <linux/math64.h>
+#include <linux/serial_8250.h>
+
+struct reg_event {
+	uint32_t  cycle_lo;  /* CPU cycle count (lower 32-bits) */
+	uint16_t  cycle_hi;  /* CPU cycle count (upper 16-bits) */
+	uint8_t   access;    /* write flag + register number */
+	uint8_t   data;      /* register data */
+};
+
+struct reg_queue {
+	uint32_t          read_idx;   /* first full (written) slot */
+	uint32_t          write_idx;  /* first empty (unwritten) slot */
+	struct reg_event *buf;        /* array to hold the data */
+	uint32_t          size;       /* array size (number of entries - must be power of 2) */
+	bool              wrap;       /* true if write_idx wrapped around and reached read_idx */
+};
+
+struct uart_debug {
+	spinlock_t        lock;
+	struct mutex      mutex;
+	bool              trace_enable;
+	bool              trace_all;
+	uint32_t          trace_size;
+	unsigned int      (*original_serial_in)(struct uart_port *p, int offset);
+	void              (*original_serial_out)(struct uart_port *p, int offset, int value);
+	struct reg_queue  register_queue;
+	char              line[64];  /* buffer to hold text of last item removed from queue */
+	uint32_t          offset;    /* offset of first unread byte in line */
+};
+
+static ssize_t      all_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos);
+static ssize_t      all_write(struct file *filp,
+			const char __user *buf, size_t size, loff_t *ppos);
+static ssize_t      buffer_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos);
+static ssize_t      enable_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos);
+static ssize_t      enable_write(struct file *filp,
+			const char __user *buf, size_t size, loff_t *ppos);
+static void         format_cycle(uint64_t cpu_cycles, char *buf, uint32_t size);
+static void         queue_add(struct uart_port *port, uint8_t reg, uint8_t data, bool write);
+static bool         queue_alloc(struct uart_port *port);
+static void         queue_free(struct uart_port *port, bool force);
+static void         queue_remove(struct uart_port *port, char *buf, uint32_t size);
+static unsigned int serial_in_wrapper(struct uart_port *port, int offset);
+static void         serial_out_wrapper(struct uart_port *port, int offset, int value);
+static ssize_t      size_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos);
+static ssize_t      size_write(struct file *filp,
+			const char __user *buf, size_t size, loff_t *ppos);
+
+static const struct file_operations all_fops = {
+	.open  = simple_open,
+	.read  = all_read,
+	.write = all_write,
+};
+
+static const struct file_operations buffer_fops = {
+	.open = simple_open,
+	.read = buffer_read,
+};
+
+static const struct file_operations enable_fops = {
+	.open  = simple_open,
+	.read  = enable_read,
+	.write = enable_write,
+};
+
+static const struct file_operations size_fops = {
+	.open  = simple_open,
+	.read  = size_read,
+	.write = size_write,
+};
+
+/*
+ *  Handle reads from the 'trace_all' pseudo-file.  Report the state of the trace_all flag.
+ */
+static ssize_t all_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
+{
+	struct uart_port *port = filp->private_data;
+	struct uart_debug *uart_debug = port->private_data;
+	char text[16];
+	uint32_t len;
+
+	len = snprintf(text, sizeof(text), "%u\n", uart_debug->trace_all);
+	return simple_read_from_buffer(buf, size, ppos, text, len);
+}
+
+/*
+ *  Handle writes to the 'trace_all' pseudo-file.  Set the state of the trace_all flag.
+ */
+static ssize_t all_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
+{
+	struct uart_port *port = filp->private_data;
+	struct uart_debug *uart_debug = port->private_data;
+	int error;
+
+	error = kstrtobool_from_user(buf, size, &uart_debug->trace_all);
+	if (error)
+		return error;
+
+	return size;
+}
+
+/*
+ *  Handle reads from the 'trace_buffer' pseudo-file.  This function consumes content in
+ *  the register event queue.  A best effort will be made to avoid splitting lines of text.
+ */
+static ssize_t buffer_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
+{
+	struct uart_port *port = filp->private_data;
+	struct uart_debug *uart_debug = port->private_data;
+	uint32_t len, num_bytes, total_bytes = 0;
+	const char *ptr;
+	int error = 0;
+
+	if (mutex_lock_interruptible(&uart_debug->mutex))
+		return -ERESTARTSYS;
+
+	while (size) {
+		if (uart_debug->line[uart_debug->offset] == '\0') {
+			uart_debug->offset = 0;
+			queue_remove(port, uart_debug->line, sizeof(uart_debug->line));
+			if (uart_debug->line[0] == '\0')
+				break;
+		}
+
+		ptr = uart_debug->line + uart_debug->offset;
+		len = strlen(ptr);
+		num_bytes = (size < len) ? size : len;
+
+		if (num_bytes < len && total_bytes)
+			break;
+		if (copy_to_user(buf, ptr, num_bytes)) {
+			error = -EFAULT;
+			break;
+		}
+
+		buf += num_bytes;
+		size -= num_bytes;
+		total_bytes += num_bytes;
+		uart_debug->offset += num_bytes;
+	}
+
+	if (!uart_debug->trace_enable)
+		queue_free(port, false);
+
+	mutex_unlock(&uart_debug->mutex);
+	if (error)
+		return total_bytes ? total_bytes : error;
+	return total_bytes;
+}
+
+/*
+ *  Handle reads from the 'trace_enable' pseudo-file.  Report the current state of the
+ *  trace_enable flag.
+ */
+static ssize_t enable_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
+{
+	struct uart_port *port = filp->private_data;
+	struct uart_debug *uart_debug = port->private_data;
+	char text[16];
+	uint32_t len;
+
+	len = snprintf(text, sizeof(text), "%u\n", uart_debug->trace_enable);
+	return simple_read_from_buffer(buf, size, ppos, text, len);
+}
+
+/*
+ *  Handle writes to the 'trace_enable' pseudo-file.  When tracing is enabled allocate
+ *  memory for the register event queue and install our hooks.  When tracing is disabled
+ *  remove our hooks and free the memory used by the register event queue if it is empty.
+ */
+static ssize_t enable_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
+{
+	struct uart_port *port = filp->private_data;
+	struct uart_debug *uart_debug = port->private_data;
+	bool trace_enable;
+	int error;
+
+	error = kstrtobool_from_user(buf, size, &trace_enable);
+	if (error)
+		return error;
+
+	if (mutex_lock_interruptible(&uart_debug->mutex))
+		return -ERESTARTSYS;
+	if (trace_enable == uart_debug->trace_enable)
+		goto exit;
+
+	if (trace_enable) {
+		uart_debug->line[0] = '\0';
+		uart_debug->offset = 0;
+		queue_free(port, true);
+		if (!queue_alloc(port))
+			goto exit;
+
+		uart_debug->original_serial_out = port->serial_out;
+		uart_debug->original_serial_in  = port->serial_in;
+		port->serial_out = serial_out_wrapper;
+		port->serial_in  = serial_in_wrapper;
+	} else {
+		port->serial_out = uart_debug->original_serial_out;
+		port->serial_in  = uart_debug->original_serial_in;
+
+		queue_free(port, false);
+	}
+
+	uart_debug->trace_enable = trace_enable;
+exit:
+	mutex_unlock(&uart_debug->mutex);
+	return size;
+}
+
+/*
+ *  Convert 'cpu_cycles' to a string that reports the time elapsed since boot in
+ *  hours/mins/seconds/microseconds.
+ */
+static void format_cycle(uint64_t cpu_cycles, char *buf, uint32_t size)
+{
+	static uint64_t cpu_freq;  /* cycles per second */
+	uint32_t h, m, s, us;
+
+	if (cpu_freq == 0)
+		cpu_freq = arch_freq_get_on_cpu(0) * 1000ULL;
+
+	s = div64_u64_rem(cpu_cycles, cpu_freq, &cpu_cycles);
+	us = div64_u64(cpu_cycles * 1000 * 1000 + 500 * 1000, cpu_freq);
+
+	m = s / 60; s = s % 60;
+	h = m / 60; m = m % 60;
+
+	snprintf(buf, size, "%02d:%02d:%02d.%06u", h, m, s, us);
+}
+
+/*
+ *  Add an entry to the register event queue.  This can run in interrupt context
+ *  so it needs to be as fast as possible.
+ */
+static void queue_add(struct uart_port *port, uint8_t reg, uint8_t data, bool write)
+{
+	struct uart_debug *uart_debug = port->private_data;
+	struct reg_queue *queue = &uart_debug->register_queue;
+	uint64_t cpu_cycle;
+	struct reg_event event;
+	unsigned long flags;
+
+	if (!uart_debug->trace_all && reg != 0x00)
+		return;
+
+	cpu_cycle      = rdtsc();
+	event.cycle_lo = (uint32_t)cpu_cycle;
+	event.cycle_hi = (uint16_t)(cpu_cycle >> 32);
+	event.access   = (write ? 0x08 : 0x00) | reg;
+	event.data     = data;
+
+	spin_lock_irqsave(&uart_debug->lock, flags);
+
+	queue->buf[queue->write_idx & (queue->size - 1)] = event;
+	queue->write_idx++;
+
+	if (queue->write_idx == queue->read_idx)
+		queue->wrap = true;
+
+	spin_unlock_irqrestore(&uart_debug->lock, flags);
+}
+
+/*
+ *  Allocate memory for the register event queue.  We use vmalloc() instead of kmalloc()
+ *  because we don't need physically contiguous memory and kmalloc() is limited to 4 MiB.
+ *  If memory allocation fails we will try again with a smaller buffer.  The user can
+ *  detect this by reading 'trace_size' after enabling tracing.
+ */
+static bool queue_alloc(struct uart_port *port)
+{
+	struct uart_debug *uart_debug = port->private_data;
+	struct reg_queue *queue = &uart_debug->register_queue;
+
+	queue->size = uart_debug->trace_size;
+	queue->buf = vmalloc(queue->size * sizeof(struct reg_event));
+
+	while (queue->buf == NULL) {
+		queue->size >>= 1;
+		if (queue->size == 0)
+			return false;
+		queue->buf = vmalloc(queue->size * sizeof(struct reg_event));
+	}
+
+	uart_debug->trace_size = queue->size;
+	return true;
+}
+
+/*
+ *  Free the memory used by the register event queue if the queue is empty.  If the 'force'
+ *  flag is 'true' free the memory even if the queue is not empty.
+ *
+ *  The noinline attribute suppresses a bug in gcc 11.3.0 that generates a bogus error:
+ *
+ *  error: ‘__builtin_memset’ offset [0, 19] is out of the bounds [0, 0] [-Werror=array-bounds]
+ */
+static noinline void queue_free(struct uart_port *port, bool force)
+{
+	struct uart_debug *uart_debug = port->private_data;
+	struct reg_queue *queue = &uart_debug->register_queue;
+
+	if (force || queue->read_idx == queue->write_idx) {
+		vfree(queue->buf);
+		memset(queue, 0, sizeof(*queue));
+	}
+}
+
+/*
+ *  Remove an entry from the register event queue.  The entry will be converted to a text
+ *  string and written to the caller's buffer.  If no entries are available an empty string
+ *  will be written.
+ */
+static void queue_remove(struct uart_port *port, char *buf, uint32_t size)
+{
+	struct uart_debug *uart_debug = port->private_data;
+	struct reg_queue *queue = &uart_debug->register_queue;
+	struct reg_event event;
+	const char * const in_regs[]  = { "RBR", "IER", "IIR", "LCR", "MCR", "LSR", "MSR", "SCR" };
+	const char * const out_regs[] = { "THR", "IER", "FCR", "LCR", "MCR", "LSR", "MSR", "SCR" };
+	uint8_t reg;
+	const char *sym, *dir;
+	char elapsed[sizeof("hh:mm:ss.mmmuuu")];
+	uint32_t num_events, events_lost;
+	unsigned long flags;
+
+	spin_lock_irqsave(&uart_debug->lock, flags);
+
+	num_events = queue->write_idx - queue->read_idx;
+	events_lost = num_events > queue->size ? num_events - queue->size : 0;
+	events_lost = queue->wrap ? 0xFFFFFFFF : events_lost;
+
+	if (events_lost) {
+		queue->read_idx += num_events - queue->size;
+		queue->wrap = false;
+	} else if (num_events) {
+		event = queue->buf[queue->read_idx & (queue->size - 1)];
+		queue->read_idx++;
+	}
+
+	spin_unlock_irqrestore(&uart_debug->lock, flags);
+
+	if (events_lost) {
+		snprintf(buf, size, "queue overflow (%u events lost)\n", events_lost);
+	} else if (num_events) {
+		reg = event.access & 0x07;
+		sym = event.access & 0x08 ? out_regs[reg] : in_regs[reg];
+		dir = event.access & 0x08 ? "<-" : "->";
+		format_cycle((((uint64_t)event.cycle_hi << 32) | event.cycle_lo),
+			elapsed, sizeof(elapsed));
+		snprintf(buf, size, "%s  %s %s %02X\n", elapsed, sym, dir, event.data);
+	} else if (size) {
+		buf[0] = '\0';
+	}
+}
+
+/*
+ *  This is the hook we install for register reads when tracing is enabled.  It calls
+ *  the original serial_in function and logs the read to the register event queue.
+ */
+static unsigned int serial_in_wrapper(struct uart_port *port, int offset)
+{
+	struct uart_debug *uart_debug = port->private_data;
+	uint8_t value;
+
+	value = uart_debug->original_serial_in(port, offset);
+	queue_add(port, offset, value, false);
+	return value;
+}
+
+/*
+ *  This is the hook we install for register writes when tracing is enabled.  It calls
+ *  the original serial_out function and logs the write to the register event queue.
+ */
+static void serial_out_wrapper(struct uart_port *port, int offset, int value)
+{
+	struct uart_debug *uart_debug = port->private_data;
+
+	uart_debug->original_serial_out(port, offset, value);
+	queue_add(port, offset, value, true);
+}
+
+/*
+ *  Handle reads from the 'trace_size' pseudo-file.  Report the configured trace size.
+ *  If it changes between (partial) reads then the output will likely be corrupted.  The
+ *  only way to fix this is to store the text buffer in the file private data.  Users
+ *  should be encouraged to provide a buffer large enough to read the entire value at once.
+ */
+static ssize_t size_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
+{
+	struct uart_port *port = filp->private_data;
+	struct uart_debug *uart_debug = port->private_data;
+	char text[16];
+	uint32_t len;
+
+	len = snprintf(text, sizeof(text), "%u\n", uart_debug->trace_size);
+	return simple_read_from_buffer(buf, size, ppos, text, len);
+}
+
+/*
+ *  Handle writes to the 'trace_size' pseudo-file.  Set the configured trace size (maximum
+ *  number of log entries).
+ */
+static ssize_t size_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
+{
+	struct uart_port *port = filp->private_data;
+	struct uart_debug *uart_debug = port->private_data;
+	uint32_t trace_size, rounded_size = 1;
+	int error;
+
+	error = kstrtouint_from_user(buf, size, 0, &trace_size);
+	if (error)
+		return error;
+
+	if (trace_size == 0)
+		return -EINVAL;
+	while (trace_size >>= 1)
+		rounded_size <<= 1;  /* round down to nearest power of 2 */
+	uart_debug->trace_size = rounded_size;
+	return size;
+}
+
+/*
+ *  Create the debugfs interface.  This should be called during port registration after
+ *  port->name, port->serial_in, and port->serial_out have been initialized.  We are
+ *  using port->private_data to store a pointer to our data structure.  That field appears
+ *  to be otherwise unused.  If this is wrong we will need to create a new field.
+ */
+void uart_debug_create(struct uart_port *port)
+{
+	struct uart_debug *uart_debug;
+	struct dentry *dir;
+
+	uart_debug = port->private_data = kzalloc(sizeof(struct uart_debug), GFP_KERNEL);
+	if (uart_debug == NULL)
+		return;
+
+	spin_lock_init(&uart_debug->lock);
+	mutex_init(&uart_debug->mutex);
+	uart_debug->trace_size = 4096;
+
+	dir = debugfs_create_dir(port->name, NULL);  /* ie. "ttyS0" */
+	debugfs_create_file("trace_all",    0644, dir, port, &all_fops);
+	debugfs_create_file("trace_buffer", 0444, dir, port, &buffer_fops);
+	debugfs_create_file("trace_enable", 0644, dir, port, &enable_fops);
+	debugfs_create_file("trace_size",   0644, dir, port, &size_fops);
+}
+
+/*
+ *  Destroy the debugfs interface.  This should be called during port deregistration.
+ */
+void uart_debug_destroy(struct uart_port *port)
+{
+	struct uart_debug *uart_debug;
+
+	uart_debug = port->private_data;
+	port->private_data = NULL;
+
+	if (uart_debug) {
+		debugfs_lookup_and_remove(port->name, NULL);  /* ie. "ttyS0 */
+		queue_free(port, true);
+		kfree(uart_debug);
+	}
+}
diff -uprN -X a/Documentation/dontdiff a/drivers/tty/serial/8250/Makefile b/drivers/tty/serial/8250/Makefile
--- a/drivers/tty/serial/8250/Makefile	2022-11-16 02:04:15.000000000 -0700
+++ b/drivers/tty/serial/8250/Makefile	2023-08-21 16:17:11.835722200 -0600
@@ -8,7 +8,7 @@ obj-$(CONFIG_SERIAL_8250)		+= 8250.o 825
  8250-$(CONFIG_ALPHA_GENERIC)		+= 8250_alpha.o
  8250-$(CONFIG_ALPHA_JENSEN)		+= 8250_alpha.o
  8250-$(CONFIG_SERIAL_8250_PNP)		+= 8250_pnp.o
-8250_base-y				:= 8250_port.o
+8250_base-y				:= 8250_port.o 8250_debug.o
  8250_base-$(CONFIG_SERIAL_8250_DMA)	+= 8250_dma.o
  8250_base-$(CONFIG_SERIAL_8250_DWLIB)	+= 8250_dwlib.o
  8250_base-$(CONFIG_SERIAL_8250_FINTEK)	+= 8250_fintek.o
