| // 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 = timer_container_of(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; | |
| int i; | |
| int wr; | |
| u8 c; | |
| 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) | |
| timer_delete_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 ssize_t bcm_vk_tty_write(struct tty_struct *tty, const u8 *buffer, | |
| size_t count) | |
| { | |
| int index; | |
| struct bcm_vk *vk; | |
| struct bcm_vk_tty *vktty; | |
| size_t 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 unsigned 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_tty_driver_kref_put; | |
| } | |
| 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_attr(&vk->tty[i].port, | |
| tty_drv, i, dev, vk, | |
| NULL); | |
| if (IS_ERR(tty_dev)) { | |
| err = PTR_ERR(tty_dev); | |
| goto unwind; | |
| } | |
| 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_tty_driver_kref_put: | |
| tty_driver_kref_put(tty_drv); | |
| return err; | |
| } | |
| void bcm_vk_tty_exit(struct bcm_vk *vk) | |
| { | |
| int i; | |
| timer_delete_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; | |
| tty_driver_kref_put(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); | |
| } | |
| } | |
| void bcm_vk_tty_wq_exit(struct bcm_vk *vk) | |
| { | |
| cancel_work_sync(&vk->tty_wq_work); | |
| destroy_workqueue(vk->tty_wq_thread); | |
| } |