1634 lines
40 KiB
C
Executable File
1634 lines
40 KiB
C
Executable File
/*
|
|
* Copyright (C) 2010 Samsung Electronics.
|
|
*
|
|
* This software is licensed under the terms of the GNU General Public
|
|
* License version 2, as published by the Free Software Foundation, and
|
|
* may be copied, distributed, and modified under those terms.
|
|
*
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
#include <linux/kernel.h>
|
|
#include <linux/init.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/poll.h>
|
|
#include <linux/if_arp.h>
|
|
#include <linux/ip.h>
|
|
#include <linux/if_ether.h>
|
|
#include <linux/etherdevice.h>
|
|
#include <linux/device.h>
|
|
#include <linux/module.h>
|
|
#include <net/ip.h>
|
|
#include <linux/ip.h>
|
|
#include <linux/tcp.h>
|
|
#include <linux/time.h>
|
|
#include <linux/timer.h>
|
|
#include <uapi/linux/net_dropdump.h>
|
|
#include "modem_prj.h"
|
|
#include "modem_utils.h"
|
|
|
|
static u8 sipc5_build_config(struct io_device *iod, struct link_device *ld,
|
|
unsigned int count);
|
|
|
|
static void sipc5_build_header(struct io_device *iod, u8 *buff, u8 cfg,
|
|
unsigned int tx_bytes, unsigned int remains);
|
|
|
|
static ssize_t show_waketime(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
unsigned int msec;
|
|
char *p = buf;
|
|
struct miscdevice *miscdev = dev_get_drvdata(dev);
|
|
struct io_device *iod = container_of(miscdev, struct io_device,
|
|
miscdev);
|
|
|
|
msec = jiffies_to_msecs(iod->waketime);
|
|
|
|
p += sprintf(buf, "raw waketime : %ums\n", msec);
|
|
|
|
return p - buf;
|
|
}
|
|
|
|
static ssize_t store_waketime(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
unsigned long msec;
|
|
int ret;
|
|
struct miscdevice *miscdev = dev_get_drvdata(dev);
|
|
struct io_device *iod = container_of(miscdev, struct io_device,
|
|
miscdev);
|
|
|
|
if (!iod) {
|
|
pr_err("mif: %s: INVALID IO device\n", miscdev->name);
|
|
return -EINVAL;
|
|
}
|
|
|
|
ret = kstrtoul(buf, 10, &msec);
|
|
if (ret)
|
|
return count;
|
|
|
|
if (!msec) {
|
|
mif_info("%s: (%ld) is not valied, use previous value(%d)\n",
|
|
iod->name, msec,
|
|
jiffies_to_msecs(iod->mc->iod->waketime));
|
|
return count;
|
|
}
|
|
|
|
iod->waketime = msecs_to_jiffies(msec);
|
|
#ifdef DEBUG_MODEM_IF
|
|
mif_err("%s: waketime = %lu ms\n", iod->name, msec);
|
|
#endif
|
|
|
|
if (iod->format == IPC_MULTI_RAW) {
|
|
struct modem_shared *msd = iod->msd;
|
|
unsigned int i;
|
|
|
|
for (i = SIPC_CH_ID_PDP_0; i < SIPC_CH_ID_BT_DUN; i++) {
|
|
iod = get_iod_with_channel(msd, i);
|
|
if (iod) {
|
|
iod->waketime = msecs_to_jiffies(msec);
|
|
#ifdef DEBUG_MODEM_IF
|
|
mif_err("%s: waketime = %lu ms\n",
|
|
iod->name, msec);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static struct device_attribute attr_waketime =
|
|
__ATTR(waketime, S_IRUGO | S_IWUSR, show_waketime, store_waketime);
|
|
|
|
static ssize_t show_loopback(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct miscdevice *miscdev = dev_get_drvdata(dev);
|
|
struct modem_shared *msd =
|
|
container_of(miscdev, struct io_device, miscdev)->msd;
|
|
unsigned char *ip = (unsigned char *)&msd->loopback_ipaddr;
|
|
char *p = buf;
|
|
|
|
p += sprintf(buf, "%u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]);
|
|
|
|
return p - buf;
|
|
}
|
|
|
|
static ssize_t store_loopback(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
struct miscdevice *miscdev = dev_get_drvdata(dev);
|
|
struct modem_shared *msd =
|
|
container_of(miscdev, struct io_device, miscdev)->msd;
|
|
|
|
msd->loopback_ipaddr = ipv4str_to_be32(buf, count);
|
|
|
|
return count;
|
|
}
|
|
|
|
static struct device_attribute attr_loopback =
|
|
__ATTR(loopback, S_IRUGO | S_IWUSR, show_loopback, store_loopback);
|
|
|
|
static void iodev_showtxlink(struct io_device *iod, void *args)
|
|
{
|
|
char **p = (char **)args;
|
|
struct link_device *ld = get_current_link(iod);
|
|
|
|
if (iod->io_typ == IODEV_NET && IS_CONNECTED(iod, ld))
|
|
*p += sprintf(*p, "%s<->%s\n", iod->name, ld->name);
|
|
}
|
|
|
|
static ssize_t show_txlink(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct miscdevice *miscdev = dev_get_drvdata(dev);
|
|
struct modem_shared *msd =
|
|
container_of(miscdev, struct io_device, miscdev)->msd;
|
|
char *p = buf;
|
|
|
|
iodevs_for_each(msd, iodev_showtxlink, &p);
|
|
|
|
return p - buf;
|
|
}
|
|
|
|
static ssize_t store_txlink(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
/* don't change without gpio dynamic switching */
|
|
return -EINVAL;
|
|
}
|
|
|
|
static struct device_attribute attr_txlink =
|
|
__ATTR(txlink, S_IRUGO | S_IWUSR, show_txlink, store_txlink);
|
|
|
|
static inline void iodev_lock_wlock(struct io_device *iod)
|
|
{
|
|
wake_lock_timeout(&iod->wakelock,
|
|
iod->waketime ?: msecs_to_jiffies(200));
|
|
}
|
|
|
|
static int queue_skb_to_iod(struct sk_buff *skb, struct io_device *iod)
|
|
{
|
|
struct sk_buff_head *rxq = &iod->sk_rx_q;
|
|
int len = skb->len;
|
|
|
|
if (iod->attrs & IODEV_ATTR(ATTR_NO_CHECK_MAXQ))
|
|
goto enqueue;
|
|
|
|
if (rxq->qlen > MAX_IOD_RXQ_LEN) {
|
|
mif_err_limited("%s: %s may be dead (rxq->qlen %d > %d)\n",
|
|
iod->name, iod->app ? iod->app : "corresponding",
|
|
rxq->qlen, MAX_IOD_RXQ_LEN);
|
|
dev_kfree_skb_any(skb);
|
|
goto exit;
|
|
}
|
|
|
|
enqueue:
|
|
mif_debug("%s: rxq->qlen = %d\n", iod->name, rxq->qlen);
|
|
skb_queue_tail(rxq, skb);
|
|
|
|
exit:
|
|
wake_up(&iod->wq);
|
|
return len;
|
|
}
|
|
|
|
static int rx_drain(struct sk_buff *skb)
|
|
{
|
|
dev_kfree_skb_any(skb);
|
|
return 0;
|
|
}
|
|
|
|
static int rx_loopback(struct sk_buff *skb)
|
|
{
|
|
struct io_device *iod = skbpriv(skb)->iod;
|
|
struct link_device *ld = skbpriv(skb)->ld;
|
|
int ret;
|
|
|
|
ret = ld->send(ld, iod, skb);
|
|
if (ret < 0) {
|
|
mif_err("%s->%s: ERR! ld->send fail (err %d)\n",
|
|
iod->name, ld->name, ret);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int gather_multi_frame(struct sipc5_link_header *hdr,
|
|
struct sk_buff *skb)
|
|
{
|
|
struct multi_frame_control ctrl = hdr->ctrl;
|
|
struct io_device *iod = skbpriv(skb)->iod;
|
|
struct modem_ctl *mc = iod->mc;
|
|
struct sk_buff_head *multi_q = &iod->sk_multi_q[ctrl.id];
|
|
int len = skb->len;
|
|
|
|
#ifdef DEBUG_MODEM_IF
|
|
/* If there has been no multiple frame with this ID, ... */
|
|
if (skb_queue_empty(multi_q)) {
|
|
struct sipc_fmt_hdr *fh = (struct sipc_fmt_hdr *)skb->data;
|
|
mif_err("%s<-%s: start of multi-frame (ID:%d len:%d)\n",
|
|
iod->name, mc->name, ctrl.id, fh->len);
|
|
}
|
|
#endif
|
|
skb_queue_tail(multi_q, skb);
|
|
|
|
if (ctrl.more) {
|
|
/* The last frame has not arrived yet. */
|
|
mif_err("%s<-%s: recv multi-frame (ID:%d rcvd:%d)\n",
|
|
iod->name, mc->name, ctrl.id, skb->len);
|
|
} else {
|
|
struct sk_buff_head *rxq = &iod->sk_rx_q;
|
|
unsigned long flags;
|
|
|
|
/* It is the last frame because the "more" bit is 0. */
|
|
mif_err("%s<-%s: end of multi-frame (ID:%d rcvd:%d)\n",
|
|
iod->name, mc->name, ctrl.id, skb->len);
|
|
|
|
spin_lock_irqsave(&rxq->lock, flags);
|
|
skb_queue_splice_tail_init(multi_q, rxq);
|
|
spin_unlock_irqrestore(&rxq->lock, flags);
|
|
|
|
wake_up(&iod->wq);
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
static inline int rx_frame_with_link_header(struct sk_buff *skb)
|
|
{
|
|
struct sipc5_link_header *hdr;
|
|
bool multi_frame = sipc5_multi_frame(skb->data);
|
|
int hdr_len = sipc5_get_hdr_len(skb->data);
|
|
|
|
/* Remove SIPC5 link header */
|
|
hdr = (struct sipc5_link_header *)skb->data;
|
|
skb_pull(skb, hdr_len);
|
|
|
|
if (multi_frame)
|
|
return gather_multi_frame(hdr, skb);
|
|
else
|
|
return queue_skb_to_iod(skb, skbpriv(skb)->iod);
|
|
}
|
|
|
|
static int rx_fmt_ipc(struct sk_buff *skb)
|
|
{
|
|
if (skbpriv(skb)->lnk_hdr)
|
|
return rx_frame_with_link_header(skb);
|
|
else
|
|
return queue_skb_to_iod(skb, skbpriv(skb)->iod);
|
|
}
|
|
|
|
static int rx_raw_misc(struct sk_buff *skb)
|
|
{
|
|
struct io_device *iod = skbpriv(skb)->iod;
|
|
|
|
if (skbpriv(skb)->lnk_hdr) {
|
|
/* Remove the SIPC5 link header */
|
|
skb_pull(skb, sipc5_get_hdr_len(skb->data));
|
|
}
|
|
|
|
return queue_skb_to_iod(skb, iod);
|
|
}
|
|
|
|
#ifdef CONFIG_MODEM_IF_NET_GRO
|
|
static int check_gro_support(struct sk_buff *skb)
|
|
{
|
|
switch (skb->data[0] & 0xF0) {
|
|
case 0x40:
|
|
return (ip_hdr(skb)->protocol == IPPROTO_TCP);
|
|
|
|
case 0x60:
|
|
return (ipv6_hdr(skb)->nexthdr == IPPROTO_TCP);
|
|
}
|
|
return 0;
|
|
}
|
|
#else
|
|
static int check_gro_support(struct sk_buff *skb)
|
|
{
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
static int rx_multi_pdp(struct sk_buff *skb)
|
|
{
|
|
struct link_device *ld = skbpriv(skb)->ld;
|
|
struct io_device *iod = skbpriv(skb)->iod;
|
|
struct net_device *ndev;
|
|
struct iphdr *iphdr;
|
|
int len = skb->len;
|
|
int ret;
|
|
|
|
ndev = iod->ndev;
|
|
if (!ndev) {
|
|
mif_info("%s: ERR! no iod->ndev\n", iod->name);
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (skbpriv(skb)->lnk_hdr) {
|
|
/* Remove the SIPC5 link header */
|
|
skb_pull(skb, sipc5_get_hdr_len(skb->data));
|
|
}
|
|
|
|
skb->dev = ndev;
|
|
ndev->stats.rx_packets++;
|
|
ndev->stats.rx_bytes += skb->len;
|
|
|
|
/* check the version of IP */
|
|
iphdr = (struct iphdr *)skb->data;
|
|
if (iphdr->version == IPv6)
|
|
skb->protocol = htons(ETH_P_IPV6);
|
|
else
|
|
skb->protocol = htons(ETH_P_IP);
|
|
|
|
if (iod->use_handover) {
|
|
struct ethhdr *ehdr;
|
|
const char source[ETH_ALEN] = SOURCE_MAC_ADDR;
|
|
|
|
ehdr = (struct ethhdr *)skb_push(skb, sizeof(struct ethhdr));
|
|
memcpy(ehdr->h_dest, ndev->dev_addr, ETH_ALEN);
|
|
memcpy(ehdr->h_source, source, ETH_ALEN);
|
|
ehdr->h_proto = skb->protocol;
|
|
skb->ip_summed = CHECKSUM_UNNECESSARY;
|
|
skb_reset_mac_header(skb);
|
|
skb_pull(skb, sizeof(struct ethhdr));
|
|
}
|
|
|
|
#ifdef DEBUG_MODEM_IF_IP_DATA
|
|
print_ipv4_packet(skb->data, RX);
|
|
#endif
|
|
#if defined(DEBUG_MODEM_IF_IODEV_RX) && defined(DEBUG_MODEM_IF_PS_DATA)
|
|
mif_pkt(iod->id, "IOD-RX", skb);
|
|
#endif
|
|
|
|
skb_reset_transport_header(skb);
|
|
skb_reset_network_header(skb);
|
|
skb_reset_mac_header(skb);
|
|
|
|
if (check_gro_support(skb)) {
|
|
ret = napi_gro_receive(napi_get_current(), skb);
|
|
if (ret == GRO_DROP) {
|
|
ndev->stats.rx_dropped++;
|
|
}
|
|
|
|
if (ld->gro_flush)
|
|
ld->gro_flush(ld);
|
|
} else {
|
|
#ifdef CONFIG_LINK_DEVICE_NAPI
|
|
ret = netif_receive_skb(skb);
|
|
#else /* !CONFIG_LINK_DEVICE_NAPI */
|
|
if (in_interrupt())
|
|
ret = netif_rx(skb);
|
|
else
|
|
ret = netif_rx_ni(skb);
|
|
#endif /* CONFIG_LINK_DEVICE_NAPI */
|
|
|
|
if (ret != NET_RX_SUCCESS) {
|
|
ndev->stats.rx_dropped++;
|
|
}
|
|
}
|
|
return len;
|
|
}
|
|
|
|
static int rx_demux(struct link_device *ld, struct sk_buff *skb)
|
|
{
|
|
struct io_device *iod;
|
|
u8 ch = skbpriv(skb)->sipc_ch;
|
|
|
|
if (unlikely(ch == 0)) {
|
|
mif_err("%s: ERR! invalid ch# %d\n", ld->name, ch);
|
|
return -ENODEV;
|
|
}
|
|
|
|
/* IP loopback */
|
|
if (ch == DATA_LOOPBACK_CHANNEL && ld->msd->loopback_ipaddr)
|
|
ch = SIPC_CH_ID_PDP_0;
|
|
|
|
iod = link_get_iod_with_channel(ld, ch);
|
|
if (unlikely(!iod)) {
|
|
mif_err("%s: ERR! no iod with ch# %d\n", ld->name, ch);
|
|
return -ENODEV;
|
|
}
|
|
|
|
/* Don't care whether or not DATA_DRAIN_CHANNEL is opened */
|
|
if (iod->id == DATA_DRAIN_CHANNEL)
|
|
return rx_drain(skb);
|
|
|
|
/* Don't care whether or not CP2AP_LOOPBACK_CHANNEL is opened. */
|
|
if (iod->id == CP2AP_LOOPBACK_CHANNEL)
|
|
return rx_loopback(skb);
|
|
|
|
if (atomic_read(&iod->opened) <= 0) {
|
|
mif_err_limited("%s: ERR! %s is not opened\n",
|
|
ld->name, iod->name);
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (sipc5_fmt_ch(ch)) {
|
|
iod->mc->receive_first_ipc = 1;
|
|
return rx_fmt_ipc(skb);
|
|
} else if (sipc_ps_ch(ch))
|
|
return rx_multi_pdp(skb);
|
|
else
|
|
return rx_raw_misc(skb);
|
|
}
|
|
|
|
static int io_dev_recv_skb_single_from_link_dev(struct io_device *iod,
|
|
struct link_device *ld,
|
|
struct sk_buff *skb)
|
|
{
|
|
int err;
|
|
|
|
iodev_lock_wlock(iod);
|
|
|
|
if (skbpriv(skb)->lnk_hdr && ld->aligned) {
|
|
/* Cut off the padding in the current SIPC5 frame */
|
|
skb_trim(skb, sipc5_get_frame_len(skb->data));
|
|
}
|
|
|
|
err = rx_demux(ld, skb);
|
|
if (err < 0) {
|
|
mif_err_limited("%s<-%s: ERR! rx_demux fail (err %d)\n",
|
|
iod->name, ld->name, err);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/*
|
|
@brief called by a link device with the "recv_net_skb" method to upload each PS
|
|
data packet to the network protocol stack
|
|
*/
|
|
static int io_dev_recv_net_skb_from_link_dev(struct io_device *iod,
|
|
struct link_device *ld,
|
|
struct sk_buff *skb)
|
|
{
|
|
if (unlikely(atomic_read(&iod->opened) <= 0)) {
|
|
struct modem_ctl *mc = iod->mc;
|
|
mif_err_limited("%s: %s<-%s: ERR! %s is not opened\n",
|
|
ld->name, iod->name, mc->name, iod->name);
|
|
return -ENODEV;
|
|
}
|
|
|
|
iodev_lock_wlock(iod);
|
|
|
|
return rx_multi_pdp(skb);
|
|
}
|
|
|
|
/* inform the IO device that the modem is now online or offline or
|
|
* crashing or whatever...
|
|
*/
|
|
static void io_dev_modem_state_changed(struct io_device *iod,
|
|
enum modem_state state)
|
|
{
|
|
struct modem_ctl *mc = iod->mc;
|
|
enum modem_state old_state = mc->phone_state;
|
|
|
|
if (state == old_state)
|
|
goto exit;
|
|
|
|
mc->phone_state = state;
|
|
mif_err("%s->state changed (%s -> %s)\n", mc->name,
|
|
cp_state_str(old_state), cp_state_str(state));
|
|
|
|
exit:
|
|
if (state == STATE_CRASH_RESET
|
|
|| state == STATE_CRASH_EXIT
|
|
|| state == STATE_NV_REBUILDING
|
|
|| state == STATE_CRASH_WATCHDOG) {
|
|
if (atomic_read(&iod->opened) > 0)
|
|
wake_up(&iod->wq);
|
|
}
|
|
}
|
|
|
|
static void io_dev_sim_state_changed(struct io_device *iod, bool sim_online)
|
|
{
|
|
if (atomic_read(&iod->opened) == 0) {
|
|
mif_info("%s: ERR! not opened\n", iod->name);
|
|
} else if (iod->mc->sim_state.online == sim_online) {
|
|
mif_info("%s: SIM state not changed\n", iod->name);
|
|
} else {
|
|
iod->mc->sim_state.online = sim_online;
|
|
iod->mc->sim_state.changed = true;
|
|
mif_info("%s: SIM state changed {online %d, changed %d}\n",
|
|
iod->name, iod->mc->sim_state.online,
|
|
iod->mc->sim_state.changed);
|
|
wake_up(&iod->wq);
|
|
}
|
|
}
|
|
|
|
static void iodev_dump_status(struct io_device *iod, void *args)
|
|
{
|
|
if (iod->format == IPC_RAW && iod->io_typ == IODEV_NET) {
|
|
struct link_device *ld = get_current_link(iod);
|
|
mif_com_log(iod->mc->msd, "%s: %s\n", iod->name, ld->name);
|
|
}
|
|
}
|
|
|
|
static int misc_open(struct inode *inode, struct file *filp)
|
|
{
|
|
struct io_device *iod = to_io_device(filp->private_data);
|
|
struct modem_shared *msd = iod->msd;
|
|
struct link_device *ld;
|
|
int ret;
|
|
|
|
filp->private_data = (void *)iod;
|
|
|
|
atomic_inc(&iod->opened);
|
|
|
|
list_for_each_entry(ld, &msd->link_dev_list, list) {
|
|
if (IS_CONNECTED(iod, ld) && ld->init_comm) {
|
|
ret = ld->init_comm(ld, iod);
|
|
if (ret < 0) {
|
|
mif_err("%s<->%s: ERR! init_comm fail(%d)\n",
|
|
iod->name, ld->name, ret);
|
|
atomic_dec(&iod->opened);
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
mif_err("%s (opened %d) by %s\n",
|
|
iod->name, atomic_read(&iod->opened), current->comm);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int misc_release(struct inode *inode, struct file *filp)
|
|
{
|
|
struct io_device *iod = (struct io_device *)filp->private_data;
|
|
struct modem_shared *msd = iod->msd;
|
|
struct link_device *ld;
|
|
int i;
|
|
|
|
if (atomic_dec_and_test(&iod->opened)) {
|
|
skb_queue_purge(&iod->sk_rx_q);
|
|
|
|
/* purge multi_frame queue */
|
|
for (i = 0; i < NUM_SIPC_MULTI_FRAME_IDS; i++)
|
|
skb_queue_purge(&iod->sk_multi_q[i]);
|
|
}
|
|
|
|
list_for_each_entry(ld, &msd->link_dev_list, list) {
|
|
if (IS_CONNECTED(iod, ld) && ld->terminate_comm)
|
|
ld->terminate_comm(ld, iod);
|
|
}
|
|
|
|
mif_err("%s (opened %d) by %s\n",
|
|
iod->name, atomic_read(&iod->opened), current->comm);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static unsigned int misc_poll(struct file *filp, struct poll_table_struct *wait)
|
|
{
|
|
struct io_device *iod = (struct io_device *)filp->private_data;
|
|
struct modem_ctl *mc;
|
|
struct sk_buff_head *rxq;
|
|
|
|
if (!iod)
|
|
return POLLERR;
|
|
|
|
mc = iod->mc;
|
|
rxq = &iod->sk_rx_q;
|
|
|
|
if (skb_queue_empty(rxq))
|
|
poll_wait(filp, &iod->wq, wait);
|
|
|
|
switch (mc->phone_state) {
|
|
case STATE_BOOTING:
|
|
case STATE_ONLINE:
|
|
if (!mc->sim_state.changed) {
|
|
if (!skb_queue_empty(rxq))
|
|
return POLLIN | POLLRDNORM;
|
|
else /* wq is waken up without rx, return for wait */
|
|
return 0;
|
|
}
|
|
/* fall through, if sim_state has been changed */
|
|
case STATE_CRASH_EXIT:
|
|
case STATE_CRASH_RESET:
|
|
case STATE_NV_REBUILDING:
|
|
case STATE_CRASH_WATCHDOG:
|
|
/* report crash only if iod is fmt/boot device */
|
|
if (iod->format == IPC_FMT) {
|
|
mif_err("%s: %s.state == %s\n", iod->name, mc->name,
|
|
mc_state(mc));
|
|
return POLLHUP;
|
|
} else if (iod->format == IPC_BOOT || sipc5_boot_ch(iod->id)) {
|
|
mif_err("%s: %s.state == %s\n", iod->name, mc->name,
|
|
mc_state(mc));
|
|
return POLLHUP;
|
|
} else if (iod->format == IPC_DUMP || sipc5_dump_ch(iod->id)) {
|
|
if (!skb_queue_empty(rxq))
|
|
return POLLIN | POLLRDNORM;
|
|
else
|
|
return 0;
|
|
} else {
|
|
mif_err("%s: %s.state == %s\n", iod->name, mc->name,
|
|
mc_state(mc));
|
|
|
|
/* give delay to prevent infinite sys_poll call from
|
|
* select() in APP layer without 'sleep' user call takes
|
|
* almost 100% cpu usage when it is looked up by 'top'
|
|
* command.
|
|
*/
|
|
msleep(20);
|
|
}
|
|
break;
|
|
|
|
case STATE_OFFLINE:
|
|
/* fall through */
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static long misc_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
|
|
{
|
|
struct io_device *iod = (struct io_device *)filp->private_data;
|
|
struct link_device *ld = get_current_link(iod);
|
|
struct modem_ctl *mc = iod->mc;
|
|
enum modem_state p_state;
|
|
unsigned long size;
|
|
int tx_link;
|
|
int ret;
|
|
|
|
switch (cmd) {
|
|
case IOCTL_MODEM_ON:
|
|
if (mc->ops.modem_on) {
|
|
mif_err("%s: IOCTL_MODEM_ON\n", iod->name);
|
|
return mc->ops.modem_on(mc);
|
|
}
|
|
mif_err("%s: !mc->ops.modem_on\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_OFF:
|
|
if (mc->ops.modem_off) {
|
|
mif_err("%s: IOCTL_MODEM_OFF\n", iod->name);
|
|
return mc->ops.modem_off(mc);
|
|
}
|
|
mif_err("%s: !mc->ops.modem_off\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_RESET:
|
|
if (mc->ops.modem_reset) {
|
|
mif_err("%s: IOCTL_MODEM_RESET\n", iod->name);
|
|
ret = mc->ops.modem_reset(mc);
|
|
if (ld->reset_zerocopy)
|
|
ld->reset_zerocopy(ld);
|
|
return ret;
|
|
}
|
|
mif_err("%s: !mc->ops.modem_reset\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_BOOT_ON:
|
|
if (mc->ops.modem_boot_on) {
|
|
mif_err("%s: IOCTL_MODEM_BOOT_ON\n", iod->name);
|
|
return mc->ops.modem_boot_on(mc);
|
|
}
|
|
mif_err("%s: !mc->ops.modem_boot_on\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_BOOT_OFF:
|
|
if (mc->ops.modem_boot_off) {
|
|
mif_err("%s: IOCTL_MODEM_BOOT_OFF\n", iod->name);
|
|
return mc->ops.modem_boot_off(mc);
|
|
}
|
|
mif_err("%s: !mc->ops.modem_boot_off\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_BOOT_DONE:
|
|
mif_err("%s: IOCTL_MODEM_BOOT_DONE\n", iod->name);
|
|
if (mc->ops.modem_boot_done)
|
|
return mc->ops.modem_boot_done(mc);
|
|
return 0;
|
|
|
|
case IOCTL_MODEM_STATUS:
|
|
mif_debug("%s: IOCTL_MODEM_STATUS\n", iod->name);
|
|
|
|
p_state = mc->phone_state;
|
|
|
|
if (p_state != STATE_ONLINE) {
|
|
mif_debug("%s: IOCTL_MODEM_STATUS (state %s)\n",
|
|
iod->name, cp_state_str(p_state));
|
|
}
|
|
|
|
if (mc->sim_state.changed) {
|
|
enum modem_state s_state = mc->sim_state.online ?
|
|
STATE_SIM_ATTACH : STATE_SIM_DETACH;
|
|
mc->sim_state.changed = false;
|
|
return s_state;
|
|
}
|
|
|
|
if (p_state == STATE_NV_REBUILDING)
|
|
mc->phone_state = STATE_ONLINE;
|
|
|
|
return p_state;
|
|
|
|
case IOCTL_MODEM_XMIT_BOOT:
|
|
if (ld->xmit_boot) {
|
|
return ld->xmit_boot(ld, iod, arg);
|
|
}
|
|
mif_err("%s: !ld->xmit_boot\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_DL_START:
|
|
if (ld->dload_start) {
|
|
int ret;
|
|
mif_info("%s: IOCTL_MODEM_DL_START\n", iod->name);
|
|
ret = ld->dload_start(ld, iod);
|
|
mc->ops.modem_boot_confirm(mc);
|
|
return ret;
|
|
}
|
|
mif_err("%s: !ld->dload_start\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_FW_UPDATE:
|
|
if (ld->firm_update) {
|
|
mif_info("%s: IOCTL_MODEM_FW_UPDATE\n", iod->name);
|
|
return ld->firm_update(ld, iod, arg);
|
|
}
|
|
mif_err("%s: !ld->firm_update\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_FORCE_CRASH_EXIT:
|
|
if (mc->ops.modem_force_crash_exit) {
|
|
if (arg)
|
|
ld->crash_type = arg;
|
|
mif_err("%s: IOCTL_MODEM_FORCE_CRASH_EXIT (%d)\n",
|
|
iod->name, ld->crash_type);
|
|
return mc->ops.modem_force_crash_exit(mc);
|
|
}
|
|
mif_err("%s: !mc->ops.modem_force_crash_exit\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_DUMP_START:
|
|
if (mc->ops.modem_dump_start) {
|
|
mif_err("%s: IOCTL_MODEM_DUMP_START\n", iod->name);
|
|
return mc->ops.modem_dump_start(mc);
|
|
} else if (ld->dump_start) {
|
|
mif_err("%s: IOCTL_MODEM_DUMP_START\n", iod->name);
|
|
return ld->dump_start(ld, iod);
|
|
}
|
|
mif_err("%s: !dump_start\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_RAMDUMP_START:
|
|
if (ld->dump_start) {
|
|
mif_info("%s: IOCTL_MODEM_RAMDUMP_START\n", iod->name);
|
|
return ld->dump_start(ld, iod);
|
|
}
|
|
mif_err("%s: !ld->dump_start\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_DUMP_UPDATE:
|
|
if (ld->dump_update) {
|
|
mif_info("%s: IOCTL_MODEM_DUMP_UPDATE\n", iod->name);
|
|
return ld->dump_update(ld, iod, arg);
|
|
}
|
|
mif_err("%s: !ld->dump_update\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_RAMDUMP_STOP:
|
|
if (ld->dump_finish) {
|
|
mif_info("%s: IOCTL_MODEM_RAMDUMP_STOP\n", iod->name);
|
|
return ld->dump_finish(ld, iod, arg);
|
|
}
|
|
mif_err("%s: !ld->dump_finish\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_CP_UPLOAD:
|
|
{
|
|
char *buff = iod->msd->cp_crash_info + strlen(CP_CRASH_TAG);
|
|
void __user *user_buff = (void __user *)arg;
|
|
|
|
mif_err("%s: ERR! IOCTL_MODEM_CP_UPLOAD\n", iod->name);
|
|
strcpy(iod->msd->cp_crash_info, CP_CRASH_TAG);
|
|
if (arg) {
|
|
if (copy_from_user(buff, user_buff, CP_CRASH_INFO_SIZE))
|
|
return -EFAULT;
|
|
}
|
|
panic(iod->msd->cp_crash_info);
|
|
return 0;
|
|
}
|
|
|
|
case IOCTL_MODEM_PROTOCOL_SUSPEND:
|
|
mif_info("%s: IOCTL_MODEM_PROTOCOL_SUSPEND\n", iod->name);
|
|
if (iod->format == IPC_MULTI_RAW) {
|
|
iodevs_for_each(iod->msd, iodev_netif_stop, 0);
|
|
return 0;
|
|
}
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_PROTOCOL_RESUME:
|
|
mif_info("%s: IOCTL_MODEM_PROTOCOL_RESUME\n", iod->name);
|
|
if (iod->format != IPC_MULTI_RAW) {
|
|
iodevs_for_each(iod->msd, iodev_netif_wake, 0);
|
|
return 0;
|
|
}
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MIF_LOG_DUMP:
|
|
{
|
|
void __user *user_buff = (void __user *)arg;
|
|
|
|
iodevs_for_each(iod->msd, iodev_dump_status, 0);
|
|
size = MAX_MIF_BUFF_SIZE;
|
|
if (copy_to_user(user_buff, &size, sizeof(unsigned long)))
|
|
return -EFAULT;
|
|
mif_dump_log(mc->msd, iod);
|
|
return 0;
|
|
}
|
|
|
|
case IOCTL_SHMEM_FULL_DUMP:
|
|
mif_info("%s: IOCTL_SHMEM_FULL_DUMP\n", iod->name);
|
|
if (ld->shmem_dump)
|
|
return ld->shmem_dump(ld, iod, arg);
|
|
else
|
|
return -EINVAL;
|
|
|
|
case IOCTL_VSS_FULL_DUMP:
|
|
mif_info("%s: IOCTL_VSS_FULL_DUMP\n", iod->name);
|
|
if (ld->vss_dump)
|
|
return ld->vss_dump(ld, iod, arg);
|
|
else
|
|
return -EINVAL;
|
|
|
|
case IOCTL_ACPM_FULL_DUMP:
|
|
mif_info("%s: IOCTL_ACPM_FULL_DUMP\n", iod->name);
|
|
if (ld->acpm_dump)
|
|
return ld->acpm_dump(ld, iod, arg);
|
|
else
|
|
return -EINVAL;
|
|
|
|
case IOCTL_CPLOG_FULL_DUMP:
|
|
mif_info("%s: IOCTL_CPLOG_FULL_DUMP\n", iod->name);
|
|
if (ld->cplog_dump)
|
|
return ld->cplog_dump(ld, iod, arg);
|
|
else
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_SET_TX_LINK:
|
|
mif_info("%s: IOCTL_MODEM_SET_TX_LINK\n", iod->name);
|
|
if (copy_from_user(&tx_link, (void __user *)arg, sizeof(int)))
|
|
return -EFAULT;
|
|
|
|
mif_info("cur link: %d, new link: %d\n",
|
|
ld->link_type, tx_link);
|
|
|
|
if (ld->link_type != tx_link) {
|
|
mif_info("change link: %d -> %d\n",
|
|
ld->link_type, tx_link);
|
|
ld = find_linkdev(iod->msd, tx_link);
|
|
if (!ld) {
|
|
mif_err("find_linkdev(%d) fail\n", tx_link);
|
|
return -ENODEV;
|
|
}
|
|
|
|
set_current_link(iod, ld);
|
|
|
|
ld = get_current_link(iod);
|
|
mif_info("%s tx_link change success\n", ld->name);
|
|
}
|
|
return 0;
|
|
|
|
case IOCTL_SECURITY_REQ:
|
|
if (ld->security_req) {
|
|
mif_info("%s: IOCTL_SECURITY_REQUEST\n", iod->name);
|
|
return ld->security_req(ld, iod, arg);
|
|
}
|
|
mif_err("%s: !ld->check_security\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_CRASH_REASON:
|
|
if (ld->crash_reason) {
|
|
mif_info("%s: IOCTL_MODEM_CRASH_REASON\n", iod->name);
|
|
return ld->crash_reason(ld, iod, arg);
|
|
}
|
|
mif_err("%s: !ld->crash_reason\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
case IOCTL_MODEM_AIRPLANE_MODE:
|
|
if (ld->airplane_mode) {
|
|
mif_info("%s: IOCTL_MODEM_AIRPLANE_MODE\n", iod->name);
|
|
return ld->airplane_mode(ld, iod, arg);
|
|
}
|
|
mif_err("%s: !ld->airplane_mode\n", iod->name);
|
|
return -EINVAL;
|
|
|
|
default:
|
|
/* If you need to handle the ioctl for specific link device,
|
|
* then assign the link ioctl handler to ld->ioctl
|
|
* It will be call for specific link ioctl */
|
|
if (ld->ioctl)
|
|
return ld->ioctl(ld, iod, cmd, arg);
|
|
|
|
mif_info("%s: ERR! undefined cmd 0x%X\n", iod->name, cmd);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t misc_write(struct file *filp, const char __user *data,
|
|
size_t count, loff_t *fpos)
|
|
{
|
|
struct io_device *iod = (struct io_device *)filp->private_data;
|
|
struct link_device *ld = get_current_link(iod);
|
|
struct modem_ctl *mc = iod->mc;
|
|
struct sk_buff *skb;
|
|
char *buff;
|
|
int ret;
|
|
u8 cfg;
|
|
unsigned int headroom;
|
|
unsigned int tailroom;
|
|
unsigned int tx_bytes;
|
|
unsigned int copied = 0, tot_frame = 0, copied_frm = 0;
|
|
unsigned int remains;
|
|
unsigned int alloc_size;
|
|
/* 64bit prevent */
|
|
unsigned int cnt = (unsigned int)count;
|
|
#ifdef DEBUG_MODEM_IF
|
|
struct timespec ts;
|
|
#endif
|
|
|
|
#ifdef DEBUG_MODEM_IF
|
|
/* Record the timestamp */
|
|
getnstimeofday(&ts);
|
|
#endif
|
|
|
|
if (iod->format <= IPC_RFS && iod->id == 0)
|
|
return -EINVAL;
|
|
|
|
if (unlikely(!cp_online(mc)) && sipc5_ipc_ch(iod->id)) {
|
|
mif_debug("%s: ERR! %s->state == %s\n",
|
|
iod->name, mc->name, mc_state(mc));
|
|
return -EPERM;
|
|
}
|
|
|
|
if (iod->link_header) {
|
|
cfg = sipc5_build_config(iod, ld, cnt);
|
|
headroom = sipc5_get_hdr_len(&cfg);
|
|
} else {
|
|
cfg = 0;
|
|
headroom = 0;
|
|
}
|
|
|
|
if (unlikely(!mc->receive_first_ipc) && sipc5_log_ch(iod->id))
|
|
return -EBUSY;
|
|
|
|
while (copied < cnt) {
|
|
remains = cnt - copied;
|
|
alloc_size = min_t(unsigned int, remains + headroom,
|
|
iod->max_tx_size ?: remains + headroom);
|
|
|
|
/* Calculate tailroom for padding size */
|
|
if (iod->link_header && ld->aligned)
|
|
tailroom = sipc5_calc_padding_size(alloc_size);
|
|
else
|
|
tailroom = 0;
|
|
|
|
alloc_size += tailroom;
|
|
|
|
skb = alloc_skb(alloc_size, GFP_KERNEL);
|
|
if (!skb) {
|
|
mif_info("%s: ERR! alloc_skb fail (alloc_size:%d)\n",
|
|
iod->name, alloc_size);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
tx_bytes = alloc_size - headroom - tailroom;
|
|
|
|
/* Reserve the space for a link header */
|
|
skb_reserve(skb, headroom);
|
|
|
|
/* Copy an IPC message from the user space to the skb */
|
|
buff = skb_put(skb, tx_bytes);
|
|
if (copy_from_user(buff, data + copied, tx_bytes)) {
|
|
mif_err("%s->%s: ERR! copy_from_user fail(count %lu)\n",
|
|
iod->name, ld->name, (unsigned long)count);
|
|
dev_kfree_skb_any(skb);
|
|
return -EFAULT;
|
|
}
|
|
|
|
/* Update size of copied payload */
|
|
copied += tx_bytes;
|
|
/* Update size of total frame included hdr, pad size */
|
|
tot_frame += alloc_size;
|
|
|
|
/* Store the IO device, the link device, etc. */
|
|
skbpriv(skb)->iod = iod;
|
|
skbpriv(skb)->ld = ld;
|
|
|
|
skbpriv(skb)->lnk_hdr = iod->link_header;
|
|
skbpriv(skb)->sipc_ch = iod->id;
|
|
|
|
#ifdef DEBUG_MODEM_IF
|
|
/* Copy the timestamp to the skb */
|
|
memcpy(&skbpriv(skb)->ts, &ts, sizeof(struct timespec));
|
|
#endif
|
|
#ifdef DEBUG_MODEM_IF_IODEV_TX
|
|
mif_pkt(iod->id, "IOD-TX", skb);
|
|
#endif
|
|
|
|
/* Build SIPC5 link header*/
|
|
if (cfg) {
|
|
buff = skb_push(skb, headroom);
|
|
sipc5_build_header(iod, buff, cfg,
|
|
tx_bytes, cnt - copied);
|
|
}
|
|
|
|
/* Apply padding */
|
|
if (tailroom)
|
|
skb_put(skb, tailroom);
|
|
|
|
/**
|
|
* Send the skb with a link device
|
|
*/
|
|
ret = ld->send(ld, iod, skb);
|
|
if (ret < 0) {
|
|
mif_err("%s->%s: %s->send fail(%d, tx:%d len:%lu)\n",
|
|
iod->name, mc->name, ld->name,
|
|
ret, tx_bytes, (unsigned long)count);
|
|
dev_kfree_skb_any(skb);
|
|
return ret;
|
|
}
|
|
copied_frm += ret;
|
|
}
|
|
|
|
if (copied_frm != tot_frame) {
|
|
mif_info("%s->%s: WARN! %s->send ret:%d (len:%lu)\n",
|
|
iod->name, mc->name, ld->name,
|
|
copied_frm, (unsigned long)count);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static ssize_t misc_read(struct file *filp, char *buf, size_t count,
|
|
loff_t *fpos)
|
|
{
|
|
struct io_device *iod = (struct io_device *)filp->private_data;
|
|
struct sk_buff_head *rxq = &iod->sk_rx_q;
|
|
struct sk_buff *skb;
|
|
int copied;
|
|
|
|
if (skb_queue_empty(rxq)) {
|
|
long tmo = msecs_to_jiffies(100);
|
|
wait_event_timeout(iod->wq, !skb_queue_empty(rxq), tmo);
|
|
}
|
|
|
|
skb = skb_dequeue(rxq);
|
|
if (unlikely(!skb)) {
|
|
mif_info("%s: NO data in RXQ\n", iod->name);
|
|
return 0;
|
|
}
|
|
|
|
copied = skb->len > count ? count : skb->len;
|
|
|
|
if (copy_to_user(buf, skb->data, copied)) {
|
|
mif_err("%s: ERR! copy_to_user fail\n", iod->name);
|
|
dev_kfree_skb_any(skb);
|
|
return -EFAULT;
|
|
}
|
|
|
|
if (iod->id == SIPC_CH_ID_CPLOG1) {
|
|
struct net_device *ndev = iod->ndev;
|
|
if (!ndev) {
|
|
mif_err("%s: ERR! no iod->ndev\n", iod->name);
|
|
} else {
|
|
ndev->stats.rx_packets++;
|
|
ndev->stats.rx_bytes += copied;
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUG_MODEM_IF_IODEV_RX
|
|
mif_pkt(iod->id, "IOD-RX", skb);
|
|
#endif
|
|
mif_debug("%s: data:%d copied:%d qlen:%d\n",
|
|
iod->name, skb->len, copied, rxq->qlen);
|
|
|
|
if (skb->len > copied) {
|
|
skb_pull(skb, copied);
|
|
skb_queue_head(rxq, skb);
|
|
} else {
|
|
dev_consume_skb_any(skb);
|
|
}
|
|
|
|
return copied;
|
|
}
|
|
|
|
static const struct file_operations misc_io_fops = {
|
|
.owner = THIS_MODULE,
|
|
.open = misc_open,
|
|
.release = misc_release,
|
|
.poll = misc_poll,
|
|
.unlocked_ioctl = misc_ioctl,
|
|
.compat_ioctl = misc_ioctl,
|
|
.write = misc_write,
|
|
.read = misc_read,
|
|
};
|
|
|
|
static int vnet_open(struct net_device *ndev)
|
|
{
|
|
struct vnet *vnet = netdev_priv(ndev);
|
|
struct io_device *iod = vnet->iod;
|
|
struct modem_shared *msd = vnet->iod->msd;
|
|
struct link_device *ld;
|
|
int ret;
|
|
|
|
atomic_inc(&iod->opened);
|
|
|
|
list_for_each_entry(ld, &msd->link_dev_list, list) {
|
|
if (IS_CONNECTED(iod, ld) && ld->init_comm) {
|
|
ret = ld->init_comm(ld, iod);
|
|
if (ret < 0) {
|
|
mif_err("%s<->%s: ERR! init_comm fail(%d)\n",
|
|
iod->name, ld->name, ret);
|
|
atomic_dec(&iod->opened);
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
list_add(&iod->node_ndev, &iod->msd->activated_ndev_list);
|
|
|
|
netif_start_queue(ndev);
|
|
|
|
mif_err("%s (opened %d) by %s\n",
|
|
iod->name, atomic_read(&iod->opened), current->comm);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vnet_stop(struct net_device *ndev)
|
|
{
|
|
struct vnet *vnet = netdev_priv(ndev);
|
|
struct io_device *iod = vnet->iod;
|
|
struct modem_shared *msd = iod->msd;
|
|
struct link_device *ld;
|
|
|
|
if (atomic_dec_and_test(&iod->opened))
|
|
skb_queue_purge(&vnet->iod->sk_rx_q);
|
|
|
|
list_for_each_entry(ld, &msd->link_dev_list, list) {
|
|
if (IS_CONNECTED(iod, ld) && ld->terminate_comm)
|
|
ld->terminate_comm(ld, iod);
|
|
}
|
|
|
|
spin_lock(&msd->active_list_lock);
|
|
list_del(&iod->node_ndev);
|
|
spin_unlock(&msd->active_list_lock);
|
|
netif_stop_queue(ndev);
|
|
|
|
mif_err("%s (opened %d) by %s\n",
|
|
iod->name, atomic_read(&iod->opened), current->comm);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vnet_xmit(struct sk_buff *skb, struct net_device *ndev)
|
|
{
|
|
struct vnet *vnet = netdev_priv(ndev);
|
|
struct io_device *iod = vnet->iod;
|
|
struct link_device *ld = get_current_link(iod);
|
|
struct modem_ctl *mc = iod->mc;
|
|
unsigned int count = skb->len;
|
|
struct sk_buff *skb_new = skb;
|
|
char *buff;
|
|
int ret;
|
|
u8 cfg;
|
|
unsigned int headroom;
|
|
unsigned int tailroom;
|
|
unsigned int tx_bytes;
|
|
#ifdef DEBUG_MODEM_IF
|
|
struct timespec ts;
|
|
#endif
|
|
|
|
#ifdef DEBUG_MODEM_IF
|
|
/* Record the timestamp */
|
|
getnstimeofday(&ts);
|
|
#endif
|
|
|
|
if (unlikely(!cp_online(mc))) {
|
|
if (!netif_queue_stopped(ndev))
|
|
netif_stop_queue(ndev);
|
|
/* Just drop the TX packet */
|
|
goto drop;
|
|
}
|
|
|
|
/* When use `handover' with Network Bridge,
|
|
* user -> bridge device(rmnet0) -> real rmnet(xxxx_rmnet0) -> here.
|
|
* bridge device is ethernet device unlike xxxx_rmnet(net device).
|
|
* We remove the an ethernet header of skb before using skb->len,
|
|
* because bridge device added an ethernet header to skb.
|
|
*/
|
|
if (iod->use_handover) {
|
|
if (iod->id >= SIPC_CH_ID_PDP_0 && iod->id <= SIPC_CH_ID_PDP_14)
|
|
skb_pull(skb, sizeof(struct ethhdr));
|
|
}
|
|
|
|
if (iod->link_header) {
|
|
cfg = sipc5_build_config(iod, ld, count);
|
|
headroom = sipc5_get_hdr_len(&cfg);
|
|
if (ld->aligned)
|
|
tailroom = sipc5_calc_padding_size(headroom + count);
|
|
else
|
|
tailroom = 0;
|
|
} else {
|
|
cfg = 0;
|
|
headroom = 0;
|
|
tailroom = 0;
|
|
}
|
|
|
|
tx_bytes = headroom + count + tailroom;
|
|
|
|
if (skb_headroom(skb) < headroom || skb_tailroom(skb) < tailroom) {
|
|
skb_new = skb_copy_expand(skb, headroom, tailroom, GFP_ATOMIC);
|
|
if (!skb_new) {
|
|
mif_info("%s: ERR! skb_copy_expand fail\n", iod->name);
|
|
goto retry;
|
|
}
|
|
}
|
|
|
|
/* Store the IO device, the link device, etc. */
|
|
skbpriv(skb_new)->iod = iod;
|
|
skbpriv(skb_new)->ld = ld;
|
|
|
|
skbpriv(skb_new)->lnk_hdr = iod->link_header;
|
|
skbpriv(skb_new)->sipc_ch = iod->id;
|
|
|
|
#ifdef DEBUG_MODEM_IF
|
|
/* Copy the timestamp to the skb */
|
|
memcpy(&skbpriv(skb_new)->ts, &ts, sizeof(struct timespec));
|
|
#endif
|
|
#if defined(DEBUG_MODEM_IF_IODEV_TX) && defined(DEBUG_MODEM_IF_PS_DATA)
|
|
mif_pkt(iod->id, "IOD-TX", skb_new);
|
|
#endif
|
|
|
|
/* Build SIPC5 link header*/
|
|
buff = skb_push(skb_new, headroom);
|
|
if (cfg)
|
|
sipc5_build_header(iod, buff, cfg, count, 0);
|
|
|
|
/* IP loop-back */
|
|
if (iod->msd->loopback_ipaddr) {
|
|
struct iphdr *ip_header = (struct iphdr *)skb->data;
|
|
if (ip_header->daddr == iod->msd->loopback_ipaddr) {
|
|
swap(ip_header->saddr, ip_header->daddr);
|
|
buff[SIPC5_CH_ID_OFFSET] = DATA_LOOPBACK_CHANNEL;
|
|
}
|
|
}
|
|
|
|
/* Apply padding */
|
|
if (tailroom)
|
|
skb_put(skb_new, tailroom);
|
|
|
|
ret = ld->send(ld, iod, skb_new);
|
|
if (unlikely(ret < 0)) {
|
|
static DEFINE_RATELIMIT_STATE(_rs, HZ, 100);
|
|
|
|
if (ret != -EBUSY) {
|
|
mif_err_limited("%s->%s: ERR! %s->send fail:%d "
|
|
"(tx_bytes:%d len:%d)\n",
|
|
iod->name, mc->name, ld->name, ret,
|
|
tx_bytes, count);
|
|
goto drop;
|
|
}
|
|
|
|
/* do 100-retry for every 1sec */
|
|
if (__ratelimit(&_rs))
|
|
goto retry;
|
|
goto drop;
|
|
}
|
|
|
|
if (ret != tx_bytes) {
|
|
ndev->stats.tx_errors++; /* packet transmit problems */
|
|
ndev->stats.tx_bytes += ret;
|
|
mif_info("%s->%s: WARN! %s->send ret:%d (tx_bytes:%d len:%d)\n",
|
|
iod->name, mc->name, ld->name, ret, tx_bytes, count);
|
|
}
|
|
else
|
|
{
|
|
ndev->stats.tx_packets++;
|
|
ndev->stats.tx_bytes += count;
|
|
}
|
|
|
|
/*
|
|
If @skb has been expanded to $skb_new, @skb must be freed here.
|
|
($skb_new will be freed by the link device.)
|
|
*/
|
|
if (skb_new != skb)
|
|
dev_consume_skb_any(skb);
|
|
|
|
return NETDEV_TX_OK;
|
|
|
|
retry:
|
|
/*
|
|
If @skb has been expanded to $skb_new, only $skb_new must be freed here
|
|
because @skb will be reused by NET_TX.
|
|
*/
|
|
if (skb_new && skb_new != skb)
|
|
dev_consume_skb_any(skb_new);
|
|
|
|
return NETDEV_TX_BUSY;
|
|
|
|
drop:
|
|
ndev->stats.tx_dropped++;
|
|
|
|
DROPDUMP_QPCAP_SKB(skb, NET_DROPDUMP_OPT_MIF_TXFAIL);
|
|
dev_kfree_skb_any(skb);
|
|
|
|
/*
|
|
If @skb has been expanded to $skb_new, $skb_new must also be freed here.
|
|
*/
|
|
if (skb_new != skb)
|
|
dev_consume_skb_any(skb_new);
|
|
|
|
return NETDEV_TX_OK;
|
|
}
|
|
|
|
#if defined(CONFIG_MODEM_IF_LEGACY_QOS) || defined(CONFIG_MODEM_IF_QOS)
|
|
static u16 vnet_select_queue(struct net_device *dev, struct sk_buff *skb,
|
|
void *accel_priv, select_queue_fallback_t fallback)
|
|
{
|
|
return (skb && skb->priomark == RAW_HPRIO) ? 1 : 0;
|
|
}
|
|
#endif
|
|
|
|
static int dummy_net_open(struct net_device *ndev)
|
|
{
|
|
return -EINVAL;
|
|
}
|
|
|
|
static struct net_device_ops dummy_net_ops = {
|
|
.ndo_open = dummy_net_open,
|
|
};
|
|
|
|
static struct net_device_ops vnet_ops = {
|
|
.ndo_open = vnet_open,
|
|
.ndo_stop = vnet_stop,
|
|
.ndo_start_xmit = vnet_xmit,
|
|
#if defined(CONFIG_MODEM_IF_LEGACY_QOS) || defined(CONFIG_MODEM_IF_QOS)
|
|
.ndo_select_queue = vnet_select_queue,
|
|
#endif
|
|
};
|
|
|
|
static void vnet_setup(struct net_device *ndev)
|
|
{
|
|
ndev->netdev_ops = &vnet_ops;
|
|
ndev->type = ARPHRD_PPP;
|
|
ndev->flags = IFF_POINTOPOINT | IFF_NOARP | IFF_MULTICAST;
|
|
ndev->addr_len = 0;
|
|
ndev->hard_header_len = 0;
|
|
ndev->tx_queue_len = 1000;
|
|
ndev->mtu = ETH_DATA_LEN;
|
|
ndev->watchdog_timeo = 5 * HZ;
|
|
#ifdef CONFIG_MODEM_IF_NET_GRO
|
|
ndev->features |= NETIF_F_GRO;
|
|
#endif
|
|
}
|
|
|
|
static void vnet_setup_ether(struct net_device *ndev)
|
|
{
|
|
ndev->netdev_ops = &vnet_ops;
|
|
ndev->type = ARPHRD_ETHER;
|
|
ndev->flags = IFF_POINTOPOINT | IFF_NOARP | IFF_MULTICAST | IFF_SLAVE;
|
|
ndev->addr_len = ETH_ALEN;
|
|
random_ether_addr(ndev->dev_addr);
|
|
ndev->hard_header_len = 0;
|
|
ndev->tx_queue_len = 1000;
|
|
ndev->mtu = ETH_DATA_LEN;
|
|
ndev->watchdog_timeo = 5 * HZ;
|
|
#ifdef CONFIG_MODEM_IF_NET_GRO
|
|
ndev->features |= NETIF_F_GRO;
|
|
#endif
|
|
}
|
|
|
|
static inline void sipc5_inc_info_id(struct io_device *iod)
|
|
{
|
|
spin_lock(&iod->info_id_lock);
|
|
iod->info_id = (iod->info_id + 1) & 0x7F;
|
|
spin_unlock(&iod->info_id_lock);
|
|
}
|
|
|
|
static u8 sipc5_build_config(struct io_device *iod, struct link_device *ld,
|
|
unsigned int count)
|
|
{
|
|
u8 cfg = SIPC5_START_MASK;
|
|
|
|
if (iod->format > IPC_DUMP)
|
|
return 0;
|
|
|
|
if (ld->aligned)
|
|
cfg |= SIPC5_PADDING_EXIST;
|
|
|
|
if (iod->max_tx_size > 0 &&
|
|
(count + SIPC5_MIN_HEADER_SIZE) > iod->max_tx_size) {
|
|
mif_info("%s: MULTI_FRAME_CFG: count=%u\n", iod->name, count);
|
|
cfg |= SIPC5_MULTI_FRAME_CFG;
|
|
sipc5_inc_info_id(iod);
|
|
}
|
|
#if 0
|
|
else if ((count + SIPC5_MIN_HEADER_SIZE) > 0xFFFF)
|
|
cfg |= SIPC5_EXT_LENGTH_CFG;
|
|
#endif
|
|
|
|
return cfg;
|
|
}
|
|
|
|
static void sipc5_build_header(struct io_device *iod, u8 *buff, u8 cfg,
|
|
unsigned int tx_bytes, unsigned int remains)
|
|
{
|
|
u16 *sz16 = (u16 *)(buff + SIPC5_LEN_OFFSET);
|
|
u32 *sz32 = (u32 *)(buff + SIPC5_LEN_OFFSET);
|
|
unsigned int hdr_len = sipc5_get_hdr_len(&cfg);
|
|
u8 ctrl;
|
|
|
|
/* Store the config field and the channel ID field */
|
|
buff[SIPC5_CONFIG_OFFSET] = cfg;
|
|
buff[SIPC5_CH_ID_OFFSET] = iod->id;
|
|
|
|
/* Store the frame length field */
|
|
if (sipc5_ext_len(buff))
|
|
*sz32 = (u32)(hdr_len + tx_bytes);
|
|
else
|
|
*sz16 = (u16)(hdr_len + tx_bytes);
|
|
|
|
/* Store the control field */
|
|
if (sipc5_multi_frame(buff)) {
|
|
ctrl = (remains > 0) ? 1 << 7 : 0;
|
|
ctrl |= iod->info_id;
|
|
buff[SIPC5_CTRL_OFFSET] = ctrl;
|
|
mif_info("MULTI: ctrl=0x%x(tx_bytes:%u, remains:%u)\n",
|
|
ctrl, tx_bytes, remains);
|
|
}
|
|
}
|
|
|
|
int sipc5_init_io_device(struct io_device *iod)
|
|
{
|
|
int ret = 0;
|
|
int i;
|
|
struct vnet *vnet;
|
|
|
|
if (iod->attrs & IODEV_ATTR(ATTR_SBD_IPC))
|
|
iod->sbd_ipc = true;
|
|
|
|
if (iod->attrs & IODEV_ATTR(ATTR_NO_LINK_HEADER))
|
|
iod->link_header = false;
|
|
else
|
|
iod->link_header = true;
|
|
|
|
/* Get modem state from modem control device */
|
|
iod->modem_state_changed = io_dev_modem_state_changed;
|
|
iod->sim_state_changed = io_dev_sim_state_changed;
|
|
|
|
/* Get data from link device */
|
|
iod->recv_skb_single = io_dev_recv_skb_single_from_link_dev;
|
|
iod->recv_net_skb = io_dev_recv_net_skb_from_link_dev;
|
|
|
|
/* Register misc or net device */
|
|
switch (iod->io_typ) {
|
|
case IODEV_MISC:
|
|
init_waitqueue_head(&iod->wq);
|
|
skb_queue_head_init(&iod->sk_rx_q);
|
|
|
|
iod->miscdev.minor = MISC_DYNAMIC_MINOR;
|
|
iod->miscdev.name = iod->name;
|
|
iod->miscdev.fops = &misc_io_fops;
|
|
|
|
ret = misc_register(&iod->miscdev);
|
|
if (ret)
|
|
mif_info("%s: ERR! misc_register failed\n", iod->name);
|
|
|
|
if (iod->id == SIPC_CH_ID_CPLOG1) {
|
|
iod->ndev = alloc_netdev(0, iod->name,
|
|
NET_NAME_UNKNOWN, vnet_setup);
|
|
if (!iod->ndev) {
|
|
mif_info("%s: ERR! alloc_netdev fail\n", iod->name);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
iod->ndev->netdev_ops = &dummy_net_ops;
|
|
ret = register_netdev(iod->ndev);
|
|
if (ret) {
|
|
mif_info("%s: ERR! register_netdev fail\n", iod->name);
|
|
free_netdev(iod->ndev);
|
|
}
|
|
|
|
vnet = netdev_priv(iod->ndev);
|
|
vnet->iod = iod;
|
|
mif_info("iod:%s, both registerd\n", iod->name);
|
|
}
|
|
break;
|
|
|
|
case IODEV_NET:
|
|
skb_queue_head_init(&iod->sk_rx_q);
|
|
INIT_LIST_HEAD(&iod->node_ndev);
|
|
|
|
if (iod->use_handover)
|
|
iod->ndev = alloc_netdev_mqs(sizeof(struct vnet),
|
|
iod->name, NET_NAME_UNKNOWN,
|
|
vnet_setup_ether, MAX_NDEV_TX_Q,
|
|
MAX_NDEV_RX_Q);
|
|
else
|
|
iod->ndev = alloc_netdev_mqs(sizeof(struct vnet),
|
|
iod->name, NET_NAME_UNKNOWN, vnet_setup,
|
|
MAX_NDEV_TX_Q, MAX_NDEV_RX_Q);
|
|
|
|
if (!iod->ndev) {
|
|
mif_info("%s: ERR! alloc_netdev fail\n", iod->name);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
ret = register_netdev(iod->ndev);
|
|
if (ret) {
|
|
mif_info("%s: ERR! register_netdev fail\n", iod->name);
|
|
free_netdev(iod->ndev);
|
|
}
|
|
|
|
mif_debug("iod 0x%pK\n", iod);
|
|
vnet = netdev_priv(iod->ndev);
|
|
mif_debug("vnet 0x%pK\n", vnet);
|
|
vnet->iod = iod;
|
|
|
|
break;
|
|
|
|
case IODEV_DUMMY:
|
|
skb_queue_head_init(&iod->sk_rx_q);
|
|
|
|
iod->miscdev.minor = MISC_DYNAMIC_MINOR;
|
|
iod->miscdev.name = iod->name;
|
|
|
|
ret = misc_register(&iod->miscdev);
|
|
if (ret)
|
|
mif_info("%s: ERR! misc_register fail\n", iod->name);
|
|
|
|
ret = device_create_file(iod->miscdev.this_device,
|
|
&attr_waketime);
|
|
if (ret)
|
|
mif_info("%s: ERR! device_create_file fail\n",
|
|
iod->name);
|
|
|
|
ret = device_create_file(iod->miscdev.this_device,
|
|
&attr_loopback);
|
|
if (ret)
|
|
mif_err("failed to create `loopback file' : %s\n",
|
|
iod->name);
|
|
|
|
ret = device_create_file(iod->miscdev.this_device,
|
|
&attr_txlink);
|
|
if (ret)
|
|
mif_err("failed to create `txlink file' : %s\n",
|
|
iod->name);
|
|
break;
|
|
|
|
default:
|
|
mif_info("%s: ERR! wrong io_type %d\n", iod->name, iod->io_typ);
|
|
return -EINVAL;
|
|
}
|
|
|
|
for (i = 0; i < NUM_SIPC_MULTI_FRAME_IDS; i++)
|
|
skb_queue_head_init(&iod->sk_multi_q[i]);
|
|
|
|
return ret;
|
|
}
|
|
|
|
void sipc5_deinit_io_device(struct io_device *iod)
|
|
{
|
|
mif_err("%s: io_typ=%d\n", iod->name, iod->io_typ);
|
|
|
|
wake_lock_destroy(&iod->wakelock);
|
|
|
|
/* De-register misc or net device */
|
|
switch (iod->io_typ) {
|
|
case IODEV_MISC:
|
|
if (iod->id == SIPC_CH_ID_CPLOG1) {
|
|
unregister_netdev(iod->ndev);
|
|
free_netdev(iod->ndev);
|
|
}
|
|
|
|
misc_deregister(&iod->miscdev);
|
|
break;
|
|
|
|
case IODEV_NET:
|
|
unregister_netdev(iod->ndev);
|
|
free_netdev(iod->ndev);
|
|
break;
|
|
|
|
case IODEV_DUMMY:
|
|
device_remove_file(iod->miscdev.this_device, &attr_waketime);
|
|
device_remove_file(iod->miscdev.this_device, &attr_loopback);
|
|
device_remove_file(iod->miscdev.this_device, &attr_txlink);
|
|
|
|
misc_deregister(&iod->miscdev);
|
|
break;
|
|
}
|
|
}
|