forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
339 lines
8.2 KiB
339 lines
8.2 KiB
// 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); |
|
} |
|
} |
|
|
|
void bcm_vk_tty_wq_exit(struct bcm_vk *vk) |
|
{ |
|
cancel_work_sync(&vk->tty_wq_work); |
|
destroy_workqueue(vk->tty_wq_thread); |
|
}
|
|
|