diff options
Diffstat (limited to 'drivers/bluetooth/btmrvl_sdio.c')
-rw-r--r-- | drivers/bluetooth/btmrvl_sdio.c | 1614 |
1 files changed, 1614 insertions, 0 deletions
diff --git a/drivers/bluetooth/btmrvl_sdio.c b/drivers/bluetooth/btmrvl_sdio.c new file mode 100644 index 000000000..452e23fca --- /dev/null +++ b/drivers/bluetooth/btmrvl_sdio.c @@ -0,0 +1,1614 @@ +/** + * Marvell BT-over-SDIO driver: SDIO interface related functions. + * + * Copyright (C) 2009, Marvell International Ltd. + * + * This software file (the "File") is distributed by Marvell International + * Ltd. under the terms of the GNU General Public License Version 2, June 1991 + * (the "License"). You may use, redistribute and/or modify this File in + * accordance with the terms and conditions of the License, a copy of which + * is available by writing to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA or on the + * worldwide web at http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * + * THE FILE IS DISTRIBUTED AS-IS, WITHOUT WARRANTY OF ANY KIND, AND THE + * IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE + * ARE EXPRESSLY DISCLAIMED. The License provides additional details about + * this warranty disclaimer. + **/ + +#include <linux/firmware.h> +#include <linux/slab.h> + +#include <linux/mmc/sdio_ids.h> +#include <linux/mmc/sdio_func.h> +#include <linux/module.h> +#include <linux/devcoredump.h> + +#include <net/bluetooth/bluetooth.h> +#include <net/bluetooth/hci_core.h> + +#include "btmrvl_drv.h" +#include "btmrvl_sdio.h" + +#define VERSION "1.0" + +static struct memory_type_mapping mem_type_mapping_tbl[] = { + {"ITCM", NULL, 0, 0xF0}, + {"DTCM", NULL, 0, 0xF1}, + {"SQRAM", NULL, 0, 0xF2}, + {"APU", NULL, 0, 0xF3}, + {"CIU", NULL, 0, 0xF4}, + {"ICU", NULL, 0, 0xF5}, + {"MAC", NULL, 0, 0xF6}, + {"EXT7", NULL, 0, 0xF7}, + {"EXT8", NULL, 0, 0xF8}, + {"EXT9", NULL, 0, 0xF9}, + {"EXT10", NULL, 0, 0xFA}, + {"EXT11", NULL, 0, 0xFB}, + {"EXT12", NULL, 0, 0xFC}, + {"EXT13", NULL, 0, 0xFD}, + {"EXTLAST", NULL, 0, 0xFE}, +}; + +/* The btmrvl_sdio_remove() callback function is called + * when user removes this module from kernel space or ejects + * the card from the slot. The driver handles these 2 cases + * differently. + * If the user is removing the module, a MODULE_SHUTDOWN_REQ + * command is sent to firmware and interrupt will be disabled. + * If the card is removed, there is no need to send command + * or disable interrupt. + * + * The variable 'user_rmmod' is used to distinguish these two + * scenarios. This flag is initialized as FALSE in case the card + * is removed, and will be set to TRUE for module removal when + * module_exit function is called. + */ +static u8 user_rmmod; +static u8 sdio_ireg; + +static const struct btmrvl_sdio_card_reg btmrvl_reg_8688 = { + .cfg = 0x03, + .host_int_mask = 0x04, + .host_intstatus = 0x05, + .card_status = 0x20, + .sq_read_base_addr_a0 = 0x10, + .sq_read_base_addr_a1 = 0x11, + .card_fw_status0 = 0x40, + .card_fw_status1 = 0x41, + .card_rx_len = 0x42, + .card_rx_unit = 0x43, + .io_port_0 = 0x00, + .io_port_1 = 0x01, + .io_port_2 = 0x02, + .int_read_to_clear = false, +}; +static const struct btmrvl_sdio_card_reg btmrvl_reg_87xx = { + .cfg = 0x00, + .host_int_mask = 0x02, + .host_intstatus = 0x03, + .card_status = 0x30, + .sq_read_base_addr_a0 = 0x40, + .sq_read_base_addr_a1 = 0x41, + .card_revision = 0x5c, + .card_fw_status0 = 0x60, + .card_fw_status1 = 0x61, + .card_rx_len = 0x62, + .card_rx_unit = 0x63, + .io_port_0 = 0x78, + .io_port_1 = 0x79, + .io_port_2 = 0x7a, + .int_read_to_clear = false, +}; + +static const struct btmrvl_sdio_card_reg btmrvl_reg_8887 = { + .cfg = 0x00, + .host_int_mask = 0x08, + .host_intstatus = 0x0C, + .card_status = 0x5C, + .sq_read_base_addr_a0 = 0x6C, + .sq_read_base_addr_a1 = 0x6D, + .card_revision = 0xC8, + .card_fw_status0 = 0x88, + .card_fw_status1 = 0x89, + .card_rx_len = 0x8A, + .card_rx_unit = 0x8B, + .io_port_0 = 0xE4, + .io_port_1 = 0xE5, + .io_port_2 = 0xE6, + .int_read_to_clear = true, + .host_int_rsr = 0x04, + .card_misc_cfg = 0xD8, +}; + +static const struct btmrvl_sdio_card_reg btmrvl_reg_8897 = { + .cfg = 0x00, + .host_int_mask = 0x02, + .host_intstatus = 0x03, + .card_status = 0x50, + .sq_read_base_addr_a0 = 0x60, + .sq_read_base_addr_a1 = 0x61, + .card_revision = 0xbc, + .card_fw_status0 = 0xc0, + .card_fw_status1 = 0xc1, + .card_rx_len = 0xc2, + .card_rx_unit = 0xc3, + .io_port_0 = 0xd8, + .io_port_1 = 0xd9, + .io_port_2 = 0xda, + .int_read_to_clear = true, + .host_int_rsr = 0x01, + .card_misc_cfg = 0xcc, + .fw_dump_ctrl = 0xe2, + .fw_dump_start = 0xe3, + .fw_dump_end = 0xea, +}; + +static const struct btmrvl_sdio_device btmrvl_sdio_sd8688 = { + .helper = "/*(DEBLOBBED)*/", + .firmware = "/*(DEBLOBBED)*/", + .reg = &btmrvl_reg_8688, + .support_pscan_win_report = false, + .sd_blksz_fw_dl = 64, + .supports_fw_dump = false, +}; + +static const struct btmrvl_sdio_device btmrvl_sdio_sd8787 = { + .helper = NULL, + .firmware = "/*(DEBLOBBED)*/", + .reg = &btmrvl_reg_87xx, + .support_pscan_win_report = false, + .sd_blksz_fw_dl = 256, + .supports_fw_dump = false, +}; + +static const struct btmrvl_sdio_device btmrvl_sdio_sd8797 = { + .helper = NULL, + .firmware = "/*(DEBLOBBED)*/", + .reg = &btmrvl_reg_87xx, + .support_pscan_win_report = false, + .sd_blksz_fw_dl = 256, + .supports_fw_dump = false, +}; + +static const struct btmrvl_sdio_device btmrvl_sdio_sd8887 = { + .helper = NULL, + .firmware = "/*(DEBLOBBED)*/", + .reg = &btmrvl_reg_8887, + .support_pscan_win_report = true, + .sd_blksz_fw_dl = 256, + .supports_fw_dump = false, +}; + +static const struct btmrvl_sdio_device btmrvl_sdio_sd8897 = { + .helper = NULL, + .firmware = "/*(DEBLOBBED)*/", + .reg = &btmrvl_reg_8897, + .support_pscan_win_report = true, + .sd_blksz_fw_dl = 256, + .supports_fw_dump = true, +}; + +static const struct sdio_device_id btmrvl_sdio_ids[] = { + /* Marvell SD8688 Bluetooth device */ + { SDIO_DEVICE(SDIO_VENDOR_ID_MARVELL, 0x9105), + .driver_data = (unsigned long) &btmrvl_sdio_sd8688 }, + /* Marvell SD8787 Bluetooth device */ + { SDIO_DEVICE(SDIO_VENDOR_ID_MARVELL, 0x911A), + .driver_data = (unsigned long) &btmrvl_sdio_sd8787 }, + /* Marvell SD8787 Bluetooth AMP device */ + { SDIO_DEVICE(SDIO_VENDOR_ID_MARVELL, 0x911B), + .driver_data = (unsigned long) &btmrvl_sdio_sd8787 }, + /* Marvell SD8797 Bluetooth device */ + { SDIO_DEVICE(SDIO_VENDOR_ID_MARVELL, 0x912A), + .driver_data = (unsigned long) &btmrvl_sdio_sd8797 }, + /* Marvell SD8887 Bluetooth device */ + { SDIO_DEVICE(SDIO_VENDOR_ID_MARVELL, 0x9136), + .driver_data = (unsigned long)&btmrvl_sdio_sd8887 }, + /* Marvell SD8897 Bluetooth device */ + { SDIO_DEVICE(SDIO_VENDOR_ID_MARVELL, 0x912E), + .driver_data = (unsigned long) &btmrvl_sdio_sd8897 }, + + { } /* Terminating entry */ +}; + +MODULE_DEVICE_TABLE(sdio, btmrvl_sdio_ids); + +static int btmrvl_sdio_get_rx_unit(struct btmrvl_sdio_card *card) +{ + u8 reg; + int ret; + + reg = sdio_readb(card->func, card->reg->card_rx_unit, &ret); + if (!ret) + card->rx_unit = reg; + + return ret; +} + +static int btmrvl_sdio_read_fw_status(struct btmrvl_sdio_card *card, u16 *dat) +{ + u8 fws0, fws1; + int ret; + + *dat = 0; + + fws0 = sdio_readb(card->func, card->reg->card_fw_status0, &ret); + if (ret) + return -EIO; + + fws1 = sdio_readb(card->func, card->reg->card_fw_status1, &ret); + if (ret) + return -EIO; + + *dat = (((u16) fws1) << 8) | fws0; + + return 0; +} + +static int btmrvl_sdio_read_rx_len(struct btmrvl_sdio_card *card, u16 *dat) +{ + u8 reg; + int ret; + + reg = sdio_readb(card->func, card->reg->card_rx_len, &ret); + if (!ret) + *dat = (u16) reg << card->rx_unit; + + return ret; +} + +static int btmrvl_sdio_enable_host_int_mask(struct btmrvl_sdio_card *card, + u8 mask) +{ + int ret; + + sdio_writeb(card->func, mask, card->reg->host_int_mask, &ret); + if (ret) { + BT_ERR("Unable to enable the host interrupt!"); + ret = -EIO; + } + + return ret; +} + +static int btmrvl_sdio_disable_host_int_mask(struct btmrvl_sdio_card *card, + u8 mask) +{ + u8 host_int_mask; + int ret; + + host_int_mask = sdio_readb(card->func, card->reg->host_int_mask, &ret); + if (ret) + return -EIO; + + host_int_mask &= ~mask; + + sdio_writeb(card->func, host_int_mask, card->reg->host_int_mask, &ret); + if (ret < 0) { + BT_ERR("Unable to disable the host interrupt!"); + return -EIO; + } + + return 0; +} + +static int btmrvl_sdio_poll_card_status(struct btmrvl_sdio_card *card, u8 bits) +{ + unsigned int tries; + u8 status; + int ret; + + for (tries = 0; tries < MAX_POLL_TRIES * 1000; tries++) { + status = sdio_readb(card->func, card->reg->card_status, &ret); + if (ret) + goto failed; + if ((status & bits) == bits) + return ret; + + udelay(1); + } + + ret = -ETIMEDOUT; + +failed: + BT_ERR("FAILED! ret=%d", ret); + + return ret; +} + +static int btmrvl_sdio_verify_fw_download(struct btmrvl_sdio_card *card, + int pollnum) +{ + u16 firmwarestat; + int tries, ret; + + /* Wait for firmware to become ready */ + for (tries = 0; tries < pollnum; tries++) { + sdio_claim_host(card->func); + ret = btmrvl_sdio_read_fw_status(card, &firmwarestat); + sdio_release_host(card->func); + if (ret < 0) + continue; + + if (firmwarestat == FIRMWARE_READY) + return 0; + + msleep(10); + } + + return -ETIMEDOUT; +} + +static int btmrvl_sdio_download_helper(struct btmrvl_sdio_card *card) +{ + const struct firmware *fw_helper = NULL; + const u8 *helper = NULL; + int ret; + void *tmphlprbuf = NULL; + int tmphlprbufsz, hlprblknow, helperlen; + u8 *helperbuf; + u32 tx_len; + + ret = reject_firmware(&fw_helper, card->helper, + &card->func->dev); + if ((ret < 0) || !fw_helper) { + BT_ERR("reject_firmware(helper) failed, error code = %d", + ret); + ret = -ENOENT; + goto done; + } + + helper = fw_helper->data; + helperlen = fw_helper->size; + + BT_DBG("Downloading helper image (%d bytes), block size %d bytes", + helperlen, SDIO_BLOCK_SIZE); + + tmphlprbufsz = ALIGN_SZ(BTM_UPLD_SIZE, BTSDIO_DMA_ALIGN); + + tmphlprbuf = kzalloc(tmphlprbufsz, GFP_KERNEL); + if (!tmphlprbuf) { + BT_ERR("Unable to allocate buffer for helper." + " Terminating download"); + ret = -ENOMEM; + goto done; + } + + helperbuf = (u8 *) ALIGN_ADDR(tmphlprbuf, BTSDIO_DMA_ALIGN); + + /* Perform helper data transfer */ + tx_len = (FIRMWARE_TRANSFER_NBLOCK * SDIO_BLOCK_SIZE) + - SDIO_HEADER_LEN; + hlprblknow = 0; + + do { + ret = btmrvl_sdio_poll_card_status(card, + CARD_IO_READY | DN_LD_CARD_RDY); + if (ret < 0) { + BT_ERR("Helper download poll status timeout @ %d", + hlprblknow); + goto done; + } + + /* Check if there is more data? */ + if (hlprblknow >= helperlen) + break; + + if (helperlen - hlprblknow < tx_len) + tx_len = helperlen - hlprblknow; + + /* Little-endian */ + helperbuf[0] = ((tx_len & 0x000000ff) >> 0); + helperbuf[1] = ((tx_len & 0x0000ff00) >> 8); + helperbuf[2] = ((tx_len & 0x00ff0000) >> 16); + helperbuf[3] = ((tx_len & 0xff000000) >> 24); + + memcpy(&helperbuf[SDIO_HEADER_LEN], &helper[hlprblknow], + tx_len); + + /* Now send the data */ + ret = sdio_writesb(card->func, card->ioport, helperbuf, + FIRMWARE_TRANSFER_NBLOCK * SDIO_BLOCK_SIZE); + if (ret < 0) { + BT_ERR("IO error during helper download @ %d", + hlprblknow); + goto done; + } + + hlprblknow += tx_len; + } while (true); + + BT_DBG("Transferring helper image EOF block"); + + memset(helperbuf, 0x0, SDIO_BLOCK_SIZE); + + ret = sdio_writesb(card->func, card->ioport, helperbuf, + SDIO_BLOCK_SIZE); + if (ret < 0) { + BT_ERR("IO error in writing helper image EOF block"); + goto done; + } + + ret = 0; + +done: + kfree(tmphlprbuf); + release_firmware(fw_helper); + return ret; +} + +static int btmrvl_sdio_download_fw_w_helper(struct btmrvl_sdio_card *card) +{ + const struct firmware *fw_firmware = NULL; + const u8 *firmware = NULL; + int firmwarelen, tmpfwbufsz, ret; + unsigned int tries, offset; + u8 base0, base1; + void *tmpfwbuf = NULL; + u8 *fwbuf; + u16 len, blksz_dl = card->sd_blksz_fw_dl; + int txlen = 0, tx_blocks = 0, count = 0; + + ret = reject_firmware(&fw_firmware, card->firmware, + &card->func->dev); + if ((ret < 0) || !fw_firmware) { + BT_ERR("reject_firmware(firmware) failed, error code = %d", + ret); + ret = -ENOENT; + goto done; + } + + firmware = fw_firmware->data; + firmwarelen = fw_firmware->size; + + BT_DBG("Downloading FW image (%d bytes)", firmwarelen); + + tmpfwbufsz = ALIGN_SZ(BTM_UPLD_SIZE, BTSDIO_DMA_ALIGN); + tmpfwbuf = kzalloc(tmpfwbufsz, GFP_KERNEL); + if (!tmpfwbuf) { + BT_ERR("Unable to allocate buffer for firmware." + " Terminating download"); + ret = -ENOMEM; + goto done; + } + + /* Ensure aligned firmware buffer */ + fwbuf = (u8 *) ALIGN_ADDR(tmpfwbuf, BTSDIO_DMA_ALIGN); + + /* Perform firmware data transfer */ + offset = 0; + do { + ret = btmrvl_sdio_poll_card_status(card, + CARD_IO_READY | DN_LD_CARD_RDY); + if (ret < 0) { + BT_ERR("FW download with helper poll status" + " timeout @ %d", offset); + goto done; + } + + /* Check if there is more data ? */ + if (offset >= firmwarelen) + break; + + for (tries = 0; tries < MAX_POLL_TRIES; tries++) { + base0 = sdio_readb(card->func, + card->reg->sq_read_base_addr_a0, &ret); + if (ret) { + BT_ERR("BASE0 register read failed:" + " base0 = 0x%04X(%d)." + " Terminating download", + base0, base0); + ret = -EIO; + goto done; + } + base1 = sdio_readb(card->func, + card->reg->sq_read_base_addr_a1, &ret); + if (ret) { + BT_ERR("BASE1 register read failed:" + " base1 = 0x%04X(%d)." + " Terminating download", + base1, base1); + ret = -EIO; + goto done; + } + + len = (((u16) base1) << 8) | base0; + if (len) + break; + + udelay(10); + } + + if (!len) + break; + else if (len > BTM_UPLD_SIZE) { + BT_ERR("FW download failure @%d, invalid length %d", + offset, len); + ret = -EINVAL; + goto done; + } + + txlen = len; + + if (len & BIT(0)) { + count++; + if (count > MAX_WRITE_IOMEM_RETRY) { + BT_ERR("FW download failure @%d, " + "over max retry count", offset); + ret = -EIO; + goto done; + } + BT_ERR("FW CRC error indicated by the helper: " + "len = 0x%04X, txlen = %d", len, txlen); + len &= ~BIT(0); + /* Set txlen to 0 so as to resend from same offset */ + txlen = 0; + } else { + count = 0; + + /* Last block ? */ + if (firmwarelen - offset < txlen) + txlen = firmwarelen - offset; + + tx_blocks = DIV_ROUND_UP(txlen, blksz_dl); + + memcpy(fwbuf, &firmware[offset], txlen); + } + + ret = sdio_writesb(card->func, card->ioport, fwbuf, + tx_blocks * blksz_dl); + + if (ret < 0) { + BT_ERR("FW download, writesb(%d) failed @%d", + count, offset); + sdio_writeb(card->func, HOST_CMD53_FIN, + card->reg->cfg, &ret); + if (ret) + BT_ERR("writeb failed (CFG)"); + } + + offset += txlen; + } while (true); + + BT_INFO("FW download over, size %d bytes", offset); + + ret = 0; + +done: + kfree(tmpfwbuf); + release_firmware(fw_firmware); + return ret; +} + +static int btmrvl_sdio_card_to_host(struct btmrvl_private *priv) +{ + u16 buf_len = 0; + int ret, num_blocks, blksz; + struct sk_buff *skb = NULL; + u32 type; + u8 *payload = NULL; + struct hci_dev *hdev = priv->btmrvl_dev.hcidev; + struct btmrvl_sdio_card *card = priv->btmrvl_dev.card; + + if (!card || !card->func) { + BT_ERR("card or function is NULL!"); + ret = -EINVAL; + goto exit; + } + + /* Read the length of data to be transferred */ + ret = btmrvl_sdio_read_rx_len(card, &buf_len); + if (ret < 0) { + BT_ERR("read rx_len failed"); + ret = -EIO; + goto exit; + } + + blksz = SDIO_BLOCK_SIZE; + num_blocks = DIV_ROUND_UP(buf_len, blksz); + + if (buf_len <= SDIO_HEADER_LEN + || (num_blocks * blksz) > ALLOC_BUF_SIZE) { + BT_ERR("invalid packet length: %d", buf_len); + ret = -EINVAL; + goto exit; + } + + /* Allocate buffer */ + skb = bt_skb_alloc(num_blocks * blksz + BTSDIO_DMA_ALIGN, GFP_ATOMIC); + if (skb == NULL) { + BT_ERR("No free skb"); + ret = -ENOMEM; + goto exit; + } + + if ((unsigned long) skb->data & (BTSDIO_DMA_ALIGN - 1)) { + skb_put(skb, (unsigned long) skb->data & + (BTSDIO_DMA_ALIGN - 1)); + skb_pull(skb, (unsigned long) skb->data & + (BTSDIO_DMA_ALIGN - 1)); + } + + payload = skb->data; + + ret = sdio_readsb(card->func, payload, card->ioport, + num_blocks * blksz); + if (ret < 0) { + BT_ERR("readsb failed: %d", ret); + ret = -EIO; + goto exit; + } + + /* This is SDIO specific header length: byte[2][1][0], type: byte[3] + * (HCI_COMMAND = 1, ACL_DATA = 2, SCO_DATA = 3, 0xFE = Vendor) + */ + + buf_len = payload[0]; + buf_len |= payload[1] << 8; + buf_len |= payload[2] << 16; + + if (buf_len > blksz * num_blocks) { + BT_ERR("Skip incorrect packet: hdrlen %d buffer %d", + buf_len, blksz * num_blocks); + ret = -EIO; + goto exit; + } + + type = payload[3]; + + switch (type) { + case HCI_ACLDATA_PKT: + case HCI_SCODATA_PKT: + case HCI_EVENT_PKT: + bt_cb(skb)->pkt_type = type; + skb_put(skb, buf_len); + skb_pull(skb, SDIO_HEADER_LEN); + + if (type == HCI_EVENT_PKT) { + if (btmrvl_check_evtpkt(priv, skb)) + hci_recv_frame(hdev, skb); + } else { + hci_recv_frame(hdev, skb); + } + + hdev->stat.byte_rx += buf_len; + break; + + case MRVL_VENDOR_PKT: + bt_cb(skb)->pkt_type = HCI_VENDOR_PKT; + skb_put(skb, buf_len); + skb_pull(skb, SDIO_HEADER_LEN); + + if (btmrvl_process_event(priv, skb)) + hci_recv_frame(hdev, skb); + + hdev->stat.byte_rx += buf_len; + break; + + default: + BT_ERR("Unknown packet type:%d", type); + BT_ERR("hex: %*ph", blksz * num_blocks, payload); + + kfree_skb(skb); + skb = NULL; + break; + } + +exit: + if (ret) { + hdev->stat.err_rx++; + kfree_skb(skb); + } + + return ret; +} + +static int btmrvl_sdio_process_int_status(struct btmrvl_private *priv) +{ + ulong flags; + u8 ireg; + struct btmrvl_sdio_card *card = priv->btmrvl_dev.card; + + spin_lock_irqsave(&priv->driver_lock, flags); + ireg = sdio_ireg; + sdio_ireg = 0; + spin_unlock_irqrestore(&priv->driver_lock, flags); + + sdio_claim_host(card->func); + if (ireg & DN_LD_HOST_INT_STATUS) { + if (priv->btmrvl_dev.tx_dnld_rdy) + BT_DBG("tx_done already received: " + " int_status=0x%x", ireg); + else + priv->btmrvl_dev.tx_dnld_rdy = true; + } + + if (ireg & UP_LD_HOST_INT_STATUS) + btmrvl_sdio_card_to_host(priv); + + sdio_release_host(card->func); + + return 0; +} + +static int btmrvl_sdio_read_to_clear(struct btmrvl_sdio_card *card, u8 *ireg) +{ + struct btmrvl_adapter *adapter = card->priv->adapter; + int ret; + + ret = sdio_readsb(card->func, adapter->hw_regs, 0, SDIO_BLOCK_SIZE); + if (ret) { + BT_ERR("sdio_readsb: read int hw_regs failed: %d", ret); + return ret; + } + + *ireg = adapter->hw_regs[card->reg->host_intstatus]; + BT_DBG("hw_regs[%#x]=%#x", card->reg->host_intstatus, *ireg); + + return 0; +} + +static int btmrvl_sdio_write_to_clear(struct btmrvl_sdio_card *card, u8 *ireg) +{ + int ret; + + *ireg = sdio_readb(card->func, card->reg->host_intstatus, &ret); + if (ret) { + BT_ERR("sdio_readb: read int status failed: %d", ret); + return ret; + } + + if (*ireg) { + /* + * DN_LD_HOST_INT_STATUS and/or UP_LD_HOST_INT_STATUS + * Clear the interrupt status register and re-enable the + * interrupt. + */ + BT_DBG("int_status = 0x%x", *ireg); + + sdio_writeb(card->func, ~(*ireg) & (DN_LD_HOST_INT_STATUS | + UP_LD_HOST_INT_STATUS), + card->reg->host_intstatus, &ret); + if (ret) { + BT_ERR("sdio_writeb: clear int status failed: %d", ret); + return ret; + } + } + + return 0; +} + +static void btmrvl_sdio_interrupt(struct sdio_func *func) +{ + struct btmrvl_private *priv; + struct btmrvl_sdio_card *card; + ulong flags; + u8 ireg = 0; + int ret; + + card = sdio_get_drvdata(func); + if (!card || !card->priv) { + BT_ERR("sbi_interrupt(%p) card or priv is NULL, card=%p", + func, card); + return; + } + + priv = card->priv; + + if (priv->surprise_removed) + return; + + if (card->reg->int_read_to_clear) + ret = btmrvl_sdio_read_to_clear(card, &ireg); + else + ret = btmrvl_sdio_write_to_clear(card, &ireg); + + if (ret) + return; + + spin_lock_irqsave(&priv->driver_lock, flags); + sdio_ireg |= ireg; + spin_unlock_irqrestore(&priv->driver_lock, flags); + + btmrvl_interrupt(priv); +} + +static int btmrvl_sdio_register_dev(struct btmrvl_sdio_card *card) +{ + struct sdio_func *func; + u8 reg; + int ret = 0; + + if (!card || !card->func) { + BT_ERR("Error: card or function is NULL!"); + ret = -EINVAL; + goto failed; + } + + func = card->func; + + sdio_claim_host(func); + + ret = sdio_enable_func(func); + if (ret) { + BT_ERR("sdio_enable_func() failed: ret=%d", ret); + ret = -EIO; + goto release_host; + } + + ret = sdio_claim_irq(func, btmrvl_sdio_interrupt); + if (ret) { + BT_ERR("sdio_claim_irq failed: ret=%d", ret); + ret = -EIO; + goto disable_func; + } + + ret = sdio_set_block_size(card->func, SDIO_BLOCK_SIZE); + if (ret) { + BT_ERR("cannot set SDIO block size"); + ret = -EIO; + goto release_irq; + } + + reg = sdio_readb(func, card->reg->io_port_0, &ret); + if (ret < 0) { + ret = -EIO; + goto release_irq; + } + + card->ioport = reg; + + reg = sdio_readb(func, card->reg->io_port_1, &ret); + if (ret < 0) { + ret = -EIO; + goto release_irq; + } + + card->ioport |= (reg << 8); + + reg = sdio_readb(func, card->reg->io_port_2, &ret); + if (ret < 0) { + ret = -EIO; + goto release_irq; + } + + card->ioport |= (reg << 16); + + BT_DBG("SDIO FUNC%d IO port: 0x%x", func->num, card->ioport); + + if (card->reg->int_read_to_clear) { + reg = sdio_readb(func, card->reg->host_int_rsr, &ret); + if (ret < 0) { + ret = -EIO; + goto release_irq; + } + sdio_writeb(func, reg | 0x3f, card->reg->host_int_rsr, &ret); + if (ret < 0) { + ret = -EIO; + goto release_irq; + } + + reg = sdio_readb(func, card->reg->card_misc_cfg, &ret); + if (ret < 0) { + ret = -EIO; + goto release_irq; + } + sdio_writeb(func, reg | 0x10, card->reg->card_misc_cfg, &ret); + if (ret < 0) { + ret = -EIO; + goto release_irq; + } + } + + sdio_set_drvdata(func, card); + + sdio_release_host(func); + + return 0; + +release_irq: + sdio_release_irq(func); + +disable_func: + sdio_disable_func(func); + +release_host: + sdio_release_host(func); + +failed: + return ret; +} + +static int btmrvl_sdio_unregister_dev(struct btmrvl_sdio_card *card) +{ + if (card && card->func) { + sdio_claim_host(card->func); + sdio_release_irq(card->func); + sdio_disable_func(card->func); + sdio_release_host(card->func); + sdio_set_drvdata(card->func, NULL); + } + + return 0; +} + +static int btmrvl_sdio_enable_host_int(struct btmrvl_sdio_card *card) +{ + int ret; + + if (!card || !card->func) + return -EINVAL; + + sdio_claim_host(card->func); + + ret = btmrvl_sdio_enable_host_int_mask(card, HIM_ENABLE); + + btmrvl_sdio_get_rx_unit(card); + + sdio_release_host(card->func); + + return ret; +} + +static int btmrvl_sdio_disable_host_int(struct btmrvl_sdio_card *card) +{ + int ret; + + if (!card || !card->func) + return -EINVAL; + + sdio_claim_host(card->func); + + ret = btmrvl_sdio_disable_host_int_mask(card, HIM_DISABLE); + + sdio_release_host(card->func); + + return ret; +} + +static int btmrvl_sdio_host_to_card(struct btmrvl_private *priv, + u8 *payload, u16 nb) +{ + struct btmrvl_sdio_card *card = priv->btmrvl_dev.card; + int ret = 0; + int buf_block_len; + int blksz; + int i = 0; + u8 *buf = NULL; + void *tmpbuf = NULL; + int tmpbufsz; + + if (!card || !card->func) { + BT_ERR("card or function is NULL!"); + return -EINVAL; + } + + buf = payload; + if ((unsigned long) payload & (BTSDIO_DMA_ALIGN - 1)) { + tmpbufsz = ALIGN_SZ(nb, BTSDIO_DMA_ALIGN); + tmpbuf = kzalloc(tmpbufsz, GFP_KERNEL); + if (!tmpbuf) + return -ENOMEM; + buf = (u8 *) ALIGN_ADDR(tmpbuf, BTSDIO_DMA_ALIGN); + memcpy(buf, payload, nb); + } + + blksz = SDIO_BLOCK_SIZE; + buf_block_len = DIV_ROUND_UP(nb, blksz); + + sdio_claim_host(card->func); + + do { + /* Transfer data to card */ + ret = sdio_writesb(card->func, card->ioport, buf, + buf_block_len * blksz); + if (ret < 0) { + i++; + BT_ERR("i=%d writesb failed: %d", i, ret); + BT_ERR("hex: %*ph", nb, payload); + ret = -EIO; + if (i > MAX_WRITE_IOMEM_RETRY) + goto exit; + } + } while (ret); + + priv->btmrvl_dev.tx_dnld_rdy = false; + +exit: + sdio_release_host(card->func); + kfree(tmpbuf); + + return ret; +} + +static int btmrvl_sdio_download_fw(struct btmrvl_sdio_card *card) +{ + int ret; + u8 fws0; + int pollnum = MAX_POLL_TRIES; + + if (!card || !card->func) { + BT_ERR("card or function is NULL!"); + return -EINVAL; + } + + if (!btmrvl_sdio_verify_fw_download(card, 1)) { + BT_DBG("Firmware already downloaded!"); + return 0; + } + + sdio_claim_host(card->func); + + /* Check if other function driver is downloading the firmware */ + fws0 = sdio_readb(card->func, card->reg->card_fw_status0, &ret); + if (ret) { + BT_ERR("Failed to read FW downloading status!"); + ret = -EIO; + goto done; + } + if (fws0) { + BT_DBG("BT not the winner (%#x). Skip FW downloading", fws0); + + /* Give other function more time to download the firmware */ + pollnum *= 10; + } else { + if (card->helper) { + ret = btmrvl_sdio_download_helper(card); + if (ret) { + BT_ERR("Failed to download helper!"); + ret = -EIO; + goto done; + } + } + + if (btmrvl_sdio_download_fw_w_helper(card)) { + BT_ERR("Failed to download firmware!"); + ret = -EIO; + goto done; + } + } + + sdio_release_host(card->func); + + /* + * winner or not, with this test the FW synchronizes when the + * module can continue its initialization + */ + if (btmrvl_sdio_verify_fw_download(card, pollnum)) { + BT_ERR("FW failed to be active in time!"); + return -ETIMEDOUT; + } + + return 0; + +done: + sdio_release_host(card->func); + return ret; +} + +static int btmrvl_sdio_wakeup_fw(struct btmrvl_private *priv) +{ + struct btmrvl_sdio_card *card = priv->btmrvl_dev.card; + int ret = 0; + + if (!card || !card->func) { + BT_ERR("card or function is NULL!"); + return -EINVAL; + } + + sdio_claim_host(card->func); + + sdio_writeb(card->func, HOST_POWER_UP, card->reg->cfg, &ret); + + sdio_release_host(card->func); + + BT_DBG("wake up firmware"); + + return ret; +} + +static void btmrvl_sdio_dump_regs(struct btmrvl_private *priv) +{ + struct btmrvl_sdio_card *card = priv->btmrvl_dev.card; + int ret = 0; + unsigned int reg, reg_start, reg_end; + char buf[256], *ptr; + u8 loop, func, data; + int MAX_LOOP = 2; + + btmrvl_sdio_wakeup_fw(priv); + sdio_claim_host(card->func); + + for (loop = 0; loop < MAX_LOOP; loop++) { + memset(buf, 0, sizeof(buf)); + ptr = buf; + + if (loop == 0) { + /* Read the registers of SDIO function0 */ + func = loop; + reg_start = 0; + reg_end = 9; + } else { + func = 2; + reg_start = 0; + reg_end = 0x09; + } + + ptr += sprintf(ptr, "SDIO Func%d (%#x-%#x): ", + func, reg_start, reg_end); + for (reg = reg_start; reg <= reg_end; reg++) { + if (func == 0) + data = sdio_f0_readb(card->func, reg, &ret); + else + data = sdio_readb(card->func, reg, &ret); + + if (!ret) { + ptr += sprintf(ptr, "%02x ", data); + } else { + ptr += sprintf(ptr, "ERR"); + break; + } + } + + BT_INFO("%s", buf); + } + + sdio_release_host(card->func); +} + +/* This function read/write firmware */ +static enum +rdwr_status btmrvl_sdio_rdwr_firmware(struct btmrvl_private *priv, + u8 doneflag) +{ + struct btmrvl_sdio_card *card = priv->btmrvl_dev.card; + int ret, tries; + u8 ctrl_data = 0; + + sdio_writeb(card->func, FW_DUMP_HOST_READY, card->reg->fw_dump_ctrl, + &ret); + + if (ret) { + BT_ERR("SDIO write err"); + return RDWR_STATUS_FAILURE; + } + + for (tries = 0; tries < MAX_POLL_TRIES; tries++) { + ctrl_data = sdio_readb(card->func, card->reg->fw_dump_ctrl, + &ret); + + if (ret) { + BT_ERR("SDIO read err"); + return RDWR_STATUS_FAILURE; + } + + if (ctrl_data == FW_DUMP_DONE) + break; + if (doneflag && ctrl_data == doneflag) + return RDWR_STATUS_DONE; + if (ctrl_data != FW_DUMP_HOST_READY) { + BT_INFO("The ctrl reg was changed, re-try again!"); + sdio_writeb(card->func, FW_DUMP_HOST_READY, + card->reg->fw_dump_ctrl, &ret); + if (ret) { + BT_ERR("SDIO write err"); + return RDWR_STATUS_FAILURE; + } + } + usleep_range(100, 200); + } + + if (ctrl_data == FW_DUMP_HOST_READY) { + BT_ERR("Fail to pull ctrl_data"); + return RDWR_STATUS_FAILURE; + } + + return RDWR_STATUS_SUCCESS; +} + +/* This function dump sdio register and memory data */ +static void btmrvl_sdio_dump_firmware(struct btmrvl_private *priv) +{ + struct btmrvl_sdio_card *card = priv->btmrvl_dev.card; + int ret = 0; + unsigned int reg, reg_start, reg_end; + enum rdwr_status stat; + u8 *dbg_ptr, *end_ptr, *fw_dump_data, *fw_dump_ptr; + u8 dump_num, idx, i, read_reg, doneflag = 0; + u32 memory_size, fw_dump_len = 0; + + /* dump sdio register first */ + btmrvl_sdio_dump_regs(priv); + + if (!card->supports_fw_dump) { + BT_ERR("Firmware dump not supported for this card!"); + return; + } + + for (idx = 0; idx < ARRAY_SIZE(mem_type_mapping_tbl); idx++) { + struct memory_type_mapping *entry = &mem_type_mapping_tbl[idx]; + + if (entry->mem_ptr) { + vfree(entry->mem_ptr); + entry->mem_ptr = NULL; + } + entry->mem_size = 0; + } + + btmrvl_sdio_wakeup_fw(priv); + sdio_claim_host(card->func); + + BT_INFO("== btmrvl firmware dump start =="); + + stat = btmrvl_sdio_rdwr_firmware(priv, doneflag); + if (stat == RDWR_STATUS_FAILURE) + goto done; + + reg = card->reg->fw_dump_start; + /* Read the number of the memories which will dump */ + dump_num = sdio_readb(card->func, reg, &ret); + + if (ret) { + BT_ERR("SDIO read memory length err"); + goto done; + } + + /* Read the length of every memory which will dump */ + for (idx = 0; idx < dump_num; idx++) { + struct memory_type_mapping *entry = &mem_type_mapping_tbl[idx]; + + stat = btmrvl_sdio_rdwr_firmware(priv, doneflag); + if (stat == RDWR_STATUS_FAILURE) + goto done; + + memory_size = 0; + reg = card->reg->fw_dump_start; + for (i = 0; i < 4; i++) { + read_reg = sdio_readb(card->func, reg, &ret); + if (ret) { + BT_ERR("SDIO read err"); + goto done; + } + memory_size |= (read_reg << i*8); + reg++; + } + + if (memory_size == 0) { + BT_INFO("Firmware dump finished!"); + break; + } + + BT_INFO("%s_SIZE=0x%x", entry->mem_name, memory_size); + entry->mem_ptr = vzalloc(memory_size + 1); + entry->mem_size = memory_size; + if (!entry->mem_ptr) { + BT_ERR("Vzalloc %s failed", entry->mem_name); + goto done; + } + + fw_dump_len += (strlen("========Start dump ") + + strlen(entry->mem_name) + + strlen("========\n") + + (memory_size + 1) + + strlen("\n========End dump========\n")); + + dbg_ptr = entry->mem_ptr; + end_ptr = dbg_ptr + memory_size; + + doneflag = entry->done_flag; + BT_INFO("Start %s output, please wait...", + entry->mem_name); + + do { + stat = btmrvl_sdio_rdwr_firmware(priv, doneflag); + if (stat == RDWR_STATUS_FAILURE) + goto done; + + reg_start = card->reg->fw_dump_start; + reg_end = card->reg->fw_dump_end; + for (reg = reg_start; reg <= reg_end; reg++) { + *dbg_ptr = sdio_readb(card->func, reg, &ret); + if (ret) { + BT_ERR("SDIO read err"); + goto done; + } + if (dbg_ptr < end_ptr) + dbg_ptr++; + else + BT_ERR("Allocated buffer not enough"); + } + + if (stat != RDWR_STATUS_DONE) { + continue; + } else { + BT_INFO("%s done: size=0x%tx", + entry->mem_name, + dbg_ptr - entry->mem_ptr); + break; + } + } while (1); + } + + BT_INFO("== btmrvl firmware dump end =="); + +done: + sdio_release_host(card->func); + + if (fw_dump_len == 0) + return; + + fw_dump_data = vzalloc(fw_dump_len+1); + if (!fw_dump_data) { + BT_ERR("Vzalloc fw_dump_data fail!"); + return; + } + fw_dump_ptr = fw_dump_data; + + /* Dump all the memory data into single file, a userspace script will + be used to split all the memory data to multiple files*/ + BT_INFO("== btmrvl firmware dump to /sys/class/devcoredump start"); + for (idx = 0; idx < dump_num; idx++) { + struct memory_type_mapping *entry = &mem_type_mapping_tbl[idx]; + + if (entry->mem_ptr) { + strcpy(fw_dump_ptr, "========Start dump "); + fw_dump_ptr += strlen("========Start dump "); + + strcpy(fw_dump_ptr, entry->mem_name); + fw_dump_ptr += strlen(entry->mem_name); + + strcpy(fw_dump_ptr, "========\n"); + fw_dump_ptr += strlen("========\n"); + + memcpy(fw_dump_ptr, entry->mem_ptr, entry->mem_size); + fw_dump_ptr += entry->mem_size; + + strcpy(fw_dump_ptr, "\n========End dump========\n"); + fw_dump_ptr += strlen("\n========End dump========\n"); + + vfree(mem_type_mapping_tbl[idx].mem_ptr); + mem_type_mapping_tbl[idx].mem_ptr = NULL; + } + } + + /* fw_dump_data will be free in device coredump release function + after 5 min*/ + dev_coredumpv(&priv->btmrvl_dev.hcidev->dev, fw_dump_data, + fw_dump_len, GFP_KERNEL); + BT_INFO("== btmrvl firmware dump to /sys/class/devcoredump end"); +} + +static int btmrvl_sdio_probe(struct sdio_func *func, + const struct sdio_device_id *id) +{ + int ret = 0; + struct btmrvl_private *priv = NULL; + struct btmrvl_sdio_card *card = NULL; + + BT_INFO("vendor=0x%x, device=0x%x, class=%d, fn=%d", + id->vendor, id->device, id->class, func->num); + + card = devm_kzalloc(&func->dev, sizeof(*card), GFP_KERNEL); + if (!card) + return -ENOMEM; + + card->func = func; + + if (id->driver_data) { + struct btmrvl_sdio_device *data = (void *) id->driver_data; + card->helper = data->helper; + card->firmware = data->firmware; + card->reg = data->reg; + card->sd_blksz_fw_dl = data->sd_blksz_fw_dl; + card->support_pscan_win_report = data->support_pscan_win_report; + card->supports_fw_dump = data->supports_fw_dump; + } + + if (btmrvl_sdio_register_dev(card) < 0) { + BT_ERR("Failed to register BT device!"); + return -ENODEV; + } + + /* Disable the interrupts on the card */ + btmrvl_sdio_disable_host_int(card); + + if (btmrvl_sdio_download_fw(card)) { + BT_ERR("Downloading firmware failed!"); + ret = -ENODEV; + goto unreg_dev; + } + + btmrvl_sdio_enable_host_int(card); + + priv = btmrvl_add_card(card); + if (!priv) { + BT_ERR("Initializing card failed!"); + ret = -ENODEV; + goto disable_host_int; + } + + card->priv = priv; + + /* Initialize the interface specific function pointers */ + priv->hw_host_to_card = btmrvl_sdio_host_to_card; + priv->hw_wakeup_firmware = btmrvl_sdio_wakeup_fw; + priv->hw_process_int_status = btmrvl_sdio_process_int_status; + priv->firmware_dump = btmrvl_sdio_dump_firmware; + + if (btmrvl_register_hdev(priv)) { + BT_ERR("Register hdev failed!"); + ret = -ENODEV; + goto disable_host_int; + } + + return 0; + +disable_host_int: + btmrvl_sdio_disable_host_int(card); +unreg_dev: + btmrvl_sdio_unregister_dev(card); + return ret; +} + +static void btmrvl_sdio_remove(struct sdio_func *func) +{ + struct btmrvl_sdio_card *card; + + if (func) { + card = sdio_get_drvdata(func); + if (card) { + /* Send SHUTDOWN command & disable interrupt + * if user removes the module. + */ + if (user_rmmod) { + btmrvl_send_module_cfg_cmd(card->priv, + MODULE_SHUTDOWN_REQ); + btmrvl_sdio_disable_host_int(card); + } + BT_DBG("unregester dev"); + card->priv->surprise_removed = true; + btmrvl_sdio_unregister_dev(card); + btmrvl_remove_card(card->priv); + } + } +} + +static int btmrvl_sdio_suspend(struct device *dev) +{ + struct sdio_func *func = dev_to_sdio_func(dev); + struct btmrvl_sdio_card *card; + struct btmrvl_private *priv; + mmc_pm_flag_t pm_flags; + struct hci_dev *hcidev; + + if (func) { + pm_flags = sdio_get_host_pm_caps(func); + BT_DBG("%s: suspend: PM flags = 0x%x", sdio_func_id(func), + pm_flags); + if (!(pm_flags & MMC_PM_KEEP_POWER)) { + BT_ERR("%s: cannot remain alive while suspended", + sdio_func_id(func)); + return -ENOSYS; + } + card = sdio_get_drvdata(func); + if (!card || !card->priv) { + BT_ERR("card or priv structure is not valid"); + return 0; + } + } else { + BT_ERR("sdio_func is not specified"); + return 0; + } + + priv = card->priv; + hcidev = priv->btmrvl_dev.hcidev; + BT_DBG("%s: SDIO suspend", hcidev->name); + hci_suspend_dev(hcidev); + skb_queue_purge(&priv->adapter->tx_queue); + + if (priv->adapter->hs_state != HS_ACTIVATED) { + if (btmrvl_enable_hs(priv)) { + BT_ERR("HS not actived, suspend failed!"); + return -EBUSY; + } + } + + priv->adapter->is_suspended = true; + + /* We will keep the power when hs enabled successfully */ + if (priv->adapter->hs_state == HS_ACTIVATED) { + BT_DBG("suspend with MMC_PM_KEEP_POWER"); + return sdio_set_host_pm_flags(func, MMC_PM_KEEP_POWER); + } else { + BT_DBG("suspend without MMC_PM_KEEP_POWER"); + return 0; + } +} + +static int btmrvl_sdio_resume(struct device *dev) +{ + struct sdio_func *func = dev_to_sdio_func(dev); + struct btmrvl_sdio_card *card; + struct btmrvl_private *priv; + mmc_pm_flag_t pm_flags; + struct hci_dev *hcidev; + + if (func) { + pm_flags = sdio_get_host_pm_caps(func); + BT_DBG("%s: resume: PM flags = 0x%x", sdio_func_id(func), + pm_flags); + card = sdio_get_drvdata(func); + if (!card || !card->priv) { + BT_ERR("card or priv structure is not valid"); + return 0; + } + } else { + BT_ERR("sdio_func is not specified"); + return 0; + } + priv = card->priv; + + if (!priv->adapter->is_suspended) { + BT_DBG("device already resumed"); + return 0; + } + + priv->hw_wakeup_firmware(priv); + priv->adapter->hs_state = HS_DEACTIVATED; + hcidev = priv->btmrvl_dev.hcidev; + BT_DBG("%s: HS DEACTIVATED in resume!", hcidev->name); + priv->adapter->is_suspended = false; + BT_DBG("%s: SDIO resume", hcidev->name); + hci_resume_dev(hcidev); + + return 0; +} + +static const struct dev_pm_ops btmrvl_sdio_pm_ops = { + .suspend = btmrvl_sdio_suspend, + .resume = btmrvl_sdio_resume, +}; + +static struct sdio_driver bt_mrvl_sdio = { + .name = "btmrvl_sdio", + .id_table = btmrvl_sdio_ids, + .probe = btmrvl_sdio_probe, + .remove = btmrvl_sdio_remove, + .drv = { + .owner = THIS_MODULE, + .pm = &btmrvl_sdio_pm_ops, + } +}; + +static int __init btmrvl_sdio_init_module(void) +{ + if (sdio_register_driver(&bt_mrvl_sdio) != 0) { + BT_ERR("SDIO Driver Registration Failed"); + return -ENODEV; + } + + /* Clear the flag in case user removes the card. */ + user_rmmod = 0; + + return 0; +} + +static void __exit btmrvl_sdio_exit_module(void) +{ + /* Set the flag as user is removing this module. */ + user_rmmod = 1; + + sdio_unregister_driver(&bt_mrvl_sdio); +} + +module_init(btmrvl_sdio_init_module); +module_exit(btmrvl_sdio_exit_module); + +MODULE_AUTHOR("Marvell International Ltd."); +MODULE_DESCRIPTION("Marvell BT-over-SDIO driver ver " VERSION); +MODULE_VERSION(VERSION); +MODULE_LICENSE("GPL v2"); +/*(DEBLOBBED)*/ |