forked from Minki/linux
9e860947d8
gma_flag was set at the time of GMA command request but it should only be set after getting successful response. Movinng this flag setting in GMA response handler. This flag is used mainly for not repeating GMA command once received MAC address. Signed-off-by: Vijay Khemka <vijaykhemka@fb.com> Reviewed-by: Samuel Mendoza-Jonas <sam@mendozajonas.com> Signed-off-by: David S. Miller <davem@davemloft.net>
1831 lines
44 KiB
C
1831 lines
44 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* Copyright Gavin Shan, IBM Corporation 2016.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/init.h>
|
|
#include <linux/netdevice.h>
|
|
#include <linux/skbuff.h>
|
|
|
|
#include <net/ncsi.h>
|
|
#include <net/net_namespace.h>
|
|
#include <net/sock.h>
|
|
#include <net/addrconf.h>
|
|
#include <net/ipv6.h>
|
|
#include <net/genetlink.h>
|
|
|
|
#include "internal.h"
|
|
#include "ncsi-pkt.h"
|
|
#include "ncsi-netlink.h"
|
|
|
|
LIST_HEAD(ncsi_dev_list);
|
|
DEFINE_SPINLOCK(ncsi_dev_lock);
|
|
|
|
bool ncsi_channel_has_link(struct ncsi_channel *channel)
|
|
{
|
|
return !!(channel->modes[NCSI_MODE_LINK].data[2] & 0x1);
|
|
}
|
|
|
|
bool ncsi_channel_is_last(struct ncsi_dev_priv *ndp,
|
|
struct ncsi_channel *channel)
|
|
{
|
|
struct ncsi_package *np;
|
|
struct ncsi_channel *nc;
|
|
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np)
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
if (nc == channel)
|
|
continue;
|
|
if (nc->state == NCSI_CHANNEL_ACTIVE &&
|
|
ncsi_channel_has_link(nc))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void ncsi_report_link(struct ncsi_dev_priv *ndp, bool force_down)
|
|
{
|
|
struct ncsi_dev *nd = &ndp->ndev;
|
|
struct ncsi_package *np;
|
|
struct ncsi_channel *nc;
|
|
unsigned long flags;
|
|
|
|
nd->state = ncsi_dev_state_functional;
|
|
if (force_down) {
|
|
nd->link_up = 0;
|
|
goto report;
|
|
}
|
|
|
|
nd->link_up = 0;
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np) {
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
|
|
if (!list_empty(&nc->link) ||
|
|
nc->state != NCSI_CHANNEL_ACTIVE) {
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
continue;
|
|
}
|
|
|
|
if (ncsi_channel_has_link(nc)) {
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
nd->link_up = 1;
|
|
goto report;
|
|
}
|
|
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
}
|
|
}
|
|
|
|
report:
|
|
nd->handler(nd);
|
|
}
|
|
|
|
static void ncsi_channel_monitor(struct timer_list *t)
|
|
{
|
|
struct ncsi_channel *nc = from_timer(nc, t, monitor.timer);
|
|
struct ncsi_package *np = nc->package;
|
|
struct ncsi_dev_priv *ndp = np->ndp;
|
|
struct ncsi_channel_mode *ncm;
|
|
struct ncsi_cmd_arg nca;
|
|
bool enabled, chained;
|
|
unsigned int monitor_state;
|
|
unsigned long flags;
|
|
int state, ret;
|
|
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
state = nc->state;
|
|
chained = !list_empty(&nc->link);
|
|
enabled = nc->monitor.enabled;
|
|
monitor_state = nc->monitor.state;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
if (!enabled || chained) {
|
|
ncsi_stop_channel_monitor(nc);
|
|
return;
|
|
}
|
|
if (state != NCSI_CHANNEL_INACTIVE &&
|
|
state != NCSI_CHANNEL_ACTIVE) {
|
|
ncsi_stop_channel_monitor(nc);
|
|
return;
|
|
}
|
|
|
|
switch (monitor_state) {
|
|
case NCSI_CHANNEL_MONITOR_START:
|
|
case NCSI_CHANNEL_MONITOR_RETRY:
|
|
nca.ndp = ndp;
|
|
nca.package = np->id;
|
|
nca.channel = nc->id;
|
|
nca.type = NCSI_PKT_CMD_GLS;
|
|
nca.req_flags = 0;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
netdev_err(ndp->ndev.dev, "Error %d sending GLS\n",
|
|
ret);
|
|
break;
|
|
case NCSI_CHANNEL_MONITOR_WAIT ... NCSI_CHANNEL_MONITOR_WAIT_MAX:
|
|
break;
|
|
default:
|
|
netdev_err(ndp->ndev.dev, "NCSI Channel %d timed out!\n",
|
|
nc->id);
|
|
ncsi_report_link(ndp, true);
|
|
ndp->flags |= NCSI_DEV_RESHUFFLE;
|
|
|
|
ncsi_stop_channel_monitor(nc);
|
|
|
|
ncm = &nc->modes[NCSI_MODE_LINK];
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
nc->state = NCSI_CHANNEL_INVISIBLE;
|
|
ncm->data[2] &= ~0x1;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
nc->state = NCSI_CHANNEL_ACTIVE;
|
|
list_add_tail_rcu(&nc->link, &ndp->channel_queue);
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
ncsi_process_next_channel(ndp);
|
|
return;
|
|
}
|
|
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
nc->monitor.state++;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
mod_timer(&nc->monitor.timer, jiffies + HZ);
|
|
}
|
|
|
|
void ncsi_start_channel_monitor(struct ncsi_channel *nc)
|
|
{
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
WARN_ON_ONCE(nc->monitor.enabled);
|
|
nc->monitor.enabled = true;
|
|
nc->monitor.state = NCSI_CHANNEL_MONITOR_START;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
mod_timer(&nc->monitor.timer, jiffies + HZ);
|
|
}
|
|
|
|
void ncsi_stop_channel_monitor(struct ncsi_channel *nc)
|
|
{
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
if (!nc->monitor.enabled) {
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
return;
|
|
}
|
|
nc->monitor.enabled = false;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
del_timer_sync(&nc->monitor.timer);
|
|
}
|
|
|
|
struct ncsi_channel *ncsi_find_channel(struct ncsi_package *np,
|
|
unsigned char id)
|
|
{
|
|
struct ncsi_channel *nc;
|
|
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
if (nc->id == id)
|
|
return nc;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
struct ncsi_channel *ncsi_add_channel(struct ncsi_package *np, unsigned char id)
|
|
{
|
|
struct ncsi_channel *nc, *tmp;
|
|
int index;
|
|
unsigned long flags;
|
|
|
|
nc = kzalloc(sizeof(*nc), GFP_ATOMIC);
|
|
if (!nc)
|
|
return NULL;
|
|
|
|
nc->id = id;
|
|
nc->package = np;
|
|
nc->state = NCSI_CHANNEL_INACTIVE;
|
|
nc->monitor.enabled = false;
|
|
timer_setup(&nc->monitor.timer, ncsi_channel_monitor, 0);
|
|
spin_lock_init(&nc->lock);
|
|
INIT_LIST_HEAD(&nc->link);
|
|
for (index = 0; index < NCSI_CAP_MAX; index++)
|
|
nc->caps[index].index = index;
|
|
for (index = 0; index < NCSI_MODE_MAX; index++)
|
|
nc->modes[index].index = index;
|
|
|
|
spin_lock_irqsave(&np->lock, flags);
|
|
tmp = ncsi_find_channel(np, id);
|
|
if (tmp) {
|
|
spin_unlock_irqrestore(&np->lock, flags);
|
|
kfree(nc);
|
|
return tmp;
|
|
}
|
|
|
|
list_add_tail_rcu(&nc->node, &np->channels);
|
|
np->channel_num++;
|
|
spin_unlock_irqrestore(&np->lock, flags);
|
|
|
|
return nc;
|
|
}
|
|
|
|
static void ncsi_remove_channel(struct ncsi_channel *nc)
|
|
{
|
|
struct ncsi_package *np = nc->package;
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
|
|
/* Release filters */
|
|
kfree(nc->mac_filter.addrs);
|
|
kfree(nc->vlan_filter.vids);
|
|
|
|
nc->state = NCSI_CHANNEL_INACTIVE;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
ncsi_stop_channel_monitor(nc);
|
|
|
|
/* Remove and free channel */
|
|
spin_lock_irqsave(&np->lock, flags);
|
|
list_del_rcu(&nc->node);
|
|
np->channel_num--;
|
|
spin_unlock_irqrestore(&np->lock, flags);
|
|
|
|
kfree(nc);
|
|
}
|
|
|
|
struct ncsi_package *ncsi_find_package(struct ncsi_dev_priv *ndp,
|
|
unsigned char id)
|
|
{
|
|
struct ncsi_package *np;
|
|
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np) {
|
|
if (np->id == id)
|
|
return np;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
struct ncsi_package *ncsi_add_package(struct ncsi_dev_priv *ndp,
|
|
unsigned char id)
|
|
{
|
|
struct ncsi_package *np, *tmp;
|
|
unsigned long flags;
|
|
|
|
np = kzalloc(sizeof(*np), GFP_ATOMIC);
|
|
if (!np)
|
|
return NULL;
|
|
|
|
np->id = id;
|
|
np->ndp = ndp;
|
|
spin_lock_init(&np->lock);
|
|
INIT_LIST_HEAD(&np->channels);
|
|
np->channel_whitelist = UINT_MAX;
|
|
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
tmp = ncsi_find_package(ndp, id);
|
|
if (tmp) {
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
kfree(np);
|
|
return tmp;
|
|
}
|
|
|
|
list_add_tail_rcu(&np->node, &ndp->packages);
|
|
ndp->package_num++;
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
return np;
|
|
}
|
|
|
|
void ncsi_remove_package(struct ncsi_package *np)
|
|
{
|
|
struct ncsi_dev_priv *ndp = np->ndp;
|
|
struct ncsi_channel *nc, *tmp;
|
|
unsigned long flags;
|
|
|
|
/* Release all child channels */
|
|
list_for_each_entry_safe(nc, tmp, &np->channels, node)
|
|
ncsi_remove_channel(nc);
|
|
|
|
/* Remove and free package */
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
list_del_rcu(&np->node);
|
|
ndp->package_num--;
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
kfree(np);
|
|
}
|
|
|
|
void ncsi_find_package_and_channel(struct ncsi_dev_priv *ndp,
|
|
unsigned char id,
|
|
struct ncsi_package **np,
|
|
struct ncsi_channel **nc)
|
|
{
|
|
struct ncsi_package *p;
|
|
struct ncsi_channel *c;
|
|
|
|
p = ncsi_find_package(ndp, NCSI_PACKAGE_INDEX(id));
|
|
c = p ? ncsi_find_channel(p, NCSI_CHANNEL_INDEX(id)) : NULL;
|
|
|
|
if (np)
|
|
*np = p;
|
|
if (nc)
|
|
*nc = c;
|
|
}
|
|
|
|
/* For two consecutive NCSI commands, the packet IDs shouldn't
|
|
* be same. Otherwise, the bogus response might be replied. So
|
|
* the available IDs are allocated in round-robin fashion.
|
|
*/
|
|
struct ncsi_request *ncsi_alloc_request(struct ncsi_dev_priv *ndp,
|
|
unsigned int req_flags)
|
|
{
|
|
struct ncsi_request *nr = NULL;
|
|
int i, limit = ARRAY_SIZE(ndp->requests);
|
|
unsigned long flags;
|
|
|
|
/* Check if there is one available request until the ceiling */
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
for (i = ndp->request_id; i < limit; i++) {
|
|
if (ndp->requests[i].used)
|
|
continue;
|
|
|
|
nr = &ndp->requests[i];
|
|
nr->used = true;
|
|
nr->flags = req_flags;
|
|
ndp->request_id = i + 1;
|
|
goto found;
|
|
}
|
|
|
|
/* Fail back to check from the starting cursor */
|
|
for (i = NCSI_REQ_START_IDX; i < ndp->request_id; i++) {
|
|
if (ndp->requests[i].used)
|
|
continue;
|
|
|
|
nr = &ndp->requests[i];
|
|
nr->used = true;
|
|
nr->flags = req_flags;
|
|
ndp->request_id = i + 1;
|
|
goto found;
|
|
}
|
|
|
|
found:
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
return nr;
|
|
}
|
|
|
|
void ncsi_free_request(struct ncsi_request *nr)
|
|
{
|
|
struct ncsi_dev_priv *ndp = nr->ndp;
|
|
struct sk_buff *cmd, *rsp;
|
|
unsigned long flags;
|
|
bool driven;
|
|
|
|
if (nr->enabled) {
|
|
nr->enabled = false;
|
|
del_timer_sync(&nr->timer);
|
|
}
|
|
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
cmd = nr->cmd;
|
|
rsp = nr->rsp;
|
|
nr->cmd = NULL;
|
|
nr->rsp = NULL;
|
|
nr->used = false;
|
|
driven = !!(nr->flags & NCSI_REQ_FLAG_EVENT_DRIVEN);
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
if (driven && cmd && --ndp->pending_req_num == 0)
|
|
schedule_work(&ndp->work);
|
|
|
|
/* Release command and response */
|
|
consume_skb(cmd);
|
|
consume_skb(rsp);
|
|
}
|
|
|
|
struct ncsi_dev *ncsi_find_dev(struct net_device *dev)
|
|
{
|
|
struct ncsi_dev_priv *ndp;
|
|
|
|
NCSI_FOR_EACH_DEV(ndp) {
|
|
if (ndp->ndev.dev == dev)
|
|
return &ndp->ndev;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void ncsi_request_timeout(struct timer_list *t)
|
|
{
|
|
struct ncsi_request *nr = from_timer(nr, t, timer);
|
|
struct ncsi_dev_priv *ndp = nr->ndp;
|
|
struct ncsi_cmd_pkt *cmd;
|
|
struct ncsi_package *np;
|
|
struct ncsi_channel *nc;
|
|
unsigned long flags;
|
|
|
|
/* If the request already had associated response,
|
|
* let the response handler to release it.
|
|
*/
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
nr->enabled = false;
|
|
if (nr->rsp || !nr->cmd) {
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
return;
|
|
}
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
if (nr->flags == NCSI_REQ_FLAG_NETLINK_DRIVEN) {
|
|
if (nr->cmd) {
|
|
/* Find the package */
|
|
cmd = (struct ncsi_cmd_pkt *)
|
|
skb_network_header(nr->cmd);
|
|
ncsi_find_package_and_channel(ndp,
|
|
cmd->cmd.common.channel,
|
|
&np, &nc);
|
|
ncsi_send_netlink_timeout(nr, np, nc);
|
|
}
|
|
}
|
|
|
|
/* Release the request */
|
|
ncsi_free_request(nr);
|
|
}
|
|
|
|
static void ncsi_suspend_channel(struct ncsi_dev_priv *ndp)
|
|
{
|
|
struct ncsi_dev *nd = &ndp->ndev;
|
|
struct ncsi_package *np;
|
|
struct ncsi_channel *nc, *tmp;
|
|
struct ncsi_cmd_arg nca;
|
|
unsigned long flags;
|
|
int ret;
|
|
|
|
np = ndp->active_package;
|
|
nc = ndp->active_channel;
|
|
nca.ndp = ndp;
|
|
nca.req_flags = NCSI_REQ_FLAG_EVENT_DRIVEN;
|
|
switch (nd->state) {
|
|
case ncsi_dev_state_suspend:
|
|
nd->state = ncsi_dev_state_suspend_select;
|
|
/* Fall through */
|
|
case ncsi_dev_state_suspend_select:
|
|
ndp->pending_req_num = 1;
|
|
|
|
nca.type = NCSI_PKT_CMD_SP;
|
|
nca.package = np->id;
|
|
nca.channel = NCSI_RESERVED_CHANNEL;
|
|
if (ndp->flags & NCSI_DEV_HWA)
|
|
nca.bytes[0] = 0;
|
|
else
|
|
nca.bytes[0] = 1;
|
|
|
|
/* To retrieve the last link states of channels in current
|
|
* package when current active channel needs fail over to
|
|
* another one. It means we will possibly select another
|
|
* channel as next active one. The link states of channels
|
|
* are most important factor of the selection. So we need
|
|
* accurate link states. Unfortunately, the link states on
|
|
* inactive channels can't be updated with LSC AEN in time.
|
|
*/
|
|
if (ndp->flags & NCSI_DEV_RESHUFFLE)
|
|
nd->state = ncsi_dev_state_suspend_gls;
|
|
else
|
|
nd->state = ncsi_dev_state_suspend_dcnt;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
goto error;
|
|
|
|
break;
|
|
case ncsi_dev_state_suspend_gls:
|
|
ndp->pending_req_num = np->channel_num;
|
|
|
|
nca.type = NCSI_PKT_CMD_GLS;
|
|
nca.package = np->id;
|
|
|
|
nd->state = ncsi_dev_state_suspend_dcnt;
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
nca.channel = nc->id;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
goto error;
|
|
}
|
|
|
|
break;
|
|
case ncsi_dev_state_suspend_dcnt:
|
|
ndp->pending_req_num = 1;
|
|
|
|
nca.type = NCSI_PKT_CMD_DCNT;
|
|
nca.package = np->id;
|
|
nca.channel = nc->id;
|
|
|
|
nd->state = ncsi_dev_state_suspend_dc;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
goto error;
|
|
|
|
break;
|
|
case ncsi_dev_state_suspend_dc:
|
|
ndp->pending_req_num = 1;
|
|
|
|
nca.type = NCSI_PKT_CMD_DC;
|
|
nca.package = np->id;
|
|
nca.channel = nc->id;
|
|
nca.bytes[0] = 1;
|
|
|
|
nd->state = ncsi_dev_state_suspend_deselect;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
goto error;
|
|
|
|
NCSI_FOR_EACH_CHANNEL(np, tmp) {
|
|
/* If there is another channel active on this package
|
|
* do not deselect the package.
|
|
*/
|
|
if (tmp != nc && tmp->state == NCSI_CHANNEL_ACTIVE) {
|
|
nd->state = ncsi_dev_state_suspend_done;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case ncsi_dev_state_suspend_deselect:
|
|
ndp->pending_req_num = 1;
|
|
|
|
nca.type = NCSI_PKT_CMD_DP;
|
|
nca.package = np->id;
|
|
nca.channel = NCSI_RESERVED_CHANNEL;
|
|
|
|
nd->state = ncsi_dev_state_suspend_done;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
goto error;
|
|
|
|
break;
|
|
case ncsi_dev_state_suspend_done:
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
nc->state = NCSI_CHANNEL_INACTIVE;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
if (ndp->flags & NCSI_DEV_RESET)
|
|
ncsi_reset_dev(nd);
|
|
else
|
|
ncsi_process_next_channel(ndp);
|
|
break;
|
|
default:
|
|
netdev_warn(nd->dev, "Wrong NCSI state 0x%x in suspend\n",
|
|
nd->state);
|
|
}
|
|
|
|
return;
|
|
error:
|
|
nd->state = ncsi_dev_state_functional;
|
|
}
|
|
|
|
/* Check the VLAN filter bitmap for a set filter, and construct a
|
|
* "Set VLAN Filter - Disable" packet if found.
|
|
*/
|
|
static int clear_one_vid(struct ncsi_dev_priv *ndp, struct ncsi_channel *nc,
|
|
struct ncsi_cmd_arg *nca)
|
|
{
|
|
struct ncsi_channel_vlan_filter *ncf;
|
|
unsigned long flags;
|
|
void *bitmap;
|
|
int index;
|
|
u16 vid;
|
|
|
|
ncf = &nc->vlan_filter;
|
|
bitmap = &ncf->bitmap;
|
|
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
index = find_next_bit(bitmap, ncf->n_vids, 0);
|
|
if (index >= ncf->n_vids) {
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
return -1;
|
|
}
|
|
vid = ncf->vids[index];
|
|
|
|
clear_bit(index, bitmap);
|
|
ncf->vids[index] = 0;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
nca->type = NCSI_PKT_CMD_SVF;
|
|
nca->words[1] = vid;
|
|
/* HW filter index starts at 1 */
|
|
nca->bytes[6] = index + 1;
|
|
nca->bytes[7] = 0x00;
|
|
return 0;
|
|
}
|
|
|
|
/* Find an outstanding VLAN tag and constuct a "Set VLAN Filter - Enable"
|
|
* packet.
|
|
*/
|
|
static int set_one_vid(struct ncsi_dev_priv *ndp, struct ncsi_channel *nc,
|
|
struct ncsi_cmd_arg *nca)
|
|
{
|
|
struct ncsi_channel_vlan_filter *ncf;
|
|
struct vlan_vid *vlan = NULL;
|
|
unsigned long flags;
|
|
int i, index;
|
|
void *bitmap;
|
|
u16 vid;
|
|
|
|
if (list_empty(&ndp->vlan_vids))
|
|
return -1;
|
|
|
|
ncf = &nc->vlan_filter;
|
|
bitmap = &ncf->bitmap;
|
|
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
|
|
rcu_read_lock();
|
|
list_for_each_entry_rcu(vlan, &ndp->vlan_vids, list) {
|
|
vid = vlan->vid;
|
|
for (i = 0; i < ncf->n_vids; i++)
|
|
if (ncf->vids[i] == vid) {
|
|
vid = 0;
|
|
break;
|
|
}
|
|
if (vid)
|
|
break;
|
|
}
|
|
rcu_read_unlock();
|
|
|
|
if (!vid) {
|
|
/* No VLAN ID is not set */
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
return -1;
|
|
}
|
|
|
|
index = find_next_zero_bit(bitmap, ncf->n_vids, 0);
|
|
if (index < 0 || index >= ncf->n_vids) {
|
|
netdev_err(ndp->ndev.dev,
|
|
"Channel %u already has all VLAN filters set\n",
|
|
nc->id);
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
return -1;
|
|
}
|
|
|
|
ncf->vids[index] = vid;
|
|
set_bit(index, bitmap);
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
nca->type = NCSI_PKT_CMD_SVF;
|
|
nca->words[1] = vid;
|
|
/* HW filter index starts at 1 */
|
|
nca->bytes[6] = index + 1;
|
|
nca->bytes[7] = 0x01;
|
|
|
|
return 0;
|
|
}
|
|
|
|
#if IS_ENABLED(CONFIG_NCSI_OEM_CMD_GET_MAC)
|
|
|
|
/* NCSI OEM Command APIs */
|
|
static int ncsi_oem_gma_handler_bcm(struct ncsi_cmd_arg *nca)
|
|
{
|
|
unsigned char data[NCSI_OEM_BCM_CMD_GMA_LEN];
|
|
int ret = 0;
|
|
|
|
nca->payload = NCSI_OEM_BCM_CMD_GMA_LEN;
|
|
|
|
memset(data, 0, NCSI_OEM_BCM_CMD_GMA_LEN);
|
|
*(unsigned int *)data = ntohl(NCSI_OEM_MFR_BCM_ID);
|
|
data[5] = NCSI_OEM_BCM_CMD_GMA;
|
|
|
|
nca->data = data;
|
|
|
|
ret = ncsi_xmit_cmd(nca);
|
|
if (ret)
|
|
netdev_err(nca->ndp->ndev.dev,
|
|
"NCSI: Failed to transmit cmd 0x%x during configure\n",
|
|
nca->type);
|
|
return ret;
|
|
}
|
|
|
|
static int ncsi_oem_gma_handler_mlx(struct ncsi_cmd_arg *nca)
|
|
{
|
|
union {
|
|
u8 data_u8[NCSI_OEM_MLX_CMD_GMA_LEN];
|
|
u32 data_u32[NCSI_OEM_MLX_CMD_GMA_LEN / sizeof(u32)];
|
|
} u;
|
|
int ret = 0;
|
|
|
|
nca->payload = NCSI_OEM_MLX_CMD_GMA_LEN;
|
|
|
|
memset(&u, 0, sizeof(u));
|
|
u.data_u32[0] = ntohl(NCSI_OEM_MFR_MLX_ID);
|
|
u.data_u8[5] = NCSI_OEM_MLX_CMD_GMA;
|
|
u.data_u8[6] = NCSI_OEM_MLX_CMD_GMA_PARAM;
|
|
|
|
nca->data = u.data_u8;
|
|
|
|
ret = ncsi_xmit_cmd(nca);
|
|
if (ret)
|
|
netdev_err(nca->ndp->ndev.dev,
|
|
"NCSI: Failed to transmit cmd 0x%x during configure\n",
|
|
nca->type);
|
|
return ret;
|
|
}
|
|
|
|
/* OEM Command handlers initialization */
|
|
static struct ncsi_oem_gma_handler {
|
|
unsigned int mfr_id;
|
|
int (*handler)(struct ncsi_cmd_arg *nca);
|
|
} ncsi_oem_gma_handlers[] = {
|
|
{ NCSI_OEM_MFR_BCM_ID, ncsi_oem_gma_handler_bcm },
|
|
{ NCSI_OEM_MFR_MLX_ID, ncsi_oem_gma_handler_mlx }
|
|
};
|
|
|
|
static int ncsi_gma_handler(struct ncsi_cmd_arg *nca, unsigned int mf_id)
|
|
{
|
|
struct ncsi_oem_gma_handler *nch = NULL;
|
|
int i;
|
|
|
|
/* This function should only be called once, return if flag set */
|
|
if (nca->ndp->gma_flag == 1)
|
|
return -1;
|
|
|
|
/* Find gma handler for given manufacturer id */
|
|
for (i = 0; i < ARRAY_SIZE(ncsi_oem_gma_handlers); i++) {
|
|
if (ncsi_oem_gma_handlers[i].mfr_id == mf_id) {
|
|
if (ncsi_oem_gma_handlers[i].handler)
|
|
nch = &ncsi_oem_gma_handlers[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!nch) {
|
|
netdev_err(nca->ndp->ndev.dev,
|
|
"NCSI: No GMA handler available for MFR-ID (0x%x)\n",
|
|
mf_id);
|
|
return -1;
|
|
}
|
|
|
|
/* Get Mac address from NCSI device */
|
|
return nch->handler(nca);
|
|
}
|
|
|
|
#endif /* CONFIG_NCSI_OEM_CMD_GET_MAC */
|
|
|
|
/* Determine if a given channel from the channel_queue should be used for Tx */
|
|
static bool ncsi_channel_is_tx(struct ncsi_dev_priv *ndp,
|
|
struct ncsi_channel *nc)
|
|
{
|
|
struct ncsi_channel_mode *ncm;
|
|
struct ncsi_channel *channel;
|
|
struct ncsi_package *np;
|
|
|
|
/* Check if any other channel has Tx enabled; a channel may have already
|
|
* been configured and removed from the channel queue.
|
|
*/
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np) {
|
|
if (!ndp->multi_package && np != nc->package)
|
|
continue;
|
|
NCSI_FOR_EACH_CHANNEL(np, channel) {
|
|
ncm = &channel->modes[NCSI_MODE_TX_ENABLE];
|
|
if (ncm->enable)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* This channel is the preferred channel and has link */
|
|
list_for_each_entry_rcu(channel, &ndp->channel_queue, link) {
|
|
np = channel->package;
|
|
if (np->preferred_channel &&
|
|
ncsi_channel_has_link(np->preferred_channel)) {
|
|
return np->preferred_channel == nc;
|
|
}
|
|
}
|
|
|
|
/* This channel has link */
|
|
if (ncsi_channel_has_link(nc))
|
|
return true;
|
|
|
|
list_for_each_entry_rcu(channel, &ndp->channel_queue, link)
|
|
if (ncsi_channel_has_link(channel))
|
|
return false;
|
|
|
|
/* No other channel has link; default to this one */
|
|
return true;
|
|
}
|
|
|
|
/* Change the active Tx channel in a multi-channel setup */
|
|
int ncsi_update_tx_channel(struct ncsi_dev_priv *ndp,
|
|
struct ncsi_package *package,
|
|
struct ncsi_channel *disable,
|
|
struct ncsi_channel *enable)
|
|
{
|
|
struct ncsi_cmd_arg nca;
|
|
struct ncsi_channel *nc;
|
|
struct ncsi_package *np;
|
|
int ret = 0;
|
|
|
|
if (!package->multi_channel && !ndp->multi_package)
|
|
netdev_warn(ndp->ndev.dev,
|
|
"NCSI: Trying to update Tx channel in single-channel mode\n");
|
|
nca.ndp = ndp;
|
|
nca.req_flags = 0;
|
|
|
|
/* Find current channel with Tx enabled */
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np) {
|
|
if (disable)
|
|
break;
|
|
if (!ndp->multi_package && np != package)
|
|
continue;
|
|
|
|
NCSI_FOR_EACH_CHANNEL(np, nc)
|
|
if (nc->modes[NCSI_MODE_TX_ENABLE].enable) {
|
|
disable = nc;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Find a suitable channel for Tx */
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np) {
|
|
if (enable)
|
|
break;
|
|
if (!ndp->multi_package && np != package)
|
|
continue;
|
|
if (!(ndp->package_whitelist & (0x1 << np->id)))
|
|
continue;
|
|
|
|
if (np->preferred_channel &&
|
|
ncsi_channel_has_link(np->preferred_channel)) {
|
|
enable = np->preferred_channel;
|
|
break;
|
|
}
|
|
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
if (!(np->channel_whitelist & 0x1 << nc->id))
|
|
continue;
|
|
if (nc->state != NCSI_CHANNEL_ACTIVE)
|
|
continue;
|
|
if (ncsi_channel_has_link(nc)) {
|
|
enable = nc;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (disable == enable)
|
|
return -1;
|
|
|
|
if (!enable)
|
|
return -1;
|
|
|
|
if (disable) {
|
|
nca.channel = disable->id;
|
|
nca.package = disable->package->id;
|
|
nca.type = NCSI_PKT_CMD_DCNT;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
netdev_err(ndp->ndev.dev,
|
|
"Error %d sending DCNT\n",
|
|
ret);
|
|
}
|
|
|
|
netdev_info(ndp->ndev.dev, "NCSI: channel %u enables Tx\n", enable->id);
|
|
|
|
nca.channel = enable->id;
|
|
nca.package = enable->package->id;
|
|
nca.type = NCSI_PKT_CMD_ECNT;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
netdev_err(ndp->ndev.dev,
|
|
"Error %d sending ECNT\n",
|
|
ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void ncsi_configure_channel(struct ncsi_dev_priv *ndp)
|
|
{
|
|
struct ncsi_package *np = ndp->active_package;
|
|
struct ncsi_channel *nc = ndp->active_channel;
|
|
struct ncsi_channel *hot_nc = NULL;
|
|
struct ncsi_dev *nd = &ndp->ndev;
|
|
struct net_device *dev = nd->dev;
|
|
struct ncsi_cmd_arg nca;
|
|
unsigned char index;
|
|
unsigned long flags;
|
|
int ret;
|
|
|
|
nca.ndp = ndp;
|
|
nca.req_flags = NCSI_REQ_FLAG_EVENT_DRIVEN;
|
|
switch (nd->state) {
|
|
case ncsi_dev_state_config:
|
|
case ncsi_dev_state_config_sp:
|
|
ndp->pending_req_num = 1;
|
|
|
|
/* Select the specific package */
|
|
nca.type = NCSI_PKT_CMD_SP;
|
|
if (ndp->flags & NCSI_DEV_HWA)
|
|
nca.bytes[0] = 0;
|
|
else
|
|
nca.bytes[0] = 1;
|
|
nca.package = np->id;
|
|
nca.channel = NCSI_RESERVED_CHANNEL;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret) {
|
|
netdev_err(ndp->ndev.dev,
|
|
"NCSI: Failed to transmit CMD_SP\n");
|
|
goto error;
|
|
}
|
|
|
|
nd->state = ncsi_dev_state_config_cis;
|
|
break;
|
|
case ncsi_dev_state_config_cis:
|
|
ndp->pending_req_num = 1;
|
|
|
|
/* Clear initial state */
|
|
nca.type = NCSI_PKT_CMD_CIS;
|
|
nca.package = np->id;
|
|
nca.channel = nc->id;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret) {
|
|
netdev_err(ndp->ndev.dev,
|
|
"NCSI: Failed to transmit CMD_CIS\n");
|
|
goto error;
|
|
}
|
|
|
|
nd->state = ncsi_dev_state_config_oem_gma;
|
|
break;
|
|
case ncsi_dev_state_config_oem_gma:
|
|
nd->state = ncsi_dev_state_config_clear_vids;
|
|
ret = -1;
|
|
|
|
#if IS_ENABLED(CONFIG_NCSI_OEM_CMD_GET_MAC)
|
|
nca.type = NCSI_PKT_CMD_OEM;
|
|
nca.package = np->id;
|
|
nca.channel = nc->id;
|
|
ndp->pending_req_num = 1;
|
|
ret = ncsi_gma_handler(&nca, nc->version.mf_id);
|
|
#endif /* CONFIG_NCSI_OEM_CMD_GET_MAC */
|
|
|
|
if (ret < 0)
|
|
schedule_work(&ndp->work);
|
|
|
|
break;
|
|
case ncsi_dev_state_config_clear_vids:
|
|
case ncsi_dev_state_config_svf:
|
|
case ncsi_dev_state_config_ev:
|
|
case ncsi_dev_state_config_sma:
|
|
case ncsi_dev_state_config_ebf:
|
|
case ncsi_dev_state_config_dgmf:
|
|
case ncsi_dev_state_config_ecnt:
|
|
case ncsi_dev_state_config_ec:
|
|
case ncsi_dev_state_config_ae:
|
|
case ncsi_dev_state_config_gls:
|
|
ndp->pending_req_num = 1;
|
|
|
|
nca.package = np->id;
|
|
nca.channel = nc->id;
|
|
|
|
/* Clear any active filters on the channel before setting */
|
|
if (nd->state == ncsi_dev_state_config_clear_vids) {
|
|
ret = clear_one_vid(ndp, nc, &nca);
|
|
if (ret) {
|
|
nd->state = ncsi_dev_state_config_svf;
|
|
schedule_work(&ndp->work);
|
|
break;
|
|
}
|
|
/* Repeat */
|
|
nd->state = ncsi_dev_state_config_clear_vids;
|
|
/* Add known VLAN tags to the filter */
|
|
} else if (nd->state == ncsi_dev_state_config_svf) {
|
|
ret = set_one_vid(ndp, nc, &nca);
|
|
if (ret) {
|
|
nd->state = ncsi_dev_state_config_ev;
|
|
schedule_work(&ndp->work);
|
|
break;
|
|
}
|
|
/* Repeat */
|
|
nd->state = ncsi_dev_state_config_svf;
|
|
/* Enable/Disable the VLAN filter */
|
|
} else if (nd->state == ncsi_dev_state_config_ev) {
|
|
if (list_empty(&ndp->vlan_vids)) {
|
|
nca.type = NCSI_PKT_CMD_DV;
|
|
} else {
|
|
nca.type = NCSI_PKT_CMD_EV;
|
|
nca.bytes[3] = NCSI_CAP_VLAN_NO;
|
|
}
|
|
nd->state = ncsi_dev_state_config_sma;
|
|
} else if (nd->state == ncsi_dev_state_config_sma) {
|
|
/* Use first entry in unicast filter table. Note that
|
|
* the MAC filter table starts from entry 1 instead of
|
|
* 0.
|
|
*/
|
|
nca.type = NCSI_PKT_CMD_SMA;
|
|
for (index = 0; index < 6; index++)
|
|
nca.bytes[index] = dev->dev_addr[index];
|
|
nca.bytes[6] = 0x1;
|
|
nca.bytes[7] = 0x1;
|
|
nd->state = ncsi_dev_state_config_ebf;
|
|
} else if (nd->state == ncsi_dev_state_config_ebf) {
|
|
nca.type = NCSI_PKT_CMD_EBF;
|
|
nca.dwords[0] = nc->caps[NCSI_CAP_BC].cap;
|
|
/* if multicast global filtering is supported then
|
|
* disable it so that all multicast packet will be
|
|
* forwarded to management controller
|
|
*/
|
|
if (nc->caps[NCSI_CAP_GENERIC].cap &
|
|
NCSI_CAP_GENERIC_MC)
|
|
nd->state = ncsi_dev_state_config_dgmf;
|
|
else if (ncsi_channel_is_tx(ndp, nc))
|
|
nd->state = ncsi_dev_state_config_ecnt;
|
|
else
|
|
nd->state = ncsi_dev_state_config_ec;
|
|
} else if (nd->state == ncsi_dev_state_config_dgmf) {
|
|
nca.type = NCSI_PKT_CMD_DGMF;
|
|
if (ncsi_channel_is_tx(ndp, nc))
|
|
nd->state = ncsi_dev_state_config_ecnt;
|
|
else
|
|
nd->state = ncsi_dev_state_config_ec;
|
|
} else if (nd->state == ncsi_dev_state_config_ecnt) {
|
|
if (np->preferred_channel &&
|
|
nc != np->preferred_channel)
|
|
netdev_info(ndp->ndev.dev,
|
|
"NCSI: Tx failed over to channel %u\n",
|
|
nc->id);
|
|
nca.type = NCSI_PKT_CMD_ECNT;
|
|
nd->state = ncsi_dev_state_config_ec;
|
|
} else if (nd->state == ncsi_dev_state_config_ec) {
|
|
/* Enable AEN if it's supported */
|
|
nca.type = NCSI_PKT_CMD_EC;
|
|
nd->state = ncsi_dev_state_config_ae;
|
|
if (!(nc->caps[NCSI_CAP_AEN].cap & NCSI_CAP_AEN_MASK))
|
|
nd->state = ncsi_dev_state_config_gls;
|
|
} else if (nd->state == ncsi_dev_state_config_ae) {
|
|
nca.type = NCSI_PKT_CMD_AE;
|
|
nca.bytes[0] = 0;
|
|
nca.dwords[1] = nc->caps[NCSI_CAP_AEN].cap;
|
|
nd->state = ncsi_dev_state_config_gls;
|
|
} else if (nd->state == ncsi_dev_state_config_gls) {
|
|
nca.type = NCSI_PKT_CMD_GLS;
|
|
nd->state = ncsi_dev_state_config_done;
|
|
}
|
|
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret) {
|
|
netdev_err(ndp->ndev.dev,
|
|
"NCSI: Failed to transmit CMD %x\n",
|
|
nca.type);
|
|
goto error;
|
|
}
|
|
break;
|
|
case ncsi_dev_state_config_done:
|
|
netdev_dbg(ndp->ndev.dev, "NCSI: channel %u config done\n",
|
|
nc->id);
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
nc->state = NCSI_CHANNEL_ACTIVE;
|
|
|
|
if (ndp->flags & NCSI_DEV_RESET) {
|
|
/* A reset event happened during config, start it now */
|
|
nc->reconfigure_needed = false;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
ncsi_reset_dev(nd);
|
|
break;
|
|
}
|
|
|
|
if (nc->reconfigure_needed) {
|
|
/* This channel's configuration has been updated
|
|
* part-way during the config state - start the
|
|
* channel configuration over
|
|
*/
|
|
nc->reconfigure_needed = false;
|
|
nc->state = NCSI_CHANNEL_INACTIVE;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
list_add_tail_rcu(&nc->link, &ndp->channel_queue);
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
netdev_dbg(dev, "Dirty NCSI channel state reset\n");
|
|
ncsi_process_next_channel(ndp);
|
|
break;
|
|
}
|
|
|
|
if (nc->modes[NCSI_MODE_LINK].data[2] & 0x1) {
|
|
hot_nc = nc;
|
|
} else {
|
|
hot_nc = NULL;
|
|
netdev_dbg(ndp->ndev.dev,
|
|
"NCSI: channel %u link down after config\n",
|
|
nc->id);
|
|
}
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
/* Update the hot channel */
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
ndp->hot_channel = hot_nc;
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
ncsi_start_channel_monitor(nc);
|
|
ncsi_process_next_channel(ndp);
|
|
break;
|
|
default:
|
|
netdev_alert(dev, "Wrong NCSI state 0x%x in config\n",
|
|
nd->state);
|
|
}
|
|
|
|
return;
|
|
|
|
error:
|
|
ncsi_report_link(ndp, true);
|
|
}
|
|
|
|
static int ncsi_choose_active_channel(struct ncsi_dev_priv *ndp)
|
|
{
|
|
struct ncsi_channel *nc, *found, *hot_nc;
|
|
struct ncsi_channel_mode *ncm;
|
|
unsigned long flags, cflags;
|
|
struct ncsi_package *np;
|
|
bool with_link;
|
|
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
hot_nc = ndp->hot_channel;
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
/* By default the search is done once an inactive channel with up
|
|
* link is found, unless a preferred channel is set.
|
|
* If multi_package or multi_channel are configured all channels in the
|
|
* whitelist are added to the channel queue.
|
|
*/
|
|
found = NULL;
|
|
with_link = false;
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np) {
|
|
if (!(ndp->package_whitelist & (0x1 << np->id)))
|
|
continue;
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
if (!(np->channel_whitelist & (0x1 << nc->id)))
|
|
continue;
|
|
|
|
spin_lock_irqsave(&nc->lock, cflags);
|
|
|
|
if (!list_empty(&nc->link) ||
|
|
nc->state != NCSI_CHANNEL_INACTIVE) {
|
|
spin_unlock_irqrestore(&nc->lock, cflags);
|
|
continue;
|
|
}
|
|
|
|
if (!found)
|
|
found = nc;
|
|
|
|
if (nc == hot_nc)
|
|
found = nc;
|
|
|
|
ncm = &nc->modes[NCSI_MODE_LINK];
|
|
if (ncm->data[2] & 0x1) {
|
|
found = nc;
|
|
with_link = true;
|
|
}
|
|
|
|
/* If multi_channel is enabled configure all valid
|
|
* channels whether or not they currently have link
|
|
* so they will have AENs enabled.
|
|
*/
|
|
if (with_link || np->multi_channel) {
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
list_add_tail_rcu(&nc->link,
|
|
&ndp->channel_queue);
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
netdev_dbg(ndp->ndev.dev,
|
|
"NCSI: Channel %u added to queue (link %s)\n",
|
|
nc->id,
|
|
ncm->data[2] & 0x1 ? "up" : "down");
|
|
}
|
|
|
|
spin_unlock_irqrestore(&nc->lock, cflags);
|
|
|
|
if (with_link && !np->multi_channel)
|
|
break;
|
|
}
|
|
if (with_link && !ndp->multi_package)
|
|
break;
|
|
}
|
|
|
|
if (list_empty(&ndp->channel_queue) && found) {
|
|
netdev_info(ndp->ndev.dev,
|
|
"NCSI: No channel with link found, configuring channel %u\n",
|
|
found->id);
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
list_add_tail_rcu(&found->link, &ndp->channel_queue);
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
} else if (!found) {
|
|
netdev_warn(ndp->ndev.dev,
|
|
"NCSI: No channel found to configure!\n");
|
|
ncsi_report_link(ndp, true);
|
|
return -ENODEV;
|
|
}
|
|
|
|
return ncsi_process_next_channel(ndp);
|
|
}
|
|
|
|
static bool ncsi_check_hwa(struct ncsi_dev_priv *ndp)
|
|
{
|
|
struct ncsi_package *np;
|
|
struct ncsi_channel *nc;
|
|
unsigned int cap;
|
|
bool has_channel = false;
|
|
|
|
/* The hardware arbitration is disabled if any one channel
|
|
* doesn't support explicitly.
|
|
*/
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np) {
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
has_channel = true;
|
|
|
|
cap = nc->caps[NCSI_CAP_GENERIC].cap;
|
|
if (!(cap & NCSI_CAP_GENERIC_HWA) ||
|
|
(cap & NCSI_CAP_GENERIC_HWA_MASK) !=
|
|
NCSI_CAP_GENERIC_HWA_SUPPORT) {
|
|
ndp->flags &= ~NCSI_DEV_HWA;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (has_channel) {
|
|
ndp->flags |= NCSI_DEV_HWA;
|
|
return true;
|
|
}
|
|
|
|
ndp->flags &= ~NCSI_DEV_HWA;
|
|
return false;
|
|
}
|
|
|
|
static void ncsi_probe_channel(struct ncsi_dev_priv *ndp)
|
|
{
|
|
struct ncsi_dev *nd = &ndp->ndev;
|
|
struct ncsi_package *np;
|
|
struct ncsi_channel *nc;
|
|
struct ncsi_cmd_arg nca;
|
|
unsigned char index;
|
|
int ret;
|
|
|
|
nca.ndp = ndp;
|
|
nca.req_flags = NCSI_REQ_FLAG_EVENT_DRIVEN;
|
|
switch (nd->state) {
|
|
case ncsi_dev_state_probe:
|
|
nd->state = ncsi_dev_state_probe_deselect;
|
|
/* Fall through */
|
|
case ncsi_dev_state_probe_deselect:
|
|
ndp->pending_req_num = 8;
|
|
|
|
/* Deselect all possible packages */
|
|
nca.type = NCSI_PKT_CMD_DP;
|
|
nca.channel = NCSI_RESERVED_CHANNEL;
|
|
for (index = 0; index < 8; index++) {
|
|
nca.package = index;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
goto error;
|
|
}
|
|
|
|
nd->state = ncsi_dev_state_probe_package;
|
|
break;
|
|
case ncsi_dev_state_probe_package:
|
|
ndp->pending_req_num = 1;
|
|
|
|
nca.type = NCSI_PKT_CMD_SP;
|
|
nca.bytes[0] = 1;
|
|
nca.package = ndp->package_probe_id;
|
|
nca.channel = NCSI_RESERVED_CHANNEL;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
goto error;
|
|
nd->state = ncsi_dev_state_probe_channel;
|
|
break;
|
|
case ncsi_dev_state_probe_channel:
|
|
ndp->active_package = ncsi_find_package(ndp,
|
|
ndp->package_probe_id);
|
|
if (!ndp->active_package) {
|
|
/* No response */
|
|
nd->state = ncsi_dev_state_probe_dp;
|
|
schedule_work(&ndp->work);
|
|
break;
|
|
}
|
|
nd->state = ncsi_dev_state_probe_cis;
|
|
schedule_work(&ndp->work);
|
|
break;
|
|
case ncsi_dev_state_probe_cis:
|
|
ndp->pending_req_num = NCSI_RESERVED_CHANNEL;
|
|
|
|
/* Clear initial state */
|
|
nca.type = NCSI_PKT_CMD_CIS;
|
|
nca.package = ndp->active_package->id;
|
|
for (index = 0; index < NCSI_RESERVED_CHANNEL; index++) {
|
|
nca.channel = index;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
goto error;
|
|
}
|
|
|
|
nd->state = ncsi_dev_state_probe_gvi;
|
|
break;
|
|
case ncsi_dev_state_probe_gvi:
|
|
case ncsi_dev_state_probe_gc:
|
|
case ncsi_dev_state_probe_gls:
|
|
np = ndp->active_package;
|
|
ndp->pending_req_num = np->channel_num;
|
|
|
|
/* Retrieve version, capability or link status */
|
|
if (nd->state == ncsi_dev_state_probe_gvi)
|
|
nca.type = NCSI_PKT_CMD_GVI;
|
|
else if (nd->state == ncsi_dev_state_probe_gc)
|
|
nca.type = NCSI_PKT_CMD_GC;
|
|
else
|
|
nca.type = NCSI_PKT_CMD_GLS;
|
|
|
|
nca.package = np->id;
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
nca.channel = nc->id;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
goto error;
|
|
}
|
|
|
|
if (nd->state == ncsi_dev_state_probe_gvi)
|
|
nd->state = ncsi_dev_state_probe_gc;
|
|
else if (nd->state == ncsi_dev_state_probe_gc)
|
|
nd->state = ncsi_dev_state_probe_gls;
|
|
else
|
|
nd->state = ncsi_dev_state_probe_dp;
|
|
break;
|
|
case ncsi_dev_state_probe_dp:
|
|
ndp->pending_req_num = 1;
|
|
|
|
/* Deselect the current package */
|
|
nca.type = NCSI_PKT_CMD_DP;
|
|
nca.package = ndp->package_probe_id;
|
|
nca.channel = NCSI_RESERVED_CHANNEL;
|
|
ret = ncsi_xmit_cmd(&nca);
|
|
if (ret)
|
|
goto error;
|
|
|
|
/* Probe next package */
|
|
ndp->package_probe_id++;
|
|
if (ndp->package_probe_id >= 8) {
|
|
/* Probe finished */
|
|
ndp->flags |= NCSI_DEV_PROBED;
|
|
break;
|
|
}
|
|
nd->state = ncsi_dev_state_probe_package;
|
|
ndp->active_package = NULL;
|
|
break;
|
|
default:
|
|
netdev_warn(nd->dev, "Wrong NCSI state 0x%0x in enumeration\n",
|
|
nd->state);
|
|
}
|
|
|
|
if (ndp->flags & NCSI_DEV_PROBED) {
|
|
/* Check if all packages have HWA support */
|
|
ncsi_check_hwa(ndp);
|
|
ncsi_choose_active_channel(ndp);
|
|
}
|
|
|
|
return;
|
|
error:
|
|
netdev_err(ndp->ndev.dev,
|
|
"NCSI: Failed to transmit cmd 0x%x during probe\n",
|
|
nca.type);
|
|
ncsi_report_link(ndp, true);
|
|
}
|
|
|
|
static void ncsi_dev_work(struct work_struct *work)
|
|
{
|
|
struct ncsi_dev_priv *ndp = container_of(work,
|
|
struct ncsi_dev_priv, work);
|
|
struct ncsi_dev *nd = &ndp->ndev;
|
|
|
|
switch (nd->state & ncsi_dev_state_major) {
|
|
case ncsi_dev_state_probe:
|
|
ncsi_probe_channel(ndp);
|
|
break;
|
|
case ncsi_dev_state_suspend:
|
|
ncsi_suspend_channel(ndp);
|
|
break;
|
|
case ncsi_dev_state_config:
|
|
ncsi_configure_channel(ndp);
|
|
break;
|
|
default:
|
|
netdev_warn(nd->dev, "Wrong NCSI state 0x%x in workqueue\n",
|
|
nd->state);
|
|
}
|
|
}
|
|
|
|
int ncsi_process_next_channel(struct ncsi_dev_priv *ndp)
|
|
{
|
|
struct ncsi_channel *nc;
|
|
int old_state;
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
nc = list_first_or_null_rcu(&ndp->channel_queue,
|
|
struct ncsi_channel, link);
|
|
if (!nc) {
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
goto out;
|
|
}
|
|
|
|
list_del_init(&nc->link);
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
old_state = nc->state;
|
|
nc->state = NCSI_CHANNEL_INVISIBLE;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
ndp->active_channel = nc;
|
|
ndp->active_package = nc->package;
|
|
|
|
switch (old_state) {
|
|
case NCSI_CHANNEL_INACTIVE:
|
|
ndp->ndev.state = ncsi_dev_state_config;
|
|
netdev_dbg(ndp->ndev.dev, "NCSI: configuring channel %u\n",
|
|
nc->id);
|
|
ncsi_configure_channel(ndp);
|
|
break;
|
|
case NCSI_CHANNEL_ACTIVE:
|
|
ndp->ndev.state = ncsi_dev_state_suspend;
|
|
netdev_dbg(ndp->ndev.dev, "NCSI: suspending channel %u\n",
|
|
nc->id);
|
|
ncsi_suspend_channel(ndp);
|
|
break;
|
|
default:
|
|
netdev_err(ndp->ndev.dev, "Invalid state 0x%x on %d:%d\n",
|
|
old_state, nc->package->id, nc->id);
|
|
ncsi_report_link(ndp, false);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
|
|
out:
|
|
ndp->active_channel = NULL;
|
|
ndp->active_package = NULL;
|
|
if (ndp->flags & NCSI_DEV_RESHUFFLE) {
|
|
ndp->flags &= ~NCSI_DEV_RESHUFFLE;
|
|
return ncsi_choose_active_channel(ndp);
|
|
}
|
|
|
|
ncsi_report_link(ndp, false);
|
|
return -ENODEV;
|
|
}
|
|
|
|
static int ncsi_kick_channels(struct ncsi_dev_priv *ndp)
|
|
{
|
|
struct ncsi_dev *nd = &ndp->ndev;
|
|
struct ncsi_channel *nc;
|
|
struct ncsi_package *np;
|
|
unsigned long flags;
|
|
unsigned int n = 0;
|
|
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np) {
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
|
|
/* Channels may be busy, mark dirty instead of
|
|
* kicking if;
|
|
* a) not ACTIVE (configured)
|
|
* b) in the channel_queue (to be configured)
|
|
* c) it's ndev is in the config state
|
|
*/
|
|
if (nc->state != NCSI_CHANNEL_ACTIVE) {
|
|
if ((ndp->ndev.state & 0xff00) ==
|
|
ncsi_dev_state_config ||
|
|
!list_empty(&nc->link)) {
|
|
netdev_dbg(nd->dev,
|
|
"NCSI: channel %p marked dirty\n",
|
|
nc);
|
|
nc->reconfigure_needed = true;
|
|
}
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
continue;
|
|
}
|
|
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
ncsi_stop_channel_monitor(nc);
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
nc->state = NCSI_CHANNEL_INACTIVE;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
list_add_tail_rcu(&nc->link, &ndp->channel_queue);
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
netdev_dbg(nd->dev, "NCSI: kicked channel %p\n", nc);
|
|
n++;
|
|
}
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
int ncsi_vlan_rx_add_vid(struct net_device *dev, __be16 proto, u16 vid)
|
|
{
|
|
struct ncsi_dev_priv *ndp;
|
|
unsigned int n_vids = 0;
|
|
struct vlan_vid *vlan;
|
|
struct ncsi_dev *nd;
|
|
bool found = false;
|
|
|
|
if (vid == 0)
|
|
return 0;
|
|
|
|
nd = ncsi_find_dev(dev);
|
|
if (!nd) {
|
|
netdev_warn(dev, "NCSI: No net_device?\n");
|
|
return 0;
|
|
}
|
|
|
|
ndp = TO_NCSI_DEV_PRIV(nd);
|
|
|
|
/* Add the VLAN id to our internal list */
|
|
list_for_each_entry_rcu(vlan, &ndp->vlan_vids, list) {
|
|
n_vids++;
|
|
if (vlan->vid == vid) {
|
|
netdev_dbg(dev, "NCSI: vid %u already registered\n",
|
|
vid);
|
|
return 0;
|
|
}
|
|
}
|
|
if (n_vids >= NCSI_MAX_VLAN_VIDS) {
|
|
netdev_warn(dev,
|
|
"tried to add vlan id %u but NCSI max already registered (%u)\n",
|
|
vid, NCSI_MAX_VLAN_VIDS);
|
|
return -ENOSPC;
|
|
}
|
|
|
|
vlan = kzalloc(sizeof(*vlan), GFP_KERNEL);
|
|
if (!vlan)
|
|
return -ENOMEM;
|
|
|
|
vlan->proto = proto;
|
|
vlan->vid = vid;
|
|
list_add_rcu(&vlan->list, &ndp->vlan_vids);
|
|
|
|
netdev_dbg(dev, "NCSI: Added new vid %u\n", vid);
|
|
|
|
found = ncsi_kick_channels(ndp) != 0;
|
|
|
|
return found ? ncsi_process_next_channel(ndp) : 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ncsi_vlan_rx_add_vid);
|
|
|
|
int ncsi_vlan_rx_kill_vid(struct net_device *dev, __be16 proto, u16 vid)
|
|
{
|
|
struct vlan_vid *vlan, *tmp;
|
|
struct ncsi_dev_priv *ndp;
|
|
struct ncsi_dev *nd;
|
|
bool found = false;
|
|
|
|
if (vid == 0)
|
|
return 0;
|
|
|
|
nd = ncsi_find_dev(dev);
|
|
if (!nd) {
|
|
netdev_warn(dev, "NCSI: no net_device?\n");
|
|
return 0;
|
|
}
|
|
|
|
ndp = TO_NCSI_DEV_PRIV(nd);
|
|
|
|
/* Remove the VLAN id from our internal list */
|
|
list_for_each_entry_safe(vlan, tmp, &ndp->vlan_vids, list)
|
|
if (vlan->vid == vid) {
|
|
netdev_dbg(dev, "NCSI: vid %u found, removing\n", vid);
|
|
list_del_rcu(&vlan->list);
|
|
found = true;
|
|
kfree(vlan);
|
|
}
|
|
|
|
if (!found) {
|
|
netdev_err(dev, "NCSI: vid %u wasn't registered!\n", vid);
|
|
return -EINVAL;
|
|
}
|
|
|
|
found = ncsi_kick_channels(ndp) != 0;
|
|
|
|
return found ? ncsi_process_next_channel(ndp) : 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ncsi_vlan_rx_kill_vid);
|
|
|
|
struct ncsi_dev *ncsi_register_dev(struct net_device *dev,
|
|
void (*handler)(struct ncsi_dev *ndev))
|
|
{
|
|
struct ncsi_dev_priv *ndp;
|
|
struct ncsi_dev *nd;
|
|
unsigned long flags;
|
|
int i;
|
|
|
|
/* Check if the device has been registered or not */
|
|
nd = ncsi_find_dev(dev);
|
|
if (nd)
|
|
return nd;
|
|
|
|
/* Create NCSI device */
|
|
ndp = kzalloc(sizeof(*ndp), GFP_ATOMIC);
|
|
if (!ndp)
|
|
return NULL;
|
|
|
|
nd = &ndp->ndev;
|
|
nd->state = ncsi_dev_state_registered;
|
|
nd->dev = dev;
|
|
nd->handler = handler;
|
|
ndp->pending_req_num = 0;
|
|
INIT_LIST_HEAD(&ndp->channel_queue);
|
|
INIT_LIST_HEAD(&ndp->vlan_vids);
|
|
INIT_WORK(&ndp->work, ncsi_dev_work);
|
|
ndp->package_whitelist = UINT_MAX;
|
|
|
|
/* Initialize private NCSI device */
|
|
spin_lock_init(&ndp->lock);
|
|
INIT_LIST_HEAD(&ndp->packages);
|
|
ndp->request_id = NCSI_REQ_START_IDX;
|
|
for (i = 0; i < ARRAY_SIZE(ndp->requests); i++) {
|
|
ndp->requests[i].id = i;
|
|
ndp->requests[i].ndp = ndp;
|
|
timer_setup(&ndp->requests[i].timer, ncsi_request_timeout, 0);
|
|
}
|
|
|
|
spin_lock_irqsave(&ncsi_dev_lock, flags);
|
|
list_add_tail_rcu(&ndp->node, &ncsi_dev_list);
|
|
spin_unlock_irqrestore(&ncsi_dev_lock, flags);
|
|
|
|
/* Register NCSI packet Rx handler */
|
|
ndp->ptype.type = cpu_to_be16(ETH_P_NCSI);
|
|
ndp->ptype.func = ncsi_rcv_rsp;
|
|
ndp->ptype.dev = dev;
|
|
dev_add_pack(&ndp->ptype);
|
|
|
|
/* Set up generic netlink interface */
|
|
ncsi_init_netlink(dev);
|
|
|
|
return nd;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ncsi_register_dev);
|
|
|
|
int ncsi_start_dev(struct ncsi_dev *nd)
|
|
{
|
|
struct ncsi_dev_priv *ndp = TO_NCSI_DEV_PRIV(nd);
|
|
|
|
if (nd->state != ncsi_dev_state_registered &&
|
|
nd->state != ncsi_dev_state_functional)
|
|
return -ENOTTY;
|
|
|
|
if (!(ndp->flags & NCSI_DEV_PROBED)) {
|
|
ndp->package_probe_id = 0;
|
|
nd->state = ncsi_dev_state_probe;
|
|
schedule_work(&ndp->work);
|
|
return 0;
|
|
}
|
|
|
|
return ncsi_reset_dev(nd);
|
|
}
|
|
EXPORT_SYMBOL_GPL(ncsi_start_dev);
|
|
|
|
void ncsi_stop_dev(struct ncsi_dev *nd)
|
|
{
|
|
struct ncsi_dev_priv *ndp = TO_NCSI_DEV_PRIV(nd);
|
|
struct ncsi_package *np;
|
|
struct ncsi_channel *nc;
|
|
bool chained;
|
|
int old_state;
|
|
unsigned long flags;
|
|
|
|
/* Stop the channel monitor on any active channels. Don't reset the
|
|
* channel state so we know which were active when ncsi_start_dev()
|
|
* is next called.
|
|
*/
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np) {
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
ncsi_stop_channel_monitor(nc);
|
|
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
chained = !list_empty(&nc->link);
|
|
old_state = nc->state;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
|
|
WARN_ON_ONCE(chained ||
|
|
old_state == NCSI_CHANNEL_INVISIBLE);
|
|
}
|
|
}
|
|
|
|
netdev_dbg(ndp->ndev.dev, "NCSI: Stopping device\n");
|
|
ncsi_report_link(ndp, true);
|
|
}
|
|
EXPORT_SYMBOL_GPL(ncsi_stop_dev);
|
|
|
|
int ncsi_reset_dev(struct ncsi_dev *nd)
|
|
{
|
|
struct ncsi_dev_priv *ndp = TO_NCSI_DEV_PRIV(nd);
|
|
struct ncsi_channel *nc, *active, *tmp;
|
|
struct ncsi_package *np;
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
|
|
if (!(ndp->flags & NCSI_DEV_RESET)) {
|
|
/* Haven't been called yet, check states */
|
|
switch (nd->state & ncsi_dev_state_major) {
|
|
case ncsi_dev_state_registered:
|
|
case ncsi_dev_state_probe:
|
|
/* Not even probed yet - do nothing */
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
return 0;
|
|
case ncsi_dev_state_suspend:
|
|
case ncsi_dev_state_config:
|
|
/* Wait for the channel to finish its suspend/config
|
|
* operation; once it finishes it will check for
|
|
* NCSI_DEV_RESET and reset the state.
|
|
*/
|
|
ndp->flags |= NCSI_DEV_RESET;
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
return 0;
|
|
}
|
|
} else {
|
|
switch (nd->state) {
|
|
case ncsi_dev_state_suspend_done:
|
|
case ncsi_dev_state_config_done:
|
|
case ncsi_dev_state_functional:
|
|
/* Ok */
|
|
break;
|
|
default:
|
|
/* Current reset operation happening */
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (!list_empty(&ndp->channel_queue)) {
|
|
/* Clear any channel queue we may have interrupted */
|
|
list_for_each_entry_safe(nc, tmp, &ndp->channel_queue, link)
|
|
list_del_init(&nc->link);
|
|
}
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
active = NULL;
|
|
NCSI_FOR_EACH_PACKAGE(ndp, np) {
|
|
NCSI_FOR_EACH_CHANNEL(np, nc) {
|
|
spin_lock_irqsave(&nc->lock, flags);
|
|
|
|
if (nc->state == NCSI_CHANNEL_ACTIVE) {
|
|
active = nc;
|
|
nc->state = NCSI_CHANNEL_INVISIBLE;
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
ncsi_stop_channel_monitor(nc);
|
|
break;
|
|
}
|
|
|
|
spin_unlock_irqrestore(&nc->lock, flags);
|
|
}
|
|
if (active)
|
|
break;
|
|
}
|
|
|
|
if (!active) {
|
|
/* Done */
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
ndp->flags &= ~NCSI_DEV_RESET;
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
return ncsi_choose_active_channel(ndp);
|
|
}
|
|
|
|
spin_lock_irqsave(&ndp->lock, flags);
|
|
ndp->flags |= NCSI_DEV_RESET;
|
|
ndp->active_channel = active;
|
|
ndp->active_package = active->package;
|
|
spin_unlock_irqrestore(&ndp->lock, flags);
|
|
|
|
nd->state = ncsi_dev_state_suspend;
|
|
schedule_work(&ndp->work);
|
|
return 0;
|
|
}
|
|
|
|
void ncsi_unregister_dev(struct ncsi_dev *nd)
|
|
{
|
|
struct ncsi_dev_priv *ndp = TO_NCSI_DEV_PRIV(nd);
|
|
struct ncsi_package *np, *tmp;
|
|
unsigned long flags;
|
|
|
|
dev_remove_pack(&ndp->ptype);
|
|
|
|
list_for_each_entry_safe(np, tmp, &ndp->packages, node)
|
|
ncsi_remove_package(np);
|
|
|
|
spin_lock_irqsave(&ncsi_dev_lock, flags);
|
|
list_del_rcu(&ndp->node);
|
|
spin_unlock_irqrestore(&ncsi_dev_lock, flags);
|
|
|
|
ncsi_unregister_netlink(nd->dev);
|
|
|
|
kfree(ndp);
|
|
}
|
|
EXPORT_SYMBOL_GPL(ncsi_unregister_dev);
|