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.
491 lines
14 KiB
491 lines
14 KiB
// SPDX-License-Identifier: GPL-2.0-only |
|
/* |
|
* Copyright (C) 2002 Intersil Americas Inc. |
|
* Copyright 2004 Jens Maurer <[email protected]> |
|
*/ |
|
|
|
#include <linux/netdevice.h> |
|
#include <linux/module.h> |
|
#include <linux/pci.h> |
|
#include <linux/sched.h> |
|
#include <linux/slab.h> |
|
|
|
#include <asm/io.h> |
|
#include <linux/if_arp.h> |
|
|
|
#include "prismcompat.h" |
|
#include "isl_38xx.h" |
|
#include "islpci_mgt.h" |
|
#include "isl_oid.h" /* additional types and defs for isl38xx fw */ |
|
#include "isl_ioctl.h" |
|
|
|
#include <net/iw_handler.h> |
|
|
|
/****************************************************************************** |
|
Global variable definition section |
|
******************************************************************************/ |
|
int pc_debug = VERBOSE; |
|
module_param(pc_debug, int, 0); |
|
|
|
/****************************************************************************** |
|
Driver general functions |
|
******************************************************************************/ |
|
#if VERBOSE > SHOW_ERROR_MESSAGES |
|
void |
|
display_buffer(char *buffer, int length) |
|
{ |
|
if ((pc_debug & SHOW_BUFFER_CONTENTS) == 0) |
|
return; |
|
|
|
while (length > 0) { |
|
printk("[%02x]", *buffer & 255); |
|
length--; |
|
buffer++; |
|
} |
|
|
|
printk("\n"); |
|
} |
|
#endif |
|
|
|
/***************************************************************************** |
|
Queue handling for management frames |
|
******************************************************************************/ |
|
|
|
/* |
|
* Helper function to create a PIMFOR management frame header. |
|
*/ |
|
static void |
|
pimfor_encode_header(int operation, u32 oid, u32 length, pimfor_header_t *h) |
|
{ |
|
h->version = PIMFOR_VERSION; |
|
h->operation = operation; |
|
h->device_id = PIMFOR_DEV_ID_MHLI_MIB; |
|
h->flags = 0; |
|
h->oid = cpu_to_be32(oid); |
|
h->length = cpu_to_be32(length); |
|
} |
|
|
|
/* |
|
* Helper function to analyze a PIMFOR management frame header. |
|
*/ |
|
static pimfor_header_t * |
|
pimfor_decode_header(void *data, int len) |
|
{ |
|
pimfor_header_t *h = data; |
|
|
|
while ((void *) h < data + len) { |
|
if (h->flags & PIMFOR_FLAG_LITTLE_ENDIAN) { |
|
le32_to_cpus(&h->oid); |
|
le32_to_cpus(&h->length); |
|
} else { |
|
be32_to_cpus(&h->oid); |
|
be32_to_cpus(&h->length); |
|
} |
|
if (h->oid != OID_INL_TUNNEL) |
|
return h; |
|
h++; |
|
} |
|
return NULL; |
|
} |
|
|
|
/* |
|
* Fill the receive queue for management frames with fresh buffers. |
|
*/ |
|
int |
|
islpci_mgmt_rx_fill(struct net_device *ndev) |
|
{ |
|
islpci_private *priv = netdev_priv(ndev); |
|
isl38xx_control_block *cb = /* volatile not needed */ |
|
(isl38xx_control_block *) priv->control_block; |
|
u32 curr = le32_to_cpu(cb->driver_curr_frag[ISL38XX_CB_RX_MGMTQ]); |
|
|
|
#if VERBOSE > SHOW_ERROR_MESSAGES |
|
DEBUG(SHOW_FUNCTION_CALLS, "islpci_mgmt_rx_fill\n"); |
|
#endif |
|
|
|
while (curr - priv->index_mgmt_rx < ISL38XX_CB_MGMT_QSIZE) { |
|
u32 index = curr % ISL38XX_CB_MGMT_QSIZE; |
|
struct islpci_membuf *buf = &priv->mgmt_rx[index]; |
|
isl38xx_fragment *frag = &cb->rx_data_mgmt[index]; |
|
|
|
if (buf->mem == NULL) { |
|
buf->mem = kmalloc(MGMT_FRAME_SIZE, GFP_ATOMIC); |
|
if (!buf->mem) |
|
return -ENOMEM; |
|
buf->size = MGMT_FRAME_SIZE; |
|
} |
|
if (buf->pci_addr == 0) { |
|
buf->pci_addr = dma_map_single(&priv->pdev->dev, |
|
buf->mem, |
|
MGMT_FRAME_SIZE, |
|
DMA_FROM_DEVICE); |
|
if (dma_mapping_error(&priv->pdev->dev, buf->pci_addr)) { |
|
printk(KERN_WARNING |
|
"Failed to make memory DMA'able.\n"); |
|
return -ENOMEM; |
|
} |
|
} |
|
|
|
/* be safe: always reset control block information */ |
|
frag->size = cpu_to_le16(MGMT_FRAME_SIZE); |
|
frag->flags = 0; |
|
frag->address = cpu_to_le32(buf->pci_addr); |
|
curr++; |
|
|
|
/* The fragment address in the control block must have |
|
* been written before announcing the frame buffer to |
|
* device */ |
|
wmb(); |
|
cb->driver_curr_frag[ISL38XX_CB_RX_MGMTQ] = cpu_to_le32(curr); |
|
} |
|
return 0; |
|
} |
|
|
|
/* |
|
* Create and transmit a management frame using "operation" and "oid", |
|
* with arguments data/length. |
|
* We either return an error and free the frame, or we return 0 and |
|
* islpci_mgt_cleanup_transmit() frees the frame in the tx-done |
|
* interrupt. |
|
*/ |
|
static int |
|
islpci_mgt_transmit(struct net_device *ndev, int operation, unsigned long oid, |
|
void *data, int length) |
|
{ |
|
islpci_private *priv = netdev_priv(ndev); |
|
isl38xx_control_block *cb = |
|
(isl38xx_control_block *) priv->control_block; |
|
void *p; |
|
int err = -EINVAL; |
|
unsigned long flags; |
|
isl38xx_fragment *frag; |
|
struct islpci_membuf buf; |
|
u32 curr_frag; |
|
int index; |
|
int frag_len = length + PIMFOR_HEADER_SIZE; |
|
|
|
#if VERBOSE > SHOW_ERROR_MESSAGES |
|
DEBUG(SHOW_FUNCTION_CALLS, "islpci_mgt_transmit\n"); |
|
#endif |
|
|
|
if (frag_len > MGMT_FRAME_SIZE) { |
|
printk(KERN_DEBUG "%s: mgmt frame too large %d\n", |
|
ndev->name, frag_len); |
|
goto error; |
|
} |
|
|
|
err = -ENOMEM; |
|
p = buf.mem = kmalloc(frag_len, GFP_KERNEL); |
|
if (!buf.mem) |
|
goto error; |
|
|
|
buf.size = frag_len; |
|
|
|
/* create the header directly in the fragment data area */ |
|
pimfor_encode_header(operation, oid, length, (pimfor_header_t *) p); |
|
p += PIMFOR_HEADER_SIZE; |
|
|
|
if (data) |
|
memcpy(p, data, length); |
|
else |
|
memset(p, 0, length); |
|
|
|
#if VERBOSE > SHOW_ERROR_MESSAGES |
|
{ |
|
pimfor_header_t *h = buf.mem; |
|
DEBUG(SHOW_PIMFOR_FRAMES, |
|
"PIMFOR: op %i, oid 0x%08lx, device %i, flags 0x%x length 0x%x\n", |
|
h->operation, oid, h->device_id, h->flags, length); |
|
|
|
/* display the buffer contents for debugging */ |
|
display_buffer((char *) h, sizeof (pimfor_header_t)); |
|
display_buffer(p, length); |
|
} |
|
#endif |
|
|
|
err = -ENOMEM; |
|
buf.pci_addr = dma_map_single(&priv->pdev->dev, buf.mem, frag_len, |
|
DMA_TO_DEVICE); |
|
if (dma_mapping_error(&priv->pdev->dev, buf.pci_addr)) { |
|
printk(KERN_WARNING "%s: cannot map PCI memory for mgmt\n", |
|
ndev->name); |
|
goto error_free; |
|
} |
|
|
|
/* Protect the control block modifications against interrupts. */ |
|
spin_lock_irqsave(&priv->slock, flags); |
|
curr_frag = le32_to_cpu(cb->driver_curr_frag[ISL38XX_CB_TX_MGMTQ]); |
|
if (curr_frag - priv->index_mgmt_tx >= ISL38XX_CB_MGMT_QSIZE) { |
|
printk(KERN_WARNING "%s: mgmt tx queue is still full\n", |
|
ndev->name); |
|
goto error_unlock; |
|
} |
|
|
|
/* commit the frame to the tx device queue */ |
|
index = curr_frag % ISL38XX_CB_MGMT_QSIZE; |
|
priv->mgmt_tx[index] = buf; |
|
frag = &cb->tx_data_mgmt[index]; |
|
frag->size = cpu_to_le16(frag_len); |
|
frag->flags = 0; /* for any other than the last fragment, set to 1 */ |
|
frag->address = cpu_to_le32(buf.pci_addr); |
|
|
|
/* The fragment address in the control block must have |
|
* been written before announcing the frame buffer to |
|
* device */ |
|
wmb(); |
|
cb->driver_curr_frag[ISL38XX_CB_TX_MGMTQ] = cpu_to_le32(curr_frag + 1); |
|
spin_unlock_irqrestore(&priv->slock, flags); |
|
|
|
/* trigger the device */ |
|
islpci_trigger(priv); |
|
return 0; |
|
|
|
error_unlock: |
|
spin_unlock_irqrestore(&priv->slock, flags); |
|
error_free: |
|
kfree(buf.mem); |
|
error: |
|
return err; |
|
} |
|
|
|
/* |
|
* Receive a management frame from the device. |
|
* This can be an arbitrary number of traps, and at most one response |
|
* frame for a previous request sent via islpci_mgt_transmit(). |
|
*/ |
|
int |
|
islpci_mgt_receive(struct net_device *ndev) |
|
{ |
|
islpci_private *priv = netdev_priv(ndev); |
|
isl38xx_control_block *cb = |
|
(isl38xx_control_block *) priv->control_block; |
|
u32 curr_frag; |
|
|
|
#if VERBOSE > SHOW_ERROR_MESSAGES |
|
DEBUG(SHOW_FUNCTION_CALLS, "islpci_mgt_receive\n"); |
|
#endif |
|
|
|
/* Only once per interrupt, determine fragment range to |
|
* process. This avoids an endless loop (i.e. lockup) if |
|
* frames come in faster than we can process them. */ |
|
curr_frag = le32_to_cpu(cb->device_curr_frag[ISL38XX_CB_RX_MGMTQ]); |
|
barrier(); |
|
|
|
for (; priv->index_mgmt_rx < curr_frag; priv->index_mgmt_rx++) { |
|
pimfor_header_t *header; |
|
u32 index = priv->index_mgmt_rx % ISL38XX_CB_MGMT_QSIZE; |
|
struct islpci_membuf *buf = &priv->mgmt_rx[index]; |
|
u16 frag_len; |
|
int size; |
|
struct islpci_mgmtframe *frame; |
|
|
|
/* I have no idea (and no documentation) if flags != 0 |
|
* is possible. Drop the frame, reuse the buffer. */ |
|
if (le16_to_cpu(cb->rx_data_mgmt[index].flags) != 0) { |
|
printk(KERN_WARNING "%s: unknown flags 0x%04x\n", |
|
ndev->name, |
|
le16_to_cpu(cb->rx_data_mgmt[index].flags)); |
|
continue; |
|
} |
|
|
|
/* The device only returns the size of the header(s) here. */ |
|
frag_len = le16_to_cpu(cb->rx_data_mgmt[index].size); |
|
|
|
/* |
|
* We appear to have no way to tell the device the |
|
* size of a receive buffer. Thus, if this check |
|
* triggers, we likely have kernel heap corruption. */ |
|
if (frag_len > MGMT_FRAME_SIZE) { |
|
printk(KERN_WARNING |
|
"%s: Bogus packet size of %d (%#x).\n", |
|
ndev->name, frag_len, frag_len); |
|
frag_len = MGMT_FRAME_SIZE; |
|
} |
|
|
|
/* Ensure the results of device DMA are visible to the CPU. */ |
|
dma_sync_single_for_cpu(&priv->pdev->dev, buf->pci_addr, |
|
buf->size, DMA_FROM_DEVICE); |
|
|
|
/* Perform endianess conversion for PIMFOR header in-place. */ |
|
header = pimfor_decode_header(buf->mem, frag_len); |
|
if (!header) { |
|
printk(KERN_WARNING "%s: no PIMFOR header found\n", |
|
ndev->name); |
|
continue; |
|
} |
|
|
|
/* The device ID from the PIMFOR packet received from |
|
* the MVC is always 0. We forward a sensible device_id. |
|
* Not that anyone upstream would care... */ |
|
header->device_id = priv->ndev->ifindex; |
|
|
|
#if VERBOSE > SHOW_ERROR_MESSAGES |
|
DEBUG(SHOW_PIMFOR_FRAMES, |
|
"PIMFOR: op %i, oid 0x%08x, device %i, flags 0x%x length 0x%x\n", |
|
header->operation, header->oid, header->device_id, |
|
header->flags, header->length); |
|
|
|
/* display the buffer contents for debugging */ |
|
display_buffer((char *) header, PIMFOR_HEADER_SIZE); |
|
display_buffer((char *) header + PIMFOR_HEADER_SIZE, |
|
header->length); |
|
#endif |
|
|
|
/* nobody sends these */ |
|
if (header->flags & PIMFOR_FLAG_APPLIC_ORIGIN) { |
|
printk(KERN_DEBUG |
|
"%s: errant PIMFOR application frame\n", |
|
ndev->name); |
|
continue; |
|
} |
|
|
|
/* Determine frame size, skipping OID_INL_TUNNEL headers. */ |
|
size = PIMFOR_HEADER_SIZE + header->length; |
|
frame = kmalloc(sizeof(struct islpci_mgmtframe) + size, |
|
GFP_ATOMIC); |
|
if (!frame) |
|
continue; |
|
|
|
frame->ndev = ndev; |
|
memcpy(&frame->buf, header, size); |
|
frame->header = (pimfor_header_t *) frame->buf; |
|
frame->data = frame->buf + PIMFOR_HEADER_SIZE; |
|
|
|
#if VERBOSE > SHOW_ERROR_MESSAGES |
|
DEBUG(SHOW_PIMFOR_FRAMES, |
|
"frame: header: %p, data: %p, size: %d\n", |
|
frame->header, frame->data, size); |
|
#endif |
|
|
|
if (header->operation == PIMFOR_OP_TRAP) { |
|
#if VERBOSE > SHOW_ERROR_MESSAGES |
|
printk(KERN_DEBUG |
|
"TRAP: oid 0x%x, device %i, flags 0x%x length %i\n", |
|
header->oid, header->device_id, header->flags, |
|
header->length); |
|
#endif |
|
|
|
/* Create work to handle trap out of interrupt |
|
* context. */ |
|
INIT_WORK(&frame->ws, prism54_process_trap); |
|
schedule_work(&frame->ws); |
|
|
|
} else { |
|
/* Signal the one waiting process that a response |
|
* has been received. */ |
|
if ((frame = xchg(&priv->mgmt_received, frame)) != NULL) { |
|
printk(KERN_WARNING |
|
"%s: mgmt response not collected\n", |
|
ndev->name); |
|
kfree(frame); |
|
} |
|
#if VERBOSE > SHOW_ERROR_MESSAGES |
|
DEBUG(SHOW_TRACING, "Wake up Mgmt Queue\n"); |
|
#endif |
|
wake_up(&priv->mgmt_wqueue); |
|
} |
|
|
|
} |
|
|
|
return 0; |
|
} |
|
|
|
/* |
|
* Cleanup the transmit queue by freeing all frames handled by the device. |
|
*/ |
|
void |
|
islpci_mgt_cleanup_transmit(struct net_device *ndev) |
|
{ |
|
islpci_private *priv = netdev_priv(ndev); |
|
isl38xx_control_block *cb = /* volatile not needed */ |
|
(isl38xx_control_block *) priv->control_block; |
|
u32 curr_frag; |
|
|
|
#if VERBOSE > SHOW_ERROR_MESSAGES |
|
DEBUG(SHOW_FUNCTION_CALLS, "islpci_mgt_cleanup_transmit\n"); |
|
#endif |
|
|
|
/* Only once per cleanup, determine fragment range to |
|
* process. This avoids an endless loop (i.e. lockup) if |
|
* the device became confused, incrementing device_curr_frag |
|
* rapidly. */ |
|
curr_frag = le32_to_cpu(cb->device_curr_frag[ISL38XX_CB_TX_MGMTQ]); |
|
barrier(); |
|
|
|
for (; priv->index_mgmt_tx < curr_frag; priv->index_mgmt_tx++) { |
|
int index = priv->index_mgmt_tx % ISL38XX_CB_MGMT_QSIZE; |
|
struct islpci_membuf *buf = &priv->mgmt_tx[index]; |
|
dma_unmap_single(&priv->pdev->dev, buf->pci_addr, buf->size, |
|
DMA_TO_DEVICE); |
|
buf->pci_addr = 0; |
|
kfree(buf->mem); |
|
buf->mem = NULL; |
|
buf->size = 0; |
|
} |
|
} |
|
|
|
/* |
|
* Perform one request-response transaction to the device. |
|
*/ |
|
int |
|
islpci_mgt_transaction(struct net_device *ndev, |
|
int operation, unsigned long oid, |
|
void *senddata, int sendlen, |
|
struct islpci_mgmtframe **recvframe) |
|
{ |
|
islpci_private *priv = netdev_priv(ndev); |
|
const long wait_cycle_jiffies = msecs_to_jiffies(ISL38XX_WAIT_CYCLE * 10); |
|
long timeout_left = ISL38XX_MAX_WAIT_CYCLES * wait_cycle_jiffies; |
|
int err; |
|
DEFINE_WAIT(wait); |
|
|
|
*recvframe = NULL; |
|
|
|
if (mutex_lock_interruptible(&priv->mgmt_lock)) |
|
return -ERESTARTSYS; |
|
|
|
prepare_to_wait(&priv->mgmt_wqueue, &wait, TASK_UNINTERRUPTIBLE); |
|
err = islpci_mgt_transmit(ndev, operation, oid, senddata, sendlen); |
|
if (err) |
|
goto out; |
|
|
|
err = -ETIMEDOUT; |
|
while (timeout_left > 0) { |
|
int timeleft; |
|
struct islpci_mgmtframe *frame; |
|
|
|
timeleft = schedule_timeout_uninterruptible(wait_cycle_jiffies); |
|
frame = xchg(&priv->mgmt_received, NULL); |
|
if (frame) { |
|
if (frame->header->oid == oid) { |
|
*recvframe = frame; |
|
err = 0; |
|
goto out; |
|
} else { |
|
printk(KERN_DEBUG |
|
"%s: expecting oid 0x%x, received 0x%x.\n", |
|
ndev->name, (unsigned int) oid, |
|
frame->header->oid); |
|
kfree(frame); |
|
frame = NULL; |
|
} |
|
} |
|
if (timeleft == 0) { |
|
printk(KERN_DEBUG |
|
"%s: timeout waiting for mgmt response %lu, " |
|
"triggering device\n", |
|
ndev->name, timeout_left); |
|
islpci_trigger(priv); |
|
} |
|
timeout_left += timeleft - wait_cycle_jiffies; |
|
} |
|
printk(KERN_WARNING "%s: timeout waiting for mgmt response\n", |
|
ndev->name); |
|
|
|
/* TODO: we should reset the device here */ |
|
out: |
|
finish_wait(&priv->mgmt_wqueue, &wait); |
|
mutex_unlock(&priv->mgmt_lock); |
|
return err; |
|
} |
|
|
|
|