1467 lines
39 KiB
C
1467 lines
39 KiB
C
/*
|
|
* dlp_tty.c
|
|
*
|
|
* Intel Mobile Communication modem protocol driver for DLP
|
|
* (Data Link Protocl (LTE)). This driver is implementing a 5-channel HSI
|
|
* protocol consisting of:
|
|
* - An internal communication control channel;
|
|
* - A multiplexed channel exporting a TTY interface;
|
|
* - Three dedicated high speed channels exporting each a network interface.
|
|
* All channels are using fixed-length pdus, although of different sizes.
|
|
*
|
|
* Copyright (C) 2010-2011 Intel Corporation. All rights reserved.
|
|
*
|
|
* Contact: Olivier Stoltz Douchet <olivierx.stoltz-douchet@intel.com>
|
|
* Faouaz Tenoutit <faouazx.tenoutit@intel.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* version 2 as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
|
* 02110-1301 USA
|
|
*/
|
|
|
|
#include <linux/log2.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/tty.h>
|
|
#include <linux/tty_flip.h>
|
|
#include <linux/jiffies.h>
|
|
#include <linux/hsi/intel_mid_hsi.h>
|
|
#include <linux/hsi/hsi_dlp.h>
|
|
#include <linux/hsi/hsi.h>
|
|
#include <linux/dma-mapping.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/version.h>
|
|
|
|
#include "dlp_main.h"
|
|
|
|
#define IPC_TTYNAME "tty"CONFIG_HSI_DLP_IPC_TTY_NAME
|
|
|
|
#define RX_TTY_FORWARDING_BIT (1<<DLP_GLOBAL_STATE_SZ)
|
|
#define RX_TTY_REFORWARD_BIT (2<<DLP_GLOBAL_STATE_SZ)
|
|
|
|
#define TX_TTY_WRITE_PENDING_BIT (1<<DLP_GLOBAL_STATE_SZ)
|
|
|
|
/*
|
|
* struct dlp_tty_context - TTY channel private data
|
|
*
|
|
* @tty_drv: TTY driver struct
|
|
* @tty_prt: TTY port struct
|
|
* @buffer_wq: Workqueue for tty buffer flush
|
|
* @ch_ctx : Channel context ref
|
|
* @do_tty_forward: dedicated TTY forwarding work structure
|
|
*/
|
|
struct dlp_tty_context {
|
|
struct tty_port tty_prt;
|
|
struct tty_driver *tty_drv;
|
|
|
|
struct dlp_channel *ch_ctx;
|
|
struct work_struct do_tty_forward;
|
|
};
|
|
|
|
|
|
/**
|
|
* Push as many RX PDUs as possible to the controller FIFO
|
|
*
|
|
* @param ch_ctx : The channel context to consider
|
|
*
|
|
* @return 0 when OK, error value otherwise
|
|
*/
|
|
static int dlp_tty_push_rx_pdus(struct dlp_channel *ch_ctx)
|
|
{
|
|
int ret = 0;
|
|
struct dlp_xfer_ctx *rx_ctx = &ch_ctx->rx;
|
|
|
|
ret = dlp_pop_recycled_push_ctrl(rx_ctx);
|
|
if (ret == -EAGAIN) {
|
|
mod_timer(&rx_ctx->timer, jiffies + rx_ctx->delay);
|
|
ret = 0;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_pdu_data_ptr - helper function for getting the actual virtual address of
|
|
* a pdu data, taking into account the header offset
|
|
*
|
|
* @pdu: a reference to the considered pdu
|
|
* @offset: an offset to add to the current virtual address of the pdu data
|
|
*
|
|
* Returns the virtual base address of the actual pdu data
|
|
*/
|
|
inline __attribute_const__
|
|
unsigned char *dlp_tty_pdu_data_ptr(struct hsi_msg *pdu, unsigned int offset)
|
|
{
|
|
u32 *addr = sg_virt(pdu->sgt.sgl);
|
|
|
|
/* Skip the Signature (+2) & Start address (+2) */
|
|
addr += 4;
|
|
return ((unsigned char *)addr) + offset;
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_pdu_set_length - write down the length information to the frame header
|
|
* @pdu: a reference to the considered pdu
|
|
* @sz: the length information to encode in the header
|
|
*/
|
|
inline void dlp_tty_pdu_set_length(struct hsi_msg *pdu, u32 sz)
|
|
{
|
|
u32 *header = (u32 *) (sg_virt(pdu->sgt.sgl));
|
|
header[1] = DLP_TTY_HEADER_LENGTH;
|
|
header[2] = DLP_HDR_NO_MORE_DESC |
|
|
DLP_HDR_COMPLETE_PACKET | DLP_HDR_DATA_SIZE(sz);
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_wakeup - wakeup an asleep TTY write function call
|
|
* @ch_ctx: a reference to the context related to this TTY
|
|
*
|
|
* This helper function awakes any asleep TTY write callback function.
|
|
*/
|
|
static void dlp_tty_wakeup(struct dlp_channel *ch_ctx)
|
|
{
|
|
struct tty_struct *tty;
|
|
struct dlp_tty_context *tty_ctx = ch_ctx->ch_data;
|
|
|
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,9,0)
|
|
tty_port_tty_wakeup(&tty_ctx->tty_prt);
|
|
#else
|
|
tty = tty_port_tty_get(&tty_ctx->tty_prt);
|
|
if (likely(tty)) {
|
|
tty_wakeup(tty);
|
|
tty_kref_put(tty);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* _dlp_forward_tty - RX data TTY forwarding helper function
|
|
* @tty: a reference to the TTY where the data shall be forwarded
|
|
* @xfer_ctx: a reference to the RX context where the FIFO of waiting pdus sits
|
|
*
|
|
* Data contained in the waiting pdu FIFO shall be forwarded to the TTY.
|
|
* This function is :
|
|
* - Pushing as much data as possible to the TTY interface
|
|
* - Recycling pdus that have been fully forwarded
|
|
* - Kicking a TTY insert
|
|
* - Restart delayed job if some data is remaining in the waiting FIFO
|
|
* or if the controller FIFO is not full yet.
|
|
*/
|
|
static void _dlp_forward_tty(struct tty_struct *tty,
|
|
struct dlp_xfer_ctx *xfer_ctx)
|
|
{
|
|
struct hsi_msg *pdu;
|
|
unsigned long flags;
|
|
unsigned char *data_addr, *start_addr;
|
|
unsigned int copied, data_size, offset, more_packets;
|
|
int *ptr, do_push, ret;
|
|
char tty_flag;
|
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,9,0)
|
|
struct dlp_channel *ch_ctx = (struct dlp_channel *)tty->driver_data;
|
|
struct dlp_tty_context *tty_ctx = ch_ctx->ch_data;
|
|
#endif
|
|
|
|
/* Initialised to 1 to prevent unexpected TTY forwarding resume
|
|
* function when there is no TTY or when it is throttled */
|
|
copied = 1;
|
|
do_push = 0;
|
|
|
|
del_timer_sync(&xfer_ctx->timer);
|
|
|
|
read_lock_irqsave(&xfer_ctx->lock, flags);
|
|
while (xfer_ctx->wait_len > 0) {
|
|
read_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
|
|
write_lock_irqsave(&xfer_ctx->lock, flags);
|
|
pdu = dlp_fifo_wait_pop(xfer_ctx);
|
|
write_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
if (!pdu)
|
|
goto no_more_tty_insert;
|
|
|
|
if (pdu->status == HSI_STATUS_COMPLETED)
|
|
tty_flag = (likely(!pdu->break_frame)) ?
|
|
TTY_NORMAL : TTY_BREAK;
|
|
else
|
|
tty_flag = TTY_FRAME;
|
|
|
|
if (unlikely(!tty))
|
|
goto free_pdu;
|
|
|
|
/* Read packets desc */
|
|
/*---------------------*/
|
|
ptr = sg_virt(pdu->sgt.sgl);
|
|
start_addr = (unsigned char *)ptr;
|
|
|
|
do {
|
|
if (test_bit(TTY_THROTTLED, &tty->flags)) {
|
|
/* Initialised to 1 to prevent unexpected TTY
|
|
* forwarding resume function schedule */
|
|
copied = 1;
|
|
dlp_fifo_wait_push_back(xfer_ctx, pdu);
|
|
goto no_more_tty_insert;
|
|
}
|
|
|
|
/* Get the start offset */
|
|
ptr++;
|
|
offset = (*ptr);
|
|
|
|
/* Get the size & address */
|
|
ptr++;
|
|
more_packets = (*ptr) & DLP_HDR_MORE_DESC;
|
|
data_size = DLP_HDR_DATA_SIZE((*ptr));
|
|
data_addr = start_addr + offset;
|
|
|
|
/* Copy the data to the TTY buffer */
|
|
do {
|
|
|
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,9,0)
|
|
copied = (unsigned int)
|
|
tty_insert_flip_string_fixed_flag(&tty_ctx->tty_prt,
|
|
data_addr,
|
|
tty_flag,
|
|
data_size);
|
|
#else
|
|
copied = (unsigned int)
|
|
tty_insert_flip_string_fixed_flag(tty,
|
|
data_addr,
|
|
tty_flag,
|
|
data_size);
|
|
#endif
|
|
|
|
data_addr += copied;
|
|
data_size -= copied;
|
|
|
|
/* We'll push the flip buffers each
|
|
* time something has been written
|
|
* to them to allow low latency */
|
|
do_push |= (copied > 0);
|
|
} while ((data_size) && (copied));
|
|
|
|
/* Still have not copied data ? */
|
|
if (data_size) {
|
|
dlp_fifo_wait_push_back(xfer_ctx, pdu);
|
|
goto no_more_tty_insert;
|
|
}
|
|
} while ((more_packets) && (copied));
|
|
|
|
free_pdu:
|
|
/* Reset the pdu offset & length */
|
|
dlp_pdu_reset(xfer_ctx,
|
|
pdu,
|
|
xfer_ctx->payload_len + DLP_TTY_HEADER_LENGTH);
|
|
|
|
/* Recycle or free the pdu */
|
|
dlp_pdu_recycle(xfer_ctx, pdu);
|
|
|
|
read_lock_irqsave(&xfer_ctx->lock, flags);
|
|
}
|
|
|
|
read_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
|
|
no_more_tty_insert:
|
|
if (do_push) {
|
|
/* Schedule a flip since called from complete_rx()
|
|
* in an interrupt context instead of
|
|
* tty_flip_buffer_push() */
|
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,9,0)
|
|
tty_schedule_flip(&tty_ctx->tty_prt);
|
|
#else
|
|
tty_schedule_flip(tty);
|
|
#endif
|
|
|
|
}
|
|
/* Push any available pdus to the CTRL */
|
|
ret = dlp_pop_recycled_push_ctrl(xfer_ctx);
|
|
|
|
/* Shoot again later if there is still pending data to serve or if
|
|
* the RX controller FIFO is not full yet */
|
|
if ((!copied) || (unlikely(ret == -EAGAIN)))
|
|
mod_timer(&xfer_ctx->timer, jiffies + xfer_ctx->delay);
|
|
}
|
|
|
|
/**
|
|
* dlp_do_tty_forward - forwarding data to the above line discipline
|
|
* @work: a reference to work queue element
|
|
*/
|
|
static void dlp_do_tty_forward(struct work_struct *work)
|
|
{
|
|
struct dlp_tty_context *tty_ctx;
|
|
struct tty_struct *tty;
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return;
|
|
}
|
|
|
|
tty_ctx = container_of(work, struct dlp_tty_context, do_tty_forward);
|
|
tty = tty_port_tty_get(&tty_ctx->tty_prt);
|
|
if (tty) {
|
|
/* Lock really needed ?
|
|
* We are using a single thread workqueue,
|
|
* so works are executed sequentially */
|
|
_dlp_forward_tty(tty, &tty_ctx->ch_ctx->rx);
|
|
tty_kref_put(tty);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_rx_forward_retry - TTY forwarding retry job
|
|
* @param: a casted reference to the to the RX context where the FIFO of
|
|
* waiting pdus sits
|
|
*
|
|
* This simply calls the TTY forwarding function in a tasklet shell.
|
|
*/
|
|
static void dlp_tty_rx_forward_retry(unsigned long param)
|
|
{
|
|
struct dlp_xfer_ctx *xfer_ctx = (struct dlp_xfer_ctx *)param;
|
|
struct dlp_tty_context *tty_ctx = xfer_ctx->channel->ch_data;
|
|
|
|
queue_work(dlp_drv.rx_wq, &tty_ctx->do_tty_forward);
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_rx_forward_resume - TTY forwarding resume callback
|
|
* @tty: a reference to the TTY requesting the resume
|
|
*
|
|
* This simply calls the TTY forwarding function as a response to a TTY
|
|
* unthrottle event.
|
|
*/
|
|
static void dlp_tty_rx_forward_resume(struct tty_struct *tty)
|
|
{
|
|
struct dlp_channel *ch_ctx;
|
|
|
|
/* Get the context reference from the driver data if already opened */
|
|
ch_ctx = (struct dlp_channel *)tty->driver_data;
|
|
|
|
if (ch_ctx) {
|
|
struct dlp_tty_context *tty_ctx = ch_ctx->ch_data;
|
|
queue_work(dlp_drv.rx_wq, &tty_ctx->do_tty_forward);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_complete_tx - bottom-up flow for the TX side
|
|
* @pdu: a reference to the completed pdu
|
|
*
|
|
* A TX transfer has completed: recycle the completed pdu and kick a new
|
|
* delayed request to enter the IDLE state if nothing else is expected.
|
|
*/
|
|
static void dlp_tty_complete_tx(struct hsi_msg *pdu)
|
|
{
|
|
struct dlp_xfer_ctx *xfer_ctx = pdu->context;
|
|
struct dlp_channel *ch_ctx = xfer_ctx->channel;
|
|
int wakeup, avail, pending;
|
|
unsigned long flags;
|
|
|
|
/* Recycle or Free the pdu */
|
|
write_lock_irqsave(&xfer_ctx->lock, flags);
|
|
dlp_pdu_delete(xfer_ctx, pdu, flags);
|
|
|
|
/* Decrease the CTRL fifo size */
|
|
dlp_hsi_controller_pop(xfer_ctx);
|
|
|
|
/* Check the wait FIFO size */
|
|
avail = (xfer_ctx->wait_len <= xfer_ctx->wait_max / 2);
|
|
write_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
|
|
/* Start new waiting pdus (if any) */
|
|
dlp_pop_wait_push_ctrl(xfer_ctx);
|
|
|
|
/* Wake-up the TTY write whenever the TX wait FIFO is half empty, and
|
|
* not before, to prevent too many wakeups */
|
|
pending = dlp_ctx_has_flag(xfer_ctx, TX_TTY_WRITE_PENDING_BIT);
|
|
wakeup = (pending && avail);
|
|
if (wakeup) {
|
|
dlp_ctx_clear_flag(xfer_ctx, TX_TTY_WRITE_PENDING_BIT);
|
|
dlp_tty_wakeup(ch_ctx);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_complete_rx - bottom-up flow for the RX side
|
|
* @pdu: a reference to the completed pdu
|
|
*
|
|
* A RX transfer has completed: push the data conveyed in the pdu to the TTY
|
|
* interface and signal any existing error.
|
|
*/
|
|
static void dlp_tty_complete_rx(struct hsi_msg *pdu)
|
|
{
|
|
struct dlp_xfer_ctx *xfer_ctx = pdu->context;
|
|
struct dlp_tty_context *tty_ctx = xfer_ctx->channel->ch_data;
|
|
unsigned long flags;
|
|
int ret;
|
|
|
|
/* Check the link readiness (TTY still opened) */
|
|
if (!dlp_tty_is_link_valid()) {
|
|
if ((EDLP_TTY_RX_DATA_REPORT) ||
|
|
(EDLP_TTY_RX_DATA_LEN_REPORT))
|
|
pr_debug(DRVNAME ": TTY: CH%d RX PDU ignored (close:%d, Time out: %d)\n",
|
|
xfer_ctx->channel->ch_id,
|
|
dlp_drv.tty_closed, dlp_drv.tx_timeout);
|
|
goto recycle;
|
|
}
|
|
|
|
/* Check the received PDU header & seq_num */
|
|
ret = dlp_pdu_header_check(xfer_ctx, pdu);
|
|
if (ret == -EINVAL) {
|
|
/* Dump the first 64 bytes */
|
|
print_hex_dump(KERN_DEBUG,
|
|
DRVNAME"_TTY", DUMP_PREFIX_OFFSET,
|
|
16, 4,
|
|
sg_virt(pdu->sgt.sgl), 64, 1);
|
|
|
|
goto recycle;
|
|
}
|
|
|
|
/* Check and update the PDU len & status */
|
|
dlp_pdu_update(tty_ctx->ch_ctx, pdu);
|
|
|
|
/* Dump the RX data/length */
|
|
if (EDLP_TTY_RX_DATA_REPORT)
|
|
print_hex_dump(KERN_DEBUG,
|
|
DRVNAME": TTY_RX", DUMP_PREFIX_OFFSET,
|
|
16, 4,
|
|
dlp_tty_pdu_data_ptr(pdu, 0),
|
|
MIN(64, pdu->actual_len), 1);
|
|
else if (EDLP_TTY_RX_DATA_LEN_REPORT)
|
|
pr_debug(DRVNAME ": TTY_RX %d bytes\n", pdu->actual_len);
|
|
|
|
/* Decrease the CTRL fifo size */
|
|
write_lock_irqsave(&xfer_ctx->lock, flags);
|
|
dlp_hsi_controller_pop(xfer_ctx);
|
|
write_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
|
|
if (pdu->status != HSI_STATUS_ERROR)
|
|
dlp_fifo_wait_push(xfer_ctx, pdu);
|
|
else {
|
|
pr_debug(DRVNAME ": TTY: CH%d RX PDU ignored\n",
|
|
xfer_ctx->channel->ch_id);
|
|
goto recycle;
|
|
}
|
|
#ifdef CONFIG_HSI_DLP_TTY_STATS
|
|
xfer_ctx->tty_stats.data_sz += pdu->actual_len;
|
|
xfer_ctx->tty_stats.pdus_cnt++;
|
|
if (!xfer_ctx->ctrl_len)
|
|
xfer_ctx->tty_stats.overflow_cnt++;
|
|
#endif
|
|
queue_work(dlp_drv.rx_wq, &tty_ctx->do_tty_forward);
|
|
return;
|
|
|
|
recycle:
|
|
/* Recycle or free the pdu */
|
|
dlp_pdu_recycle(xfer_ctx, pdu);
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_tx_fifo_wait_recycle - recycle the whole content of the TX waiting FIFO
|
|
* @xfer_ctx: a reference to the TX context to consider
|
|
*
|
|
* This helper function is emptying a waiting TX FIFO and recycling all its
|
|
* pdus.
|
|
*/
|
|
static void dlp_tty_tx_fifo_wait_recycle(struct dlp_xfer_ctx *xfer_ctx)
|
|
{
|
|
struct hsi_msg *pdu;
|
|
unsigned long flags;
|
|
|
|
dlp_ctx_clear_flag(xfer_ctx, TX_TTY_WRITE_PENDING_BIT);
|
|
|
|
write_lock_irqsave(&xfer_ctx->lock, flags);
|
|
|
|
while ((pdu = dlp_fifo_wait_pop(xfer_ctx))) {
|
|
xfer_ctx->room -= dlp_pdu_room_in(pdu);
|
|
|
|
/* check if pdu is active in dlp_tty_do_write */
|
|
if (pdu->status != HSI_STATUS_PENDING)
|
|
dlp_pdu_delete(xfer_ctx, pdu, flags);
|
|
else
|
|
pdu->break_frame = 0;
|
|
}
|
|
|
|
write_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_rx_fifo_wait_recycle - recycle the whole content of the RX waiting FIFO
|
|
* @xfer_ctx: a reference to the RX context to consider
|
|
*
|
|
* This helper function is emptying a waiting RX FIFO and recycling all its
|
|
* pdus.
|
|
*/
|
|
static void dlp_tty_rx_fifo_wait_recycle(struct dlp_xfer_ctx *xfer_ctx)
|
|
{
|
|
struct hsi_msg *pdu;
|
|
unsigned long flags;
|
|
unsigned int length = xfer_ctx->payload_len + DLP_TTY_HEADER_LENGTH;
|
|
|
|
dlp_ctx_clear_flag(xfer_ctx,
|
|
RX_TTY_FORWARDING_BIT | RX_TTY_REFORWARD_BIT);
|
|
|
|
write_lock_irqsave(&xfer_ctx->lock, flags);
|
|
|
|
while ((pdu = dlp_fifo_wait_pop(xfer_ctx))) {
|
|
/* Reset offset & length */
|
|
dlp_pdu_reset(xfer_ctx, pdu, length);
|
|
|
|
/* Recycle or Free the pdu */
|
|
dlp_pdu_delete(xfer_ctx, pdu, flags);
|
|
}
|
|
|
|
write_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
}
|
|
|
|
/*
|
|
* TTY handling methods
|
|
*/
|
|
|
|
/**
|
|
* dlp_tty_wait_until_ctx_sent - waits for all the TX FIFO to be empty
|
|
* @ch_ctx: a reference to the considered context
|
|
* @timeout: a timeout value expressed in jiffies
|
|
*/
|
|
inline void dlp_tty_wait_until_ctx_sent(struct dlp_channel *ch_ctx, int timeout)
|
|
{
|
|
wait_event_interruptible_timeout(ch_ctx->tx_empty_event,
|
|
dlp_ctx_is_empty(&ch_ctx->tx),
|
|
timeout);
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_cleanup - clear timers and flush all TX/RX pending
|
|
* @ch_ctx : Channel context ref
|
|
*
|
|
*/
|
|
static void dlp_tty_cleanup(struct dlp_channel *ch_ctx)
|
|
{
|
|
struct dlp_tty_context *tty_ctx;
|
|
struct dlp_xfer_ctx *tx_ctx;
|
|
struct dlp_xfer_ctx *rx_ctx;
|
|
int ret;
|
|
|
|
tty_ctx = ch_ctx->ch_data;
|
|
tx_ctx = &ch_ctx->tx;
|
|
rx_ctx = &ch_ctx->rx;
|
|
|
|
del_timer_sync(&dlp_drv.timer[ch_ctx->ch_id]);
|
|
|
|
/* Flush any pending fw work */
|
|
flush_work_sync(&tty_ctx->do_tty_forward);
|
|
|
|
/* RX */
|
|
del_timer_sync(&rx_ctx->timer);
|
|
dlp_tty_rx_fifo_wait_recycle(rx_ctx);
|
|
dlp_stop_rx(rx_ctx, ch_ctx);
|
|
|
|
/* TX */
|
|
del_timer_sync(&tx_ctx->timer);
|
|
dlp_stop_tx(tx_ctx);
|
|
dlp_tty_tx_fifo_wait_recycle(tx_ctx);
|
|
|
|
dlp_ctx_set_state(tx_ctx, IDLE);
|
|
|
|
/* Close the HSI channel */
|
|
ret = dlp_ctrl_close_channel(ch_ctx);
|
|
if (ret)
|
|
pr_err(DRVNAME ": TT close channel failed :%d\n", ret);
|
|
|
|
/* Flush the ACWAKE works */
|
|
cancel_work_sync(&ch_ctx->start_tx_w);
|
|
cancel_work_sync(&ch_ctx->stop_tx_w);
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_port_activate - callback to the TTY port activate function
|
|
* @port: a reference to the calling TTY port
|
|
* @tty: a reference to the calling TTY
|
|
*
|
|
* Return 0 on success or a negative error code on error.
|
|
*
|
|
* The TTY port activate is only called on the first port open.
|
|
*/
|
|
static int dlp_tty_port_activate(struct tty_port *port, struct tty_struct *tty)
|
|
{
|
|
struct dlp_channel *ch_ctx;
|
|
struct dlp_xfer_ctx *tx_ctx;
|
|
struct dlp_xfer_ctx *rx_ctx;
|
|
int ret = 0;
|
|
|
|
pr_debug(DRVNAME": port activate request\n");
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return -ENODEV;
|
|
}
|
|
|
|
/* Get the context reference stored in the TTY open() */
|
|
ch_ctx = (struct dlp_channel *)tty->driver_data;
|
|
tx_ctx = &ch_ctx->tx;
|
|
rx_ctx = &ch_ctx->rx;
|
|
|
|
/* Update the TX and RX HSI configuration */
|
|
dlp_ctx_update_status(tx_ctx);
|
|
dlp_ctx_update_status(rx_ctx);
|
|
|
|
/* Configure the DLP channel */
|
|
ret = dlp_ctrl_open_channel(ch_ctx);
|
|
if (ret)
|
|
pr_err(DRVNAME ": TTY open channel failed :%d)\n", ret);
|
|
|
|
pr_debug(DRVNAME ": port activate done (ret: %d)\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_port_shutdown - callback to the TTY port shutdown function
|
|
* @port: a reference to the calling TTY port
|
|
*
|
|
* The TTY port shutdown is only called on the last port close.
|
|
*/
|
|
static void dlp_tty_port_shutdown(struct tty_port *port)
|
|
{
|
|
struct dlp_channel *ch_ctx;
|
|
struct dlp_tty_context *tty_ctx;
|
|
struct dlp_xfer_ctx *tx_ctx;
|
|
struct dlp_xfer_ctx *rx_ctx;
|
|
|
|
pr_debug(DRVNAME": port shutdown request\n");
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return;
|
|
}
|
|
|
|
tty_ctx = container_of(port, struct dlp_tty_context, tty_prt);
|
|
ch_ctx = tty_ctx->ch_ctx;
|
|
tx_ctx = &ch_ctx->tx;
|
|
rx_ctx = &ch_ctx->rx;
|
|
|
|
/* Don't wait if already in TX timeout state */
|
|
if (dlp_tty_is_link_valid())
|
|
dlp_tty_wait_until_ctx_sent(ch_ctx, 0);
|
|
|
|
/* TTY channel cleanup */
|
|
dlp_tty_cleanup(ch_ctx);
|
|
|
|
/* device closed => Set the channel state flag */
|
|
dlp_ctrl_set_channel_state(ch_ctx->hsi_channel,
|
|
DLP_CH_STATE_CLOSED);
|
|
|
|
pr_debug(DRVNAME ": port shutdown done\n");
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_open - callback to the TTY open function
|
|
* @tty: a reference to the calling TTY
|
|
* @filp: a reference to the calling file
|
|
*
|
|
* Return 0 on success or a negative error code on error.
|
|
*
|
|
* The HSI layer is only initialised during the first opening.
|
|
*/
|
|
static int dlp_tty_open(struct tty_struct *tty, struct file *filp)
|
|
{
|
|
struct dlp_channel *ch_ctx;
|
|
struct dlp_tty_context *tty_ctx;
|
|
int ret;
|
|
|
|
pr_debug(DRVNAME": TTY device open request (%s, %d)\n",
|
|
current->comm, current->tgid);
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
ret = -ENODEV;
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
goto out;
|
|
}
|
|
|
|
/* Get the context reference from the driver data if already opened */
|
|
ch_ctx = (struct dlp_channel *)tty->driver_data;
|
|
|
|
/* First open ? */
|
|
if (!ch_ctx) {
|
|
ch_ctx = dlp_drv.channels[DLP_CHANNEL_TTY];
|
|
tty->driver_data = ch_ctx;
|
|
}
|
|
|
|
if (unlikely(!ch_ctx)) {
|
|
ret = -ENODEV;
|
|
pr_err(DRVNAME ": Cannot find TTY context\n");
|
|
goto out;
|
|
}
|
|
|
|
/* Needed only once */
|
|
if (tty->count == 1) {
|
|
/* Reset the Tx timeout/TTY close flag */
|
|
dlp_tty_set_link_valid(0, 0);
|
|
|
|
/* Claim & Setup the HSI port */
|
|
dlp_hsi_port_claim();
|
|
|
|
/* Register the event callback */
|
|
hsi_register_port_event(dlp_drv.client, dlp_drv.ehandler);
|
|
|
|
/* Push RX pdus for ALL channels */
|
|
dlp_push_rx_pdus();
|
|
}
|
|
|
|
/* Update/Set the eDLP channel id */
|
|
dlp_drv.channels_hsi[ch_ctx->hsi_channel].edlp_channel = ch_ctx->ch_id;
|
|
|
|
/* Open the TTY port (calls port->activate on first opening) */
|
|
tty_ctx = ch_ctx->ch_data;
|
|
ret = tty_port_open(&tty_ctx->tty_prt, tty, filp);
|
|
if (ret)
|
|
pr_err(DRVNAME ": TTY port open failed (%d)\n", ret);
|
|
|
|
/* Set the TTY_NO_WRITE_SPLIT to transfer as much data as possible on
|
|
* the first write request. This shall not introduce denial of service
|
|
* as this flag will later adapt to the available TX buffer size. */
|
|
set_bit(TTY_NO_WRITE_SPLIT, &tty->flags);
|
|
|
|
out:
|
|
pr_debug(DRVNAME ": TTY device open done (ret: %d)\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_flush_tx_buffer - flushes the TX waiting FIFO
|
|
* @tty: a reference to the requesting TTY
|
|
*/
|
|
static void dlp_tty_flush_tx_buffer(struct tty_struct *tty)
|
|
{
|
|
struct dlp_channel *ch_ctx = (struct dlp_channel *)tty->driver_data;
|
|
struct dlp_xfer_ctx *xfer_ctx = &ch_ctx->tx;
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return;
|
|
}
|
|
|
|
dlp_tty_tx_fifo_wait_recycle(xfer_ctx);
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_tx_stop - update the TX state machine after expiration of the TX active
|
|
* timeout further to a no outstanding TX transaction status
|
|
* @param: a hidden reference to the TX context to consider
|
|
*
|
|
* This helper function updates the TX state if it is currently active and
|
|
* inform the HSI pduwork and attached controller.
|
|
*/
|
|
void dlp_tty_tx_stop(unsigned long param)
|
|
{
|
|
struct dlp_xfer_ctx *xfer_ctx = (struct dlp_xfer_ctx *)param;
|
|
|
|
dlp_stop_tx(xfer_ctx);
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_hsi_tx_timeout_cb - Called when we have an HSI TX timeout
|
|
* @ch_ctx : Channel context ref
|
|
*/
|
|
static void dlp_tty_hsi_tx_timeout_cb(struct dlp_channel *ch_ctx)
|
|
{
|
|
struct dlp_tty_context *tty_ctx = ch_ctx->ch_data;
|
|
struct tty_struct *tty;
|
|
|
|
tty = tty_port_tty_get(&tty_ctx->tty_prt);
|
|
if (tty) {
|
|
/* Let's stop all queue and cleanup */
|
|
dlp_tty_cleanup(ch_ctx);
|
|
|
|
/* Clean any waiting data to release potential
|
|
dlp_tty_wait_until_sent lock */
|
|
hsi_flush(dlp_drv.client);
|
|
|
|
tty_vhangup(tty);
|
|
tty_kref_put(tty);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_hangup - callback to a TTY hangup request
|
|
* @tty: a reference to the requesting TTY
|
|
*/
|
|
static void dlp_tty_hangup(struct tty_struct *tty)
|
|
{
|
|
struct dlp_tty_context *tty_ctx =
|
|
(((struct dlp_channel *)tty->driver_data))->ch_data;
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return;
|
|
}
|
|
|
|
pr_err(DRVNAME ": TTY hangup\n");
|
|
|
|
/* Will call the port_shutdown function */
|
|
tty_port_hangup(&tty_ctx->tty_prt);
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_wait_until_sent - callback to a TTY wait until sent request
|
|
* @tty: a reference to the requesting TTY
|
|
* @timeout: a timeout value expressed in jiffies
|
|
*/
|
|
static void dlp_tty_wait_until_sent(struct tty_struct *tty, int timeout)
|
|
{
|
|
struct dlp_channel *ch_ctx = (struct dlp_channel *)tty->driver_data;
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return;
|
|
}
|
|
|
|
dlp_tty_wait_until_ctx_sent(ch_ctx, timeout);
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_close - callback to the TTY close function
|
|
* @tty: a reference to the calling TTY
|
|
* @filp: a reference to the calling file
|
|
*
|
|
* The HSI layer is only released during the last closing.
|
|
*/
|
|
static void dlp_tty_close(struct tty_struct *tty, struct file *filp)
|
|
{
|
|
struct dlp_channel *ch_ctx = NULL;
|
|
int need_cleanup = 0;
|
|
if (tty) {
|
|
need_cleanup = (tty->count == 1);
|
|
|
|
pr_debug(DRVNAME ": TTY device close request (%s, %d)\n",
|
|
current->comm, current->tgid);
|
|
|
|
/* Set TTY as closed to prevent RX/TX transactions */
|
|
if (need_cleanup)
|
|
tty->flow_stopped = 1;
|
|
|
|
ch_ctx = (struct dlp_channel *)tty->driver_data;
|
|
if (filp && ch_ctx) {
|
|
struct dlp_tty_context *tty_ctx = ch_ctx->ch_data;
|
|
if (&tty_ctx->tty_prt)
|
|
tty_port_close(&tty_ctx->tty_prt, tty, filp);
|
|
}
|
|
}
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return;
|
|
}
|
|
|
|
if (need_cleanup)
|
|
dlp_tty_set_link_valid(1, dlp_drv.tx_timeout);
|
|
|
|
/* Flush everything & Release the HSI port */
|
|
if (likely(!atomic_read(&dlp_drv.drv_remove_ongoing)) && need_cleanup) {
|
|
pr_debug(DRVNAME": Flushing the HSI controller\n");
|
|
hsi_flush(dlp_drv.client);
|
|
dlp_hsi_port_unclaim();
|
|
}
|
|
|
|
dlp_ctrl_clean_stored_cmd();
|
|
|
|
pr_debug(DRVNAME ": TTY device close done\n");
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_do_write - writes data coming from the TTY to the TX FIFO
|
|
* @xfer_ctx: a reference to the considered TX context
|
|
* @buf: the virtual address of the current input buffer (from TTY)
|
|
* @len: the remaining buffer size
|
|
*
|
|
* Returns the total size of what has been transferred.
|
|
*
|
|
* This is a recursive function, the core of the TTY write callback function.
|
|
*/
|
|
int dlp_tty_do_write(struct dlp_xfer_ctx *xfer_ctx, unsigned char *buf,
|
|
int len)
|
|
{
|
|
struct hsi_msg *pdu;
|
|
int offset, avail, copied;
|
|
unsigned int updated_actual_len;
|
|
unsigned long flags;
|
|
|
|
offset = 0;
|
|
avail = 0;
|
|
copied = 0;
|
|
|
|
spin_lock_irqsave(&xfer_ctx->channel->lock, flags);
|
|
if (!dlp_ctx_have_credits(xfer_ctx, xfer_ctx->channel)) {
|
|
spin_unlock_irqrestore(&xfer_ctx->channel->lock, flags);
|
|
if ((EDLP_TTY_TX_DATA_REPORT) ||
|
|
(EDLP_TTY_TX_DATA_LEN_REPORT))
|
|
pr_warn(DRVNAME ": CH%d (HSI CH%d) out of credits (%d)",
|
|
xfer_ctx->channel->ch_id,
|
|
xfer_ctx->channel->hsi_channel,
|
|
xfer_ctx->seq_num);
|
|
goto out;
|
|
}
|
|
spin_unlock_irqrestore(&xfer_ctx->channel->lock, flags);
|
|
|
|
write_lock_irqsave(&xfer_ctx->lock, flags);
|
|
pdu = dlp_fifo_tail(&xfer_ctx->wait_pdus);
|
|
if (pdu) {
|
|
if (pdu->status != HSI_STATUS_PENDING) {
|
|
offset = pdu->actual_len;
|
|
avail = xfer_ctx->payload_len - offset;
|
|
if (avail)
|
|
pdu->status = HSI_STATUS_PENDING;
|
|
}
|
|
}
|
|
write_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
|
|
if (avail == 0) {
|
|
pdu = dlp_fifo_recycled_pop(xfer_ctx);
|
|
if (pdu) {
|
|
offset = 0;
|
|
|
|
read_lock_irqsave(&xfer_ctx->lock, flags);
|
|
avail = xfer_ctx->payload_len;
|
|
read_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
|
|
dlp_fifo_wait_push(xfer_ctx, pdu);
|
|
}
|
|
}
|
|
|
|
if (!pdu)
|
|
goto out;
|
|
|
|
/* Do a start TX on new frames only and after having marked
|
|
* the current frame as pending, e.g. don't touch ! */
|
|
if (offset == 0) {
|
|
dlp_start_tx(xfer_ctx);
|
|
} else {
|
|
dlp_ctx_set_flag(xfer_ctx, TX_TTY_WRITE_PENDING_BIT);
|
|
#ifdef CONFIG_HSI_DLP_TTY_STATS
|
|
xfer_ctx->tty_stats.overflow_cnt++;
|
|
#endif
|
|
}
|
|
|
|
copied = min(avail, len);
|
|
updated_actual_len = pdu->actual_len + copied;
|
|
dlp_tty_pdu_set_length(pdu, updated_actual_len);
|
|
(void)memcpy(dlp_tty_pdu_data_ptr(pdu, offset), buf, copied);
|
|
|
|
if (pdu->status != HSI_STATUS_ERROR) { /* still valid ? */
|
|
pdu->actual_len = updated_actual_len;
|
|
|
|
write_lock_irqsave(&xfer_ctx->lock, flags);
|
|
xfer_ctx->buffered += copied;
|
|
xfer_ctx->room -= copied;
|
|
pdu->status = HSI_STATUS_COMPLETED;
|
|
write_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
|
|
if (dlp_ctx_get_state(xfer_ctx) != IDLE)
|
|
dlp_pop_wait_push_ctrl(xfer_ctx);
|
|
} else {
|
|
/* ERROR frames have already been popped from the wait FIFO */
|
|
write_lock_irqsave(&xfer_ctx->lock, flags);
|
|
dlp_pdu_delete(xfer_ctx, pdu, flags);
|
|
write_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
copied = 0;
|
|
}
|
|
|
|
out:
|
|
return copied;
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_write - writes data coming from the TTY to the TX FIFO
|
|
* @tty: a reference to the calling TTY
|
|
* @buf: the virtual address of the current input buffer (from TTY)
|
|
* @len: the TTY buffer size
|
|
*
|
|
* Returns the total size of what has been transferred in the TX FIFO
|
|
*
|
|
* This is the TTY write callback function.
|
|
*/
|
|
static int dlp_tty_write(struct tty_struct *tty, const unsigned char *buf,
|
|
int len)
|
|
{
|
|
struct dlp_xfer_ctx *xfer_ctx =
|
|
&((struct dlp_channel *)tty->driver_data)->tx;
|
|
int already_copied, copied;
|
|
unsigned char *ptr;
|
|
unsigned long flags;
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return -ENODEV;
|
|
}
|
|
|
|
/* Dump the TX data/length */
|
|
if (EDLP_TTY_TX_DATA_REPORT)
|
|
print_hex_dump(KERN_DEBUG,
|
|
DRVNAME": TTY_TX", DUMP_PREFIX_OFFSET,
|
|
16, 4,
|
|
buf, len, 1);
|
|
else if (EDLP_TTY_TX_DATA_LEN_REPORT)
|
|
pr_debug(DRVNAME ": TTY_TX %d bytes\n", len);
|
|
|
|
read_lock_irqsave(&xfer_ctx->lock, flags);
|
|
if (xfer_ctx->room >= len)
|
|
set_bit(TTY_NO_WRITE_SPLIT, &tty->flags);
|
|
else
|
|
clear_bit(TTY_NO_WRITE_SPLIT, &tty->flags);
|
|
read_unlock_irqrestore(&xfer_ctx->lock, flags);
|
|
|
|
already_copied = 0;
|
|
while (len > 0) {
|
|
ptr = (unsigned char *)(buf + already_copied);
|
|
copied = dlp_tty_do_write(xfer_ctx, ptr, len);
|
|
if (copied == 0)
|
|
break;
|
|
already_copied += copied;
|
|
len -= copied;
|
|
}
|
|
|
|
return already_copied;
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_write_room - returns the available buffer size on the TX FIFO
|
|
* @tty: a reference to the calling TTY
|
|
*
|
|
* Returns the total available size in the TX wait FIFO.
|
|
*/
|
|
static int dlp_tty_write_room(struct tty_struct *tty)
|
|
{
|
|
struct dlp_xfer_ctx *ch_ctx =
|
|
&((struct dlp_channel *)tty->driver_data)->tx;
|
|
int room;
|
|
unsigned long flags;
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return -ENODEV;
|
|
}
|
|
|
|
read_lock_irqsave(&ch_ctx->lock, flags);
|
|
room = ch_ctx->room;
|
|
read_unlock_irqrestore(&ch_ctx->lock, flags);
|
|
return room;
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_chars_in_buffer - returns the size of the data hold in the TX FIFO
|
|
* @tty: a reference to the calling TTY
|
|
*
|
|
* Returns the total size of data hold in the TX wait FIFO. It does not take
|
|
* into account the data which has already been passed to the HSI controller
|
|
* in both in software and hardware FIFO.
|
|
*/
|
|
static int dlp_tty_chars_in_buffer(struct tty_struct *tty)
|
|
{
|
|
struct dlp_xfer_ctx *ch_ctx =
|
|
&((struct dlp_channel *)tty->driver_data)->tx;
|
|
int buffered;
|
|
unsigned long flags;
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return -ENODEV;
|
|
}
|
|
|
|
read_lock_irqsave(&ch_ctx->lock, flags);
|
|
buffered = ch_ctx->buffered;
|
|
read_unlock_irqrestore(&ch_ctx->lock, flags);
|
|
return buffered;
|
|
}
|
|
|
|
/**
|
|
* dlp_tty_ioctl - manages the IOCTL read and write requests
|
|
* @tty: a reference to the calling TTY
|
|
* @cmd: the IOCTL command
|
|
* @arg: the I/O argument to pass or retrieve data
|
|
*
|
|
* Returns 0 upon normal completion or the error code in case of an error.
|
|
*/
|
|
static int dlp_tty_ioctl(struct tty_struct *tty,
|
|
unsigned int cmd, unsigned long arg)
|
|
{
|
|
struct dlp_channel *ch_ctx = (struct dlp_channel *)tty->driver_data;
|
|
#ifdef CONFIG_HSI_DLP_TTY_STATS
|
|
struct hsi_dlp_stats stats;
|
|
#endif
|
|
unsigned long flags;
|
|
int ret;
|
|
|
|
if (unlikely(atomic_read(&dlp_drv.drv_remove_ongoing))) {
|
|
pr_err(DRVNAME ": %s: Driver is currently removed by the system",
|
|
__func__);
|
|
return -ENODEV;
|
|
}
|
|
|
|
switch (cmd) {
|
|
#ifdef CONFIG_HSI_DLP_TTY_STATS
|
|
case HSI_DLP_RESET_TX_STATS:
|
|
write_lock_irqsave(&ch_ctx->tx.lock, flags);
|
|
ch_ctx->tx.tty_stats.data_sz = 0;
|
|
ch_ctx->tx.tty_stats.pdus_cnt = 0;
|
|
ch_ctx->tx.tty_stats.overflow_cnt = 0;
|
|
write_unlock_irqrestore(&ch_ctx->tx.lock, flags);
|
|
break;
|
|
|
|
case HSI_DLP_GET_TX_STATS:
|
|
read_lock_irqsave(&ch_ctx->tx.lock, flags);
|
|
stats.data_sz = ch_ctx->tx.tty_stats.data_sz;
|
|
stats.pdus_cnt = ch_ctx->tx.tty_stats.pdus_cnt;
|
|
stats.overflow_cnt = ch_ctx->tx.tty_stats.overflow_cnt;
|
|
read_unlock_irqrestore(&ch_ctx->tx.lock, flags);
|
|
return copy_to_user((void __user *)arg, &stats, sizeof(stats));
|
|
break;
|
|
|
|
case HSI_DLP_RESET_RX_STATS:
|
|
write_lock_irqsave(&ch_ctx->rx.lock, flags);
|
|
ch_ctx->rx.tty_stats.data_sz = 0;
|
|
ch_ctx->rx.tty_stats.pdus_cnt = 0;
|
|
ch_ctx->rx.tty_stats.overflow_cnt = 0;
|
|
write_unlock_irqrestore(&ch_ctx->rx.lock, flags);
|
|
break;
|
|
|
|
case HSI_DLP_GET_RX_STATS:
|
|
read_lock_irqsave(&ch_ctx->rx.lock, flags);
|
|
stats.data_sz = ch_ctx->rx.tty_stats.data_sz;
|
|
stats.pdus_cnt = ch_ctx->rx.tty_stats.pdus_cnt;
|
|
stats.overflow_cnt = ch_ctx->rx.tty_stats.overflow_cnt;
|
|
read_unlock_irqrestore(&ch_ctx->rx.lock, flags);
|
|
return copy_to_user((void __user *)arg, &stats, sizeof(stats));
|
|
break;
|
|
#endif
|
|
|
|
case HSI_DLP_SET_FLASHING_MODE:
|
|
ret = dlp_set_flashing_mode(arg);
|
|
break;
|
|
|
|
default:
|
|
return -ENOIOCTLCMD;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Protocol driver handling routines
|
|
*/
|
|
|
|
/*
|
|
* dlp_termios_init - default termios initialisation
|
|
*/
|
|
static const struct ktermios dlp_termios_init = {
|
|
.c_iflag = 0,
|
|
.c_oflag = 0,
|
|
.c_cflag = B115200 | CS8,
|
|
.c_lflag = 0,
|
|
.c_cc = INIT_C_CC,
|
|
.c_ispeed = 0,
|
|
.c_ospeed = 0
|
|
};
|
|
|
|
/*
|
|
* dlp_driver_tty_ops - table of supported TTY operations
|
|
*/
|
|
static const struct tty_operations dlp_driver_tty_ops = {
|
|
.open = dlp_tty_open,
|
|
.close = dlp_tty_close,
|
|
.write = dlp_tty_write,
|
|
.write_room = dlp_tty_write_room,
|
|
.chars_in_buffer = dlp_tty_chars_in_buffer,
|
|
.ioctl = dlp_tty_ioctl,
|
|
.hangup = dlp_tty_hangup,
|
|
.wait_until_sent = dlp_tty_wait_until_sent,
|
|
.unthrottle = dlp_tty_rx_forward_resume,
|
|
.flush_buffer = dlp_tty_flush_tx_buffer,
|
|
};
|
|
|
|
/*
|
|
* dlp_port_tty_ops - table of supported TTY port operations
|
|
*/
|
|
static const struct tty_port_operations dlp_port_tty_ops = {
|
|
.activate = dlp_tty_port_activate,
|
|
.shutdown = dlp_tty_port_shutdown,
|
|
};
|
|
|
|
/****************************************************************************
|
|
*
|
|
* Exported functions
|
|
*
|
|
***************************************************************************/
|
|
|
|
static int dlp_tty_ctx_cleanup(struct dlp_channel *ch_ctx);
|
|
|
|
struct dlp_channel *dlp_tty_ctx_create(unsigned int ch_id,
|
|
unsigned int hsi_channel,
|
|
struct device *dev)
|
|
{
|
|
struct hsi_client *client = to_hsi_client(dev);
|
|
struct tty_driver *new_drv;
|
|
struct dlp_channel *ch_ctx;
|
|
struct dlp_tty_context *tty_ctx;
|
|
struct hsi_msg *hsi_msg, *hsi_msg_tmp;
|
|
int ret;
|
|
|
|
ch_ctx = kzalloc(sizeof(struct dlp_channel), GFP_KERNEL);
|
|
if (!ch_ctx) {
|
|
pr_err(DRVNAME ": Out of memory (tty_ch_ctx)\n");
|
|
return NULL;
|
|
}
|
|
|
|
/* Allocate the context private data */
|
|
tty_ctx = kzalloc(sizeof(struct dlp_tty_context), GFP_KERNEL);
|
|
if (!tty_ctx) {
|
|
pr_err(DRVNAME ": Out of memory (tty_ctx)\n");
|
|
goto free_ch;
|
|
}
|
|
|
|
/* Allocate & configure the TTY driver */
|
|
new_drv = alloc_tty_driver(1);
|
|
if (!new_drv) {
|
|
pr_err(DRVNAME ": alloc_tty_driver failed\n");
|
|
goto free_ctx;
|
|
}
|
|
|
|
new_drv->magic = TTY_DRIVER_MAGIC;
|
|
new_drv->owner = THIS_MODULE;
|
|
new_drv->driver_name = DRVNAME;
|
|
new_drv->name = IPC_TTYNAME;
|
|
new_drv->minor_start = 0;
|
|
new_drv->num = DLP_TTY_DEV_NUM;
|
|
new_drv->type = TTY_DRIVER_TYPE_SERIAL;
|
|
new_drv->subtype = SERIAL_TYPE_NORMAL;
|
|
new_drv->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;
|
|
new_drv->init_termios = dlp_termios_init;
|
|
|
|
tty_set_operations(new_drv, &dlp_driver_tty_ops);
|
|
|
|
/* Register the TTY driver */
|
|
ret = tty_register_driver(new_drv);
|
|
if (ret) {
|
|
pr_err(DRVNAME ": tty_register_driver failed (%d)\n", ret);
|
|
goto free_drv;
|
|
}
|
|
|
|
ch_ctx->ch_data = tty_ctx;
|
|
ch_ctx->ch_id = ch_id;
|
|
ch_ctx->hsi_channel = hsi_channel;
|
|
/* Temporay test waiting for the modem FW */
|
|
if (dlp_drv.flow_ctrl)
|
|
ch_ctx->use_flow_ctrl = 1;
|
|
ch_ctx->rx.config = client->rx_cfg;
|
|
ch_ctx->tx.config = client->tx_cfg;
|
|
|
|
spin_lock_init(&ch_ctx->lock);
|
|
init_waitqueue_head(&ch_ctx->tx_empty_event);
|
|
|
|
/* Hangup context */
|
|
dlp_ctrl_hangup_ctx_init(ch_ctx, dlp_tty_hsi_tx_timeout_cb);
|
|
|
|
/* Register the PDUs push, Reset/Coredump, cleanup CBs */
|
|
ch_ctx->push_rx_pdus = dlp_tty_push_rx_pdus;
|
|
ch_ctx->dump_state = dlp_dump_channel_state;
|
|
ch_ctx->cleanup = dlp_tty_ctx_cleanup;
|
|
|
|
/* TX & RX contexts */
|
|
dlp_xfer_ctx_init(ch_ctx,
|
|
DLP_TTY_TX_PDU_SIZE, DLP_HSI_TX_DELAY,
|
|
DLP_HSI_TX_WAIT_FIFO, DLP_HSI_TX_CTRL_FIFO,
|
|
dlp_tty_complete_tx, HSI_MSG_WRITE);
|
|
|
|
dlp_xfer_ctx_init(ch_ctx,
|
|
DLP_TTY_RX_PDU_SIZE, DLP_HSI_RX_DELAY,
|
|
DLP_HSI_RX_WAIT_FIFO, DLP_HSI_RX_CTRL_FIFO,
|
|
dlp_tty_complete_rx, HSI_MSG_READ);
|
|
|
|
INIT_WORK(&ch_ctx->start_tx_w, dlp_do_start_tx);
|
|
INIT_WORK(&ch_ctx->stop_tx_w, dlp_do_send_nop);
|
|
|
|
INIT_WORK(&tty_ctx->do_tty_forward, dlp_do_tty_forward);
|
|
|
|
ch_ctx->tx.timer.function = dlp_tty_tx_stop;
|
|
ch_ctx->rx.timer.function = dlp_tty_rx_forward_retry;
|
|
|
|
/* Register the TTY device (port) */
|
|
tty_port_init(&(tty_ctx->tty_prt));
|
|
tty_ctx->tty_prt.ops = &dlp_port_tty_ops;
|
|
|
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,9,0)
|
|
if (!tty_port_register_device(&tty_ctx->tty_prt, new_drv, 0, dev)) {
|
|
#else
|
|
if (!tty_register_device(new_drv, 0, dev)) {
|
|
#endif
|
|
|
|
pr_err(DRVNAME ": tty_register_device failed (%d)\n", ret);
|
|
goto unreg_drv;
|
|
}
|
|
|
|
tty_ctx->ch_ctx = ch_ctx;
|
|
tty_ctx->tty_drv = new_drv;
|
|
|
|
/* Allocate TX FIFO */
|
|
ret = dlp_allocate_pdus_pool(ch_ctx, &ch_ctx->tx);
|
|
if (ret) {
|
|
pr_err(DRVNAME ": Cant allocate TX FIFO pdus for ch%d\n",
|
|
ch_id);
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Allocate RX FIFO */
|
|
ret = dlp_allocate_pdus_pool(ch_ctx, &ch_ctx->rx);
|
|
if (ret) {
|
|
pr_err(DRVNAME ": Cant allocate RX FIFO pdus for ch%d\n",
|
|
ch_id);
|
|
goto free_tx_fifo;
|
|
}
|
|
|
|
return ch_ctx;
|
|
|
|
unreg_drv:
|
|
tty_unregister_driver(new_drv);
|
|
|
|
free_drv:
|
|
put_tty_driver(new_drv);
|
|
|
|
free_ctx:
|
|
kfree(tty_ctx);
|
|
|
|
free_ch:
|
|
kfree(ch_ctx);
|
|
|
|
pr_err(DRVNAME": Failed to create context for ch%d", ch_id);
|
|
return NULL;
|
|
|
|
free_tx_fifo:
|
|
/* Free tx fifo */
|
|
list_for_each_entry_safe(hsi_msg, hsi_msg_tmp,
|
|
&ch_ctx->tx.recycled_pdus, link) {
|
|
list_del(&hsi_msg->link);
|
|
dlp_pdu_free(hsi_msg, hsi_msg->channel);
|
|
}
|
|
|
|
cleanup:
|
|
dlp_tty_ctx_delete(ch_ctx);
|
|
|
|
pr_err(DRVNAME": Failed to create context for ch%d", ch_id);
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* @brief This function will:
|
|
* - Delete TX/RX timer
|
|
* - Flush RX/TX queues
|
|
* - Unregister the TTY device
|
|
*
|
|
* @param ch_ctx: TTY channel context
|
|
*
|
|
* @return 0 when sucess, error code otherwise
|
|
*/
|
|
static int dlp_tty_ctx_cleanup(struct dlp_channel *ch_ctx)
|
|
{
|
|
int ret = 0;
|
|
struct dlp_tty_context *tty_ctx = ch_ctx->ch_data;
|
|
struct tty_struct *tty;
|
|
|
|
dlp_tty_cleanup(ch_ctx);
|
|
|
|
tty = tty_port_tty_get(&tty_ctx->tty_prt);
|
|
if (tty) {
|
|
/* Clean any waiting data to release potential
|
|
dlp_tty_wait_until_sent lock */
|
|
hsi_flush(dlp_drv.client);
|
|
|
|
tty_vhangup(tty);
|
|
tty_kref_put(tty);
|
|
}
|
|
|
|
/* Clear the hangup context */
|
|
dlp_ctrl_hangup_ctx_deinit(ch_ctx);
|
|
|
|
/* Unregister device */
|
|
tty_unregister_device(tty_ctx->tty_drv, 0);
|
|
|
|
/* Unregister driver */
|
|
tty_unregister_driver(tty_ctx->tty_drv);
|
|
|
|
/* Free */
|
|
put_tty_driver(tty_ctx->tty_drv);
|
|
tty_ctx->tty_drv = NULL;
|
|
|
|
/* Delete the xfers context */
|
|
dlp_xfer_ctx_clear(&ch_ctx->tx);
|
|
dlp_xfer_ctx_clear(&ch_ctx->rx);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* This function will release the allocated memory
|
|
* done in the _ctx_create function
|
|
*/
|
|
int dlp_tty_ctx_delete(struct dlp_channel *ch_ctx)
|
|
{
|
|
struct dlp_tty_context *tty_ctx = ch_ctx->ch_data;
|
|
|
|
/* Free the tty_ctx */
|
|
kfree(tty_ctx);
|
|
|
|
/* Free the ch_ctx */
|
|
kfree(ch_ctx);
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Set the TTY close/TX timeout state
|
|
*/
|
|
void dlp_tty_set_link_valid(int tty_closed, int tx_timeout)
|
|
{
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&dlp_drv.lock, flags);
|
|
dlp_drv.tty_closed = tty_closed;
|
|
dlp_drv.tx_timeout = tx_timeout;
|
|
spin_unlock_irqrestore(&dlp_drv.lock, flags);
|
|
}
|
|
|
|
/*
|
|
* Check the TTY link readiness state
|
|
*
|
|
* @return:
|
|
* - 0 (invalid) : in case of TTY close or TX timeout
|
|
* - 1 (valid) : Otherwise
|
|
*/
|
|
int dlp_tty_is_link_valid(void)
|
|
{
|
|
int valid = 1;
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&dlp_drv.lock, flags);
|
|
if ((dlp_drv.tty_closed) || (dlp_drv.tx_timeout))
|
|
valid = 0;
|
|
spin_unlock_irqrestore(&dlp_drv.lock, flags);
|
|
|
|
return valid;
|
|
}
|