UART baudrate for RK3568

Hello, I have a system with an RK3568 processor, using OpenWRT version 6.1.
The board has 1 console port and 3 UART ports that need to be configured.
As an example, I will show the UART4 configuration.
Here is the setting in the system itself:
rk3568.dts:

&uart4 {
	/* Serial RS232*/
	dma-names = "tx", "rx";
	pinctrl-0 = <&uart4m0_xfer>;
	status = "okay";
};

pinctrl.dtsi:

uart4 {
		/omit-if-no-ref/
		uart4m0_xfer: uart4m0-xfer {
			rockchip,pins =
				/* uart4_rxm0 */
				<1 RK_PA4 2 &pcfg_pull_up>,
				/* uart4_txm0 */
				<1 RK_PA6 2 &pcfg_pull_up>;
		};

		/omit-if-no-ref/
		uart4m0_ctsn: uart4m0-ctsn {
			rockchip,pins =
				/* uart4m0_ctsn */
				<1 RK_PA7 2 &pcfg_pull_none>;
		};

		/omit-if-no-ref/
		uart4m0_rtsn: uart4m0-rtsn {
			rockchip,pins =
				/* uart4m0_rtsn */
				<1 RK_PA5 2 &pcfg_pull_none>;
		};

		/omit-if-no-ref/
		uart4m1_xfer: uart4m1-xfer {
			rockchip,pins =
				/* uart4_rxm1 */
				<3 RK_PB1 4 &pcfg_pull_up>,
				/* uart4_txm1 */
				<3 RK_PB2 4 &pcfg_pull_up>;
		};
	};

u-boot.dtsi:

&uart4 {
	clock-frequency = <24000000>;
	u-boot,dm-spl;
	status = "okay";
};

u-boot.dts:

&uart4 {
	/* Serial RS232*/
	dma-names = "tx", "rx";
	pinctrl-0 = <&uart4m0_xfer>;
	status = "okay";
};

The problem is that, despite the correctly specified baud rate, when connecting another device through this port (that is, using the rs232 protocol), garbage falls into the terminal. I checked the driver, all the numbers are correct, I tried different configurations of dts and dtsi files, all to no avail.
Used for console UART2, that is ttyS2. I also decided to try changing the console port to UART4, that is, ttyS0 and everything worked! The processor began to produce the required frequency for the console port, but the remaining non-working UARTs continued to throw out garbage. Please help me configure these ports, because... I don’t understand at all why the required baudrate doesn’t rise.

In addition, I can say that at startup the UART registers are completely empty, or, conversely, filled with the value 0x00043FF2, which is the value of the UART_CPR (Component Parameter Register). Also, maybe this will lead to the right idea, the level conversion chip used here is ADM3202ARUZ.

I also learned interesting things about my ports using setserial. It turns out that the UART does not even turn on, it is seen by the system, some registers are given to it, but the processor does not fully raise it.
In the configs and driver files it is stated that the default UART speed is 115200, but after startup it is 9600 and it is not always possible to change it. For some reason the divisor is 0. The system uses the 8250 driver, but the UARTs are listed as 16550, which is strange. In addition, the physical port address is not specified.

Hello.
I have a Linux 6.1.53 system based on the rk3568 microchip.
I already described my problem here, but I didn’t present it entirely correctly.
The situation is that I have 1 console port and 3 UARTs.
All of them use the dw-apb-uart driver.
The console port starts and works fine, it raises the speed to 115200 and everything works great.
I need to use the remaining UARTs to control other devices, but by default they are set to 9600 speed.
I tried to change it using stty and setserial, but it doesn't change!
It says a different speed, but when connected there are exactly the same artifacts...
I thought, okay, what if, without changing anything, I connect to a device with a speed of 9600 and everything works!
And if you change the port speed to 115200, and then back to 9600, the port stops working.
That is, some problem arises when changing the speed of an enabled UART.
I found on the Internet that many people have this problem with the physical UART, but they solve this problem through code.
How can you solve such a problem in OpenWRT without getting into the kernel and drivers?

you have to touch the kernel and drivers.

Yes, but the values ​​in the driver are all correct. This driver uses 2 drivers: ns8250 and ns16550A.
When a console port is assigned, it switches to 8250, then back to 16550. But regular UARTs do not. In theory, if you force it to change the mode and so that it assigns the necessary clocks, and then switches back to the 16550A mode, then it can work.
But how to do that?

It's very low level code on the specific IC, you have to check it step by step to find where is the problem.
I donot know. sorry.

Very sad :frowning:

Hello again everyone.
I figured out this problem and hope that my solution will help someone.

The problem turned out to be in the driver, namely in the files 8250_dw.c and 8250_port.c.

When calling the function to change the speed, one of the registers is changed to the port configuration value, and after the configuration it was not changed back, which is why the port stopped working.

This git helped me, from which I took several functions and reworked them for my project:

As a result, I ended up with this patch:

--- a/drivers/tty/serial/8250/8250_dw.c	2024-06-21 15:38:16.654830271 +0400
+++ b/drivers/tty/serial/8250/8250_dw.c	2024-06-21 15:18:39.458800042 +0400
@@ -352,25 +352,60 @@ dw8250_do_pm(struct uart_port *port, uns
 static void dw8250_set_termios(struct uart_port *p, struct ktermios *termios,
 			       const struct ktermios *old)
 {
-	unsigned long newrate = tty_termios_baud_rate(termios) * 16;
-	struct dw8250_data *d = to_dw8250_data(p->private_data);
-	long rate;
+	unsigned int baud = tty_termios_baud_rate(termios);
+	struct dw8250_data *d = p->private_data;
+	unsigned int rate;
+#ifdef CONFIG_ARCH_ROCKCHIP
+	unsigned int rate_temp, diff;
+#endif
 	int ret;
 
+	if (IS_ERR(d->clk))
+		goto out;
+
 	clk_disable_unprepare(d->clk);
-	rate = clk_round_rate(d->clk, newrate);
-	if (rate > 0) {
-		/*
-		 * Note that any clock-notifer worker will block in
-		 * serial8250_update_uartclk() until we are done.
-		 */
-		ret = clk_set_rate(d->clk, newrate);
-		if (!ret)
-			p->uartclk = rate;
+#ifdef CONFIG_ARCH_ROCKCHIP
+	if (baud <= 115200)
+		rate = 24000000;
+	else if (baud == 230400)
+		rate = baud * 16 * 2;
+	else if (baud == 1152000)
+		rate = baud * 16 * 2;
+	else
+		rate = baud * 16;
+
+	ret = clk_set_rate(d->clk, rate);
+	rate_temp = clk_get_rate(d->clk);
+	diff = rate * 20 / 1000;
+	/*
+	 * If rate_temp is not equal to rate, is means fractional frequency
+	 * division is failed. Then use Integer frequency division, and
+	 * the buad rate error must be under -+2%
+	 */
+	if ((rate_temp < rate) && ((rate - rate_temp) > diff)) {
+		ret = clk_set_rate(d->clk, rate + diff);
+		rate_temp = clk_get_rate(d->clk);
+		if ((rate_temp < rate) && ((rate - rate_temp) > diff))
+			dev_info(p->dev, "set rate:%d, but get rate:%d\n",
+				 rate, rate_temp);
+		else if ((rate < rate_temp) && ((rate_temp - rate) > diff))
+			dev_info(p->dev, "set rate:%d, but get rate:%d\n",
+				 rate, rate_temp);
 	}
+#else
+	rate = clk_round_rate(d->clk, baud * 16);
+	ret = clk_set_rate(d->clk, rate);
+#endif
 	clk_prepare_enable(d->clk);
 
-	dw8250_do_set_termios(p, termios, old);
+	if (!ret)
+		p->uartclk = rate;
+out:
+	p->status &= ~UPSTAT_AUTOCTS;
+	if (termios->c_cflag & CRTSCTS)
+		p->status |= UPSTAT_AUTOCTS;
+
+	serial8250_do_set_termios(p, termios, old);
 }
 
 static void dw8250_set_ldisc(struct uart_port *p, struct ktermios *termios)
--- a/drivers/tty/serial/8250/8250_port.c	2024-06-21 15:38:22.015061526 +0400
+++ b/drivers/tty/serial/8250/8250_port.c	2024-06-21 15:18:39.458800042 +0400
@@ -2644,6 +2644,10 @@ void serial8250_do_set_divisor(struct ua
 {
 	struct uart_8250_port *up = up_to_u8250p(port);
 
+#ifdef CONFIG_ARCH_ROCKCHIP
+	serial_port_out(port, UART_MCR, UART_MCR_LOOP);
+#endif
+
 	/* Workaround to enable 115200 baud on OMAP1510 internal ports */
 	if (is_omap1510_8250(up)) {
 		if (baud == 115200) {
@@ -2663,6 +2667,24 @@ void serial8250_do_set_divisor(struct ua
 		serial_port_out(port, UART_LCR, up->lcr | UART_LCR_DLAB);
 
 	serial_dl_write(up, quot);
+#ifdef CONFIG_ARCH_ROCKCHIP
+	if (quot != serial_dl_read(up))
+		dev_warn_ratelimited(port->dev, "ttyS%d set divisor fail, quot:%d != dll,dlh:%d\n",
+					serial_index(port), quot, serial_dl_read(up));
+#endif
+	if (port->type != PORT_16750)
+		serial_port_out(port, UART_LCR, up->lcr);	/* reset DLAB */
+
+#ifdef CONFIG_ARCH_ROCKCHIP
+	serial_port_out(port, UART_MCR, up->mcr);
+#endif
+
+	/* XR17V35x UARTs have an extra fractional divisor register (DLD) */
+	if (up->port.type == PORT_XR17V35X) {
+		/* Preserve bits not related to baudrate; DLD[7:4]. */
+		quot_frac |= serial_port_in(port, 0x2) & 0xf0;
+		serial_port_out(port, 0x2, quot_frac);
+	}
 }
 EXPORT_SYMBOL_GPL(serial8250_do_set_divisor);
 
@@ -2770,12 +2792,6 @@ serial8250_do_set_termios(struct uart_po
 	unsigned long flags;
 	unsigned int baud, quot, frac = 0;
 
-	if (up->capabilities & UART_CAP_MINI) {
-		termios->c_cflag &= ~(CSTOPB | PARENB | PARODD | CMSPAR);
-		if ((termios->c_cflag & CSIZE) == CS5 ||
-		    (termios->c_cflag & CSIZE) == CS6)
-			termios->c_cflag = (termios->c_cflag & ~CSIZE) | CS7;
-	}
 	cval = serial8250_compute_lcr(up, termios->c_cflag);
 
 	baud = serial8250_get_baud_rate(port, termios, old);
@@ -2791,6 +2807,7 @@ serial8250_do_set_termios(struct uart_po
 	up->lcr = cval;					/* Save computed LCR */
 
 	if (up->capabilities & UART_CAP_FIFO && port->fifosize > 1) {
+		/* NOTE: If fifo_bug is not set, a user can set RX_trigger. */
 		if (baud < 2400 && !up->dma) {
 			up->fcr &= ~UART_FCR_TRIGGER_MASK;
 			up->fcr |= UART_FCR_TRIGGER_1;
@@ -2800,9 +2817,12 @@ serial8250_do_set_termios(struct uart_po
 	/*
 	 * MCR-based auto flow control.  When AFE is enabled, RTS will be
 	 * deasserted when the receive FIFO contains more characters than
-	 * the trigger, or the MCR RTS bit is cleared.
+	 * the trigger, or the MCR RTS bit is cleared.  In the case where
+	 * the remote UART is not using CTS auto flow control, we must
+	 * have sufficient FIFO entries for the latency of the remote
+	 * UART to respond.  IOW, at least 32 bytes of FIFO.
 	 */
-	if (up->capabilities & UART_CAP_AFE) {
+	if (up->capabilities & UART_CAP_AFE && port->fifosize >= 32) {
 		up->mcr &= ~UART_MCR_AFE;
 		if (termios->c_cflag & CRTSCTS)
 			up->mcr |= UART_MCR_AFE;
@@ -2820,7 +2840,7 @@ serial8250_do_set_termios(struct uart_po
 		port->read_status_mask |= UART_LSR_BI;
 
 	/*
-	 * Characters to ignore
+	 * Characteres to ignore
 	 */
 	port->ignore_status_mask = 0;
 	if (termios->c_iflag & IGNPAR)
@@ -2841,6 +2861,7 @@ serial8250_do_set_termios(struct uart_po
 	if ((termios->c_cflag & CREAD) == 0)
 		port->ignore_status_mask |= UART_LSR_DR;
 
+#ifndef CONFIG_ARCH_ROCKCHIP
 	/*
 	 * CTS flow control flag and modem status interrupts
 	 */
@@ -2854,6 +2875,7 @@ serial8250_do_set_termios(struct uart_po
 		up->ier |= UART_IER_RTOIE;
 
 	serial_port_out(port, UART_IER, up->ier);
+#endif
 
 	if (up->capabilities & UART_CAP_EFR) {
 		unsigned char efr = 0;
@@ -2872,16 +2894,25 @@ serial8250_do_set_termios(struct uart_po
 			serial_port_out(port, UART_EFR, efr);
 	}
 
+#ifdef CONFIG_ARCH_ROCKCHIP
+	/* Reset uart to make sure it is idle, then set buad rate */
+	serial_port_out(port, 0x88 >> 2, 0x7);
+#endif
+
 	serial8250_set_divisor(port, baud, quot, frac);
 
+#ifdef CONFIG_ARCH_ROCKCHIP
+	up->fcr = UART_FCR_ENABLE_FIFO | UART_FCR_T_TRIG_10 | UART_FCR_R_TRIG_10;
+#endif
 	/*
 	 * LCR DLAB must be set to enable 64-byte FIFO mode. If the FCR
 	 * is written without DLAB set, this mode will be disabled.
 	 */
-	if (port->type == PORT_16750)
+	if (port->type == PORT_16750) {
 		serial_port_out(port, UART_FCR, up->fcr);
+		serial_port_out(port, UART_LCR, up->lcr);	/* reset DLAB */
+	}
 
-	serial_port_out(port, UART_LCR, up->lcr);	/* reset DLAB */
 	if (port->type != PORT_16750) {
 		/* emulated UARTs (Lucent Venus 167x) need two steps */
 		if (up->fcr & UART_FCR_ENABLE_FIFO)
@@ -2889,6 +2920,23 @@ serial8250_do_set_termios(struct uart_po
 		serial_port_out(port, UART_FCR, up->fcr);	/* set fcr */
 	}
 	serial8250_set_mctrl(port, port->mctrl);
+
+#ifdef CONFIG_ARCH_ROCKCHIP
+	/*
+	 * CTS flow control flag and modem status interrupts
+	 */
+	up->ier &= ~UART_IER_MSI;
+	if (!(up->bugs & UART_BUG_NOMSR) &&
+			UART_ENABLE_MS(&up->port, termios->c_cflag))
+		up->ier |= UART_IER_MSI;
+	if (up->capabilities & UART_CAP_UUE)
+		up->ier |= UART_IER_UUE;
+	if (up->capabilities & UART_CAP_RTOIE)
+		up->ier |= UART_IER_RTOIE;
+
+	serial_port_out(port, UART_IER, up->ier);
+#endif
+
 	spin_unlock_irqrestore(&port->lock, flags);
 	serial8250_rpm_put(up);
2 Likes