root/usr/src/uts/common/io/zyd/zyd_usb.c
/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * Copyright (c) 2007 by  Lukas Turek <turek@ksvi.mff.cuni.cz>
 * Copyright (c) 2007 by  Jiri Svoboda <jirik.svoboda@seznam.cz>
 * Copyright (c) 2007 by  Martin Krulis <martin.krulis@matfyz.cz>
 * Copyright (c) 2006 by Damien Bergamini <damien.bergamini@free.fr>
 * Copyright (c) 2006 by Florian Stoehr <ich@florian-stoehr.de>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

/*
 * ZD1211 wLAN driver
 * USB communication
 *
 * Manage USB communication with the ZD-based device.
 */

#include <sys/byteorder.h>
#include <sys/stream.h>
#include <sys/strsun.h>
#include <sys/mac_provider.h>

#include "zyd.h"
#include "zyd_reg.h"

static int zyd_usb_disconnect(dev_info_t *dip);
static int zyd_usb_reconnect(dev_info_t *dip);
static zyd_res zyd_usb_data_in_start_request(struct zyd_usb *uc);

static zyd_usb_info_t usb_ids[] = {
        {0x53, 0x5301, ZYD_ZD1211B},
        {0x105, 0x145f, ZYD_ZD1211},
        {0x411, 0xda, ZYD_ZD1211B},
        {0x471, 0x1236, ZYD_ZD1211B},
        {0x471, 0x1237, ZYD_ZD1211B},
        {0x50d, 0x705c, ZYD_ZD1211B},
        {0x586, 0x3401, ZYD_ZD1211},
        {0x586, 0x3402, ZYD_ZD1211},
        {0x586, 0x3407, ZYD_ZD1211},
        {0x586, 0x3409, ZYD_ZD1211},
        {0x586, 0x3410, ZYD_ZD1211B},
        {0x586, 0x3412, ZYD_ZD1211B},
        {0x586, 0x3413, ZYD_ZD1211B},
        {0x586, 0x340a, ZYD_ZD1211B},
        {0x586, 0x340f, ZYD_ZD1211B},
        {0x79b, 0x4a, ZYD_ZD1211},
        {0x79b, 0x62, ZYD_ZD1211B},
        {0x7b8, 0x6001, ZYD_ZD1211},
        {0x83a, 0x4505, ZYD_ZD1211B},
        {0xace, 0x1211, ZYD_ZD1211},
        {0xace, 0x1215, ZYD_ZD1211B},
        {0xb05, 0x170c, ZYD_ZD1211},
        {0xb05, 0x171b, ZYD_ZD1211B},
        {0xb3b, 0x1630, ZYD_ZD1211},
        {0xb3b, 0x5630, ZYD_ZD1211},
        {0xbaf, 0x121, ZYD_ZD1211B},
        {0xcde, 0x1a, ZYD_ZD1211B},
        {0xdf6, 0x9071, ZYD_ZD1211},
        {0xdf6, 0x9075, ZYD_ZD1211},
        {0x126f, 0xa006, ZYD_ZD1211},
        {0x129b, 0x1666, ZYD_ZD1211},
        {0x129b, 0x1667, ZYD_ZD1211B},
        {0x13b1, 0x1e, ZYD_ZD1211},
        {0x13b1, 0x24, ZYD_ZD1211B},
        {0x1435, 0x711, ZYD_ZD1211},
        {0x14ea, 0xab13, ZYD_ZD1211},
        {0x157e, 0x300b, ZYD_ZD1211},
        {0x157e, 0x300d, ZYD_ZD1211B},
        {0x157e, 0x3204, ZYD_ZD1211},
        {0x1582, 0x6003, ZYD_ZD1211B},
        {0x1740, 0x2000, ZYD_ZD1211},
        {0x2019, 0x5303, ZYD_ZD1211B},
        {0x6891, 0xa727, ZYD_ZD1211}
};

/*
 * Get mac rev for usb vendor/product id.
 */
zyd_mac_rev_t
zyd_usb_mac_rev(uint16_t vendor, uint16_t product)
{
        int i;

        ZYD_DEBUG((ZYD_DBG_USB, "matching device usb%x,%x\n", vendor, product));
        for (i = 0; i < sizeof (usb_ids) / sizeof (zyd_usb_info_t); i++) {
                if (vendor == usb_ids[i].vendor_id &&
                    product == usb_ids[i].product_id)
                        return (usb_ids[i].mac_rev);
        }

        ZYD_DEBUG((ZYD_DBG_USB, "assuming ZD1211B\n"));
        return (ZYD_ZD1211B);
}

/*
 * Vendor-specific write to the default control pipe.
 */
static zyd_res
zyd_usb_ctrl_send(struct zyd_usb *uc, uint8_t request, uint16_t value,
    uint8_t *data, uint16_t len)
{
        int err;
        int retry = 0;
        mblk_t *msg;
        usb_ctrl_setup_t setup;

        /* Always clean structures before use */
        bzero(&setup, sizeof (setup));
        setup.bmRequestType =
            USB_DEV_REQ_TYPE_VENDOR | USB_DEV_REQ_HOST_TO_DEV;
        setup.bRequest = request;
        setup.wValue = value;
        setup.wIndex = 0;
        setup.wLength = len;
        setup.attrs = USB_ATTRS_NONE;

        if ((msg = allocb(len, BPRI_HI)) == NULL)
                return (ZYD_FAILURE);

        bcopy(data, msg->b_wptr, len);
        msg->b_wptr += len;

        while ((err = usb_pipe_ctrl_xfer_wait(uc->cdata->dev_default_ph,
            &setup, &msg, NULL, NULL, 0)) != USB_SUCCESS) {
                if (retry++ > 3)
                        break;
        }

        freemsg(msg);

        if (err != USB_SUCCESS) {
                ZYD_DEBUG((ZYD_DBG_USB,
                    "control pipe send failure (%d)\n", err));
                return (ZYD_FAILURE);
        }

        return (ZYD_SUCCESS);
}

/*
 * Vendor-specific read from the default control pipe.
 */
static zyd_res
zyd_usb_ctrl_recv(struct zyd_usb *uc, uint8_t request, uint16_t value,
    uint8_t *data, uint16_t len)
{
        int err;
        mblk_t *msg, *tmp_msg;
        usb_ctrl_setup_t setup;
        size_t msg_len;

        ASSERT(data != NULL);

        bzero(&setup, sizeof (setup));
        setup.bmRequestType =
            USB_DEV_REQ_TYPE_VENDOR | USB_DEV_REQ_DEV_TO_HOST;
        setup.bRequest = request;
        setup.wValue = value;
        setup.wIndex = 0;
        setup.wLength = len;
        setup.attrs = USB_ATTRS_NONE;

        /* Pointer msg must be either set to NULL or point to a valid mblk! */
        msg = NULL;
        err = usb_pipe_ctrl_xfer_wait(uc->cdata->dev_default_ph,
            &setup, &msg, NULL, NULL, 0);

        if (err != USB_SUCCESS) {
                ZYD_WARN("control pipe receive failure (%d)\n", err);
                return (ZYD_FAILURE);
        }

        msg_len = msgsize(msg);

        if (msg_len != len) {
                ZYD_WARN("control pipe failure: "
                    "received %d bytes, %d expected\n", (int)msg_len, len);
                return (ZYD_FAILURE);
        }

        if (msg->b_cont != NULL) {
                /* Fragmented message, concatenate */
                tmp_msg = msgpullup(msg, -1);
                freemsg(msg);
                msg = tmp_msg;
        }

        /*
         * Now we can be sure the message is in a single block
         * so we can copy it.
         */
        bcopy(msg->b_rptr, data, len);
        freemsg(msg);

        return (ZYD_SUCCESS);
}

/*
 * Load firmware into the chip.
 */
zyd_res
zyd_usb_loadfirmware(struct zyd_usb *uc, uint8_t *fw, size_t size)
{
        uint16_t addr;
        uint8_t stat;

        ZYD_DEBUG((ZYD_DBG_FW, "firmware size: %lu\n", size));

        addr = ZYD_FIRMWARE_START_ADDR;
        while (size > 0) {
                const uint16_t mlen = (uint16_t)min(size, 4096);

                if (zyd_usb_ctrl_send(uc, ZYD_DOWNLOADREQ, addr, fw, mlen)
                    != USB_SUCCESS)
                        return (ZYD_FAILURE);

                addr += mlen / 2;
                fw += mlen;
                size -= mlen;
        }

        /* check whether the upload succeeded */
        if (zyd_usb_ctrl_recv(uc, ZYD_DOWNLOADSTS, 0, &stat, sizeof (stat))
            != ZYD_SUCCESS)
                return (ZYD_FAILURE);

        return ((stat & 0x80) ? ZYD_FAILURE : ZYD_SUCCESS);
}

/*
 * Return a specific alt_if from the device descriptor tree.
 */
static usb_alt_if_data_t *
usb_lookup_alt_if(usb_client_dev_data_t *cdd, uint_t config,
    uint_t interface, uint_t alt)
{
        usb_cfg_data_t *dcfg;
        usb_if_data_t *cfgif;
        usb_alt_if_data_t *ifalt;

        /*
         * Assume everything is in the tree for now,
         * (USB_PARSE_LVL_ALL)
         * so we can directly index the array.
         */

        /* Descend to configuration, configs are 1-based */
        if (config < 1 || config > cdd->dev_n_cfg)
                return (NULL);
        dcfg = &cdd->dev_cfg[config - 1];

        /* Descend to interface */
        if (interface > dcfg->cfg_n_if - 1)
                return (NULL);
        cfgif = &dcfg->cfg_if[interface];

        /* Descend to alt */
        if (alt > cfgif->if_n_alt - 1)
                return (NULL);
        ifalt = &cfgif->if_alt[alt];

        return (ifalt);
}

/*
 * Print all endpoints of an alt_if.
 */
static void
usb_list_all_endpoints(usb_alt_if_data_t *ifalt)
{
        usb_ep_data_t *ep_data;
        usb_ep_descr_t *ep_descr;
        int i;

        for (i = 0; i < ifalt->altif_n_ep; i++) {
                ep_data = &ifalt->altif_ep[i];
                ep_descr = &ep_data->ep_descr;
                cmn_err(CE_NOTE, "EP: %u\n", ep_descr->bEndpointAddress);
        }
}

/*
 * For the given alt_if, find an endpoint with the given
 * address and direction.
 *
 *      ep_direction    USB_EP_DIR_IN or USB_EP_DIR_OUT
 */
static usb_ep_data_t *
usb_find_endpoint(usb_alt_if_data_t *alt_if,
    uint_t ep_address, uint_t ep_direction)
{
        usb_ep_data_t *ep_data;
        usb_ep_descr_t *ep_descr;
        uint_t ep_addr, ep_dir;
        int i;

        for (i = 0; i < alt_if->altif_n_ep; i++) {
                ep_data = &alt_if->altif_ep[i];
                ep_descr = &ep_data->ep_descr;
                ep_addr = ep_descr->bEndpointAddress & USB_EP_NUM_MASK;
                ep_dir = ep_descr->bEndpointAddress & USB_EP_DIR_MASK;

                if (ep_addr == ep_address && ep_dir == ep_direction) {
                        return (ep_data);
                }
        }

        ZYD_WARN("no endpoint with addr %u, dir %u\n", ep_address,
            ep_direction);
        return (NULL);
}

enum zyd_usb_use_attr
{
        ZYD_USB_USE_ATTR = 1,
        ZYD_USB_NO_ATTR = 0
};

/*
 * Open a pipe to a given endpoint address/direction in the given
 * alt_if. Furthemore, if use_attr == ZYD_USB_USE_ATTR,
 * check whether the endpoint's transfer type is attr.
 */
static zyd_res
zyd_usb_open_pipe(struct zyd_usb *uc,
    usb_alt_if_data_t *alt_if,
    uint_t ep_address,
    uint_t ep_direction,
    uint_t attr,
    enum zyd_usb_use_attr use_attr,
    usb_pipe_handle_t *pipe, usb_ep_data_t *endpoint)
{
        usb_pipe_policy_t pipe_policy;

        *endpoint = *usb_find_endpoint(alt_if, ep_address, ep_direction);

        if ((use_attr == ZYD_USB_USE_ATTR) &&
            (endpoint->ep_descr.bmAttributes & USB_EP_ATTR_MASK) != attr) {

                ZYD_WARN("endpoint %u/%s is not of type %s\n", ep_address,
                    (ep_direction == USB_EP_DIR_IN) ? "IN" : "OUT",
                    (attr == USB_EP_ATTR_BULK) ? "bulk" : "intr");
                return (ZYD_FAILURE);
        }

        bzero(&pipe_policy, sizeof (usb_pipe_policy_t));
        pipe_policy.pp_max_async_reqs = ZYD_USB_REQ_COUNT;

        if (usb_pipe_open(uc->dip, &endpoint->ep_descr,
            &pipe_policy, USB_FLAGS_SLEEP, pipe) != USB_SUCCESS) {
                ZYD_WARN("failed to open pipe %u\n", ep_address);
                return (ZYD_FAILURE);
        }

        return (ZYD_SUCCESS);
}

/*
 * Open communication pipes.
 *
 * The following pipes are used by the ZD1211:
 *
 *      1/OUT BULK
 *      2/IN  BULK
 *      3/IN  INTR
 *      4/OUT BULK or INTR
 */
zyd_res
zyd_usb_open_pipes(struct zyd_usb *uc)
{
        usb_alt_if_data_t *alt_if;

        ZYD_DEBUG((ZYD_DBG_USB, "opening pipes\n"));

        alt_if = usb_lookup_alt_if(uc->cdata, ZYD_USB_CONFIG_NUMBER,
            ZYD_USB_IFACE_INDEX, ZYD_USB_ALT_IF_INDEX);

        if (alt_if == NULL) {
                ZYD_WARN("alt_if not found\n");
                return (ZYD_FAILURE);
        }

#ifdef DEBUG
        if (zyd_dbg_flags & ZYD_DBG_USB)
                usb_list_all_endpoints(alt_if);
#endif

        if (zyd_usb_open_pipe(uc, alt_if, 1, USB_EP_DIR_OUT, USB_EP_ATTR_BULK,
            ZYD_USB_USE_ATTR, &uc->pipe_data_out, &uc->ep_data_out) !=
            ZYD_SUCCESS) {
                ZYD_WARN("failed to open data OUT pipe\n");
                goto fail;
        }

        if (zyd_usb_open_pipe(uc, alt_if, 2, USB_EP_DIR_IN, USB_EP_ATTR_BULK,
            ZYD_USB_USE_ATTR, &uc->pipe_data_in, &uc->ep_data_in) !=
            ZYD_SUCCESS) {
                ZYD_WARN("failed to open data IN pipe\n");
                goto fail;
        }

        if (zyd_usb_open_pipe(uc, alt_if, 3, USB_EP_DIR_IN, USB_EP_ATTR_INTR,
            ZYD_USB_USE_ATTR, &uc->pipe_cmd_in, &uc->ep_cmd_in) !=
            ZYD_SUCCESS) {
                ZYD_WARN("failed to open command IN pipe\n");
                goto fail;
        }

        /*
         * Pipe 4/OUT is either a bulk or interrupt pipe.
         */
        if (zyd_usb_open_pipe(uc, alt_if, 4, USB_EP_DIR_OUT, 0,
            ZYD_USB_NO_ATTR, &uc->pipe_cmd_out, &uc->ep_cmd_out) !=
            ZYD_SUCCESS) {
                ZYD_WARN("failed to open command OUT pipe\n");
                goto fail;
        }

        return (ZYD_SUCCESS);

fail:
        zyd_usb_close_pipes(uc);
        return (ZYD_FAILURE);
}

/*
 * Close communication pipes.
 */
void
zyd_usb_close_pipes(struct zyd_usb *uc)
{
        ZYD_DEBUG((ZYD_DBG_USB, "closing pipes\n"));

        if (uc->pipe_data_out != NULL) {
                usb_pipe_close(uc->dip, uc->pipe_data_out, USB_FLAGS_SLEEP,
                    NULL, NULL);
                uc->pipe_data_out = NULL;
        }

        if (uc->pipe_data_in != NULL) {
                usb_pipe_close(uc->dip, uc->pipe_data_in, USB_FLAGS_SLEEP,
                    NULL, NULL);
                uc->pipe_data_in = NULL;
        }

        if (uc->pipe_cmd_in != NULL) {
                usb_pipe_close(uc->dip, uc->pipe_cmd_in, USB_FLAGS_SLEEP,
                    NULL, NULL);
                uc->pipe_cmd_in = NULL;
        }

        if (uc->pipe_cmd_out != NULL) {
                usb_pipe_close(uc->dip, uc->pipe_cmd_out, USB_FLAGS_SLEEP,
                    NULL, NULL);
                uc->pipe_cmd_out = NULL;
        }
}

/*
 * Send a sequence of bytes to a bulk pipe.
 *
 *      uc      pointer to usb module state
 *      data    pointer to a buffer of bytes
 *      len     size of the buffer (bytes)
 */
/*ARGSUSED*/
static void
zyd_data_out_cb(usb_pipe_handle_t pipe, usb_bulk_req_t *req)
{
        struct zyd_softc *sc = (struct zyd_softc *)req->bulk_client_private;
        struct ieee80211com *ic = &sc->ic;
        boolean_t resched;

        if (req->bulk_completion_reason != USB_CR_OK)
                ZYD_DEBUG((ZYD_DBG_USB, "data OUT exception\n"));

        (void) zyd_serial_enter(sc, ZYD_NO_SIG);
        if (sc->tx_queued > 0)
                sc->tx_queued--;
        else
                ZYD_DEBUG((ZYD_DBG_TX, "tx queue underrun\n"));

        if (sc->resched && (sc->tx_queued < ZYD_TX_LIST_COUNT)) {
                resched = sc->resched;
                sc->resched = B_FALSE;
        }
        zyd_serial_exit(sc);

        if (resched)
                mac_tx_update(ic->ic_mach);

        usb_free_bulk_req(req);
}

/*
 * Called when the transfer from zyd_usb_bulk_pipe_send() terminates
 * or an exception occurs on the pipe.
 */
/*ARGSUSED*/
static void
zyd_bulk_pipe_cb(usb_pipe_handle_t pipe, struct usb_bulk_req *req)
{
        struct zyd_cb_lock *lock;
        lock = (struct zyd_cb_lock *)req->bulk_client_private;

        /* Just signal that something happened */
        zyd_cb_lock_signal(lock);
}

static zyd_res
zyd_usb_bulk_pipe_send(struct zyd_usb *uc,
    usb_pipe_handle_t pipe, const void *data, size_t len)
{
        usb_bulk_req_t *send_req;
        mblk_t *mblk;
        int res;
        struct zyd_cb_lock lock;

        send_req = usb_alloc_bulk_req(uc->dip, len, USB_FLAGS_SLEEP);
        if (send_req == NULL) {
                ZYD_WARN("failed to allocate bulk request\n");
                return (ZYD_FAILURE);
        }
        send_req->bulk_len = (uint_t)len;
        send_req->bulk_client_private = (usb_opaque_t)&lock;
        send_req->bulk_attributes = USB_ATTRS_AUTOCLEARING;
        send_req->bulk_timeout = 5;
        send_req->bulk_cb = zyd_bulk_pipe_cb;
        send_req->bulk_exc_cb = zyd_bulk_pipe_cb;

        mblk = send_req->bulk_data;
        bcopy(data, mblk->b_wptr, len);
        mblk->b_wptr += len;

        zyd_cb_lock_init(&lock);

        res = usb_pipe_bulk_xfer(pipe, send_req, USB_FLAGS_NOSLEEP);
        if (res != USB_SUCCESS) {
                ZYD_DEBUG((ZYD_DBG_USB,
                    "failed writing to bulk OUT pipe (%d)\n", res));
                usb_free_bulk_req(send_req);
                zyd_cb_lock_destroy(&lock);
                return (ZYD_FAILURE);
        }

        if (zyd_cb_lock_wait(&lock, 1000000) != ZYD_SUCCESS) {
                ZYD_WARN("timeout - pipe reset\n");
                usb_pipe_reset(uc->dip, pipe, USB_FLAGS_SLEEP, NULL, 0);
                (void) zyd_cb_lock_wait(&lock, -1);
                res = ZYD_FAILURE;
        } else {
                res = (send_req->bulk_completion_reason == USB_CR_OK) ?
                    ZYD_SUCCESS : ZYD_FAILURE;
        }

        usb_free_bulk_req(send_req);
        zyd_cb_lock_destroy(&lock);
        return (res);
}

/*
 * Called when the transfer from zyd_usb_intr_pipe_send() terminates
 * or an exception occurs on the pipe.
 */
/*ARGSUSED*/
static void
zyd_intr_pipe_cb(usb_pipe_handle_t pipe, struct usb_intr_req *req)
{
        struct zyd_cb_lock *lock;
        lock = (struct zyd_cb_lock *)req->intr_client_private;

        /* Just signal that something happened */
        zyd_cb_lock_signal(lock);
}

/*
 * Send a sequence of bytes to an interrupt pipe.
 *
 *      uc      pointer to usb module state
 *      data    pointer to a buffer of bytes
 *      len     size of the buffer (bytes)
 */
static zyd_res
zyd_usb_intr_pipe_send(struct zyd_usb *uc,
    usb_pipe_handle_t pipe, const void *data, size_t len)
{
        usb_intr_req_t *send_req;
        mblk_t *mblk;
        int res;
        struct zyd_cb_lock lock;

        send_req = usb_alloc_intr_req(uc->dip, len, USB_FLAGS_SLEEP);
        if (send_req == NULL) {
                ZYD_WARN("failed to allocate interupt request\n");
                return (ZYD_FAILURE);
        }
        send_req->intr_len = (uint_t)len;
        send_req->intr_client_private = (usb_opaque_t)&lock;
        send_req->intr_attributes = USB_ATTRS_AUTOCLEARING;
        send_req->intr_timeout = 5;
        send_req->intr_cb = zyd_intr_pipe_cb;
        send_req->intr_exc_cb = zyd_intr_pipe_cb;

        mblk = send_req->intr_data;
        bcopy(data, mblk->b_wptr, len);
        mblk->b_wptr += len;

        zyd_cb_lock_init(&lock);

        res = usb_pipe_intr_xfer(pipe, send_req, 0);
        if (res != USB_SUCCESS) {
                ZYD_DEBUG((ZYD_DBG_USB,
                    "failed writing to intr/out pipe (%d)\n", res));
                usb_free_intr_req(send_req);
                zyd_cb_lock_destroy(&lock);
                return (ZYD_FAILURE);
        }

        if (zyd_cb_lock_wait(&lock, 1000000) != ZYD_SUCCESS) {
                ZYD_WARN("timeout - pipe reset\n");
                usb_pipe_reset(uc->dip, pipe, USB_FLAGS_SLEEP, NULL, 0);
                (void) zyd_cb_lock_wait(&lock, -1);
                res = ZYD_FAILURE;
        } else {
                res = (send_req->intr_completion_reason == USB_CR_OK) ?
                    ZYD_SUCCESS : ZYD_FAILURE;
        }

        usb_free_intr_req(send_req);
        zyd_cb_lock_destroy(&lock);
        return (res);
}

/*
 * Send a sequence of bytes to the cmd_out pipe. (in a single USB transfer)
 *
 *      uc      pointer to usb module state
 *      data    pointer to a buffer of bytes
 *      len     size of the buffer (bytes)
 */
static zyd_res
zyd_usb_cmd_pipe_send(struct zyd_usb *uc, const void *data, size_t len)
{
        zyd_res res;
        uint8_t type;

        /* Determine the type of cmd_out */
        type = uc->ep_cmd_out.ep_descr.bmAttributes & USB_EP_ATTR_MASK;
        if (type == USB_EP_ATTR_BULK)
                res = zyd_usb_bulk_pipe_send(uc, uc->pipe_cmd_out, data, len);
        else
                res = zyd_usb_intr_pipe_send(uc, uc->pipe_cmd_out, data, len);

        return (res);
}


/*
 * Format and send a command to the cmd_out pipe.
 *
 *      uc      pointer to usb module state
 *      code    ZD command code (16-bit)
 *      data    raw buffer containing command data
 *      len     size of the data buffer (bytes)
 */
zyd_res
zyd_usb_cmd_send(struct zyd_usb *uc,
    uint16_t code, const void *data, size_t len)
{
        zyd_res res;
        struct zyd_cmd cmd;

        cmd.cmd_code = LE_16(code);
        bcopy(data, cmd.data, len);

        res = zyd_usb_cmd_pipe_send(uc, &cmd, sizeof (uint16_t) + len);
        if (res != ZYD_SUCCESS) {
                ZYD_DEBUG((ZYD_DBG_USB, "failed writing command (%d)\n", res));
                return (ZYD_FAILURE);
        }

        return (ZYD_SUCCESS);
}

/*
 * Issue an ioread request.
 *
 * Issues a ZD ioread command (with a vector of addresses passed in raw
 * form as in_data) and blocks until the response is received
 * and filled into the response buffer.
 *
 *      uc              pointer to usb module state
 *      in_data         pointer to request data
 *      in_len          request data size (bytes)
 *      out_data        pointer to response buffer
 *      out_len         response buffer size (bytes)
 */
zyd_res
zyd_usb_ioread_req(struct zyd_usb *uc,
    const void *in_data, size_t in_len, void *out_data, size_t out_len)
{
        zyd_res res;
        int cnt;

        /* Initialise io_read structure */
        uc->io_read.done = B_FALSE;
        uc->io_read.buffer = out_data;
        uc->io_read.buf_len = (int)out_len;

        uc->io_read.pending = B_TRUE;

        res = zyd_usb_cmd_send(uc, ZYD_CMD_IORD, in_data, in_len);
        if (res != ZYD_SUCCESS) {
                ZYD_DEBUG((ZYD_DBG_USB, "IO read request: pipe failure(%d)\n"));
                return (ZYD_FAILURE);
        }

        cnt = 0;
        while (uc->io_read.done != B_TRUE && cnt < 500) {
                delay(drv_usectohz(10 * 1000));
                ++cnt;
        }

        if (uc->io_read.done != B_TRUE) {
                ZYD_WARN("I/O read request: timeout\n");
                return (ZYD_FAILURE);
        }

        if (uc->io_read.exc != B_FALSE) {
                ZYD_DEBUG((ZYD_DBG_USB, "I/O read request: exception\n"));
                return (ZYD_FAILURE);
        }

        return (ZYD_SUCCESS);
}


/*
 * Called when data arrives from the cmd_in pipe.
 */
/*ARGSUSED*/
static void
zyd_cmd_in_cb(usb_pipe_handle_t pipe, usb_intr_req_t *req)
{
        struct zyd_usb *uc;
        struct zyd_ioread *rdp;
        mblk_t *mblk, *tmp_blk;
        unsigned char *data;
        size_t len;
        uint16_t code;

        uc = (struct zyd_usb *)req->intr_client_private;
        ASSERT(uc != NULL);
        rdp = &uc->io_read;
        mblk = req->intr_data;

        if (mblk->b_cont != NULL) {
                /* Fragmented message, concatenate */
                tmp_blk = msgpullup(mblk, -1);
                data = tmp_blk->b_rptr;
                len = MBLKL(tmp_blk);
        } else {
                /* Non-fragmented message, use directly */
                tmp_blk = NULL;
                data = mblk->b_rptr;
                len = MBLKL(mblk);
        }

        code = LE_16(*(uint16_t *)(uintptr_t)data);
        if (code != ZYD_RESPONSE_IOREAD) {
                /* Other response types not handled yet */
                usb_free_intr_req(req);
                return;
        }

        if (rdp->pending != B_TRUE) {
                ZYD_WARN("no ioread pending\n");
                usb_free_intr_req(req);
                return;
        }
        rdp->pending = B_FALSE;

        /* Now move on to the data part */
        data += sizeof (uint16_t);
        len -= sizeof (uint16_t);
        if (rdp->buf_len > len) {
                ZYD_WARN("too few bytes received\n");
        }

        bcopy(data, rdp->buffer, rdp->buf_len);

        if (tmp_blk != NULL)
                freemsg(tmp_blk);

        rdp->exc = B_FALSE;
        rdp->done = B_TRUE;
        usb_free_intr_req(req);
}

/*
 * Called when an exception occurs on the cmd_in pipe.
 */
/*ARGSUSED*/
static void
zyd_cmd_in_exc_cb(usb_pipe_handle_t pipe, usb_intr_req_t *req)
{
        struct zyd_usb *uc;
        struct zyd_ioread *rdp;

        ZYD_DEBUG((ZYD_DBG_USB, "command IN exception\n"));

        uc = (struct zyd_usb *)req->intr_client_private;
        ASSERT(uc != NULL);
        rdp = &uc->io_read;

        if (rdp->pending == B_TRUE) {
                rdp->exc = B_TRUE;
                rdp->done = B_TRUE;
        }
        usb_free_intr_req(req);
}

/*
 * Start interrupt polling on the cmd_in pipe.
 */
zyd_res
zyd_usb_cmd_in_start_polling(struct zyd_usb *uc)
{
        usb_intr_req_t *intr_req;
        int res;

        intr_req = usb_alloc_intr_req(uc->dip, 0, USB_FLAGS_SLEEP);
        if (intr_req == NULL) {
                ZYD_WARN("failed to allocate interrupt request\n");
                return (ZYD_FAILURE);
        }

        intr_req->intr_attributes = USB_ATTRS_SHORT_XFER_OK;
        intr_req->intr_len = uc->ep_cmd_in.ep_descr.wMaxPacketSize;
        intr_req->intr_cb = zyd_cmd_in_cb;
        intr_req->intr_exc_cb = zyd_cmd_in_exc_cb;
        intr_req->intr_client_private = (usb_opaque_t)uc;

        res = usb_pipe_intr_xfer(uc->pipe_cmd_in, intr_req, USB_FLAGS_NOSLEEP);
        if (res != USB_SUCCESS) {
                ZYD_WARN("failed starting command IN polling: pipe failure\n");
                usb_free_intr_req(intr_req);
                return (ZYD_FAILURE);
        }

        return (ZYD_SUCCESS);
}

/*
 * Stop interrupt polling on the cmd_in pipe.
 */
void
zyd_usb_cmd_in_stop_polling(struct zyd_usb *uc)
{
        ZYD_DEBUG((ZYD_DBG_USB, "stopping command IN polling\n"));

        usb_pipe_stop_intr_polling(uc->pipe_cmd_in, USB_FLAGS_SLEEP);
}

/*
 * Called when data arrives on the data_in pipe.
 */
/*ARGSUSED*/
static void
zyd_data_in_cb(usb_pipe_handle_t pipe, usb_bulk_req_t *req)
{
        struct zyd_softc *sc;
        struct zyd_usb *uc;
        mblk_t *mblk, *tmp_blk;
        struct zyd_rx_desc *desc;
        unsigned char *data;
        size_t len;

        uc = (struct zyd_usb *)req->bulk_client_private;
        ASSERT(uc != NULL);
        sc = ZYD_USB_TO_SOFTC(uc);
        ASSERT(sc != NULL);
        mblk = req->bulk_data;

        /* Fragmented STREAMS message? */
        if (mblk->b_cont != NULL) {
                /* Fragmented, concatenate it into a single block */
                tmp_blk = msgpullup(mblk, -1);
                if (tmp_blk == NULL) {
                        ZYD_WARN("failed to concatenate fragments\n");
                        goto error;
                }
                data = tmp_blk->b_rptr;
                len = MBLKL(tmp_blk);
        } else {
                /* Not fragmented, use directly */
                tmp_blk = NULL;
                data = mblk->b_rptr;
                len = MBLKL(mblk);
        }

        if (len < 2) {
                ZYD_WARN("received usb transfer too short\n");
                goto error;
        }

        /*
         * If this is a composite packet, the last two bytes contain
         * two special signature bytes.
         */
        desc = (struct zyd_rx_desc *)(data + len) - 1;
        /* multi-frame transfer */
        if (LE_16(desc->tag) == ZYD_TAG_MULTIFRAME) {
                const uint8_t *p = data, *end = data + len;
                int i;

                ZYD_DEBUG((ZYD_DBG_RX, "composite packet\n"));

                for (i = 0; i < ZYD_MAX_RXFRAMECNT; i++) {
                        const uint16_t len16 = LE_16(desc->len[i]);
                        if (len16 == 0 || p + len16 > end)
                                break;
                        zyd_receive(ZYD_USB_TO_SOFTC(uc), p, len16);
                        /* next frame is aligned on a 32-bit boundary */
                        p += (len16 + 3) & ~3;
                }
        } else {
                /* single-frame transfer */
                zyd_receive(ZYD_USB_TO_SOFTC(uc), data, MBLKL(mblk));
        }

error:
        if (tmp_blk != NULL)
                freemsg(tmp_blk);

        usb_free_bulk_req(req);

        if (!sc->running)
                return;

        if (zyd_usb_data_in_start_request(uc) != ZYD_SUCCESS) {
                ZYD_WARN("error restarting data_in transfer\n");
        }
}

/*
 * Called when an exception occurs on the data_in pipe.
 */
/*ARGSUSED*/
static void
zyd_data_in_exc_cb(usb_pipe_handle_t pipe, usb_bulk_req_t *req)
{
        struct zyd_usb *uc;

        ZYD_DEBUG((ZYD_DBG_USB, "data IN exception\n"));

        uc = (struct zyd_usb *)req->bulk_client_private;
        ASSERT(uc != NULL);

        usb_free_bulk_req(req);
}

/*
 * Start a receive request on the data_in pipe.
 */
static zyd_res
zyd_usb_data_in_start_request(struct zyd_usb *uc)
{
        usb_bulk_req_t *req;
        int res;

        req = usb_alloc_bulk_req(uc->dip, ZYD_RX_BUF_SIZE, USB_FLAGS_SLEEP);
        if (req == NULL) {
                ZYD_WARN("failed to allocate bulk IN request\n");
                return (ZYD_FAILURE);
        }

        req->bulk_len = (uint_t)ZYD_RX_BUF_SIZE;
        req->bulk_timeout = 0;
        req->bulk_client_private = (usb_opaque_t)uc;
        req->bulk_attributes =
            USB_ATTRS_SHORT_XFER_OK | USB_ATTRS_AUTOCLEARING;
        req->bulk_cb = zyd_data_in_cb;
        req->bulk_exc_cb = zyd_data_in_exc_cb;

        res = usb_pipe_bulk_xfer(uc->pipe_data_in, req, USB_FLAGS_NOSLEEP);
        if (res != USB_SUCCESS) {
                ZYD_WARN("error starting receive request on data_in pipe\n");
                usb_free_bulk_req(req);
                return (ZYD_FAILURE);
        }

        return (ZYD_SUCCESS);
}


/*
 * Start receiving packets on the data_in pipe.
 */
zyd_res
zyd_usb_data_in_enable(struct zyd_usb *uc)
{
        for (int i = 0; i < ZYD_RX_LIST_COUNT; i++) {
                if (zyd_usb_data_in_start_request(uc) != ZYD_SUCCESS) {
                        ZYD_WARN("failed to start data IN requests\n");
                        return (ZYD_FAILURE);
                }
        }
        return (ZYD_SUCCESS);
}

/*
 * Stop receiving packets on the data_in pipe.
 */
void
zyd_usb_data_in_disable(struct zyd_usb *uc)
{
        usb_pipe_reset(uc->dip, uc->pipe_data_in, USB_FLAGS_SLEEP,
            NULL, NULL);
}

/*
 * Send a packet to data_out.
 *
 * A packet consists of a zyd_tx_header + the IEEE802.11 frame.
 */
zyd_res
zyd_usb_send_packet(struct zyd_usb *uc, mblk_t *mp)
{
        usb_bulk_req_t *send_req;
        int res;

        send_req = usb_alloc_bulk_req(uc->dip, 0, USB_FLAGS_SLEEP);
        if (send_req == NULL) {
                ZYD_WARN("failed to allocate bulk request\n");
                return (ZYD_FAILURE);
        }

        send_req->bulk_len = msgdsize(mp);
        send_req->bulk_data = mp;
        send_req->bulk_timeout = 5;
        send_req->bulk_attributes = USB_ATTRS_AUTOCLEARING;
        send_req->bulk_client_private = (usb_opaque_t)ZYD_USB_TO_SOFTC(uc);
        send_req->bulk_cb = zyd_data_out_cb;
        send_req->bulk_exc_cb = zyd_data_out_cb;
        send_req->bulk_completion_reason = 0;
        send_req->bulk_cb_flags = 0;

        res = usb_pipe_bulk_xfer(uc->pipe_data_out, send_req, 0);
        if (res != USB_SUCCESS) {
                ZYD_DEBUG((ZYD_DBG_USB,
                    "failed writing to bulk/out pipe (%d)\n", res));
                usb_free_bulk_req(send_req);
                return (USB_FAILURE);
        }

        return (USB_SUCCESS);
}

/*
 * Initialize USB device communication and USB module state.
 *
 *      uc      pointer to usb module state
 *      dip     pointer to device info structure
 */
zyd_res
zyd_usb_init(struct zyd_softc *sc)
{
        struct zyd_usb *uc = &sc->usb;
        dev_info_t *dip = sc->dip;
        int ures;

        uc->dip = dip;

        ures = usb_client_attach(uc->dip, USBDRV_VERSION, 0);
        if (ures != USB_SUCCESS) {
                ZYD_WARN("usb_client_attach failed, error code: %d\n", ures);
                return (ZYD_FAILURE);
        }

        /*
         * LVL_ALL is needed for later endpoint scanning,
         * and the tree must not be freed before that.
         */
        ures = usb_get_dev_data(uc->dip, &uc->cdata, USB_PARSE_LVL_ALL, 0);
        if (ures != USB_SUCCESS) {
                ZYD_WARN("usb_get_dev_data failed, error code: %d\n", ures);
                ASSERT(uc->cdata == NULL);
                goto fail;
        }

        ures = usb_reset_device(uc->dip, USB_RESET_LVL_DEFAULT);
        if (ures != USB_SUCCESS) {
                ZYD_WARN("usb_reset_device failed, error code: %d\n", ures);
                goto fail;
        }

        uc->connected = B_TRUE;

        ures = usb_register_hotplug_cbs(dip, zyd_usb_disconnect,
            zyd_usb_reconnect);
        if (ures != USB_SUCCESS) {
                ZYD_WARN("usb_register_hotplug_cbs failed, error code: %d\n",
                    ures);
                goto fail;
        }

        return (ZYD_SUCCESS);
fail:
        usb_client_detach(uc->dip, uc->cdata);
        uc->cdata = NULL;
        return (ZYD_FAILURE);
}

/*
 * Deinitialize USB device communication.
 */
void
zyd_usb_deinit(struct zyd_softc *sc)
{
        struct zyd_usb *uc = &sc->usb;

        usb_unregister_hotplug_cbs(sc->dip);

        usb_client_detach(uc->dip, uc->cdata);
        uc->cdata = NULL;
        uc->connected = B_FALSE;
}

/*
 * Device connected
 */
static int
zyd_usb_reconnect(dev_info_t *dip)
{
        struct zyd_softc *sc;
        struct zyd_usb *uc;

        sc = ddi_get_soft_state(zyd_ssp, ddi_get_instance(dip));
        ASSERT(sc != NULL);
        uc = &sc->usb;
        ASSERT(!uc->connected);

        if (sc->suspended)
                ZYD_DEBUG((ZYD_DBG_RESUME | ZYD_DBG_USB,
                    "reconnect before resume\n"));

        /* check device changes after disconnect */
        if (usb_check_same_device(sc->dip, NULL, USB_LOG_L2, -1,
            USB_CHK_BASIC | USB_CHK_CFG, NULL) != USB_SUCCESS) {
                ZYD_DEBUG((ZYD_DBG_USB, "different device connected\n"));
                return (DDI_FAILURE);
        }

        (void) zyd_serial_enter(sc, ZYD_NO_SIG);
        if (zyd_hw_init(sc) != ZYD_SUCCESS) {
                ZYD_WARN("failed to reinit hardware\n");
                zyd_serial_exit(sc);
                return (DDI_FAILURE);
        }
        if (sc->running) {
                if (zyd_hw_start(sc) != ZYD_SUCCESS) {
                        ZYD_WARN("failed to restart hardware\n");
                        zyd_serial_exit(sc);
                        goto fail;
                }
        }
        zyd_serial_exit(sc);

        uc->connected = B_TRUE;

        return (DDI_SUCCESS);
fail:
        usb_client_detach(uc->dip, uc->cdata);
        uc->cdata = NULL;
        return (DDI_FAILURE);
}

static int
zyd_usb_disconnect(dev_info_t *dip)
{
        struct zyd_softc *sc;
        struct zyd_usb *uc;

        sc = ddi_get_soft_state(zyd_ssp, ddi_get_instance(dip));
        ASSERT(sc != NULL);
        uc = &sc->usb;

        if (!uc->connected) {
                ZYD_DEBUG((ZYD_DBG_USB, "different device disconnected\n"));
                return (DDI_FAILURE);
        }
        uc->connected = B_FALSE;

        if (sc->suspended) {
                ZYD_DEBUG((ZYD_DBG_USB, "disconnect after suspend\n"));
                return (DDI_SUCCESS);
        }
        ieee80211_new_state(&sc->ic, IEEE80211_S_INIT, -1);

        (void) zyd_serial_enter(sc, ZYD_NO_SIG);
        zyd_hw_stop(sc);
        zyd_hw_deinit(sc);
        zyd_serial_exit(sc);

        return (DDI_SUCCESS);
}

int
zyd_suspend(struct zyd_softc *sc)
{
        struct zyd_usb *uc = &sc->usb;

        if (!uc->connected) {
                ZYD_DEBUG((ZYD_DBG_USB | ZYD_DBG_RESUME,
                    "suspend after disconnect\n"));
                sc->suspended = B_TRUE;
                return (DDI_SUCCESS);
        }
        ZYD_DEBUG((ZYD_DBG_RESUME, "suspend\n"));

        sc->suspended = B_TRUE;
        ieee80211_new_state(&sc->ic, IEEE80211_S_INIT, -1);

        (void) zyd_serial_enter(sc, ZYD_NO_SIG);
        zyd_hw_stop(sc);
        zyd_hw_deinit(sc);
        zyd_serial_exit(sc);

        ZYD_DEBUG((ZYD_DBG_RESUME, "suspend complete\n"));
        return (DDI_SUCCESS);
}

int
zyd_resume(struct zyd_softc *sc)
{
        struct zyd_usb *uc = &sc->usb;

        if (!uc->connected) {
                ZYD_DEBUG((ZYD_DBG_USB | ZYD_DBG_RESUME,
                    "resume after disconnect\n"));
                sc->suspended = B_FALSE;
                return (DDI_SUCCESS);
        }
        ZYD_DEBUG((ZYD_DBG_RESUME, "resume\n"));

        /* check device changes after disconnect */
        if (usb_check_same_device(sc->dip, NULL, USB_LOG_L2, -1,
            USB_CHK_BASIC | USB_CHK_CFG, NULL) != USB_SUCCESS) {
                ZYD_WARN("different device connected to same port\n");
                sc->suspended = B_FALSE;
                uc->connected = B_FALSE;
                return (DDI_SUCCESS);
        }

        (void) zyd_serial_enter(sc, ZYD_NO_SIG);
        if (zyd_hw_init(sc) != ZYD_SUCCESS) {
                ZYD_WARN("failed to reinit hardware\n");
                zyd_serial_exit(sc);
                return (DDI_FAILURE);
        }
        if (sc->running) {
                if (zyd_hw_start(sc) != ZYD_SUCCESS) {
                        ZYD_WARN("failed to restart hardware\n");
                        zyd_serial_exit(sc);
                        return (DDI_FAILURE);
                }
        }
        zyd_serial_exit(sc);

        sc->suspended = B_FALSE;

        ZYD_DEBUG((ZYD_DBG_RESUME, "resume complete\n"));
        return (DDI_SUCCESS);
}