mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-10-28 11:25:21 +01:00
460 lines
13 KiB
C
460 lines
13 KiB
C
/*
|
|
* aQuantia Corporation Network Driver
|
|
* Copyright (C) 2014-2017 aQuantia Corporation. All rights reserved
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* (1) Redistributions of source code must retain the above
|
|
* copyright notice, this list of conditions and the following
|
|
* disclaimer.
|
|
*
|
|
* (2) Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following
|
|
* disclaimer in the documentation and/or other materials provided
|
|
* with the distribution.
|
|
*
|
|
* (3)The name of the author may not be used to endorse or promote
|
|
* products derived from this software without specific prior
|
|
* written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
|
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
|
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
|
|
#include <sys/param.h>
|
|
#include <sys/bitstring.h>
|
|
#include <sys/kernel.h>
|
|
#include <sys/sbuf.h>
|
|
#include <sys/socket.h>
|
|
#include <net/ethernet.h>
|
|
#include <net/if.h>
|
|
#include <net/if_var.h>
|
|
#include <net/iflib.h>
|
|
|
|
#include "aq_common.h"
|
|
#include "aq_device.h"
|
|
#include "aq_ring.h"
|
|
#include "aq_dbg.h"
|
|
#include "aq_hw.h"
|
|
#include "aq_hw_llh.h"
|
|
#include "aq_fw.h"
|
|
|
|
int
|
|
aq_update_hw_stats(struct aq_dev *aq_dev)
|
|
{
|
|
struct aq_hw *hw = &aq_dev->hw;
|
|
struct aq_hw_stats stats;
|
|
|
|
memset(&stats, 0, sizeof(stats));
|
|
if (aq_hw_mpi_read_stats(hw, &stats) != 0)
|
|
return (0);
|
|
|
|
#define AQ_SDELTA(_N_) do { \
|
|
aq_dev->curr_stats._N_ += stats._N_ - aq_dev->last_stats._N_; \
|
|
} while (0)
|
|
if (aq_dev->linkup) {
|
|
AQ_SDELTA(uprc);
|
|
AQ_SDELTA(mprc);
|
|
AQ_SDELTA(bprc);
|
|
AQ_SDELTA(cprc);
|
|
AQ_SDELTA(erpt);
|
|
|
|
AQ_SDELTA(uptc);
|
|
AQ_SDELTA(mptc);
|
|
AQ_SDELTA(bptc);
|
|
AQ_SDELTA(erpr);
|
|
|
|
AQ_SDELTA(ubrc);
|
|
AQ_SDELTA(ubtc);
|
|
AQ_SDELTA(mbrc);
|
|
AQ_SDELTA(mbtc);
|
|
AQ_SDELTA(bbrc);
|
|
AQ_SDELTA(bbtc);
|
|
|
|
AQ_SDELTA(ptc);
|
|
AQ_SDELTA(prc);
|
|
|
|
AQ_SDELTA(dpc);
|
|
|
|
/*
|
|
* Per-cast octets present: derive the aggregate from them.
|
|
* Otherwise (B0) accumulate the firmware-reported aggregate.
|
|
*/
|
|
if (stats.ubrc | stats.mbrc | stats.bbrc)
|
|
aq_dev->curr_stats.brc = aq_dev->curr_stats.ubrc +
|
|
aq_dev->curr_stats.mbrc + aq_dev->curr_stats.bbrc;
|
|
else
|
|
AQ_SDELTA(brc);
|
|
|
|
if (stats.ubtc | stats.mbtc | stats.bbtc)
|
|
aq_dev->curr_stats.btc = aq_dev->curr_stats.ubtc +
|
|
aq_dev->curr_stats.mbtc + aq_dev->curr_stats.bbtc;
|
|
else
|
|
AQ_SDELTA(btc);
|
|
}
|
|
#undef AQ_SDELTA
|
|
|
|
memcpy(&aq_dev->last_stats, &stats, sizeof(stats));
|
|
|
|
return (0);
|
|
}
|
|
|
|
|
|
#define AQ_THERMAL_HYSTERESIS_MC 18000 /* recover this far below the limit */
|
|
#define AQ_THERMAL_RECOVER_MC 90000 /* fallback when the limit is unreadable */
|
|
#define AQ_THERMAL_SETTLE_POLLS 5 /* ~5 s for the PHY reset to settle */
|
|
#define AQ_THERMAL_RETRY_SECS 60 /* minimum spacing between recoveries */
|
|
#define AQ_INIT_MAX_RETRIES 5 /* re-init attempts after a failed init */
|
|
|
|
/* Temperature here is post-trip; the PHY is already dropping to low power. */
|
|
static void
|
|
aq_thermal_report_shutdown(struct aq_dev *aq_dev)
|
|
{
|
|
struct aq_hw *hw = &aq_dev->hw;
|
|
int temp_mc, limit_mc;
|
|
bool have_temp, have_limit;
|
|
|
|
have_temp = hw->fw_ops->get_temp(hw, &temp_mc) == 0;
|
|
have_limit = hw->fw_ops->get_thermal_limit(hw, &limit_mc) == 0;
|
|
if (have_temp)
|
|
aq_dev->thermal_temp_mc = temp_mc;
|
|
/* The F/W also exposes a cold_temperature hysteresis point. */
|
|
aq_dev->thermal_recover_mc = have_limit ?
|
|
limit_mc - AQ_THERMAL_HYSTERESIS_MC : AQ_THERMAL_RECOVER_MC;
|
|
|
|
if (have_temp && have_limit)
|
|
device_printf(aq_dev->dev, "PHY thermal shutdown; "
|
|
"limit %d C, temp %d C; holding link down until it cools\n",
|
|
limit_mc / 1000, temp_mc / 1000);
|
|
else if (have_temp)
|
|
device_printf(aq_dev->dev, "PHY thermal shutdown; "
|
|
"temp %d C; holding link down until it cools\n",
|
|
temp_mc / 1000);
|
|
else
|
|
device_printf(aq_dev->dev, "PHY thermal shutdown; "
|
|
"holding link down until it cools\n");
|
|
}
|
|
|
|
/* Pre-trip warning: the PHY is hot but the firmware has not shut it down. */
|
|
static void
|
|
aq_thermal_report_hot(struct aq_dev *aq_dev)
|
|
{
|
|
struct aq_hw *hw = &aq_dev->hw;
|
|
int temp_mc;
|
|
bool hot;
|
|
|
|
if (hw->fw_ops->get_phy_hot_warning == NULL ||
|
|
hw->fw_ops->get_phy_hot_warning(hw, &hot) != 0)
|
|
return;
|
|
|
|
/* Report each edge once; the bit stays set for the whole excursion. */
|
|
if (hot == aq_dev->phy_hot_last)
|
|
return;
|
|
aq_dev->phy_hot_last = hot;
|
|
|
|
if (!hot) {
|
|
device_printf(aq_dev->dev, "PHY temperature normal again\n");
|
|
return;
|
|
}
|
|
if (hw->fw_ops->get_temp(hw, &temp_mc) == 0)
|
|
device_printf(aq_dev->dev, "PHY over-temperature warning, "
|
|
"temp %d C\n", temp_mc / 1000);
|
|
else
|
|
device_printf(aq_dev->dev, "PHY over-temperature warning\n");
|
|
}
|
|
|
|
/* reconcile thermal arm with knob value */
|
|
static void
|
|
aq_thermal_apply(struct aq_dev *aq_dev)
|
|
{
|
|
struct aq_hw *hw = &aq_dev->hw;
|
|
bool enable;
|
|
int err;
|
|
|
|
/* A queued re-init would undo the write; the poll after it applies. */
|
|
if (hw->fw_ops->thermal_arm == NULL || aq_dev->reset_pending)
|
|
return;
|
|
|
|
enable = aq_dev->thermal_shutdown_enabled;
|
|
err = hw->fw_ops->thermal_arm(hw, enable);
|
|
/* No sensor to arm against; the knob is absent on such an adapter. */
|
|
if (err == ENOTSUP)
|
|
return;
|
|
/* Print only on a change, so a lasting failure is reported once. */
|
|
if (err == aq_dev->thermal_arm_err)
|
|
return;
|
|
aq_dev->thermal_arm_err = err;
|
|
if (err != 0)
|
|
device_printf(aq_dev->dev, "could not %s PHY thermal shutdown, "
|
|
"error %d\n", enable ? "arm" : "disarm", err);
|
|
else
|
|
device_printf(aq_dev->dev, "PHY thermal shutdown %s\n",
|
|
enable ? "armed" : "disarmed");
|
|
}
|
|
|
|
/* Recover after cooldown: A1 needs a PHY reset then re-init, A2 re-inits alone. */
|
|
static void
|
|
aq_thermal_poll(struct aq_dev *aq_dev)
|
|
{
|
|
struct aq_hw *hw = &aq_dev->hw;
|
|
uint16_t fault;
|
|
int temp_mc;
|
|
|
|
switch (aq_dev->thermal_state) {
|
|
case AQ_THERMAL_NORMAL:
|
|
aq_thermal_apply(aq_dev);
|
|
aq_thermal_report_hot(aq_dev);
|
|
if (aq_dev->linkup)
|
|
return;
|
|
/* The F/W raises the fault a poll after it drops the link. */
|
|
if (hw->fw_ops->get_phy_fault(hw, &fault) != 0 || fault == 0)
|
|
return;
|
|
/* Report each code once; do not mask a later shutdown. */
|
|
if (fault == aq_dev->phy_fault_last)
|
|
return;
|
|
aq_dev->phy_fault_last = fault;
|
|
if (fault != AQ_PHY_FAULT_THERMAL_SHUTDOWN) {
|
|
device_printf(aq_dev->dev,
|
|
"PHY fault 0x%04x\n", fault);
|
|
return;
|
|
}
|
|
aq_thermal_report_shutdown(aq_dev);
|
|
aq_dev->thermal_state = AQ_THERMAL_COOLING;
|
|
return;
|
|
|
|
case AQ_THERMAL_COOLING:
|
|
if (hw->fw_ops->get_temp(hw, &temp_mc) != 0 ||
|
|
temp_mc > aq_dev->thermal_recover_mc)
|
|
return;
|
|
aq_dev->thermal_temp_mc = temp_mc;
|
|
if (hw->fw_ops->phy_reset != NULL) {
|
|
if (hw->fw_ops->phy_reset(hw) != 0)
|
|
return;
|
|
aq_dev->thermal_settle = 0;
|
|
aq_dev->thermal_state = AQ_THERMAL_SETTLING;
|
|
return;
|
|
}
|
|
/* No PHY reset needed (A2): re-init below restores the link. */
|
|
break;
|
|
|
|
case AQ_THERMAL_SETTLING:
|
|
if (++aq_dev->thermal_settle < AQ_THERMAL_SETTLE_POLLS)
|
|
return;
|
|
break;
|
|
}
|
|
|
|
/* Space attempts out: recovery costs a re-init and a renegotiation. */
|
|
if ((int)(ticks - aq_dev->thermal_retry_ticks) < 0)
|
|
return;
|
|
aq_dev->thermal_retry_ticks = ticks + AQ_THERMAL_RETRY_SECS * hz;
|
|
|
|
device_printf(aq_dev->dev, "PHY cooled to %d C; restoring "
|
|
"link\n", aq_dev->thermal_temp_mc / 1000);
|
|
aq_dev->thermal_state = AQ_THERMAL_NORMAL;
|
|
aq_dev->reset_pending = true;
|
|
iflib_request_reset(aq_dev->ctx);
|
|
iflib_admin_intr_deferred(aq_dev->ctx);
|
|
}
|
|
|
|
static const char *
|
|
aq_fc_string(const struct aq_hw_fc_info *fc)
|
|
{
|
|
|
|
if (fc->fc_rx && fc->fc_tx)
|
|
return ("rx/tx");
|
|
if (fc->fc_rx)
|
|
return ("rx");
|
|
if (fc->fc_tx)
|
|
return ("tx");
|
|
return ("none");
|
|
}
|
|
|
|
static void
|
|
aq_link_report_up(struct aq_dev *aq_dev, uint32_t link_speed,
|
|
const struct aq_hw_fc_info *fc)
|
|
{
|
|
struct aq_hw *hw = &aq_dev->hw;
|
|
struct aq_hw_link_info info;
|
|
|
|
if (hw->fw_ops->get_link_info != NULL &&
|
|
hw->fw_ops->get_link_info(hw, &info) == 0) {
|
|
device_printf(aq_dev->dev, "link UP: speed=%u, %s-duplex, "
|
|
"flowcontrol %s, EEE %s\n", link_speed,
|
|
info.full_duplex ? "full" : "half", aq_fc_string(fc),
|
|
info.eee ? "on" : "off");
|
|
return;
|
|
}
|
|
device_printf(aq_dev->dev, "link UP: speed=%u, flowcontrol %s\n",
|
|
link_speed, aq_fc_string(fc));
|
|
}
|
|
|
|
/*
|
|
* The firmware raises a PHY fault a poll after it drops the link, so a
|
|
* thermal trip usually reports only its temperature here; aq_thermal_poll()
|
|
* names it on the following poll.
|
|
*/
|
|
static void
|
|
aq_link_report_down(struct aq_dev *aq_dev)
|
|
{
|
|
struct aq_hw *hw = &aq_dev->hw;
|
|
struct aq_hw_link_info info;
|
|
struct sbuf sb;
|
|
char detail[96];
|
|
int temp_mc;
|
|
uint16_t fault;
|
|
|
|
sbuf_new(&sb, detail, sizeof(detail), SBUF_FIXEDLEN);
|
|
|
|
if (hw->fw_ops->get_phy_fault != NULL &&
|
|
hw->fw_ops->get_phy_fault(hw, &fault) == 0 && fault != 0)
|
|
sbuf_printf(&sb, ", PHY fault 0x%04x", fault);
|
|
if (hw->fw_ops->get_link_info != NULL &&
|
|
hw->fw_ops->get_link_info(hw, &info) == 0)
|
|
sbuf_printf(&sb, ", F/W link state %u", info.state);
|
|
if (hw->fw_ops->get_temp != NULL &&
|
|
hw->fw_ops->get_temp(hw, &temp_mc) == 0)
|
|
sbuf_printf(&sb, ", temp %d C", temp_mc / 1000);
|
|
|
|
if (sbuf_finish(&sb) != 0)
|
|
device_printf(aq_dev->dev, "link DOWN\n");
|
|
else
|
|
device_printf(aq_dev->dev, "link DOWN%s\n", sbuf_data(&sb));
|
|
sbuf_delete(&sb);
|
|
}
|
|
|
|
void
|
|
aq_if_update_admin_status(if_ctx_t ctx)
|
|
{
|
|
struct aq_dev *aq_dev = iflib_get_softc(ctx);
|
|
struct aq_hw *hw = &aq_dev->hw;
|
|
uint32_t link_speed;
|
|
bool running;
|
|
|
|
|
|
struct aq_hw_fc_info fc_neg;
|
|
int err;
|
|
|
|
err = aq_hw_get_link_state(hw, &link_speed, &fc_neg);
|
|
if (err != 0 && !aq_dev->link_read_failed) {
|
|
aq_dev->link_read_failed = true;
|
|
device_printf(aq_dev->dev, "link state read failed, error %d; "
|
|
"holding the last known state\n", err);
|
|
} else if (err == 0 && aq_dev->link_read_failed) {
|
|
aq_dev->link_read_failed = false;
|
|
device_printf(aq_dev->dev, "link state read recovered\n");
|
|
}
|
|
|
|
/* A stopped or half-initialized interface has no link. */
|
|
running = (if_getdrvflags(iflib_get_ifp(ctx)) & IFF_DRV_RUNNING) != 0;
|
|
if (!running || aq_dev->init_failed)
|
|
link_speed = 0;
|
|
|
|
/* A retrain can change the speed without ever dropping the link. */
|
|
if (err == 0 && link_speed != 0U &&
|
|
(!aq_dev->linkup || link_speed != aq_dev->link_speed)) {
|
|
if (!aq_dev->linkup) {
|
|
aq_link_report_up(aq_dev, link_speed, &fc_neg);
|
|
aq_dev->phy_fault_last = 0;
|
|
} else
|
|
device_printf(aq_dev->dev, "link speed=%d\n",
|
|
link_speed);
|
|
|
|
aq_dev->linkup = 1;
|
|
aq_dev->link_speed = link_speed;
|
|
|
|
/* turn on/off RX Pause in RPB */
|
|
rpb_rx_xoff_en_per_tc_set(hw, fc_neg.fc_rx, 0);
|
|
|
|
|
|
iflib_link_state_change(ctx, LINK_STATE_UP, IF_Mbps(link_speed));
|
|
aq_mediastatus_update(aq_dev, link_speed, &fc_neg);
|
|
|
|
/* update ITR settings according new link speed */
|
|
if (aq_hw_interrupt_moderation_set(hw) != 0)
|
|
device_printf(aq_dev->dev,
|
|
"could not update interrupt moderation\n");
|
|
} else if (err == 0 && link_speed == 0U && aq_dev->linkup) {
|
|
aq_link_report_down(aq_dev);
|
|
|
|
aq_dev->linkup = 0;
|
|
aq_dev->link_speed = 0;
|
|
|
|
/* turn off RX Pause in RPB */
|
|
rpb_rx_xoff_en_per_tc_set(hw, 0, 0);
|
|
|
|
iflib_link_state_change(ctx, LINK_STATE_DOWN, 0);
|
|
aq_mediastatus_update(aq_dev, link_speed, &fc_neg);
|
|
}
|
|
|
|
/* A stopped interface must not be re-initialized behind the operator. */
|
|
if (!running)
|
|
return;
|
|
|
|
/* Re-arming while a reset is queued would re-init once too often. */
|
|
if (aq_dev->init_failed) {
|
|
if (aq_dev->reset_pending)
|
|
return;
|
|
if (aq_dev->init_retries < AQ_INIT_MAX_RETRIES) {
|
|
aq_dev->init_retries++;
|
|
aq_dev->reset_pending = true;
|
|
iflib_request_reset(ctx);
|
|
} else if (aq_dev->init_retries == AQ_INIT_MAX_RETRIES) {
|
|
aq_dev->init_retries++;
|
|
device_printf(aq_dev->dev, "initialization failed; "
|
|
"giving up after %d retries, link held down\n",
|
|
AQ_INIT_MAX_RETRIES);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (hw->fw_ops->get_phy_fault != NULL)
|
|
aq_thermal_poll(aq_dev);
|
|
|
|
aq_update_hw_stats(aq_dev);
|
|
}
|
|
|
|
/**************************************************************************/
|
|
/* interrupt service routine (Top half) */
|
|
/**************************************************************************/
|
|
int
|
|
aq_isr_rx(void *arg)
|
|
{
|
|
struct aq_ring *ring = arg;
|
|
struct aq_dev *aq_dev = ring->dev;
|
|
struct aq_hw *hw = &aq_dev->hw;
|
|
|
|
itr_irq_status_clearlsw_set(hw, BIT(ring->msix));
|
|
AQ_HW_FLUSH(hw);
|
|
counter_u64_add(ring->stats.irq, 1);
|
|
return (FILTER_SCHEDULE_THREAD);
|
|
}
|
|
|
|
/**************************************************************************/
|
|
/* interrupt service routine (Top half) */
|
|
/**************************************************************************/
|
|
int
|
|
aq_linkstat_isr(void *arg)
|
|
{
|
|
struct aq_dev *aq_dev = arg;
|
|
struct aq_hw *hw = &aq_dev->hw;
|
|
|
|
itr_irq_status_clearlsw_set(hw, BIT(aq_dev->msix));
|
|
AQ_HW_FLUSH(hw);
|
|
|
|
iflib_admin_intr_deferred(aq_dev->ctx);
|
|
|
|
return (FILTER_HANDLED);
|
|
}
|