#include <linux/atomic.h>
 #include <linux/firmware.h>
+#include <linux/irq.h>
 #include <linux/kref.h>
 #include <linux/miscdevice.h>
 #include <linux/mutex.h>
 #include <linux/pci.h>
 #include <linux/poll.h>
 #include <linux/sched/signal.h>
+#include <linux/tty.h>
 #include <linux/uaccess.h>
 #include <uapi/linux/misc/bcm_vk.h>
 
 #define CODEPUSH_BOOT2_ENTRY           0x60000000
 
 #define BAR_CARD_STATUS                        0x410
+/* CARD_STATUS definitions */
+#define CARD_STATUS_TTYVK0_READY       BIT(0)
+#define CARD_STATUS_TTYVK1_READY       BIT(1)
 
 #define BAR_BOOT1_STDALONE_PROGRESS    0x420
 #define BOOT1_STDALONE_SUCCESS         (BIT(13) | BIT(14))
 
 #define BCM_VK_NUM_TTY 2
 
+struct bcm_vk_tty {
+       struct tty_port port;
+       u32 to_offset;  /* bar offset to use */
+       u32 to_size;    /* to VK buffer size */
+       u32 wr;         /* write offset shadow */
+       u32 from_offset;        /* bar offset to use */
+       u32 from_size;  /* from VK buffer size */
+       u32 rd;         /* read offset shadow */
+       pid_t pid;
+       bool irq_enabled;
+       bool is_opened;         /* tracks tty open/close */
+};
+
 /* VK device max power state, supports 3, full, reduced and low */
 #define MAX_OPP 3
 #define MAX_CARD_INFO_TAG_SIZE 64
        struct miscdevice miscdev;
        int devid; /* dev id allocated */
 
+       struct tty_driver *tty_drv;
+       struct timer_list serial_timer;
+       struct bcm_vk_tty tty[BCM_VK_NUM_TTY];
+       struct workqueue_struct *tty_wq_thread;
+       struct work_struct tty_wq_work;
+
        /* Reference-counting to handle file operations */
        struct kref kref;
 
 void bcm_vk_release_data(struct kref *kref);
 irqreturn_t bcm_vk_msgq_irqhandler(int irq, void *dev_id);
 irqreturn_t bcm_vk_notf_irqhandler(int irq, void *dev_id);
+irqreturn_t bcm_vk_tty_irqhandler(int irq, void *dev_id);
 int bcm_vk_msg_init(struct bcm_vk *vk);
 void bcm_vk_msg_remove(struct bcm_vk *vk);
 void bcm_vk_drain_msg_on_reset(struct bcm_vk *vk);
                             const pid_t pid, const u32 q_num);
 void bcm_to_v_q_doorbell(struct bcm_vk *vk, u32 q_num, u32 db_val);
 int bcm_vk_auto_load_all_images(struct bcm_vk *vk);
+int bcm_vk_tty_init(struct bcm_vk *vk, char *name);
+void bcm_vk_tty_exit(struct bcm_vk *vk);
+void bcm_vk_tty_terminate_tty_user(struct bcm_vk *vk);
 void bcm_vk_hb_init(struct bcm_vk *vk);
 void bcm_vk_hb_deinit(struct bcm_vk *vk);
 void bcm_vk_handle_notf(struct bcm_vk *vk);
 
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2018-2020 Broadcom.
+ */
+
+#include <linux/tty.h>
+#include <linux/tty_driver.h>
+#include <linux/tty_flip.h>
+
+#include "bcm_vk.h"
+
+/* TTYVK base offset is 0x30000 into BAR1 */
+#define BAR1_TTYVK_BASE_OFFSET 0x300000
+/* Each TTYVK channel (TO or FROM) is 0x10000 */
+#define BAR1_TTYVK_CHAN_OFFSET 0x100000
+/* Each TTYVK channel has TO and FROM, hence the * 2 */
+#define BAR1_TTYVK_BASE(index) (BAR1_TTYVK_BASE_OFFSET + \
+                                ((index) * BAR1_TTYVK_CHAN_OFFSET * 2))
+/* TO TTYVK channel base comes before FROM for each index */
+#define TO_TTYK_BASE(index)    BAR1_TTYVK_BASE(index)
+#define FROM_TTYK_BASE(index)  (BAR1_TTYVK_BASE(index) + \
+                                BAR1_TTYVK_CHAN_OFFSET)
+
+struct bcm_vk_tty_chan {
+       u32 reserved;
+       u32 size;
+       u32 wr;
+       u32 rd;
+       u32 *data;
+};
+
+#define VK_BAR_CHAN(v, DIR, e) ((v)->DIR##_offset \
+                                + offsetof(struct bcm_vk_tty_chan, e))
+#define VK_BAR_CHAN_SIZE(v, DIR)       VK_BAR_CHAN(v, DIR, size)
+#define VK_BAR_CHAN_WR(v, DIR)         VK_BAR_CHAN(v, DIR, wr)
+#define VK_BAR_CHAN_RD(v, DIR)         VK_BAR_CHAN(v, DIR, rd)
+#define VK_BAR_CHAN_DATA(v, DIR, off)  (VK_BAR_CHAN(v, DIR, data) + (off))
+
+#define VK_BAR0_REGSEG_TTY_DB_OFFSET   0x86c
+
+/* Poll every 1/10 of second - temp hack till we use MSI interrupt */
+#define SERIAL_TIMER_VALUE (HZ / 10)
+
+static void bcm_vk_tty_poll(struct timer_list *t)
+{
+       struct bcm_vk *vk = from_timer(vk, t, serial_timer);
+
+       queue_work(vk->tty_wq_thread, &vk->tty_wq_work);
+       mod_timer(&vk->serial_timer, jiffies + SERIAL_TIMER_VALUE);
+}
+
+irqreturn_t bcm_vk_tty_irqhandler(int irq, void *dev_id)
+{
+       struct bcm_vk *vk = dev_id;
+
+       queue_work(vk->tty_wq_thread, &vk->tty_wq_work);
+
+       return IRQ_HANDLED;
+}
+
+static void bcm_vk_tty_wq_handler(struct work_struct *work)
+{
+       struct bcm_vk *vk = container_of(work, struct bcm_vk, tty_wq_work);
+       struct bcm_vk_tty *vktty;
+       int card_status;
+       int count;
+       unsigned char c;
+       int i;
+       int wr;
+
+       card_status = vkread32(vk, BAR_0, BAR_CARD_STATUS);
+       if (BCM_VK_INTF_IS_DOWN(card_status))
+               return;
+
+       for (i = 0; i < BCM_VK_NUM_TTY; i++) {
+               count = 0;
+               /* Check the card status that the tty channel is ready */
+               if ((card_status & BIT(i)) == 0)
+                       continue;
+
+               vktty = &vk->tty[i];
+
+               /* Don't increment read index if tty app is closed */
+               if (!vktty->is_opened)
+                       continue;
+
+               /* Fetch the wr offset in buffer from VK */
+               wr = vkread32(vk, BAR_1, VK_BAR_CHAN_WR(vktty, from));
+
+               /* safe to ignore until bar read gives proper size */
+               if (vktty->from_size == 0)
+                       continue;
+
+               if (wr >= vktty->from_size) {
+                       dev_err(&vk->pdev->dev,
+                               "ERROR: wq handler ttyVK%d wr:0x%x > 0x%x\n",
+                               i, wr, vktty->from_size);
+                       /* Need to signal and close device in this case */
+                       continue;
+               }
+
+               /*
+                * Simple read of circular buffer and
+                * insert into tty flip buffer
+                */
+               while (vk->tty[i].rd != wr) {
+                       c = vkread8(vk, BAR_1,
+                                   VK_BAR_CHAN_DATA(vktty, from, vktty->rd));
+                       vktty->rd++;
+                       if (vktty->rd >= vktty->from_size)
+                               vktty->rd = 0;
+                       tty_insert_flip_char(&vktty->port, c, TTY_NORMAL);
+                       count++;
+               }
+
+               if (count) {
+                       tty_flip_buffer_push(&vktty->port);
+
+                       /* Update read offset from shadow register to card */
+                       vkwrite32(vk, vktty->rd, BAR_1,
+                                 VK_BAR_CHAN_RD(vktty, from));
+               }
+       }
+}
+
+static int bcm_vk_tty_open(struct tty_struct *tty, struct file *file)
+{
+       int card_status;
+       struct bcm_vk *vk;
+       struct bcm_vk_tty *vktty;
+       int index;
+
+       /* initialize the pointer in case something fails */
+       tty->driver_data = NULL;
+
+       vk = (struct bcm_vk *)dev_get_drvdata(tty->dev);
+       index = tty->index;
+
+       if (index >= BCM_VK_NUM_TTY)
+               return -EINVAL;
+
+       vktty = &vk->tty[index];
+
+       vktty->pid = task_pid_nr(current);
+       vktty->to_offset = TO_TTYK_BASE(index);
+       vktty->from_offset = FROM_TTYK_BASE(index);
+
+       /* Do not allow tty device to be opened if tty on card not ready */
+       card_status = vkread32(vk, BAR_0, BAR_CARD_STATUS);
+       if (BCM_VK_INTF_IS_DOWN(card_status) || ((card_status & BIT(index)) == 0))
+               return -EBUSY;
+
+       /*
+        * Get shadow registers of the buffer sizes and the "to" write offset
+        * and "from" read offset
+        */
+       vktty->to_size = vkread32(vk, BAR_1, VK_BAR_CHAN_SIZE(vktty, to));
+       vktty->wr = vkread32(vk, BAR_1,  VK_BAR_CHAN_WR(vktty, to));
+       vktty->from_size = vkread32(vk, BAR_1, VK_BAR_CHAN_SIZE(vktty, from));
+       vktty->rd = vkread32(vk, BAR_1,  VK_BAR_CHAN_RD(vktty, from));
+       vktty->is_opened = true;
+
+       if (tty->count == 1 && !vktty->irq_enabled) {
+               timer_setup(&vk->serial_timer, bcm_vk_tty_poll, 0);
+               mod_timer(&vk->serial_timer, jiffies + SERIAL_TIMER_VALUE);
+       }
+       return 0;
+}
+
+static void bcm_vk_tty_close(struct tty_struct *tty, struct file *file)
+{
+       struct bcm_vk *vk = dev_get_drvdata(tty->dev);
+
+       if (tty->index >= BCM_VK_NUM_TTY)
+               return;
+
+       vk->tty[tty->index].is_opened = false;
+
+       if (tty->count == 1)
+               del_timer_sync(&vk->serial_timer);
+}
+
+static void bcm_vk_tty_doorbell(struct bcm_vk *vk, u32 db_val)
+{
+       vkwrite32(vk, db_val, BAR_0,
+                 VK_BAR0_REGSEG_DB_BASE + VK_BAR0_REGSEG_TTY_DB_OFFSET);
+}
+
+static int bcm_vk_tty_write(struct tty_struct *tty,
+                           const unsigned char *buffer,
+                           int count)
+{
+       int index;
+       struct bcm_vk *vk;
+       struct bcm_vk_tty *vktty;
+       int i;
+
+       index = tty->index;
+       vk = dev_get_drvdata(tty->dev);
+       vktty = &vk->tty[index];
+
+       /* Simple write each byte to circular buffer */
+       for (i = 0; i < count; i++) {
+               vkwrite8(vk, buffer[i], BAR_1,
+                        VK_BAR_CHAN_DATA(vktty, to, vktty->wr));
+               vktty->wr++;
+               if (vktty->wr >= vktty->to_size)
+                       vktty->wr = 0;
+       }
+       /* Update write offset from shadow register to card */
+       vkwrite32(vk, vktty->wr, BAR_1, VK_BAR_CHAN_WR(vktty, to));
+       bcm_vk_tty_doorbell(vk, 0);
+
+       return count;
+}
+
+static int bcm_vk_tty_write_room(struct tty_struct *tty)
+{
+       struct bcm_vk *vk = dev_get_drvdata(tty->dev);
+
+       return vk->tty[tty->index].to_size - 1;
+}
+
+static const struct tty_operations serial_ops = {
+       .open = bcm_vk_tty_open,
+       .close = bcm_vk_tty_close,
+       .write = bcm_vk_tty_write,
+       .write_room = bcm_vk_tty_write_room,
+};
+
+int bcm_vk_tty_init(struct bcm_vk *vk, char *name)
+{
+       int i;
+       int err;
+       struct tty_driver *tty_drv;
+       struct device *dev = &vk->pdev->dev;
+
+       tty_drv = tty_alloc_driver
+                               (BCM_VK_NUM_TTY,
+                                TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV);
+       if (IS_ERR(tty_drv))
+               return PTR_ERR(tty_drv);
+
+       /* Save struct tty_driver for uninstalling the device */
+       vk->tty_drv = tty_drv;
+
+       /* initialize the tty driver */
+       tty_drv->driver_name = KBUILD_MODNAME;
+       tty_drv->name = kstrdup(name, GFP_KERNEL);
+       if (!tty_drv->name) {
+               err = -ENOMEM;
+               goto err_put_tty_driver;
+       }
+       tty_drv->type = TTY_DRIVER_TYPE_SERIAL;
+       tty_drv->subtype = SERIAL_TYPE_NORMAL;
+       tty_drv->init_termios = tty_std_termios;
+       tty_set_operations(tty_drv, &serial_ops);
+
+       /* register the tty driver */
+       err = tty_register_driver(tty_drv);
+       if (err) {
+               dev_err(dev, "tty_register_driver failed\n");
+               goto err_kfree_tty_name;
+       }
+
+       for (i = 0; i < BCM_VK_NUM_TTY; i++) {
+               struct device *tty_dev;
+
+               tty_port_init(&vk->tty[i].port);
+               tty_dev = tty_port_register_device(&vk->tty[i].port, tty_drv,
+                                                  i, dev);
+               if (IS_ERR(tty_dev)) {
+                       err = PTR_ERR(tty_dev);
+                       goto unwind;
+               }
+               dev_set_drvdata(tty_dev, vk);
+               vk->tty[i].is_opened = false;
+       }
+
+       INIT_WORK(&vk->tty_wq_work, bcm_vk_tty_wq_handler);
+       vk->tty_wq_thread = create_singlethread_workqueue("tty");
+       if (!vk->tty_wq_thread) {
+               dev_err(dev, "Fail to create tty workqueue thread\n");
+               err = -ENOMEM;
+               goto unwind;
+       }
+       return 0;
+
+unwind:
+       while (--i >= 0)
+               tty_port_unregister_device(&vk->tty[i].port, tty_drv, i);
+       tty_unregister_driver(tty_drv);
+
+err_kfree_tty_name:
+       kfree(tty_drv->name);
+       tty_drv->name = NULL;
+
+err_put_tty_driver:
+       put_tty_driver(tty_drv);
+
+       return err;
+}
+
+void bcm_vk_tty_exit(struct bcm_vk *vk)
+{
+       int i;
+
+       del_timer_sync(&vk->serial_timer);
+       for (i = 0; i < BCM_VK_NUM_TTY; ++i) {
+               tty_port_unregister_device(&vk->tty[i].port,
+                                          vk->tty_drv,
+                                          i);
+               tty_port_destroy(&vk->tty[i].port);
+       }
+       tty_unregister_driver(vk->tty_drv);
+
+       kfree(vk->tty_drv->name);
+       vk->tty_drv->name = NULL;
+
+       put_tty_driver(vk->tty_drv);
+}
+
+void bcm_vk_tty_terminate_tty_user(struct bcm_vk *vk)
+{
+       struct bcm_vk_tty *vktty;
+       int i;
+
+       for (i = 0; i < BCM_VK_NUM_TTY; ++i) {
+               vktty = &vk->tty[i];
+               if (vktty->pid)
+                       kill_pid(find_vpid(vktty->pid), SIGKILL, 1);
+       }
+}