mirror of
https://github.com/torvalds/linux.git
synced 2024-12-29 14:21:47 +00:00
ba6d10ab80
This is mostly update of the usual drivers: qla2xxx, hpsa, lpfc, ufs, mpt3sas, ibmvscsi, megaraid_sas, bnx2fc and hisi_sas as well as the removal of the osst driver (I heard from Willem privately that he would like the driver removed because all his test hardware has failed). Plus number of minor changes, spelling fixes and other trivia. Signed-off-by: James E.J. Bottomley <jejb@linux.ibm.com> -----BEGIN PGP SIGNATURE----- iJwEABMIAEQWIQTnYEDbdso9F2cI+arnQslM7pishQUCXSTl4yYcamFtZXMuYm90 dG9tbGV5QGhhbnNlbnBhcnRuZXJzaGlwLmNvbQAKCRDnQslM7pishdcxAQDCJVbd fPUX76/V1ldupunF97+3DTharxxbst+VnkOnCwD8D4c0KFFFOI9+F36cnMGCPegE fjy17dQLvsJ4GsidHy8= =aS5B -----END PGP SIGNATURE----- Merge tag 'scsi-misc' of git://git.kernel.org/pub/scm/linux/kernel/git/jejb/scsi Pull SCSI updates from James Bottomley: "This is mostly update of the usual drivers: qla2xxx, hpsa, lpfc, ufs, mpt3sas, ibmvscsi, megaraid_sas, bnx2fc and hisi_sas as well as the removal of the osst driver (I heard from Willem privately that he would like the driver removed because all his test hardware has failed). Plus number of minor changes, spelling fixes and other trivia. The big merge conflict this time around is the SPDX licence tags. Following discussion on linux-next, we believe our version to be more accurate than the one in the tree, so the resolution is to take our version for all the SPDX conflicts" Note on the SPDX license tag conversion conflicts: the SCSI tree had done its own SPDX conversion, which in some cases conflicted with the treewide ones done by Thomas & co. In almost all cases, the conflicts were purely syntactic: the SCSI tree used the old-style SPDX tags ("GPL-2.0" and "GPL-2.0+") while the treewide conversion had used the new-style ones ("GPL-2.0-only" and "GPL-2.0-or-later"). In these cases I picked the new-style one. In a few cases, the SPDX conversion was actually different, though. As explained by James above, and in more detail in a pre-pull-request thread: "The other problem is actually substantive: In the libsas code Luben Tuikov originally specified gpl 2.0 only by dint of stating: * This file is licensed under GPLv2. In all the libsas files, but then muddied the water by quoting GPLv2 verbatim (which includes the or later than language). So for these files Christoph did the conversion to v2 only SPDX tags and Thomas converted to v2 or later tags" So in those cases, where the spdx tag substantially mattered, I took the SCSI tree conversion of it, but then also took the opportunity to turn the old-style "GPL-2.0" into a new-style "GPL-2.0-only" tag. Similarly, when there were whitespace differences or other differences to the comments around the copyright notices, I took the version from the SCSI tree as being the more specific conversion. Finally, in the spdx conversions that had no conflicts (because the treewide ones hadn't been done for those files), I just took the SCSI tree version as-is, even if it was old-style. The old-style conversions are perfectly valid, even if the "-only" and "-or-later" versions are perhaps more descriptive. * tag 'scsi-misc' of git://git.kernel.org/pub/scm/linux/kernel/git/jejb/scsi: (185 commits) scsi: qla2xxx: move IO flush to the front of NVME rport unregistration scsi: qla2xxx: Fix NVME cmd and LS cmd timeout race condition scsi: qla2xxx: on session delete, return nvme cmd scsi: qla2xxx: Fix kernel crash after disconnecting NVMe devices scsi: megaraid_sas: Update driver version to 07.710.06.00-rc1 scsi: megaraid_sas: Introduce various Aero performance modes scsi: megaraid_sas: Use high IOPS queues based on IO workload scsi: megaraid_sas: Set affinity for high IOPS reply queues scsi: megaraid_sas: Enable coalescing for high IOPS queues scsi: megaraid_sas: Add support for High IOPS queues scsi: megaraid_sas: Add support for MPI toolbox commands scsi: megaraid_sas: Offload Aero RAID5/6 division calculations to driver scsi: megaraid_sas: RAID1 PCI bandwidth limit algorithm is applicable for only Ventura scsi: megaraid_sas: megaraid_sas: Add check for count returned by HOST_DEVICE_LIST DCMD scsi: megaraid_sas: Handle sequence JBOD map failure at driver level scsi: megaraid_sas: Don't send FPIO to RL Bypass queue scsi: megaraid_sas: In probe context, retry IOC INIT once if firmware is in fault scsi: megaraid_sas: Release Mutex lock before OCR in case of DCMD timeout scsi: megaraid_sas: Call disable_irq from process IRQ poll scsi: megaraid_sas: Remove few debug counters from IO path ...
993 lines
27 KiB
C
993 lines
27 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Driver for Western Digital WD7193, WD7197 and WD7296 SCSI cards
|
|
* Copyright 2013 Ondrej Zary
|
|
*
|
|
* Original driver by
|
|
* Aaron Dewell <dewell@woods.net>
|
|
* Gaerti <Juergen.Gaertner@mbox.si.uni-hannover.de>
|
|
*
|
|
* HW documentation available in book:
|
|
*
|
|
* SPIDER Command Protocol
|
|
* by Chandru M. Sippy
|
|
* SCSI Storage Products (MCP)
|
|
* Western Digital Corporation
|
|
* 09-15-95
|
|
*
|
|
* http://web.archive.org/web/20070717175254/http://sun1.rrzn.uni-hannover.de/gaertner.juergen/wd719x/Linux/Docu/Spider/
|
|
*/
|
|
|
|
/*
|
|
* Driver workflow:
|
|
* 1. SCSI command is transformed to SCB (Spider Control Block) by the
|
|
* queuecommand function.
|
|
* 2. The address of the SCB is stored in a list to be able to access it, if
|
|
* something goes wrong.
|
|
* 3. The address of the SCB is written to the Controller, which loads the SCB
|
|
* via BM-DMA and processes it.
|
|
* 4. After it has finished, it generates an interrupt, and sets registers.
|
|
*
|
|
* flaws:
|
|
* - abort/reset functions
|
|
*
|
|
* ToDo:
|
|
* - tagged queueing
|
|
*/
|
|
|
|
#include <linux/interrupt.h>
|
|
#include <linux/module.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/pci.h>
|
|
#include <linux/firmware.h>
|
|
#include <linux/eeprom_93cx6.h>
|
|
#include <scsi/scsi_cmnd.h>
|
|
#include <scsi/scsi_device.h>
|
|
#include <scsi/scsi_host.h>
|
|
#include "wd719x.h"
|
|
|
|
/* low-level register access */
|
|
static inline u8 wd719x_readb(struct wd719x *wd, u8 reg)
|
|
{
|
|
return ioread8(wd->base + reg);
|
|
}
|
|
|
|
static inline u32 wd719x_readl(struct wd719x *wd, u8 reg)
|
|
{
|
|
return ioread32(wd->base + reg);
|
|
}
|
|
|
|
static inline void wd719x_writeb(struct wd719x *wd, u8 reg, u8 val)
|
|
{
|
|
iowrite8(val, wd->base + reg);
|
|
}
|
|
|
|
static inline void wd719x_writew(struct wd719x *wd, u8 reg, u16 val)
|
|
{
|
|
iowrite16(val, wd->base + reg);
|
|
}
|
|
|
|
static inline void wd719x_writel(struct wd719x *wd, u8 reg, u32 val)
|
|
{
|
|
iowrite32(val, wd->base + reg);
|
|
}
|
|
|
|
/* wait until the command register is ready */
|
|
static inline int wd719x_wait_ready(struct wd719x *wd)
|
|
{
|
|
int i = 0;
|
|
|
|
do {
|
|
if (wd719x_readb(wd, WD719X_AMR_COMMAND) == WD719X_CMD_READY)
|
|
return 0;
|
|
udelay(1);
|
|
} while (i++ < WD719X_WAIT_FOR_CMD_READY);
|
|
|
|
dev_err(&wd->pdev->dev, "command register is not ready: 0x%02x\n",
|
|
wd719x_readb(wd, WD719X_AMR_COMMAND));
|
|
|
|
return -ETIMEDOUT;
|
|
}
|
|
|
|
/* poll interrupt status register until command finishes */
|
|
static inline int wd719x_wait_done(struct wd719x *wd, int timeout)
|
|
{
|
|
u8 status;
|
|
|
|
while (timeout > 0) {
|
|
status = wd719x_readb(wd, WD719X_AMR_INT_STATUS);
|
|
if (status)
|
|
break;
|
|
timeout--;
|
|
udelay(1);
|
|
}
|
|
|
|
if (timeout <= 0) {
|
|
dev_err(&wd->pdev->dev, "direct command timed out\n");
|
|
return -ETIMEDOUT;
|
|
}
|
|
|
|
if (status != WD719X_INT_NOERRORS) {
|
|
u8 sue = wd719x_readb(wd, WD719X_AMR_SCB_ERROR);
|
|
/* we get this after wd719x_dev_reset, it's not an error */
|
|
if (sue == WD719X_SUE_TERM)
|
|
return 0;
|
|
/* we get this after wd719x_bus_reset, it's not an error */
|
|
if (sue == WD719X_SUE_RESET)
|
|
return 0;
|
|
dev_err(&wd->pdev->dev, "direct command failed, status 0x%02x, SUE 0x%02x\n",
|
|
status, sue);
|
|
return -EIO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int wd719x_direct_cmd(struct wd719x *wd, u8 opcode, u8 dev, u8 lun,
|
|
u8 tag, dma_addr_t data, int timeout)
|
|
{
|
|
int ret = 0;
|
|
|
|
/* clear interrupt status register (allow command register to clear) */
|
|
wd719x_writeb(wd, WD719X_AMR_INT_STATUS, WD719X_INT_NONE);
|
|
|
|
/* Wait for the Command register to become free */
|
|
if (wd719x_wait_ready(wd))
|
|
return -ETIMEDOUT;
|
|
|
|
/* disable interrupts except for RESET/ABORT (it breaks them) */
|
|
if (opcode != WD719X_CMD_BUSRESET && opcode != WD719X_CMD_ABORT &&
|
|
opcode != WD719X_CMD_ABORT_TAG && opcode != WD719X_CMD_RESET)
|
|
dev |= WD719X_DISABLE_INT;
|
|
wd719x_writeb(wd, WD719X_AMR_CMD_PARAM, dev);
|
|
wd719x_writeb(wd, WD719X_AMR_CMD_PARAM_2, lun);
|
|
wd719x_writeb(wd, WD719X_AMR_CMD_PARAM_3, tag);
|
|
if (data)
|
|
wd719x_writel(wd, WD719X_AMR_SCB_IN, data);
|
|
|
|
/* clear interrupt status register again */
|
|
wd719x_writeb(wd, WD719X_AMR_INT_STATUS, WD719X_INT_NONE);
|
|
|
|
/* Now, write the command */
|
|
wd719x_writeb(wd, WD719X_AMR_COMMAND, opcode);
|
|
|
|
if (timeout) /* wait for the command to complete */
|
|
ret = wd719x_wait_done(wd, timeout);
|
|
|
|
/* clear interrupt status register (clean up) */
|
|
if (opcode != WD719X_CMD_READ_FIRMVER)
|
|
wd719x_writeb(wd, WD719X_AMR_INT_STATUS, WD719X_INT_NONE);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void wd719x_destroy(struct wd719x *wd)
|
|
{
|
|
/* stop the RISC */
|
|
if (wd719x_direct_cmd(wd, WD719X_CMD_SLEEP, 0, 0, 0, 0,
|
|
WD719X_WAIT_FOR_RISC))
|
|
dev_warn(&wd->pdev->dev, "RISC sleep command failed\n");
|
|
/* disable RISC */
|
|
wd719x_writeb(wd, WD719X_PCI_MODE_SELECT, 0);
|
|
|
|
WARN_ON_ONCE(!list_empty(&wd->active_scbs));
|
|
|
|
/* free internal buffers */
|
|
dma_free_coherent(&wd->pdev->dev, wd->fw_size, wd->fw_virt,
|
|
wd->fw_phys);
|
|
wd->fw_virt = NULL;
|
|
dma_free_coherent(&wd->pdev->dev, WD719X_HASH_TABLE_SIZE, wd->hash_virt,
|
|
wd->hash_phys);
|
|
wd->hash_virt = NULL;
|
|
dma_free_coherent(&wd->pdev->dev, sizeof(struct wd719x_host_param),
|
|
wd->params, wd->params_phys);
|
|
wd->params = NULL;
|
|
free_irq(wd->pdev->irq, wd);
|
|
}
|
|
|
|
/* finish a SCSI command, unmap buffers */
|
|
static void wd719x_finish_cmd(struct wd719x_scb *scb, int result)
|
|
{
|
|
struct scsi_cmnd *cmd = scb->cmd;
|
|
struct wd719x *wd = shost_priv(cmd->device->host);
|
|
|
|
list_del(&scb->list);
|
|
|
|
dma_unmap_single(&wd->pdev->dev, scb->phys,
|
|
sizeof(struct wd719x_scb), DMA_BIDIRECTIONAL);
|
|
scsi_dma_unmap(cmd);
|
|
dma_unmap_single(&wd->pdev->dev, cmd->SCp.dma_handle,
|
|
SCSI_SENSE_BUFFERSIZE, DMA_FROM_DEVICE);
|
|
|
|
cmd->result = result << 16;
|
|
cmd->scsi_done(cmd);
|
|
}
|
|
|
|
/* Build a SCB and send it to the card */
|
|
static int wd719x_queuecommand(struct Scsi_Host *sh, struct scsi_cmnd *cmd)
|
|
{
|
|
int i, count_sg;
|
|
unsigned long flags;
|
|
struct wd719x_scb *scb = scsi_cmd_priv(cmd);
|
|
struct wd719x *wd = shost_priv(sh);
|
|
|
|
scb->cmd = cmd;
|
|
|
|
scb->CDB_tag = 0; /* Tagged queueing not supported yet */
|
|
scb->devid = cmd->device->id;
|
|
scb->lun = cmd->device->lun;
|
|
|
|
/* copy the command */
|
|
memcpy(scb->CDB, cmd->cmnd, cmd->cmd_len);
|
|
|
|
/* map SCB */
|
|
scb->phys = dma_map_single(&wd->pdev->dev, scb, sizeof(*scb),
|
|
DMA_BIDIRECTIONAL);
|
|
|
|
if (dma_mapping_error(&wd->pdev->dev, scb->phys))
|
|
goto out_error;
|
|
|
|
/* map sense buffer */
|
|
scb->sense_buf_length = SCSI_SENSE_BUFFERSIZE;
|
|
cmd->SCp.dma_handle = dma_map_single(&wd->pdev->dev, cmd->sense_buffer,
|
|
SCSI_SENSE_BUFFERSIZE, DMA_FROM_DEVICE);
|
|
if (dma_mapping_error(&wd->pdev->dev, cmd->SCp.dma_handle))
|
|
goto out_unmap_scb;
|
|
scb->sense_buf = cpu_to_le32(cmd->SCp.dma_handle);
|
|
|
|
/* request autosense */
|
|
scb->SCB_options |= WD719X_SCB_FLAGS_AUTO_REQUEST_SENSE;
|
|
|
|
/* check direction */
|
|
if (cmd->sc_data_direction == DMA_TO_DEVICE)
|
|
scb->SCB_options |= WD719X_SCB_FLAGS_CHECK_DIRECTION
|
|
| WD719X_SCB_FLAGS_PCI_TO_SCSI;
|
|
else if (cmd->sc_data_direction == DMA_FROM_DEVICE)
|
|
scb->SCB_options |= WD719X_SCB_FLAGS_CHECK_DIRECTION;
|
|
|
|
/* Scather/gather */
|
|
count_sg = scsi_dma_map(cmd);
|
|
if (count_sg < 0)
|
|
goto out_unmap_sense;
|
|
BUG_ON(count_sg > WD719X_SG);
|
|
|
|
if (count_sg) {
|
|
struct scatterlist *sg;
|
|
|
|
scb->data_length = cpu_to_le32(count_sg *
|
|
sizeof(struct wd719x_sglist));
|
|
scb->data_p = cpu_to_le32(scb->phys +
|
|
offsetof(struct wd719x_scb, sg_list));
|
|
|
|
scsi_for_each_sg(cmd, sg, count_sg, i) {
|
|
scb->sg_list[i].ptr = cpu_to_le32(sg_dma_address(sg));
|
|
scb->sg_list[i].length = cpu_to_le32(sg_dma_len(sg));
|
|
}
|
|
scb->SCB_options |= WD719X_SCB_FLAGS_DO_SCATTER_GATHER;
|
|
} else { /* zero length */
|
|
scb->data_length = 0;
|
|
scb->data_p = 0;
|
|
}
|
|
|
|
spin_lock_irqsave(wd->sh->host_lock, flags);
|
|
|
|
/* check if the Command register is free */
|
|
if (wd719x_readb(wd, WD719X_AMR_COMMAND) != WD719X_CMD_READY) {
|
|
spin_unlock_irqrestore(wd->sh->host_lock, flags);
|
|
return SCSI_MLQUEUE_HOST_BUSY;
|
|
}
|
|
|
|
list_add(&scb->list, &wd->active_scbs);
|
|
|
|
/* write pointer to the AMR */
|
|
wd719x_writel(wd, WD719X_AMR_SCB_IN, scb->phys);
|
|
/* send SCB opcode */
|
|
wd719x_writeb(wd, WD719X_AMR_COMMAND, WD719X_CMD_PROCESS_SCB);
|
|
|
|
spin_unlock_irqrestore(wd->sh->host_lock, flags);
|
|
return 0;
|
|
|
|
out_unmap_sense:
|
|
dma_unmap_single(&wd->pdev->dev, cmd->SCp.dma_handle,
|
|
SCSI_SENSE_BUFFERSIZE, DMA_FROM_DEVICE);
|
|
out_unmap_scb:
|
|
dma_unmap_single(&wd->pdev->dev, scb->phys, sizeof(*scb),
|
|
DMA_BIDIRECTIONAL);
|
|
out_error:
|
|
cmd->result = DID_ERROR << 16;
|
|
cmd->scsi_done(cmd);
|
|
return 0;
|
|
}
|
|
|
|
static int wd719x_chip_init(struct wd719x *wd)
|
|
{
|
|
int i, ret;
|
|
u32 risc_init[3];
|
|
const struct firmware *fw_wcs, *fw_risc;
|
|
const char fwname_wcs[] = "wd719x-wcs.bin";
|
|
const char fwname_risc[] = "wd719x-risc.bin";
|
|
|
|
memset(wd->hash_virt, 0, WD719X_HASH_TABLE_SIZE);
|
|
|
|
/* WCS (sequencer) firmware */
|
|
ret = request_firmware(&fw_wcs, fwname_wcs, &wd->pdev->dev);
|
|
if (ret) {
|
|
dev_err(&wd->pdev->dev, "Unable to load firmware %s: %d\n",
|
|
fwname_wcs, ret);
|
|
return ret;
|
|
}
|
|
/* RISC firmware */
|
|
ret = request_firmware(&fw_risc, fwname_risc, &wd->pdev->dev);
|
|
if (ret) {
|
|
dev_err(&wd->pdev->dev, "Unable to load firmware %s: %d\n",
|
|
fwname_risc, ret);
|
|
release_firmware(fw_wcs);
|
|
return ret;
|
|
}
|
|
wd->fw_size = ALIGN(fw_wcs->size, 4) + fw_risc->size;
|
|
|
|
if (!wd->fw_virt)
|
|
wd->fw_virt = dma_alloc_coherent(&wd->pdev->dev, wd->fw_size,
|
|
&wd->fw_phys, GFP_KERNEL);
|
|
if (!wd->fw_virt) {
|
|
ret = -ENOMEM;
|
|
goto wd719x_init_end;
|
|
}
|
|
|
|
/* make a fresh copy of WCS and RISC code */
|
|
memcpy(wd->fw_virt, fw_wcs->data, fw_wcs->size);
|
|
memcpy(wd->fw_virt + ALIGN(fw_wcs->size, 4), fw_risc->data,
|
|
fw_risc->size);
|
|
|
|
/* Reset the Spider Chip and adapter itself */
|
|
wd719x_writeb(wd, WD719X_PCI_PORT_RESET, WD719X_PCI_RESET);
|
|
udelay(WD719X_WAIT_FOR_RISC);
|
|
/* Clear PIO mode bits set by BIOS */
|
|
wd719x_writeb(wd, WD719X_AMR_CMD_PARAM, 0);
|
|
/* ensure RISC is not running */
|
|
wd719x_writeb(wd, WD719X_PCI_MODE_SELECT, 0);
|
|
/* ensure command port is ready */
|
|
wd719x_writeb(wd, WD719X_AMR_COMMAND, 0);
|
|
if (wd719x_wait_ready(wd)) {
|
|
ret = -ETIMEDOUT;
|
|
goto wd719x_init_end;
|
|
}
|
|
|
|
/* Transfer the first 2K words of RISC code to kick start the uP */
|
|
risc_init[0] = wd->fw_phys; /* WCS FW */
|
|
risc_init[1] = wd->fw_phys + ALIGN(fw_wcs->size, 4); /* RISC FW */
|
|
risc_init[2] = wd->hash_phys; /* hash table */
|
|
|
|
/* clear DMA status */
|
|
wd719x_writeb(wd, WD719X_PCI_CHANNEL2_3STATUS, 0);
|
|
|
|
/* address to read firmware from */
|
|
wd719x_writel(wd, WD719X_PCI_EXTERNAL_ADDR, risc_init[1]);
|
|
/* base address to write firmware to (on card) */
|
|
wd719x_writew(wd, WD719X_PCI_INTERNAL_ADDR, WD719X_PRAM_BASE_ADDR);
|
|
/* size: first 2K words */
|
|
wd719x_writew(wd, WD719X_PCI_DMA_TRANSFER_SIZE, 2048 * 2);
|
|
/* start DMA */
|
|
wd719x_writeb(wd, WD719X_PCI_CHANNEL2_3CMD, WD719X_START_CHANNEL2_3DMA);
|
|
|
|
/* wait for DMA to complete */
|
|
i = WD719X_WAIT_FOR_RISC;
|
|
while (i-- > 0) {
|
|
u8 status = wd719x_readb(wd, WD719X_PCI_CHANNEL2_3STATUS);
|
|
if (status == WD719X_START_CHANNEL2_3DONE)
|
|
break;
|
|
if (status == WD719X_START_CHANNEL2_3ABORT) {
|
|
dev_warn(&wd->pdev->dev, "RISC bootstrap failed: DMA aborted\n");
|
|
ret = -EIO;
|
|
goto wd719x_init_end;
|
|
}
|
|
udelay(1);
|
|
}
|
|
if (i < 1) {
|
|
dev_warn(&wd->pdev->dev, "RISC bootstrap failed: DMA timeout\n");
|
|
ret = -ETIMEDOUT;
|
|
goto wd719x_init_end;
|
|
}
|
|
|
|
/* firmware is loaded, now initialize and wake up the RISC */
|
|
/* write RISC initialization long words to Spider */
|
|
wd719x_writel(wd, WD719X_AMR_SCB_IN, risc_init[0]);
|
|
wd719x_writel(wd, WD719X_AMR_SCB_IN + 4, risc_init[1]);
|
|
wd719x_writel(wd, WD719X_AMR_SCB_IN + 8, risc_init[2]);
|
|
|
|
/* disable interrupts during initialization of RISC */
|
|
wd719x_writeb(wd, WD719X_AMR_CMD_PARAM, WD719X_DISABLE_INT);
|
|
|
|
/* issue INITIALIZE RISC comand */
|
|
wd719x_writeb(wd, WD719X_AMR_COMMAND, WD719X_CMD_INIT_RISC);
|
|
/* enable advanced mode (wake up RISC) */
|
|
wd719x_writeb(wd, WD719X_PCI_MODE_SELECT, WD719X_ENABLE_ADVANCE_MODE);
|
|
udelay(WD719X_WAIT_FOR_RISC);
|
|
|
|
ret = wd719x_wait_done(wd, WD719X_WAIT_FOR_RISC);
|
|
/* clear interrupt status register */
|
|
wd719x_writeb(wd, WD719X_AMR_INT_STATUS, WD719X_INT_NONE);
|
|
if (ret) {
|
|
dev_warn(&wd->pdev->dev, "Unable to initialize RISC\n");
|
|
goto wd719x_init_end;
|
|
}
|
|
/* RISC is up and running */
|
|
|
|
/* Read FW version from RISC */
|
|
ret = wd719x_direct_cmd(wd, WD719X_CMD_READ_FIRMVER, 0, 0, 0, 0,
|
|
WD719X_WAIT_FOR_RISC);
|
|
if (ret) {
|
|
dev_warn(&wd->pdev->dev, "Unable to read firmware version\n");
|
|
goto wd719x_init_end;
|
|
}
|
|
dev_info(&wd->pdev->dev, "RISC initialized with firmware version %.2x.%.2x\n",
|
|
wd719x_readb(wd, WD719X_AMR_SCB_OUT + 1),
|
|
wd719x_readb(wd, WD719X_AMR_SCB_OUT));
|
|
|
|
/* RESET SCSI bus */
|
|
ret = wd719x_direct_cmd(wd, WD719X_CMD_BUSRESET, 0, 0, 0, 0,
|
|
WD719X_WAIT_FOR_SCSI_RESET);
|
|
if (ret) {
|
|
dev_warn(&wd->pdev->dev, "SCSI bus reset failed\n");
|
|
goto wd719x_init_end;
|
|
}
|
|
|
|
/* use HostParameter structure to set Spider's Host Parameter Block */
|
|
ret = wd719x_direct_cmd(wd, WD719X_CMD_SET_PARAM, 0,
|
|
sizeof(struct wd719x_host_param), 0,
|
|
wd->params_phys, WD719X_WAIT_FOR_RISC);
|
|
if (ret) {
|
|
dev_warn(&wd->pdev->dev, "Failed to set HOST PARAMETERS\n");
|
|
goto wd719x_init_end;
|
|
}
|
|
|
|
/* initiate SCAM (does nothing if disabled in BIOS) */
|
|
/* bug?: we should pass a mask of static IDs which we don't have */
|
|
ret = wd719x_direct_cmd(wd, WD719X_CMD_INIT_SCAM, 0, 0, 0, 0,
|
|
WD719X_WAIT_FOR_SCSI_RESET);
|
|
if (ret) {
|
|
dev_warn(&wd->pdev->dev, "SCAM initialization failed\n");
|
|
goto wd719x_init_end;
|
|
}
|
|
|
|
/* clear AMR_BIOS_SHARE_INT register */
|
|
wd719x_writeb(wd, WD719X_AMR_BIOS_SHARE_INT, 0);
|
|
|
|
wd719x_init_end:
|
|
release_firmware(fw_wcs);
|
|
release_firmware(fw_risc);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int wd719x_abort(struct scsi_cmnd *cmd)
|
|
{
|
|
int action, result;
|
|
unsigned long flags;
|
|
struct wd719x_scb *scb = scsi_cmd_priv(cmd);
|
|
struct wd719x *wd = shost_priv(cmd->device->host);
|
|
|
|
dev_info(&wd->pdev->dev, "abort command, tag: %x\n", cmd->tag);
|
|
|
|
action = /*cmd->tag ? WD719X_CMD_ABORT_TAG : */WD719X_CMD_ABORT;
|
|
|
|
spin_lock_irqsave(wd->sh->host_lock, flags);
|
|
result = wd719x_direct_cmd(wd, action, cmd->device->id,
|
|
cmd->device->lun, cmd->tag, scb->phys, 0);
|
|
wd719x_finish_cmd(scb, DID_ABORT);
|
|
spin_unlock_irqrestore(wd->sh->host_lock, flags);
|
|
if (result)
|
|
return FAILED;
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
static int wd719x_reset(struct scsi_cmnd *cmd, u8 opcode, u8 device)
|
|
{
|
|
int result;
|
|
unsigned long flags;
|
|
struct wd719x *wd = shost_priv(cmd->device->host);
|
|
struct wd719x_scb *scb, *tmp;
|
|
|
|
dev_info(&wd->pdev->dev, "%s reset requested\n",
|
|
(opcode == WD719X_CMD_BUSRESET) ? "bus" : "device");
|
|
|
|
spin_lock_irqsave(wd->sh->host_lock, flags);
|
|
result = wd719x_direct_cmd(wd, opcode, device, 0, 0, 0,
|
|
WD719X_WAIT_FOR_SCSI_RESET);
|
|
/* flush all SCBs (or all for a device if dev_reset) */
|
|
list_for_each_entry_safe(scb, tmp, &wd->active_scbs, list) {
|
|
if (opcode == WD719X_CMD_BUSRESET ||
|
|
scb->cmd->device->id == device)
|
|
wd719x_finish_cmd(scb, DID_RESET);
|
|
}
|
|
spin_unlock_irqrestore(wd->sh->host_lock, flags);
|
|
if (result)
|
|
return FAILED;
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
static int wd719x_dev_reset(struct scsi_cmnd *cmd)
|
|
{
|
|
return wd719x_reset(cmd, WD719X_CMD_RESET, cmd->device->id);
|
|
}
|
|
|
|
static int wd719x_bus_reset(struct scsi_cmnd *cmd)
|
|
{
|
|
return wd719x_reset(cmd, WD719X_CMD_BUSRESET, 0);
|
|
}
|
|
|
|
static int wd719x_host_reset(struct scsi_cmnd *cmd)
|
|
{
|
|
struct wd719x *wd = shost_priv(cmd->device->host);
|
|
struct wd719x_scb *scb, *tmp;
|
|
unsigned long flags;
|
|
|
|
dev_info(&wd->pdev->dev, "host reset requested\n");
|
|
spin_lock_irqsave(wd->sh->host_lock, flags);
|
|
/* stop the RISC */
|
|
if (wd719x_direct_cmd(wd, WD719X_CMD_SLEEP, 0, 0, 0, 0,
|
|
WD719X_WAIT_FOR_RISC))
|
|
dev_warn(&wd->pdev->dev, "RISC sleep command failed\n");
|
|
/* disable RISC */
|
|
wd719x_writeb(wd, WD719X_PCI_MODE_SELECT, 0);
|
|
|
|
/* flush all SCBs */
|
|
list_for_each_entry_safe(scb, tmp, &wd->active_scbs, list)
|
|
wd719x_finish_cmd(scb, DID_RESET);
|
|
spin_unlock_irqrestore(wd->sh->host_lock, flags);
|
|
|
|
/* Try to reinit the RISC */
|
|
return wd719x_chip_init(wd) == 0 ? SUCCESS : FAILED;
|
|
}
|
|
|
|
static int wd719x_biosparam(struct scsi_device *sdev, struct block_device *bdev,
|
|
sector_t capacity, int geom[])
|
|
{
|
|
if (capacity >= 0x200000) {
|
|
geom[0] = 255; /* heads */
|
|
geom[1] = 63; /* sectors */
|
|
} else {
|
|
geom[0] = 64; /* heads */
|
|
geom[1] = 32; /* sectors */
|
|
}
|
|
geom[2] = sector_div(capacity, geom[0] * geom[1]); /* cylinders */
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* process a SCB-completion interrupt */
|
|
static inline void wd719x_interrupt_SCB(struct wd719x *wd,
|
|
union wd719x_regs regs,
|
|
struct wd719x_scb *scb)
|
|
{
|
|
int result;
|
|
|
|
/* now have to find result from card */
|
|
switch (regs.bytes.SUE) {
|
|
case WD719X_SUE_NOERRORS:
|
|
result = DID_OK;
|
|
break;
|
|
case WD719X_SUE_REJECTED:
|
|
dev_err(&wd->pdev->dev, "command rejected\n");
|
|
result = DID_ERROR;
|
|
break;
|
|
case WD719X_SUE_SCBQFULL:
|
|
dev_err(&wd->pdev->dev, "SCB queue is full\n");
|
|
result = DID_ERROR;
|
|
break;
|
|
case WD719X_SUE_TERM:
|
|
dev_dbg(&wd->pdev->dev, "SCB terminated by direct command\n");
|
|
result = DID_ABORT; /* or DID_RESET? */
|
|
break;
|
|
case WD719X_SUE_CHAN1ABORT:
|
|
case WD719X_SUE_CHAN23ABORT:
|
|
result = DID_ABORT;
|
|
dev_err(&wd->pdev->dev, "DMA abort\n");
|
|
break;
|
|
case WD719X_SUE_CHAN1PAR:
|
|
case WD719X_SUE_CHAN23PAR:
|
|
result = DID_PARITY;
|
|
dev_err(&wd->pdev->dev, "DMA parity error\n");
|
|
break;
|
|
case WD719X_SUE_TIMEOUT:
|
|
result = DID_TIME_OUT;
|
|
dev_dbg(&wd->pdev->dev, "selection timeout\n");
|
|
break;
|
|
case WD719X_SUE_RESET:
|
|
dev_dbg(&wd->pdev->dev, "bus reset occurred\n");
|
|
result = DID_RESET;
|
|
break;
|
|
case WD719X_SUE_BUSERROR:
|
|
dev_dbg(&wd->pdev->dev, "SCSI bus error\n");
|
|
result = DID_ERROR;
|
|
break;
|
|
case WD719X_SUE_WRONGWAY:
|
|
dev_err(&wd->pdev->dev, "wrong data transfer direction\n");
|
|
result = DID_ERROR;
|
|
break;
|
|
case WD719X_SUE_BADPHASE:
|
|
dev_err(&wd->pdev->dev, "invalid SCSI phase\n");
|
|
result = DID_ERROR;
|
|
break;
|
|
case WD719X_SUE_TOOLONG:
|
|
dev_err(&wd->pdev->dev, "record too long\n");
|
|
result = DID_ERROR;
|
|
break;
|
|
case WD719X_SUE_BUSFREE:
|
|
dev_err(&wd->pdev->dev, "unexpected bus free\n");
|
|
result = DID_NO_CONNECT; /* or DID_ERROR ???*/
|
|
break;
|
|
case WD719X_SUE_ARSDONE:
|
|
dev_dbg(&wd->pdev->dev, "auto request sense\n");
|
|
if (regs.bytes.SCSI == 0)
|
|
result = DID_OK;
|
|
else
|
|
result = DID_PARITY;
|
|
break;
|
|
case WD719X_SUE_IGNORED:
|
|
dev_err(&wd->pdev->dev, "target id %d ignored command\n",
|
|
scb->cmd->device->id);
|
|
result = DID_NO_CONNECT;
|
|
break;
|
|
case WD719X_SUE_WRONGTAGS:
|
|
dev_err(&wd->pdev->dev, "reversed tags\n");
|
|
result = DID_ERROR;
|
|
break;
|
|
case WD719X_SUE_BADTAGS:
|
|
dev_err(&wd->pdev->dev, "tag type not supported by target\n");
|
|
result = DID_ERROR;
|
|
break;
|
|
case WD719X_SUE_NOSCAMID:
|
|
dev_err(&wd->pdev->dev, "no SCAM soft ID available\n");
|
|
result = DID_ERROR;
|
|
break;
|
|
default:
|
|
dev_warn(&wd->pdev->dev, "unknown SUE error code: 0x%x\n",
|
|
regs.bytes.SUE);
|
|
result = DID_ERROR;
|
|
break;
|
|
}
|
|
|
|
wd719x_finish_cmd(scb, result);
|
|
}
|
|
|
|
static irqreturn_t wd719x_interrupt(int irq, void *dev_id)
|
|
{
|
|
struct wd719x *wd = dev_id;
|
|
union wd719x_regs regs;
|
|
unsigned long flags;
|
|
u32 SCB_out;
|
|
|
|
spin_lock_irqsave(wd->sh->host_lock, flags);
|
|
/* read SCB pointer back from card */
|
|
SCB_out = wd719x_readl(wd, WD719X_AMR_SCB_OUT);
|
|
/* read all status info at once */
|
|
regs.all = cpu_to_le32(wd719x_readl(wd, WD719X_AMR_OP_CODE));
|
|
|
|
switch (regs.bytes.INT) {
|
|
case WD719X_INT_NONE:
|
|
spin_unlock_irqrestore(wd->sh->host_lock, flags);
|
|
return IRQ_NONE;
|
|
case WD719X_INT_LINKNOSTATUS:
|
|
dev_err(&wd->pdev->dev, "linked command completed with no status\n");
|
|
break;
|
|
case WD719X_INT_BADINT:
|
|
dev_err(&wd->pdev->dev, "unsolicited interrupt\n");
|
|
break;
|
|
case WD719X_INT_NOERRORS:
|
|
case WD719X_INT_LINKNOERRORS:
|
|
case WD719X_INT_ERRORSLOGGED:
|
|
case WD719X_INT_SPIDERFAILED:
|
|
/* was the cmd completed a direct or SCB command? */
|
|
if (regs.bytes.OPC == WD719X_CMD_PROCESS_SCB) {
|
|
struct wd719x_scb *scb;
|
|
list_for_each_entry(scb, &wd->active_scbs, list)
|
|
if (SCB_out == scb->phys)
|
|
break;
|
|
if (SCB_out == scb->phys)
|
|
wd719x_interrupt_SCB(wd, regs, scb);
|
|
else
|
|
dev_err(&wd->pdev->dev, "card returned invalid SCB pointer\n");
|
|
} else
|
|
dev_dbg(&wd->pdev->dev, "direct command 0x%x completed\n",
|
|
regs.bytes.OPC);
|
|
break;
|
|
case WD719X_INT_PIOREADY:
|
|
dev_err(&wd->pdev->dev, "card indicates PIO data ready but we never use PIO\n");
|
|
/* interrupt will not be cleared until all data is read */
|
|
break;
|
|
default:
|
|
dev_err(&wd->pdev->dev, "unknown interrupt reason: %d\n",
|
|
regs.bytes.INT);
|
|
|
|
}
|
|
/* clear interrupt so another can happen */
|
|
wd719x_writeb(wd, WD719X_AMR_INT_STATUS, WD719X_INT_NONE);
|
|
spin_unlock_irqrestore(wd->sh->host_lock, flags);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static void wd719x_eeprom_reg_read(struct eeprom_93cx6 *eeprom)
|
|
{
|
|
struct wd719x *wd = eeprom->data;
|
|
u8 reg = wd719x_readb(wd, WD719X_PCI_GPIO_DATA);
|
|
|
|
eeprom->reg_data_out = reg & WD719X_EE_DO;
|
|
}
|
|
|
|
static void wd719x_eeprom_reg_write(struct eeprom_93cx6 *eeprom)
|
|
{
|
|
struct wd719x *wd = eeprom->data;
|
|
u8 reg = 0;
|
|
|
|
if (eeprom->reg_data_in)
|
|
reg |= WD719X_EE_DI;
|
|
if (eeprom->reg_data_clock)
|
|
reg |= WD719X_EE_CLK;
|
|
if (eeprom->reg_chip_select)
|
|
reg |= WD719X_EE_CS;
|
|
|
|
wd719x_writeb(wd, WD719X_PCI_GPIO_DATA, reg);
|
|
}
|
|
|
|
/* read config from EEPROM so it can be downloaded by the RISC on (re-)init */
|
|
static void wd719x_read_eeprom(struct wd719x *wd)
|
|
{
|
|
struct eeprom_93cx6 eeprom;
|
|
u8 gpio;
|
|
struct wd719x_eeprom_header header;
|
|
|
|
eeprom.data = wd;
|
|
eeprom.register_read = wd719x_eeprom_reg_read;
|
|
eeprom.register_write = wd719x_eeprom_reg_write;
|
|
eeprom.width = PCI_EEPROM_WIDTH_93C46;
|
|
|
|
/* set all outputs to low */
|
|
wd719x_writeb(wd, WD719X_PCI_GPIO_DATA, 0);
|
|
/* configure GPIO pins */
|
|
gpio = wd719x_readb(wd, WD719X_PCI_GPIO_CONTROL);
|
|
/* GPIO outputs */
|
|
gpio &= (~(WD719X_EE_CLK | WD719X_EE_DI | WD719X_EE_CS));
|
|
/* GPIO input */
|
|
gpio |= WD719X_EE_DO;
|
|
wd719x_writeb(wd, WD719X_PCI_GPIO_CONTROL, gpio);
|
|
|
|
/* read EEPROM header */
|
|
eeprom_93cx6_multireadb(&eeprom, 0, (u8 *)&header, sizeof(header));
|
|
|
|
if (header.sig1 == 'W' && header.sig2 == 'D')
|
|
eeprom_93cx6_multireadb(&eeprom, header.cfg_offset,
|
|
(u8 *)wd->params,
|
|
sizeof(struct wd719x_host_param));
|
|
else { /* default EEPROM values */
|
|
dev_warn(&wd->pdev->dev, "EEPROM signature is invalid (0x%02x 0x%02x), using default values\n",
|
|
header.sig1, header.sig2);
|
|
wd->params->ch_1_th = 0x10; /* 16 DWs = 64 B */
|
|
wd->params->scsi_conf = 0x4c; /* 48ma, spue, parity check */
|
|
wd->params->own_scsi_id = 0x07; /* ID 7, SCAM disabled */
|
|
wd->params->sel_timeout = 0x4d; /* 250 ms */
|
|
wd->params->sleep_timer = 0x01;
|
|
wd->params->cdb_size = cpu_to_le16(0x5555); /* all 6 B */
|
|
wd->params->scsi_pad = 0x1b;
|
|
if (wd->type == WD719X_TYPE_7193) /* narrow card - disable */
|
|
wd->params->wide = cpu_to_le32(0x00000000);
|
|
else /* initiate & respond to WIDE messages */
|
|
wd->params->wide = cpu_to_le32(0xffffffff);
|
|
wd->params->sync = cpu_to_le32(0xffffffff);
|
|
wd->params->soft_mask = 0x00; /* all disabled */
|
|
wd->params->unsol_mask = 0x00; /* all disabled */
|
|
}
|
|
/* disable TAGGED messages */
|
|
wd->params->tag_en = cpu_to_le16(0x0000);
|
|
}
|
|
|
|
/* Read card type from GPIO bits 1 and 3 */
|
|
static enum wd719x_card_type wd719x_detect_type(struct wd719x *wd)
|
|
{
|
|
u8 card = wd719x_readb(wd, WD719X_PCI_GPIO_CONTROL);
|
|
|
|
card |= WD719X_GPIO_ID_BITS;
|
|
wd719x_writeb(wd, WD719X_PCI_GPIO_CONTROL, card);
|
|
card = wd719x_readb(wd, WD719X_PCI_GPIO_DATA) & WD719X_GPIO_ID_BITS;
|
|
switch (card) {
|
|
case 0x08:
|
|
return WD719X_TYPE_7193;
|
|
case 0x02:
|
|
return WD719X_TYPE_7197;
|
|
case 0x00:
|
|
return WD719X_TYPE_7296;
|
|
default:
|
|
dev_warn(&wd->pdev->dev, "unknown card type 0x%x\n", card);
|
|
return WD719X_TYPE_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
static int wd719x_board_found(struct Scsi_Host *sh)
|
|
{
|
|
struct wd719x *wd = shost_priv(sh);
|
|
static const char * const card_types[] = {
|
|
"Unknown card", "WD7193", "WD7197", "WD7296"
|
|
};
|
|
int ret;
|
|
|
|
INIT_LIST_HEAD(&wd->active_scbs);
|
|
|
|
sh->base = pci_resource_start(wd->pdev, 0);
|
|
|
|
wd->type = wd719x_detect_type(wd);
|
|
|
|
wd->sh = sh;
|
|
sh->irq = wd->pdev->irq;
|
|
wd->fw_virt = NULL;
|
|
|
|
/* memory area for host (EEPROM) parameters */
|
|
wd->params = dma_alloc_coherent(&wd->pdev->dev,
|
|
sizeof(struct wd719x_host_param),
|
|
&wd->params_phys, GFP_KERNEL);
|
|
if (!wd->params) {
|
|
dev_warn(&wd->pdev->dev, "unable to allocate parameter buffer\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
/* memory area for the RISC for hash table of outstanding requests */
|
|
wd->hash_virt = dma_alloc_coherent(&wd->pdev->dev,
|
|
WD719X_HASH_TABLE_SIZE,
|
|
&wd->hash_phys, GFP_KERNEL);
|
|
if (!wd->hash_virt) {
|
|
dev_warn(&wd->pdev->dev, "unable to allocate hash buffer\n");
|
|
ret = -ENOMEM;
|
|
goto fail_free_params;
|
|
}
|
|
|
|
ret = request_irq(wd->pdev->irq, wd719x_interrupt, IRQF_SHARED,
|
|
"wd719x", wd);
|
|
if (ret) {
|
|
dev_warn(&wd->pdev->dev, "unable to assign IRQ %d\n",
|
|
wd->pdev->irq);
|
|
goto fail_free_hash;
|
|
}
|
|
|
|
/* read parameters from EEPROM */
|
|
wd719x_read_eeprom(wd);
|
|
|
|
ret = wd719x_chip_init(wd);
|
|
if (ret)
|
|
goto fail_free_irq;
|
|
|
|
sh->this_id = wd->params->own_scsi_id & WD719X_EE_SCSI_ID_MASK;
|
|
|
|
dev_info(&wd->pdev->dev, "%s at I/O 0x%lx, IRQ %u, SCSI ID %d\n",
|
|
card_types[wd->type], sh->base, sh->irq, sh->this_id);
|
|
|
|
return 0;
|
|
|
|
fail_free_irq:
|
|
free_irq(wd->pdev->irq, wd);
|
|
fail_free_hash:
|
|
dma_free_coherent(&wd->pdev->dev, WD719X_HASH_TABLE_SIZE, wd->hash_virt,
|
|
wd->hash_phys);
|
|
fail_free_params:
|
|
dma_free_coherent(&wd->pdev->dev, sizeof(struct wd719x_host_param),
|
|
wd->params, wd->params_phys);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static struct scsi_host_template wd719x_template = {
|
|
.module = THIS_MODULE,
|
|
.name = "Western Digital 719x",
|
|
.cmd_size = sizeof(struct wd719x_scb),
|
|
.queuecommand = wd719x_queuecommand,
|
|
.eh_abort_handler = wd719x_abort,
|
|
.eh_device_reset_handler = wd719x_dev_reset,
|
|
.eh_bus_reset_handler = wd719x_bus_reset,
|
|
.eh_host_reset_handler = wd719x_host_reset,
|
|
.bios_param = wd719x_biosparam,
|
|
.proc_name = "wd719x",
|
|
.can_queue = 255,
|
|
.this_id = 7,
|
|
.sg_tablesize = WD719X_SG,
|
|
};
|
|
|
|
static int wd719x_pci_probe(struct pci_dev *pdev, const struct pci_device_id *d)
|
|
{
|
|
int err;
|
|
struct Scsi_Host *sh;
|
|
struct wd719x *wd;
|
|
|
|
err = pci_enable_device(pdev);
|
|
if (err)
|
|
goto fail;
|
|
|
|
if (dma_set_mask(&pdev->dev, DMA_BIT_MASK(32))) {
|
|
dev_warn(&pdev->dev, "Unable to set 32-bit DMA mask\n");
|
|
goto disable_device;
|
|
}
|
|
|
|
err = pci_request_regions(pdev, "wd719x");
|
|
if (err)
|
|
goto disable_device;
|
|
pci_set_master(pdev);
|
|
|
|
err = -ENODEV;
|
|
if (pci_resource_len(pdev, 0) == 0)
|
|
goto release_region;
|
|
|
|
err = -ENOMEM;
|
|
sh = scsi_host_alloc(&wd719x_template, sizeof(struct wd719x));
|
|
if (!sh)
|
|
goto release_region;
|
|
|
|
wd = shost_priv(sh);
|
|
wd->base = pci_iomap(pdev, 0, 0);
|
|
if (!wd->base)
|
|
goto free_host;
|
|
wd->pdev = pdev;
|
|
|
|
err = wd719x_board_found(sh);
|
|
if (err)
|
|
goto unmap;
|
|
|
|
err = scsi_add_host(sh, &wd->pdev->dev);
|
|
if (err)
|
|
goto destroy;
|
|
|
|
scsi_scan_host(sh);
|
|
|
|
pci_set_drvdata(pdev, sh);
|
|
return 0;
|
|
|
|
destroy:
|
|
wd719x_destroy(wd);
|
|
unmap:
|
|
pci_iounmap(pdev, wd->base);
|
|
free_host:
|
|
scsi_host_put(sh);
|
|
release_region:
|
|
pci_release_regions(pdev);
|
|
disable_device:
|
|
pci_disable_device(pdev);
|
|
fail:
|
|
return err;
|
|
}
|
|
|
|
|
|
static void wd719x_pci_remove(struct pci_dev *pdev)
|
|
{
|
|
struct Scsi_Host *sh = pci_get_drvdata(pdev);
|
|
struct wd719x *wd = shost_priv(sh);
|
|
|
|
scsi_remove_host(sh);
|
|
wd719x_destroy(wd);
|
|
pci_iounmap(pdev, wd->base);
|
|
pci_release_regions(pdev);
|
|
pci_disable_device(pdev);
|
|
|
|
scsi_host_put(sh);
|
|
}
|
|
|
|
static const struct pci_device_id wd719x_pci_table[] = {
|
|
{ PCI_DEVICE(PCI_VENDOR_ID_WD, 0x3296) },
|
|
{}
|
|
};
|
|
|
|
MODULE_DEVICE_TABLE(pci, wd719x_pci_table);
|
|
|
|
static struct pci_driver wd719x_pci_driver = {
|
|
.name = "wd719x",
|
|
.id_table = wd719x_pci_table,
|
|
.probe = wd719x_pci_probe,
|
|
.remove = wd719x_pci_remove,
|
|
};
|
|
|
|
module_pci_driver(wd719x_pci_driver);
|
|
|
|
MODULE_DESCRIPTION("Western Digital WD7193/7197/7296 SCSI driver");
|
|
MODULE_AUTHOR("Ondrej Zary, Aaron Dewell, Juergen Gaertner");
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_FIRMWARE("wd719x-wcs.bin");
|
|
MODULE_FIRMWARE("wd719x-risc.bin");
|