Re: [PATCH v5 7/7] tty: i3c: add TTY over I3C master support

From: Jiri Slaby
Date: Tue Dec 05 2023 - 04:57:00 EST


On 30. 11. 23, 23:44, Frank Li wrote:
In typical embedded Linux systems, UART consoles require at least two pins,
TX and RX. In scenarios where I2C/I3C devices like sensors or PMICs are
present, we can save these two pins by using this driver. Pins is crucial
resources, especially in small chip packages.

This introduces support for using the I3C bus to transfer console tty data,
effectively replacing the need for dedicated UART pins. This not only
conserves valuable pin resources but also facilitates testing of I3C's
advanced features, including early termination, in-band interrupt (IBI)
support, and the creation of more complex data patterns. Additionally,
it aids in identifying and addressing issues within the I3C controller
driver.

Signed-off-by: Frank Li <Frank.Li@xxxxxxx>
...
--- a/drivers/tty/Kconfig
+++ b/drivers/tty/Kconfig
@@ -412,6 +412,19 @@ config RPMSG_TTY
To compile this driver as a module, choose M here: the module will be
called rpmsg_tty.
+config I3C_TTY
+ tristate "TTY over I3C"
+ depends on I3C
+ help
+ Select this options if you'd like use TTY over I3C master controller

this option
to use
add a period to the end.

+
+ This makes it possible for user-space programs to send and receive
+ data as a standard tty protocol. I3C provide relatively higher data
+ transfer rate and less pin numbers, SDA/SCL are shared with other
+ devices.
+
+ If unsure, say N
+
endif # TTY
source "drivers/tty/serdev/Kconfig"
...
--- /dev/null
+++ b/drivers/tty/i3c_tty.c
@@ -0,0 +1,443 @@
...
+struct ttyi3c_port {
+ struct tty_port port;
+ int minor;
+ spinlock_t xlock; /* protect xmit */
+ char tx_buff[I3C_TTY_TRANS_SIZE];
+ char rx_buff[I3C_TTY_TRANS_SIZE];

These should be u8 as per the other changes throughout the tty layer.

+ struct i3c_device *i3cdev;
+ struct work_struct txwork;
+ struct work_struct rxwork;
+ struct completion txcomplete;
+ unsigned long status;
+ int buf_overrun;

Can this be ever negative?

+};
+
+struct workqueue_struct *workqueue;

Is this related:
Still below items not be fixed (according to Jiri Slaby's comments)
- rxwork thread: need trigger from two position.
- common thread queue: need some suggestion
?

As I don't remember, could you elaborate again why you need your own workqueue? You need to do it in the commit log anyway.

...
+static ssize_t i3c_write(struct tty_struct *tty, const unsigned char *buf, size_t count)
+{
+ struct ttyi3c_port *sport = tty->driver_data;
+ unsigned long flags;
+ bool is_empty;
+ int ret;
+
+ spin_lock_irqsave(&sport->xlock, flags);
+ ret = kfifo_in(&sport->port.xmit_fifo, buf, count);
+ is_empty = kfifo_is_empty(&sport->port.xmit_fifo);
+ spin_unlock_irqrestore(&sport->xlock, flags);
+
+ if (!is_empty)
+ queue_work(workqueue, &sport->txwork);
+
+ return ret;
+}
+
+static int i3c_put_char(struct tty_struct *tty, unsigned char ch)
+{
+ struct ttyi3c_port *sport = tty->driver_data;
+ unsigned long flags;
+ int ret = 0;

Unneeded initialization.

+
+ spin_lock_irqsave(&sport->xlock, flags);
+ ret = kfifo_put(&sport->port.xmit_fifo, ch);
+ spin_unlock_irqrestore(&sport->xlock, flags);
+
+ return ret;
+}
...
+static void tty_i3c_rxwork(struct work_struct *work)
+{
+ struct ttyi3c_port *sport = container_of(work, struct ttyi3c_port, rxwork);
+ struct i3c_priv_xfer xfers;
+ int retry = I3C_TTY_RETRY;

Likely, should be unsigned.

+ u16 status = BIT(0);
+ int ret;
+
+ memset(&xfers, 0, sizeof(xfers));
+ xfers.data.in = sport->rx_buff;
+ xfers.len = I3C_TTY_TRANS_SIZE;
+ xfers.rnw = 1;
+
+ do {
+ if (test_bit(I3C_TTY_RX_STOP, &sport->status))
+ break;
+
+ i3c_device_do_priv_xfers(sport->i3cdev, &xfers, 1);
+
+ if (xfers.actual_len) {
+ ret = tty_insert_flip_string(&sport->port, sport->rx_buff,
+ xfers.actual_len);
+ if (ret < xfers.actual_len)
+ sport->buf_overrun++;
+
+ retry = I3C_TTY_RETRY;
+ continue;
+ }
+
+ status = BIT(0);
+ i3c_device_getstatus_format1(sport->i3cdev, &status);
+ /*
+ * Target side needs some time to fill data into fifo. Target side may not
+ * have hardware update status in real time. Software update status always
+ * needs some delays.
+ *
+ * Generally, target side have circular buffer in memory, it will be moved
+ * into FIFO by CPU or DMA. 'status' just show if circular buffer empty. But
+ * there are gap, especially CPU have not response irq to fill FIFO in time.
+ * So xfers.actual will be zero, wait for little time to avoid flood
+ * transfer in i3c bus.
+ */
+ usleep_range(I3C_TTY_YIELD_US, 10 * I3C_TTY_YIELD_US);
+ retry--;
+
+ } while (retry && (status & BIT(0)));
+
+ tty_flip_buffer_push(&sport->port);
+}
+
+static void tty_i3c_txwork(struct work_struct *work)
+{
+ struct ttyi3c_port *sport = container_of(work, struct ttyi3c_port, txwork);
+ struct i3c_priv_xfer xfers;
+ int retry = I3C_TTY_RETRY;

Detto.

+ unsigned long flags;
+ int ret;
+
+ xfers.rnw = 0;
+ xfers.data.out = sport->tx_buff;
+
+ while (!kfifo_is_empty(&sport->port.xmit_fifo) && retry) {
+ xfers.len = kfifo_len(&sport->port.xmit_fifo);
+ xfers.len = min_t(u16, I3C_TTY_TRANS_SIZE, xfers.len);
+
+ xfers.len = kfifo_out_peek(&sport->port.xmit_fifo, sport->tx_buff, xfers.len);

Can this simply be:
xfers.len = kfifo_out_peek(&sport->port.xmit_fifo, sport->tx_buff, I3C_TTY_TRANS_SIZE);
?

+
+ ret = i3c_device_do_priv_xfers(sport->i3cdev, &xfers, 1);
+ if (ret) {
+ /*
+ * Target side may not move data out of FIFO. delay can't resolve problem,
+ * just reduce some possiblity. Target can't end I3C SDR mode write
+ * transfer, discard data is reasonable when FIFO overrun.
+ */
+ usleep_range(I3C_TTY_YIELD_US, 10 * I3C_TTY_YIELD_US);
+ retry--;
+ } else {
+ retry = I3C_TTY_RETRY;
+ ret = kfifo_out(&sport->port.xmit_fifo, sport->tx_buff, xfers.len);

Just to make sure: xfers.len is nor overwritten by i3c_device_do_priv_xfers(), right?

+ }
+ }
+
+ spin_lock_irqsave(&sport->xlock, flags);

Why do you take the lock here, but not during the kfifo operations above?

+ if (kfifo_is_empty(&sport->port.xmit_fifo))
+ complete(&sport->txcomplete);
+ spin_unlock_irqrestore(&sport->xlock, flags);
+}

+static int __init i3c_tty_init(void)
+{
+ int ret;
+
+ i3c_tty_driver = tty_alloc_driver(I3C_TTY_MINORS,
+ TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV);
+
+ if (IS_ERR(i3c_tty_driver))
+ return PTR_ERR(i3c_tty_driver);
+
+ i3c_tty_driver->driver_name = "ttyI3C";
+ i3c_tty_driver->name = "ttyI3C";
+ i3c_tty_driver->minor_start = 0,
+ i3c_tty_driver->type = TTY_DRIVER_TYPE_SERIAL,
+ i3c_tty_driver->subtype = SERIAL_TYPE_NORMAL,
+ i3c_tty_driver->init_termios = tty_std_termios;
+ i3c_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL |
+ CLOCAL;
+ i3c_tty_driver->init_termios.c_lflag = 0;
+
+ tty_set_operations(i3c_tty_driver, &i3c_tty_ops);
+
+ ret = tty_register_driver(i3c_tty_driver);
+ if (ret)
+ goto err_tty_register_driver;
+
+ ret = i3c_driver_register(&i3c_driver);
+ if (ret)
+ goto err_i3c_driver_register;
+
+ workqueue = alloc_workqueue("ttyI3C", 0, 0);

Can it happen that you already queue something on this wq, while not allocated yet? I mean: should this be done first in i3c_tty_init()?

+ if (!workqueue) {
+ ret = PTR_ERR(workqueue);
+ goto err_alloc_workqueue;
+ }
+
+ return 0;
+
+err_alloc_workqueue:
+ i3c_driver_unregister(&i3c_driver);
+
+err_i3c_driver_register:
+ tty_unregister_driver(i3c_tty_driver);
+
+err_tty_register_driver:
+ tty_driver_kref_put(i3c_tty_driver);
+
+ return ret;
+}
+
+static void __exit i3c_tty_exit(void)
+{
+ i3c_driver_unregister(&i3c_driver);
+ tty_unregister_driver(i3c_tty_driver);
+ tty_driver_kref_put(i3c_tty_driver);
+ idr_destroy(&i3c_tty_minors);

What about the wq?

+}

regards,
--
js
suse labs