diff --git a/arch/mips/Kconfig.debug b/arch/mips/Kconfig.debug
index f4ae7900fcd3..892e31804982 100644
--- a/arch/mips/Kconfig.debug
+++ b/arch/mips/Kconfig.debug
@@ -164,3 +164,83 @@ config MIPS_CPS_NS16550_WIDTH
 	  lb/sb instructions.
 
 endif # MIPS_CPS_NS16550_BOOL
+
+# These options are only for real kernel hackers who want to get their hands dirty.
+config DEBUG_LL
+	bool "Kernel low-level debugging functions (read help!)"
+	depends on DEBUG_KERNEL
+	help
+	  Say Y here to include definitions of printascii, printch, printhex
+	  in the kernel.  This is helpful if you are debugging code that
+	  executes before the console is initialized.
+
+	  Note that selecting this option will limit the kernel to a single
+	  UART or semihosting definition, as specified below. Attempting to
+	  boot the kernel image on a different platform *will not work*, so
+	  this option should not be enabled for kernels that are intended to
+	  be portable.
+
+choice
+	prompt "Kernel low-level debugging port"
+	depends on DEBUG_LL
+
+	config DEBUG_MIPS_UHI
+		bool "Kernel low-level debugging via UHI semihosting"
+		help
+		  Say Y here if you want kernel low-level debugging support
+		  via MIPS UHI semihosting.
+
+	config DEBUG_LL_UART_8250
+		bool "Kernel low-level debugging via 8250 UART"
+		select DEBUG_LL_UART
+		help
+		  Say Y here if you wish the debug print routes to direct
+		  their output to an 8250 UART.  You can use this option
+		  to provide the parameters for the 8250 UART rather than
+		  selecting one of the platform specific options below if
+		  you know the parameters for the port.
+
+endchoice
+
+config DEBUG_LL_INCLUDE
+	string
+	default "debug/8250.S" if DEBUG_LL_UART_8250 || DEBUG_UART_8250
+	default "debug/uhi.S" if DEBUG_MIPS_UHI
+	default "debug-macro.S"
+
+# Compatibility options for 8250
+config DEBUG_UART_8250
+	bool
+	select DEBUG_LL_UART
+
+config DEBUG_LL_UART
+	bool
+
+config DEBUG_UART_FLOW_CONTROL
+	bool "Enable flow control (CTS) for the debug UART"
+	depends on DEBUG_LL_UART
+	default n
+	help
+	  Some UART ports are connected to terminals that will use modem
+	  control signals to indicate whether they are ready to receive text.
+	  In practice this means that the terminal is asserting the special
+	  control signal CTS (Clear To Send). If your debug UART supports
+	  this and your debug terminal will require it, enable this option.
+
+config DEBUG_UART_PHYS
+	hex "Physical base address of debug UART"
+	depends on DEBUG_LL_UART
+	help
+	  This is the physical base address of the debug UART. It must be
+	  accessible from unmapped kernel space (i.e. KSEG1 for 32bit kernels
+	  or XKPHYS for 64bit kernels).
+
+config DEBUG_UART_8250_SHIFT
+	int "Register offset shift for the 8250 debug UART"
+	depends on DEBUG_LL_UART_8250 || DEBUG_UART_8250
+	default 2
+
+config DEBUG_UART_8250_WIDTH
+	int "Register width for the 8250 debug UART"
+	depends on DEBUG_LL_UART_8250 || DEBUG_UART_8250
+	default 1
diff --git a/arch/mips/include/debug/8250.S b/arch/mips/include/debug/8250.S
new file mode 100644
index 000000000000..f8608d3e271a
--- /dev/null
+++ b/arch/mips/include/debug/8250.S
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2023, Jiaxun Yang <jiaxun.yang@flygoat.com>
+ * MIPS Low level debug include file for 8250 UART
+ */
+
+#include <asm/addrspace.h>
+#include <asm/asm.h>
+#include <linux/serial_reg.h>
+
+#define DEBUG_LL_UART
+
+#define UART_BASE	CKSEG1ADDR(CONFIG_DEBUG_UART_PHYS)
+
+#define UART_TX_OFS	(UART_TX  << CONFIG_DEBUG_UART_8250_SHIFT)
+#define UART_LSR_OFS	(UART_LSR << CONFIG_DEBUG_UART_8250_SHIFT)
+#define UART_MSR_OFS	(UART_MSR << CONFIG_DEBUG_UART_8250_SHIFT)
+
+#if CONFIG_DEBUG_UART_8250_WIDTH == 1
+# define UART_L		lb
+# define UART_S		sb
+#elif CONFIG_DEBUG_UART_8250_WIDTH == 2
+# define UART_L		lh
+# define UART_S		sh
+#elif CONFIG_DEBUG_UART_8250_WIDTH == 4
+# define UART_L		lw
+# define UART_S		sw
+#elif defined(CONFIG_64BIT) && CONFIG_DEBUG_UART_8250_WIDTH == 8
+# define UART_L		ld
+# define UART_S		sd
+#else
+# define UART_L		lb
+# define UART_S		sb
+#endif
+
+		.macro	addruart,rd,rx
+		PTR_LA	\rd, UART_BASE
+		.endm
+
+		.macro	senduart,rd,rx
+		UART_S   \rd, UART_TX_OFS(\rx)
+		.endm
+
+		.macro	busyuart,rd,rx
+1002:
+		UART_L	\rd, UART_LSR_OFS(\rx)
+		andi	\rd, \rd, (UART_LSR_TEMT | UART_LSR_THRE)
+		xori	\rd, (UART_LSR_TEMT | UART_LSR_THRE)
+		bnez	\rd, 1002b
+		.endm
+
+		.macro	waituarttxrdy,rd,rx
+		.endm
+
+		.macro	waituartcts,rd,rx
+1001:
+		UART_L	\rd, UART_MSR_OFS(\rx)
+		andi	\rd, UART_MSR_CTS
+		beqz	\rd, 1001b
+		.endm
diff --git a/arch/mips/include/debug/uhi.S b/arch/mips/include/debug/uhi.S
new file mode 100644
index 000000000000..20554523e151
--- /dev/null
+++ b/arch/mips/include/debug/uhi.S
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2023, Jiaxun Yang <jiaxun.yang@flygoat.com>
+ * MIPS Low level debug include file for UHI semihosting
+ */
+
+#include <asm/asm.h>
+
+/**
+ * printch() - write a character to UHI Plog
+ * @a0: ASCII character to write
+ *
+ * clobbers v0, t9
+ * UHI may clobber k0, k1
+ */
+		.pushsection .bss
+charbuf_addr:	.space 8
+		.popsection
+NESTED(printch, 0, ra)
+	PTR_LI	t9, charbuf_addr
+	sb	a0, 0(t9)
+	sb	zero, 1(t9)
+	move	a0, t9
+	li	v0, 0x1
+	li	t9, 13 /* plog */
+	.set push
+	.set MIPS_ISA_LEVEL_RAW
+	sdbbp	1
+	.set pop
+	jr	ra
+	END(printch)
+
+/**
+ * printascii() - write a string to UHI Plog
+ * @a0: pointer to NULL-terminated ASCII string
+ *
+ * clobbers v0
+ * UHI may clobber k0, k1
+ */
+NESTED(printascii, 0, ra)
+	li	v0, 0x1
+	li	t9, 13 /* plog */
+	.set push
+	.set MIPS_ISA_LEVEL_RAW
+	sdbbp	1
+	.set pop
+	jr		ra
+	END(printascii)
diff --git a/arch/mips/kernel/Makefile b/arch/mips/kernel/Makefile
index 853a43ee4b44..10c0a224efe4 100644
--- a/arch/mips/kernel/Makefile
+++ b/arch/mips/kernel/Makefile
@@ -78,6 +78,8 @@ obj-$(CONFIG_MIPS32_COMPAT)	+= linux32.o ptrace32.o signal32.o
 obj-$(CONFIG_MIPS32_N32)	+= scall64-n32.o signal_n32.o
 obj-$(CONFIG_MIPS32_O32)	+= scall64-o32.o signal_o32.o
 
+obj-$(CONFIG_DEBUG_LL)		+= debug.o
+
 obj-$(CONFIG_KGDB)		+= kgdb.o
 obj-$(CONFIG_PROC_FS)		+= proc.o
 obj-$(CONFIG_MAGIC_SYSRQ)	+= sysrq.o
diff --git a/arch/mips/kernel/debug.S b/arch/mips/kernel/debug.S
new file mode 100644
index 000000000000..f71c6c969ef6
--- /dev/null
+++ b/arch/mips/kernel/debug.S
@@ -0,0 +1,130 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2023, Jiaxun Yang <jiaxun.yang@flygoat.com>
+ * MIPS Low level debug routines
+ *
+ * Note: Those functions are designed carefully to only clobber
+ * t0, t1, t9, so they can be used without stack. Also they are
+ * position independent to be called from any place.
+ */
+
+#include <linux/compiler.h>
+
+#include <asm/addrspace.h>
+#include <asm/asm.h>
+#include <asm/asm-offsets.h>
+#include <asm/mipsregs.h>
+#include <asm/regdef.h>
+
+#include CONFIG_DEBUG_LL_INCLUDE
+
+#ifdef DEBUG_LL_UART
+/**
+ * printch() - write a character to the UART
+ * @a0: ASCII character to write
+ *
+ * clobbers t0, t9
+ */
+NESTED(printch, 0, ra)
+	addruart	t9, t0
+#ifdef CONFIG_DEBUG_UART_FLOW_CONTROL
+	waituartcts	t0, t9
+#endif
+	waituarttxrdy	t0, t9
+	senduart	a0, t9
+	busyuart	t0, t9
+	jr		ra
+	END(printch)
+
+/**
+ * printascii() - write a string to the UART
+ * @a0: pointer to NULL-terminated ASCII string
+ *
+ * clobbers t0, t1, t9
+ *
+ * Write a null-terminated ASCII string to the UART.
+ */
+NESTED(printascii, 0, ra)
+	addruart	t9, t0
+	move		t1, a0
+
+1:	lb		a0, 0(t1)
+	beqz		a0, 2f
+#ifdef CONFIG_DEBUG_UART_FLOW_CONTROL
+	waituartcts	t0, t9
+#endif
+	waituarttxrdy	t0, t9
+	senduart	a0, t9
+	busyuart	t0, t9
+	PTR_ADDIU	t1, t1, 1
+	b		1b
+
+2:	jr		ra
+	END(printascii)
+#endif /* DEBUG_LL_UART */
+
+/**
+ * printhex() - write n byte hex value to the UART
+ * @a0: Hex value to write to the UART
+ * @a1: number of bytes to write to the UART
+ *
+ * clobbers: t0, t1, t9
+ */
+
+		.pushsection .bss
+hexbuf_addr:	.space 32
+		.popsection
+
+NESTED(printhex, 0, ra)
+	PTR_LA		t9, hexbuf_addr
+	sll		a1, a1, 1
+1:
+    	subu		t0, a1, 1
+	sll		t0, t0, 2
+	LONG_SRLV	t0, a0, t0
+	andi		t0, t0, 0xf
+	li		t1, '0'
+	blt		t0, 10, 2f
+	li		t1, 'a'
+	addiu		t0, t0, -10
+2:
+	addu		t1, t1, t0
+	sb		t1, 0(t9)
+	PTR_ADDIU	t9, t9, 1
+	addiu		a1, a1, -1
+	bnez		a1, 1b
+	sb		zero, 0(t9)
+
+	move		a1, ra
+	PTR_LA		a0, hexbuf_addr
+	bal		printascii
+
+	jr		a1
+	END(printhex)
+
+NESTED(printhex1, 0, ra)
+	li		a1, 1
+	b		printhex
+	END(printhex1)
+
+NESTED(printhex2, 0, ra)
+	li		a1, 2
+	b		printhex
+	END(printhex2)
+
+NESTED(printhex4, 0, ra)
+	li		a1, 4
+	b		printhex
+	END(printhex4)
+
+#ifdef CONFIG_64BIT
+NESTED(printhex8, 0, ra)
+	li		a1, 8
+	b		printhex
+	END(printhex8)
+#endif
+
+NESTED(printhexl, 0, ra)
+	li		a1, PTRSIZE
+	b		printhex
+	END(printhexl)
