root/drivers/scsi/libsas/sas_port.c
// SPDX-License-Identifier: GPL-2.0
/*
 * Serial Attached SCSI (SAS) Port class
 *
 * Copyright (C) 2005 Adaptec, Inc.  All rights reserved.
 * Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com>
 */

#include "sas_internal.h"

#include <scsi/scsi_transport.h>
#include <scsi/scsi_transport_sas.h>
#include "scsi_sas_internal.h"

static bool phy_is_wideport_member(struct asd_sas_port *port, struct asd_sas_phy *phy)
{
        struct sas_ha_struct *sas_ha = phy->ha;

        if (memcmp(port->attached_sas_addr, phy->attached_sas_addr,
                   SAS_ADDR_SIZE) != 0 || (sas_ha->strict_wide_ports &&
             memcmp(port->sas_addr, phy->sas_addr, SAS_ADDR_SIZE) != 0))
                return false;
        return true;
}

static void sas_resume_port(struct asd_sas_phy *phy)
{
        struct domain_device *dev, *n;
        struct asd_sas_port *port = phy->port;
        struct sas_ha_struct *sas_ha = phy->ha;
        struct sas_internal *si = to_sas_internal(sas_ha->shost->transportt);

        if (si->dft->lldd_port_formed)
                si->dft->lldd_port_formed(phy);

        if (port->suspended)
                port->suspended = 0;
        else {
                /* we only need to handle "link returned" actions once */
                return;
        }

        /* if the port came back:
         * 1/ presume every device came back
         * 2/ force the next revalidation to check all expander phys
         */
        list_for_each_entry_safe(dev, n, &port->dev_list, dev_list_node) {
                int i, rc;

                rc = sas_notify_lldd_dev_found(dev);
                if (rc) {
                        sas_unregister_dev(port, dev);
                        sas_destruct_devices(port);
                        continue;
                }

                if (dev_is_expander(dev->dev_type)) {
                        dev->ex_dev.ex_change_count = -1;
                        for (i = 0; i < dev->ex_dev.num_phys; i++) {
                                struct ex_phy *phy = &dev->ex_dev.ex_phy[i];

                                phy->phy_change_count = -1;
                        }
                }
        }

        sas_discover_event(port, DISCE_RESUME);
}

static void sas_form_port_add_phy(struct asd_sas_port *port,
                                  struct asd_sas_phy *phy, bool wideport)
{
        list_add_tail(&phy->port_phy_el, &port->phy_list);
        sas_phy_set_target(phy, port->port_dev);
        phy->port = port;
        port->num_phys++;
        port->phy_mask |= (1U << phy->id);

        if (wideport)
                pr_debug("phy%d matched wide port%d\n", phy->id,
                         port->id);
        else
                memcpy(port->sas_addr, phy->sas_addr, SAS_ADDR_SIZE);

        if (*(u64 *)port->attached_sas_addr == 0) {
                memcpy(port->attached_sas_addr, phy->attached_sas_addr,
                       SAS_ADDR_SIZE);
                port->iproto = phy->iproto;
                port->tproto = phy->tproto;
                port->oob_mode = phy->oob_mode;
                port->linkrate = phy->linkrate;
        } else {
                port->linkrate = max(port->linkrate, phy->linkrate);
        }
}

/**
 * sas_form_port - add this phy to a port
 * @phy: the phy of interest
 *
 * This function adds this phy to an existing port, thus creating a wide
 * port, or it creates a port and adds the phy to the port.
 */
static void sas_form_port(struct asd_sas_phy *phy)
{
        int i;
        struct sas_ha_struct *sas_ha = phy->ha;
        struct asd_sas_port *port = phy->port;
        struct domain_device *port_dev = NULL;
        struct sas_internal *si =
                to_sas_internal(sas_ha->shost->transportt);
        unsigned long flags;

        if (port) {
                if (!phy_is_wideport_member(port, phy))
                        sas_deform_port(phy, false);
                else if (phy->suspended) {
                        phy->suspended = 0;
                        sas_resume_port(phy);

                        /* phy came back, try to cancel the timeout */
                        wake_up(&sas_ha->eh_wait_q);
                        return;
                } else {
                        pr_info("%s: phy%d belongs to port%d already(%d)!\n",
                                __func__, phy->id, phy->port->id,
                                phy->port->num_phys);
                        return;
                }
        }

        /* see if the phy should be part of a wide port */
        spin_lock_irqsave(&sas_ha->phy_port_lock, flags);
        for (i = 0; i < sas_ha->num_phys; i++) {
                port = sas_ha->sas_port[i];
                spin_lock(&port->phy_list_lock);
                if (*(u64 *) port->sas_addr &&
                    phy_is_wideport_member(port, phy) && port->num_phys > 0) {
                        /* wide port */
                        port_dev = port->port_dev;
                        sas_form_port_add_phy(port, phy, true);
                        spin_unlock(&port->phy_list_lock);
                        break;
                }
                spin_unlock(&port->phy_list_lock);
        }
        /* The phy does not match any existing port, create a new one */
        if (i == sas_ha->num_phys) {
                for (i = 0; i < sas_ha->num_phys; i++) {
                        port = sas_ha->sas_port[i];
                        spin_lock(&port->phy_list_lock);
                        if (*(u64 *)port->sas_addr == 0
                            && port->num_phys == 0) {
                                port_dev = port->port_dev;
                                sas_form_port_add_phy(port, phy, false);
                                spin_unlock(&port->phy_list_lock);
                                break;
                        }
                        spin_unlock(&port->phy_list_lock);
                }

                if (i >= sas_ha->num_phys) {
                        pr_err("%s: couldn't find a free port, bug?\n",
                               __func__);
                        spin_unlock_irqrestore(&sas_ha->phy_port_lock, flags);
                        return;
                }
        }
        spin_unlock_irqrestore(&sas_ha->phy_port_lock, flags);

        if (!port->port) {
                port->port = sas_port_alloc(phy->phy->dev.parent, port->id);
                BUG_ON(!port->port);
                sas_port_add(port->port);
        }
        sas_port_add_phy(port->port, phy->phy);

        pr_debug("%s added to %s, phy_mask:0x%x (%016llx)\n",
                 dev_name(&phy->phy->dev), dev_name(&port->port->dev),
                 port->phy_mask,
                 SAS_ADDR(port->attached_sas_addr));

        if (port_dev)
                port_dev->pathways = port->num_phys;

        /* Tell the LLDD about this port formation. */
        if (si->dft->lldd_port_formed)
                si->dft->lldd_port_formed(phy);

        sas_discover_event(phy->port, DISCE_DISCOVER_DOMAIN);
        /* Only insert a revalidate event after initial discovery */
        if (port_dev && dev_is_expander(port_dev->dev_type)) {
                struct expander_device *ex_dev = &port_dev->ex_dev;

                ex_dev->ex_change_count = -1;
                sas_discover_event(port, DISCE_REVALIDATE_DOMAIN);
        }
        flush_workqueue(sas_ha->disco_q);
}

/**
 * sas_deform_port - remove this phy from the port it belongs to
 * @phy: the phy of interest
 * @gone: whether or not the PHY is gone
 *
 * This is called when the physical link to the other phy has been
 * lost (on this phy), in Event thread context. We cannot delay here.
 */
void sas_deform_port(struct asd_sas_phy *phy, bool gone)
{
        struct sas_ha_struct *sas_ha = phy->ha;
        struct asd_sas_port *port = phy->port;
        struct sas_internal *si =
                to_sas_internal(sas_ha->shost->transportt);
        struct domain_device *dev;
        unsigned long flags;

        if (!port)
                return;           /* done by a phy event */

        dev = port->port_dev;
        if (dev)
                dev->pathways--;

        if (port->num_phys == 1) {
                sas_unregister_domain_devices(port, gone);
                sas_destruct_devices(port);
                sas_port_delete(port->port);
                port->port = NULL;
        } else {
                sas_port_delete_phy(port->port, phy->phy);
                sas_device_set_phy(dev, port->port);
        }

        if (si->dft->lldd_port_deformed)
                si->dft->lldd_port_deformed(phy);

        spin_lock_irqsave(&sas_ha->phy_port_lock, flags);
        spin_lock(&port->phy_list_lock);

        list_del_init(&phy->port_phy_el);
        sas_phy_set_target(phy, NULL);
        phy->port = NULL;
        port->num_phys--;
        port->phy_mask &= ~(1U << phy->id);

        if (port->num_phys == 0) {
                INIT_LIST_HEAD(&port->phy_list);
                memset(port->sas_addr, 0, SAS_ADDR_SIZE);
                memset(port->attached_sas_addr, 0, SAS_ADDR_SIZE);
                port->iproto = 0;
                port->tproto = 0;
                port->oob_mode = 0;
                port->phy_mask = 0;
        }
        spin_unlock(&port->phy_list_lock);
        spin_unlock_irqrestore(&sas_ha->phy_port_lock, flags);

        /* Only insert revalidate event if the port still has members */
        if (port->port && dev && dev_is_expander(dev->dev_type)) {
                struct expander_device *ex_dev = &dev->ex_dev;

                ex_dev->ex_change_count = -1;
                sas_discover_event(port, DISCE_REVALIDATE_DOMAIN);
        }
        flush_workqueue(sas_ha->disco_q);

        return;
}

/* ---------- SAS port events ---------- */

void sas_porte_bytes_dmaed(struct work_struct *work)
{
        struct asd_sas_event *ev = to_asd_sas_event(work);
        struct asd_sas_phy *phy = ev->phy;

        sas_form_port(phy);
}

void sas_porte_broadcast_rcvd(struct work_struct *work)
{
        struct asd_sas_event *ev = to_asd_sas_event(work);
        struct asd_sas_phy *phy = ev->phy;
        unsigned long flags;
        u32 prim;

        spin_lock_irqsave(&phy->sas_prim_lock, flags);
        prim = phy->sas_prim;
        spin_unlock_irqrestore(&phy->sas_prim_lock, flags);

        pr_debug("broadcast received: %d\n", prim);
        sas_discover_event(phy->port, DISCE_REVALIDATE_DOMAIN);

        if (phy->port)
                flush_workqueue(phy->port->ha->disco_q);
}

void sas_porte_link_reset_err(struct work_struct *work)
{
        struct asd_sas_event *ev = to_asd_sas_event(work);
        struct asd_sas_phy *phy = ev->phy;

        sas_deform_port(phy, true);
}

void sas_porte_timer_event(struct work_struct *work)
{
        struct asd_sas_event *ev = to_asd_sas_event(work);
        struct asd_sas_phy *phy = ev->phy;

        sas_deform_port(phy, true);
}

void sas_porte_hard_reset(struct work_struct *work)
{
        struct asd_sas_event *ev = to_asd_sas_event(work);
        struct asd_sas_phy *phy = ev->phy;

        sas_deform_port(phy, true);
}

/* ---------- SAS port registration ---------- */

static void sas_init_port(struct asd_sas_port *port,
                          struct sas_ha_struct *sas_ha, int i)
{
        memset(port, 0, sizeof(*port));
        port->id = i;
        INIT_LIST_HEAD(&port->dev_list);
        INIT_LIST_HEAD(&port->disco_list);
        INIT_LIST_HEAD(&port->destroy_list);
        INIT_LIST_HEAD(&port->sas_port_del_list);
        spin_lock_init(&port->phy_list_lock);
        INIT_LIST_HEAD(&port->phy_list);
        port->ha = sas_ha;

        spin_lock_init(&port->dev_list_lock);
}

int sas_register_ports(struct sas_ha_struct *sas_ha)
{
        int i;

        /* initialize the ports and discovery */
        for (i = 0; i < sas_ha->num_phys; i++) {
                struct asd_sas_port *port = sas_ha->sas_port[i];

                sas_init_port(port, sas_ha, i);
                sas_init_disc(&port->disc, port);
        }
        return 0;
}

void sas_unregister_ports(struct sas_ha_struct *sas_ha)
{
        int i;

        for (i = 0; i < sas_ha->num_phys; i++)
                if (sas_ha->sas_phy[i]->port)
                        sas_deform_port(sas_ha->sas_phy[i], false);
}

const work_func_t sas_port_event_fns[PORT_NUM_EVENTS] = {
        [PORTE_BYTES_DMAED] = sas_porte_bytes_dmaed,
        [PORTE_BROADCAST_RCVD] = sas_porte_broadcast_rcvd,
        [PORTE_LINK_RESET_ERR] = sas_porte_link_reset_err,
        [PORTE_TIMER_EVENT] = sas_porte_timer_event,
        [PORTE_HARD_RESET] = sas_porte_hard_reset,
};