|  | /* | 
|  | * Driver for audio on multifunction CS5535/6 companion device | 
|  | * Copyright (C) Jaya Kumar | 
|  | * | 
|  | * Based on Jaroslav Kysela and Takashi Iwai's examples. | 
|  | * This work was sponsored by CIS(M) Sdn Bhd. | 
|  | * | 
|  | * This program is free software; you can redistribute it and/or modify | 
|  | * it under the terms of the GNU General Public License as published by | 
|  | * the Free Software Foundation; either version 2 of the License, or | 
|  | * (at your option) any later version. | 
|  | * | 
|  | * This program is distributed in the hope that it will be useful, | 
|  | * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
|  | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
|  | * GNU General Public License for more details. | 
|  | * | 
|  | * You should have received a copy of the GNU General Public License | 
|  | * along with this program; if not, write to the Free Software | 
|  | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA | 
|  | * | 
|  | */ | 
|  |  | 
|  | #include <linux/delay.h> | 
|  | #include <linux/interrupt.h> | 
|  | #include <linux/init.h> | 
|  | #include <linux/pci.h> | 
|  | #include <linux/slab.h> | 
|  | #include <linux/module.h> | 
|  | #include <linux/io.h> | 
|  | #include <sound/core.h> | 
|  | #include <sound/control.h> | 
|  | #include <sound/pcm.h> | 
|  | #include <sound/rawmidi.h> | 
|  | #include <sound/ac97_codec.h> | 
|  | #include <sound/initval.h> | 
|  | #include <sound/asoundef.h> | 
|  | #include "cs5535audio.h" | 
|  |  | 
|  | #define DRIVER_NAME "cs5535audio" | 
|  |  | 
|  | static char *ac97_quirk; | 
|  | module_param(ac97_quirk, charp, 0444); | 
|  | MODULE_PARM_DESC(ac97_quirk, "AC'97 board specific workarounds."); | 
|  |  | 
|  | static const struct ac97_quirk ac97_quirks[] = { | 
|  | #if 0 /* Not yet confirmed if all 5536 boards are HP only */ | 
|  | { | 
|  | .subvendor = PCI_VENDOR_ID_AMD, | 
|  | .subdevice = PCI_DEVICE_ID_AMD_CS5536_AUDIO, | 
|  | .name = "AMD RDK", | 
|  | .type = AC97_TUNE_HP_ONLY | 
|  | }, | 
|  | #endif | 
|  | {} | 
|  | }; | 
|  |  | 
|  | static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX; | 
|  | static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR; | 
|  | static bool enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP; | 
|  |  | 
|  | module_param_array(index, int, NULL, 0444); | 
|  | MODULE_PARM_DESC(index, "Index value for " DRIVER_NAME); | 
|  | module_param_array(id, charp, NULL, 0444); | 
|  | MODULE_PARM_DESC(id, "ID string for " DRIVER_NAME); | 
|  | module_param_array(enable, bool, NULL, 0444); | 
|  | MODULE_PARM_DESC(enable, "Enable " DRIVER_NAME); | 
|  |  | 
|  | static const struct pci_device_id snd_cs5535audio_ids[] = { | 
|  | { PCI_DEVICE(PCI_VENDOR_ID_NS, PCI_DEVICE_ID_NS_CS5535_AUDIO) }, | 
|  | { PCI_DEVICE(PCI_VENDOR_ID_AMD, PCI_DEVICE_ID_AMD_CS5536_AUDIO) }, | 
|  | {} | 
|  | }; | 
|  |  | 
|  | MODULE_DEVICE_TABLE(pci, snd_cs5535audio_ids); | 
|  |  | 
|  | static void wait_till_cmd_acked(struct cs5535audio *cs5535au, unsigned long timeout) | 
|  | { | 
|  | unsigned int tmp; | 
|  | do { | 
|  | tmp = cs_readl(cs5535au, ACC_CODEC_CNTL); | 
|  | if (!(tmp & CMD_NEW)) | 
|  | break; | 
|  | udelay(1); | 
|  | } while (--timeout); | 
|  | if (!timeout) | 
|  | dev_err(cs5535au->card->dev, | 
|  | "Failure writing to cs5535 codec\n"); | 
|  | } | 
|  |  | 
|  | static unsigned short snd_cs5535audio_codec_read(struct cs5535audio *cs5535au, | 
|  | unsigned short reg) | 
|  | { | 
|  | unsigned int regdata; | 
|  | unsigned int timeout; | 
|  | unsigned int val; | 
|  |  | 
|  | regdata = ((unsigned int) reg) << 24; | 
|  | regdata |= ACC_CODEC_CNTL_RD_CMD; | 
|  | regdata |= CMD_NEW; | 
|  |  | 
|  | cs_writel(cs5535au, ACC_CODEC_CNTL, regdata); | 
|  | wait_till_cmd_acked(cs5535au, 50); | 
|  |  | 
|  | timeout = 50; | 
|  | do { | 
|  | val = cs_readl(cs5535au, ACC_CODEC_STATUS); | 
|  | if ((val & STS_NEW) && reg == (val >> 24)) | 
|  | break; | 
|  | udelay(1); | 
|  | } while (--timeout); | 
|  | if (!timeout) | 
|  | dev_err(cs5535au->card->dev, | 
|  | "Failure reading codec reg 0x%x, Last value=0x%x\n", | 
|  | reg, val); | 
|  |  | 
|  | return (unsigned short) val; | 
|  | } | 
|  |  | 
|  | static void snd_cs5535audio_codec_write(struct cs5535audio *cs5535au, | 
|  | unsigned short reg, unsigned short val) | 
|  | { | 
|  | unsigned int regdata; | 
|  |  | 
|  | regdata = ((unsigned int) reg) << 24; | 
|  | regdata |= val; | 
|  | regdata &= CMD_MASK; | 
|  | regdata |= CMD_NEW; | 
|  | regdata &= ACC_CODEC_CNTL_WR_CMD; | 
|  |  | 
|  | cs_writel(cs5535au, ACC_CODEC_CNTL, regdata); | 
|  | wait_till_cmd_acked(cs5535au, 50); | 
|  | } | 
|  |  | 
|  | static void snd_cs5535audio_ac97_codec_write(struct snd_ac97 *ac97, | 
|  | unsigned short reg, unsigned short val) | 
|  | { | 
|  | struct cs5535audio *cs5535au = ac97->private_data; | 
|  | snd_cs5535audio_codec_write(cs5535au, reg, val); | 
|  | } | 
|  |  | 
|  | static unsigned short snd_cs5535audio_ac97_codec_read(struct snd_ac97 *ac97, | 
|  | unsigned short reg) | 
|  | { | 
|  | struct cs5535audio *cs5535au = ac97->private_data; | 
|  | return snd_cs5535audio_codec_read(cs5535au, reg); | 
|  | } | 
|  |  | 
|  | static int snd_cs5535audio_mixer(struct cs5535audio *cs5535au) | 
|  | { | 
|  | struct snd_card *card = cs5535au->card; | 
|  | struct snd_ac97_bus *pbus; | 
|  | struct snd_ac97_template ac97; | 
|  | int err; | 
|  | static struct snd_ac97_bus_ops ops = { | 
|  | .write = snd_cs5535audio_ac97_codec_write, | 
|  | .read = snd_cs5535audio_ac97_codec_read, | 
|  | }; | 
|  |  | 
|  | if ((err = snd_ac97_bus(card, 0, &ops, NULL, &pbus)) < 0) | 
|  | return err; | 
|  |  | 
|  | memset(&ac97, 0, sizeof(ac97)); | 
|  | ac97.scaps = AC97_SCAP_AUDIO | AC97_SCAP_SKIP_MODEM | 
|  | | AC97_SCAP_POWER_SAVE; | 
|  | ac97.private_data = cs5535au; | 
|  | ac97.pci = cs5535au->pci; | 
|  |  | 
|  | /* set any OLPC-specific scaps */ | 
|  | olpc_prequirks(card, &ac97); | 
|  |  | 
|  | if ((err = snd_ac97_mixer(pbus, &ac97, &cs5535au->ac97)) < 0) { | 
|  | dev_err(card->dev, "mixer failed\n"); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | snd_ac97_tune_hardware(cs5535au->ac97, ac97_quirks, ac97_quirk); | 
|  |  | 
|  | err = olpc_quirks(card, cs5535au->ac97); | 
|  | if (err < 0) { | 
|  | dev_err(card->dev, "olpc quirks failed\n"); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void process_bm0_irq(struct cs5535audio *cs5535au) | 
|  | { | 
|  | u8 bm_stat; | 
|  | spin_lock(&cs5535au->reg_lock); | 
|  | bm_stat = cs_readb(cs5535au, ACC_BM0_STATUS); | 
|  | spin_unlock(&cs5535au->reg_lock); | 
|  | if (bm_stat & EOP) { | 
|  | struct cs5535audio_dma *dma; | 
|  | dma = cs5535au->playback_substream->runtime->private_data; | 
|  | snd_pcm_period_elapsed(cs5535au->playback_substream); | 
|  | } else { | 
|  | dev_err(cs5535au->card->dev, | 
|  | "unexpected bm0 irq src, bm_stat=%x\n", | 
|  | bm_stat); | 
|  | } | 
|  | } | 
|  |  | 
|  | static void process_bm1_irq(struct cs5535audio *cs5535au) | 
|  | { | 
|  | u8 bm_stat; | 
|  | spin_lock(&cs5535au->reg_lock); | 
|  | bm_stat = cs_readb(cs5535au, ACC_BM1_STATUS); | 
|  | spin_unlock(&cs5535au->reg_lock); | 
|  | if (bm_stat & EOP) { | 
|  | struct cs5535audio_dma *dma; | 
|  | dma = cs5535au->capture_substream->runtime->private_data; | 
|  | snd_pcm_period_elapsed(cs5535au->capture_substream); | 
|  | } | 
|  | } | 
|  |  | 
|  | static irqreturn_t snd_cs5535audio_interrupt(int irq, void *dev_id) | 
|  | { | 
|  | u16 acc_irq_stat; | 
|  | unsigned char count; | 
|  | struct cs5535audio *cs5535au = dev_id; | 
|  |  | 
|  | if (cs5535au == NULL) | 
|  | return IRQ_NONE; | 
|  |  | 
|  | acc_irq_stat = cs_readw(cs5535au, ACC_IRQ_STATUS); | 
|  |  | 
|  | if (!acc_irq_stat) | 
|  | return IRQ_NONE; | 
|  | for (count = 0; count < 4; count++) { | 
|  | if (acc_irq_stat & (1 << count)) { | 
|  | switch (count) { | 
|  | case IRQ_STS: | 
|  | cs_readl(cs5535au, ACC_GPIO_STATUS); | 
|  | break; | 
|  | case WU_IRQ_STS: | 
|  | cs_readl(cs5535au, ACC_GPIO_STATUS); | 
|  | break; | 
|  | case BM0_IRQ_STS: | 
|  | process_bm0_irq(cs5535au); | 
|  | break; | 
|  | case BM1_IRQ_STS: | 
|  | process_bm1_irq(cs5535au); | 
|  | break; | 
|  | default: | 
|  | dev_err(cs5535au->card->dev, | 
|  | "Unexpected irq src: 0x%x\n", | 
|  | acc_irq_stat); | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  | return IRQ_HANDLED; | 
|  | } | 
|  |  | 
|  | static int snd_cs5535audio_free(struct cs5535audio *cs5535au) | 
|  | { | 
|  | synchronize_irq(cs5535au->irq); | 
|  | pci_set_power_state(cs5535au->pci, PCI_D3hot); | 
|  |  | 
|  | if (cs5535au->irq >= 0) | 
|  | free_irq(cs5535au->irq, cs5535au); | 
|  |  | 
|  | pci_release_regions(cs5535au->pci); | 
|  | pci_disable_device(cs5535au->pci); | 
|  | kfree(cs5535au); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int snd_cs5535audio_dev_free(struct snd_device *device) | 
|  | { | 
|  | struct cs5535audio *cs5535au = device->device_data; | 
|  | return snd_cs5535audio_free(cs5535au); | 
|  | } | 
|  |  | 
|  | static int snd_cs5535audio_create(struct snd_card *card, | 
|  | struct pci_dev *pci, | 
|  | struct cs5535audio **rcs5535au) | 
|  | { | 
|  | struct cs5535audio *cs5535au; | 
|  |  | 
|  | int err; | 
|  | static struct snd_device_ops ops = { | 
|  | .dev_free =	snd_cs5535audio_dev_free, | 
|  | }; | 
|  |  | 
|  | *rcs5535au = NULL; | 
|  | if ((err = pci_enable_device(pci)) < 0) | 
|  | return err; | 
|  |  | 
|  | if (dma_set_mask(&pci->dev, DMA_BIT_MASK(32)) < 0 || | 
|  | dma_set_coherent_mask(&pci->dev, DMA_BIT_MASK(32)) < 0) { | 
|  | dev_warn(card->dev, "unable to get 32bit dma\n"); | 
|  | err = -ENXIO; | 
|  | goto pcifail; | 
|  | } | 
|  |  | 
|  | cs5535au = kzalloc(sizeof(*cs5535au), GFP_KERNEL); | 
|  | if (cs5535au == NULL) { | 
|  | err = -ENOMEM; | 
|  | goto pcifail; | 
|  | } | 
|  |  | 
|  | spin_lock_init(&cs5535au->reg_lock); | 
|  | cs5535au->card = card; | 
|  | cs5535au->pci = pci; | 
|  | cs5535au->irq = -1; | 
|  |  | 
|  | if ((err = pci_request_regions(pci, "CS5535 Audio")) < 0) { | 
|  | kfree(cs5535au); | 
|  | goto pcifail; | 
|  | } | 
|  |  | 
|  | cs5535au->port = pci_resource_start(pci, 0); | 
|  |  | 
|  | if (request_irq(pci->irq, snd_cs5535audio_interrupt, | 
|  | IRQF_SHARED, KBUILD_MODNAME, cs5535au)) { | 
|  | dev_err(card->dev, "unable to grab IRQ %d\n", pci->irq); | 
|  | err = -EBUSY; | 
|  | goto sndfail; | 
|  | } | 
|  |  | 
|  | cs5535au->irq = pci->irq; | 
|  | pci_set_master(pci); | 
|  |  | 
|  | if ((err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, | 
|  | cs5535au, &ops)) < 0) | 
|  | goto sndfail; | 
|  |  | 
|  | *rcs5535au = cs5535au; | 
|  | return 0; | 
|  |  | 
|  | sndfail: /* leave the device alive, just kill the snd */ | 
|  | snd_cs5535audio_free(cs5535au); | 
|  | return err; | 
|  |  | 
|  | pcifail: | 
|  | pci_disable_device(pci); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | static int snd_cs5535audio_probe(struct pci_dev *pci, | 
|  | const struct pci_device_id *pci_id) | 
|  | { | 
|  | static int dev; | 
|  | struct snd_card *card; | 
|  | struct cs5535audio *cs5535au; | 
|  | int err; | 
|  |  | 
|  | if (dev >= SNDRV_CARDS) | 
|  | return -ENODEV; | 
|  | if (!enable[dev]) { | 
|  | dev++; | 
|  | return -ENOENT; | 
|  | } | 
|  |  | 
|  | err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE, | 
|  | 0, &card); | 
|  | if (err < 0) | 
|  | return err; | 
|  |  | 
|  | if ((err = snd_cs5535audio_create(card, pci, &cs5535au)) < 0) | 
|  | goto probefail_out; | 
|  |  | 
|  | card->private_data = cs5535au; | 
|  |  | 
|  | if ((err = snd_cs5535audio_mixer(cs5535au)) < 0) | 
|  | goto probefail_out; | 
|  |  | 
|  | if ((err = snd_cs5535audio_pcm(cs5535au)) < 0) | 
|  | goto probefail_out; | 
|  |  | 
|  | strcpy(card->driver, DRIVER_NAME); | 
|  |  | 
|  | strcpy(card->shortname, "CS5535 Audio"); | 
|  | sprintf(card->longname, "%s %s at 0x%lx, irq %i", | 
|  | card->shortname, card->driver, | 
|  | cs5535au->port, cs5535au->irq); | 
|  |  | 
|  | if ((err = snd_card_register(card)) < 0) | 
|  | goto probefail_out; | 
|  |  | 
|  | pci_set_drvdata(pci, card); | 
|  | dev++; | 
|  | return 0; | 
|  |  | 
|  | probefail_out: | 
|  | snd_card_free(card); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | static void snd_cs5535audio_remove(struct pci_dev *pci) | 
|  | { | 
|  | olpc_quirks_cleanup(); | 
|  | snd_card_free(pci_get_drvdata(pci)); | 
|  | } | 
|  |  | 
|  | static struct pci_driver cs5535audio_driver = { | 
|  | .name = KBUILD_MODNAME, | 
|  | .id_table = snd_cs5535audio_ids, | 
|  | .probe = snd_cs5535audio_probe, | 
|  | .remove = snd_cs5535audio_remove, | 
|  | #ifdef CONFIG_PM_SLEEP | 
|  | .driver = { | 
|  | .pm = &snd_cs5535audio_pm, | 
|  | }, | 
|  | #endif | 
|  | }; | 
|  |  | 
|  | module_pci_driver(cs5535audio_driver); | 
|  |  | 
|  | MODULE_AUTHOR("Jaya Kumar"); | 
|  | MODULE_LICENSE("GPL"); | 
|  | MODULE_DESCRIPTION("CS5535 Audio"); | 
|  | MODULE_SUPPORTED_DEVICE("CS5535 Audio"); |