RZ-G/RZ-G2 RS-485 Half-Duplex Communication

From Renesas.info
  • RS-485 half-duplex communication is widely used. But lots of MPUs can not support RS-485 directly, they support UART full-duplex communication commonly. Renesas RZ/G2 series MPU integrates SCIF(i.e UART) which also can not support RS-485 directly. But we can implement RS-485 half-duplex communication by SCIF with a UART-to-RS485 converter IC. Typical block diagram is as follows.
uart-to-rs485-1.png


Because the RS-485 converter is working at transmitter mode(controlled by DE pin) or receiver mode(controlled by /RE pin). RZ/G2 series MPU's SCIF can not support this kind of mode switching by hardware, so we need to use a GPIO to realize it. Here, the RTS signal of UART of RZ/G2 MPU is a part of SCIF peripheral signals, but it is used for modem feature. So we just configure it as a GPIO. The DE and /RE signals of the converter are always opposite. Users can select one other GPIO to replace RTS(GPIO) or select two other GPIOs to control DE and /RE separately.

Following description will show the RS-485 feature support on Renesas RZ/G2L MPU. Simply speaking, we just need to add GPIO control code in the kernel driver code. That is driving the GPIO to HIGH at the beginning of data transmission and driving the GPIO to LOW at the end of data transmission. In data receiving mode, the code is not needed to be changed. The device tree, tty driver and SCIF driver inside kernel will be modified. The tty driver is shared by a lot of applications(ssh, telnet, serial, etc), so we must carefully check and add necessary judgment. The user space application code does not need to care about the GPIO control. It just needs to read and write the UART device simply as if the RS-485 half-duplex is supported by hardware.

The commonly concerned thing is the point of driving GPIO to LOW. Or the interval between the last STOP bit of MPU sending data and the RS-485 device’s first START bit. The device side may send back some data to MPU quickly after it received some data, and this interval may be less than 10us. If the GPIO was not driven to LOW in time some data from the device will be lost. In fact, this interval is RS-485 device or protocol specific. This time between the last STOP bit of sending data and driving GPIO to LOW on RZ/G2L SMARC board is around 10 ~ 20us. But it is only for reference, this interval may be affected by lots of things(CPU load, real-time tasks, interrupts latency, etc).

Another thing needs to be mentioned is that the read out data size by application’s single read(). The default RZ/G2 SCIF driver implementation supports reading out 8 bytes at most for a single read() in application. It is not an issue or bug. But some users want to read out data as many as possible by a single read(). The following code can support this kind of usage. Please refer to the patch of sci_transmit_chars() function. In fact, there is still a limitation. And the limitation depends on the kernel tty driver’s circular buffer size which is often very big(over 1024).


Preconditions:

  1. Linux kernel is 5.10 cip13-rt5, VLP3.0.1.
  2. RZ/G2L SMARC board is used.
  3. SCIF2 is used.
  4. SCIF DMA must be disabled(using PIO mode).
  5. RTS pin of SCIF2 is used as the GPIO.


arch/arm64/boot/dts/renesas/rzg2l-smarc-pinfunction.dtsi

diff --git a/arch/arm64/boot/dts/renesas/rzg2l-smarc-pinfunction.dtsi b/arch/arm64/boot/dts/renesas/rzg2l-smarc-pinfunction.dtsi
index a3ca3709db80..6cd6ae776dd6 100644
--- a/arch/arm64/boot/dts/renesas/rzg2l-smarc-pinfunction.dtsi
+++ b/arch/arm64/boot/dts/renesas/rzg2l-smarc-pinfunction.dtsi
@@ -62,8 +62,8 @@ scif0_pins: scif0 {
 	scif2_pins: scif2 {
 		pinmux = <RZG2L_PORT_PINMUX(48, 0, 1)>, /* TxD */
 			 <RZG2L_PORT_PINMUX(48, 1, 1)>, /* RxD */
-			 <RZG2L_PORT_PINMUX(48, 3, 1)>, /* CTS# */
-			 <RZG2L_PORT_PINMUX(48, 4, 1)>; /* RTS# */
+			 <RZG2L_PORT_PINMUX(48, 3, 1)>; /* CTS# */
+			 /*<RZG2L_PORT_PINMUX(48, 4, 1)>;*/ /* RTS# */
 	};
 #endif


arch/arm64/boot/dts/renesas/rzg2l-smarc.dtsi

diff --git a/arch/arm64/boot/dts/renesas/rzg2l-smarc.dtsi b/arch/arm64/boot/dts/renesas/rzg2l-smarc.dtsi
index 4f1828caf097..056b0db3cc42 100644
--- a/arch/arm64/boot/dts/renesas/rzg2l-smarc.dtsi
+++ b/arch/arm64/boot/dts/renesas/rzg2l-smarc.dtsi
@@ -9,7 +9,7 @@
 #include <dt-bindings/pinctrl/rzg2l-pinctrl.h>
 
 /* comment the #define statement to disable SCIF2 (SER0) on PMOD1 (CN7) */
-//#define PMOD1_SER0	1
+#define PMOD1_SER0	1
 
 / {
 	aliases {
@@ -36,7 +36,9 @@ &scif2 {
 	pinctrl-0 = <&scif2_pins>;
 	pinctrl-names = "default";
 
-	uart-has-rtscts;
+	rs485-sw-rts;
+	gpios = <&pinctrl RZG2L_GPIO(48, 4) GPIO_ACTIVE_LOW>;
+	/*uart-has-rtscts;*/
 	status = "okay";
 };
 #endif


drivers/tty/serial/serial_core.c

diff --git a/drivers/tty/serial/serial_core.c b/drivers/tty/serial/serial_core.c
index 32d09d024f6c..7ee95d22ce3f 100644
--- a/drivers/tty/serial/serial_core.c
+++ b/drivers/tty/serial/serial_core.c
@@ -1434,6 +1434,17 @@ uart_ioctl(struct tty_struct *tty, unsigned int cmd, unsigned long arg)
 	if (ret != -ENOIOCTLCMD)
 		goto out;
 
+	switch(cmd)
+	{
+		case TIOCSSWRTS:
+			uport = state->uart_port;
+			ret = uport->ops->swrts_ctl(uport, arg);
+			goto out;
+
+		default:
+			break;
+	}
+
 	mutex_lock(&port->mutex);
 	uport = uart_port_check(state);
 
@@ -2556,6 +2567,12 @@ static const struct tty_port_operations uart_port_ops = {
 	.shutdown	= uart_tty_port_shutdown,
 };
 
+int is_serial_core_tty(struct tty_struct *tty)
+{
+	return ((tty->ops == &uart_ops) && tty->ops->ioctl(tty, TIOCSSWRTS, SWRTS_GET_I));
+}
+EXPORT_SYMBOL_GPL(is_serial_core_tty);
+
 /**
  *	uart_register_driver - register a driver with the uart core layer
  *	@drv: low level driver structure


drivers/tty/serial/sh-sci.c

diff --git a/drivers/tty/serial/sh-sci.c b/drivers/tty/serial/sh-sci.c
index 626b5507d1ed..b416574bad3e 100644
--- a/drivers/tty/serial/sh-sci.c
+++ b/drivers/tty/serial/sh-sci.c
@@ -48,6 +48,9 @@
 #include <linux/timer.h>
 #include <linux/tty.h>
 #include <linux/tty_flip.h>
+#include <linux/gpio.h>
+#include <linux/of_gpio.h>
+#include <linux/semaphore.h>
 
 #ifdef CONFIG_SUPERH
 #include <asm/sh_bios.h>
@@ -157,6 +160,11 @@ struct sci_port {
 
 	bool has_rtscts;
 	bool autorts;
+
+	int rs485_sw_rts;
+	int sw_rts_gpio;
+	int sw_rts_count;
+	struct semaphore sw_rts_sema;
 };
 
 #define SCI_NPORTS CONFIG_SERIAL_SH_SCI_NR_UARTS
@@ -609,7 +617,7 @@ static void sci_stop_tx(struct uart_port *port)
 	if (port->type == PORT_SCIFA || port->type == PORT_SCIFB)
 		ctrl &= ~SCSCR_TDRQE;
 
-	ctrl &= ~SCSCR_TIE;
+	ctrl &= ~(SCSCR_TIE | SCSCR_TOIE);
 
 	serial_port_out(port, SCSCR, ctrl);
 
@@ -805,6 +813,7 @@ static int sci_rxfill(struct uart_port *port)
 static void sci_transmit_chars(struct uart_port *port)
 {
 	struct circ_buf *xmit = &port->state->xmit;
+	struct sci_port *sport = to_sci_port(port);
 	unsigned int stopped = uart_tx_stopped(port);
 	unsigned short status;
 	unsigned short ctrl;
@@ -839,6 +848,9 @@ static void sci_transmit_chars(struct uart_port *port)
 		serial_port_out(port, SCxTDR, c);
 
 		port->icount.tx++;
+
+		if(sport->rs485_sw_rts)
+			sport->sw_rts_count--;
 	} while (--count > 0);
 
 	sci_clear_SCxSR(port, SCxSR_TDxE_CLEAR(port));
@@ -846,7 +858,7 @@ static void sci_transmit_chars(struct uart_port *port)
 	if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)
 		uart_write_wakeup(port);
 	if (uart_circ_empty(xmit))
-		sci_stop_tx(port);
+		serial_port_out(port, SCSCR, (serial_port_in(port, SCSCR) & ~SCSCR_TIE) | SCSCR_TOIE);
 
 }
 
@@ -856,71 +868,98 @@ static void sci_transmit_chars(struct uart_port *port)
 static void sci_receive_chars(struct uart_port *port)
 {
 	struct tty_port *tport = &port->state->port;
-	int i, count, copied = 0;
+	struct sci_port *sport = to_sci_port(port);
+	int i, count, copied = 0, rx_to;
 	unsigned short status;
 	unsigned char flag;
 
+	if(1 == sport->rx_trigger)
+		dev_warn(port->dev, "rx trigger '1' is too low, no DRI interrupt\n");
+
 	status = serial_port_in(port, SCxSR);
-	if (!(status & SCxSR_RDxF(port)))
+	if (!(status & (SCxSR_TEND(port) | SCxSR_RDxF(port))))
 		return;
 
-	while (1) {
-		/* Don't copy more bytes than there is room for in the buffer */
-		count = tty_buffer_request_room(tport, sci_rxfill(port));
+	/*
+		TEI & DRI share interrupt vector
+		Priority: TEI > DRI
+		Return after TEI handling, let CPU handles next interrupt(maybe DRI).
+	*/
+	if(status & SCxSR_TEND(port)) {
+		if(sport->rs485_sw_rts && (sport->sw_rts_count <= 0)) {
+			gpio_set_value(sport->sw_rts_gpio, 0);
+			up(&sport->sw_rts_sema);
+		}
 
-		/* If for any reason we can't copy more data, we're done! */
-		if (count == 0)
-			break;
+		sci_clear_SCxSR(port, SCxSR_TEND_CLEAR(port));
+		sci_stop_tx(port);
 
-		if (port->type == PORT_SCI) {
-			char c = serial_port_in(port, SCxRDR);
-			if (uart_handle_sysrq_char(port, c))
-				count = 0;
-			else
-				tty_insert_flip_char(tport, c, TTY_NORMAL);
-		} else {
-			for (i = 0; i < count; i++) {
-				char c;
-
-				if (port->type == PORT_SCIF ||
-				    port->type == PORT_HSCIF) {
-					status = serial_port_in(port, SCxSR);
-					c = serial_port_in(port, SCxRDR);
-				} else {
-					c = serial_port_in(port, SCxRDR);
-					status = serial_port_in(port, SCxSR);
-				}
-				if (uart_handle_sysrq_char(port, c)) {
-					count--; i--;
-					continue;
-				}
-
-				/* Store data and status */
-				if (status & SCxSR_FER(port)) {
-					flag = TTY_FRAME;
-					port->icount.frame++;
-					dev_notice(port->dev, "frame error\n");
-				} else if (status & SCxSR_PER(port)) {
-					flag = TTY_PARITY;
-					port->icount.parity++;
-					dev_notice(port->dev, "parity error\n");
-				} else
-					flag = TTY_NORMAL;
-
-				tty_insert_flip_char(tport, c, flag);
+		return;
+	}
+
+	rx_to = SCIF_DR & status;
+
+	/* Don't copy more bytes than there is room for in the buffer */
+	count = sci_rxfill(port);
+	if(count >= sport->rx_trigger)
+		count--;
+
+	count = tty_buffer_request_room(tport, count);
+
+	/* If for any reason we can't copy more data, we're done! */
+	if (count == 0)
+		goto next;
+
+	if (port->type == PORT_SCI) {
+		char c = serial_port_in(port, SCxRDR);
+		if (uart_handle_sysrq_char(port, c))
+			count = 0;
+		else
+			tty_insert_flip_char(tport, c, TTY_NORMAL);
+	} else {
+		for (i = 0; i < count; i++) {
+			char c;
+
+			if (port->type == PORT_SCIF ||
+			    port->type == PORT_HSCIF) {
+				status = serial_port_in(port, SCxSR);
+				c = serial_port_in(port, SCxRDR);
+			} else {
+				c = serial_port_in(port, SCxRDR);
+				status = serial_port_in(port, SCxSR);
+			}
+			if (uart_handle_sysrq_char(port, c)) {
+				count--; i--;
+				continue;
 			}
+
+			/* Store data and status */
+			if (status & SCxSR_FER(port)) {
+				flag = TTY_FRAME;
+				port->icount.frame++;
+				dev_notice(port->dev, "frame error\n");
+			} else if (status & SCxSR_PER(port)) {
+				flag = TTY_PARITY;
+				port->icount.parity++;
+				dev_notice(port->dev, "parity error\n");
+			} else
+				flag = TTY_NORMAL;
+
+			tty_insert_flip_char(tport, c, flag);
 		}
+	}
 
-		serial_port_in(port, SCxSR); /* dummy read */
-		sci_clear_SCxSR(port, SCxSR_RDxF_CLEAR(port));
+	serial_port_in(port, SCxSR); /* dummy read */
+	sci_clear_SCxSR(port, SCxSR_RDxF_CLEAR(port));
 
-		copied += count;
-		port->icount.rx += count;
-	}
+	copied += count;
+	port->icount.rx += count;
 
+next:
 	if (copied) {
 		/* Tell the rest of the system the news. New characters! */
-		tty_flip_buffer_push(tport);
+		if(rx_to)
+			tty_flip_buffer_push(tport);
 	} else {
 		/* TTY buffers full; read from RX reg to prevent lockup */
 		serial_port_in(port, SCxRDR);
@@ -2775,6 +2814,51 @@ static int sci_verify_port(struct uart_port *port, struct serial_struct *ser)
 	return 0;
 }
 
+static int sci_swrts_ctl(struct uart_port *port, unsigned long arg)
+{
+	//struct circ_buf *xmit = &port->state->xmit;
+	struct sci_port *sport = to_sci_port(port);
+	int cmd, ret = 0;
+
+	if(!sport->rs485_sw_rts)
+		return 0;
+
+	cmd = arg & 0xff;
+
+	switch(cmd)
+	{
+		case SWRTS_GET_I: /* check rs485-sw-rts device */
+
+			return 1;
+
+		case SWRTS_SET_H:
+
+			gpio_set_value(sport->sw_rts_gpio, 1);
+
+			sport->sw_rts_count = (int)(arg >> 8);
+
+			break;
+
+		case SWRTS_SET_L:
+
+			/* Reset RTS GPIO to Low in SCIF TX Empty interrupt, not here */
+
+			if(down_interruptible(&sport->sw_rts_sema)) {
+				ret = -ERESTART;
+				break;
+			}
+
+			break;
+
+		default:
+
+			dev_err(port->dev, "unsupported cmd 0x%lx for %s\n", arg, __func__);
+			return -EPERM;
+	}
+
+	return ret;
+}
+
 static const struct uart_ops sci_uart_ops = {
 	.tx_empty	= sci_tx_empty,
 	.set_mctrl	= sci_set_mctrl,
@@ -2798,6 +2882,7 @@ static const struct uart_ops sci_uart_ops = {
 	.poll_get_char	= sci_poll_get_char,
 	.poll_put_char	= sci_poll_put_char,
 #endif
+	.swrts_ctl	= sci_swrts_ctl
 };
 
 static int sci_init_clocks(struct sci_port *sci_port, struct device *dev)
@@ -3185,6 +3270,12 @@ static int sci_remove(struct platform_device *dev)
 	if (type == PORT_SCIFA || type == PORT_SCIFB || type == PORT_HSCIF)
 		device_remove_file(&dev->dev, &dev_attr_rx_fifo_timeout);
 
+	if(dev->dev.of_node)
+	{
+		if(port->rs485_sw_rts)
+			devm_gpio_free(&dev->dev, port->sw_rts_gpio);
+	}
+
 	return 0;
 }
 
@@ -3307,6 +3398,29 @@ static struct plat_sci_port *sci_parse_dt(struct platform_device *pdev,
 
 	sp->has_rtscts = of_property_read_bool(np, "uart-has-rtscts");
 
+	sp->rs485_sw_rts = of_property_read_bool(np, "rs485-sw-rts");
+
+	if(sp->rs485_sw_rts)
+	{
+		sp->sw_rts_gpio = of_get_gpio(np, 0);
+
+		if(!gpio_is_valid(sp->sw_rts_gpio))
+		{
+			dev_err(&pdev->dev, "'sw_rts_gpio' invalid in device tree node\n");
+			return ERR_PTR(-EINVAL);
+		}
+
+		if(devm_gpio_request_one(&pdev->dev, sp->sw_rts_gpio, GPIOF_DIR_OUT, pdev->name) < 0)
+		{
+			dev_err(&pdev->dev, "Unable to request GPIO for sw rs485\n");
+			return ERR_PTR(-EINVAL);
+		}
+
+		gpio_direction_output(sp->sw_rts_gpio, 0); // output LOW(RS-485 INPUT Mode) first
+
+		sema_init(&sp->sw_rts_sema, 0);
+	}
+
 	return p;
 }


drivers/tty/serial/sh-sch.h

diff --git a/drivers/tty/serial/sh-sci.h b/drivers/tty/serial/sh-sci.h
index c0ae78632dda..440b363d9362 100644
--- a/drivers/tty/serial/sh-sci.h
+++ b/drivers/tty/serial/sh-sci.h
@@ -80,6 +80,7 @@ enum {
 #define SCI_RDxF_CLEAR	(u32)(~(SCI_RESERVED | SCI_RDRF))
 #define SCI_ERROR_CLEAR	(u32)(~(SCI_RESERVED | SCI_PER | SCI_FER | SCI_ORER))
 #define SCI_TDxE_CLEAR	(u32)(~(SCI_RESERVED | SCI_TEND | SCI_TDRE))
+#define SCI_TEND_CLEAR	(u32)(~(SCI_RESERVED | SCI_TEND))
 #define SCI_BREAK_CLEAR	(u32)(~(SCI_RESERVED | SCI_PER | SCI_FER | SCI_ORER))
 
 /* SCxSR (Serial Status Register) on SCIF, SCIFA, SCIFB, HSCIF */
@@ -102,6 +103,7 @@ enum {
 #define SCIF_RDxF_CLEAR		(u32)(~(SCIF_DR | SCIF_RDF))
 #define SCIF_ERROR_CLEAR	(u32)(~(SCIF_PER | SCIF_FER | SCIF_ER))
 #define SCIF_TDxE_CLEAR		(u32)(~(SCIF_TDFE))
+#define SCIF_TEND_CLEAR		(u32)(~(SCIF_TEND))
 #define SCIF_BREAK_CLEAR	(u32)(~(SCIF_PER | SCIF_FER | SCIF_BRK))
 
 /* SCFCR (FIFO Control Register) */
@@ -173,5 +175,7 @@ enum {
 	(to_sci_port(port)->params->error_clear)
 #define SCxSR_TDxE_CLEAR(port) \
 	(((port)->type == PORT_SCI) ? SCI_TDxE_CLEAR : SCIF_TDxE_CLEAR)
+#define SCxSR_TEND_CLEAR(port) \
+		(((port)->type == PORT_SCI) ? SCI_TEND_CLEAR : SCIF_TEND_CLEAR)
 #define SCxSR_BREAK_CLEAR(port) \
 	(((port)->type == PORT_SCI) ? SCI_BREAK_CLEAR : SCIF_BREAK_CLEAR)


drivers/tty/tty_io.c

diff --git a/drivers/tty/tty_io.c b/drivers/tty/tty_io.c
index 669aef77a0bd..21f30a864cc1 100644
--- a/drivers/tty/tty_io.c
+++ b/drivers/tty/tty_io.c
@@ -514,6 +514,7 @@ static DEFINE_SPINLOCK(redirect_lock);
 static struct file *redirect;
 
 extern void tty_sysctl_init(void);
+extern int is_serial_core_tty(struct tty_struct *tty);
 
 /**
  *	tty_wakeup	-	request more data
@@ -968,9 +969,12 @@ static inline ssize_t do_tty_write(
 	struct file *file,
 	struct iov_iter *from)
 {
-	size_t count = iov_iter_count(from);
+	size_t count = iov_iter_count(from), par;
 	ssize_t ret, written = 0;
 	unsigned int chunk;
+	int serial_core_tty;
+
+	serial_core_tty = is_serial_core_tty(tty);
 
 	ret = tty_write_lock(tty, file->f_flags & O_NDELAY);
 	if (ret < 0)
@@ -1015,6 +1019,14 @@ static inline ssize_t do_tty_write(
 		tty->write_buf = buf_chunk;
 	}
 
+	if(serial_core_tty) {
+		par = count << 8;
+		if(tty->ops->ioctl(tty, TIOCSSWRTS, SWRTS_SET_H | par)) {
+			ret = -ERESTART;
+			goto out;
+		}
+	}
+
 	/* Do the write .. */
 	for (;;) {
 		size_t size = count;
@@ -1045,6 +1057,14 @@ static inline ssize_t do_tty_write(
 			break;
 		cond_resched();
 	}
+
+	if(serial_core_tty) {
+		if(tty->ops->ioctl(tty, TIOCSSWRTS, SWRTS_SET_L)) {
+			ret = -ERESTART;
+			goto out;
+		}
+	}
+
 	if (written) {
 		tty_update_time(&file_inode(file)->i_mtime);
 		ret = written;


include/linux/serial_core.h

diff --git a/include/linux/serial_core.h b/include/linux/serial_core.h
index 35b26743dbb2..783608a94437 100644
--- a/include/linux/serial_core.h
+++ b/include/linux/serial_core.h
@@ -80,6 +80,7 @@ struct uart_ops {
 	void		(*poll_put_char)(struct uart_port *, unsigned char);
 	int		(*poll_get_char)(struct uart_port *);
 #endif
+	int		(*swrts_ctl)(struct uart_port *, unsigned long);
 };
 
 #define NO_POLL_CHAR		0x00ff0000


include/uapi/asm-generic/ioctls.h

diff --git a/include/uapi/asm-generic/ioctls.h b/include/uapi/asm-generic/ioctls.h
index cdc9f4ca8c27..6dde21d68cff 100644
--- a/include/uapi/asm-generic/ioctls.h
+++ b/include/uapi/asm-generic/ioctls.h
@@ -106,6 +106,18 @@
 # define FIOQSIZE	0x5460
 #endif
 
+/*
+ * Add ioctl commands for software RS-485 RTS control
+ */
+#define TIOCSSWRTS	0x5500
+enum
+{
+	SWRTS_GET_I = 0, /* Get software RS-485 RTS device support info */
+	SWRTS_SET_H, /* Set RTS GPIO to HIGH */
+	SWRTS_SET_L /* Set RTS GPIO to LOW */
+};
+
+
 /* Used for packet mode */
 #define TIOCPKT_DATA		 0
 #define TIOCPKT_FLUSHREAD	 1