root/usr/src/uts/common/io/audio/drv/audiop16x/audiop16x.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * Purpose: Driver for the Creative P16X AC97 audio controller
 */
/*
 *
 * Copyright (C) 4Front Technologies 1996-2009.
 *
 * This software is released under CDDL 1.0 source license.
 * See the COPYING file included in the main directory of this source
 * distribution for the license terms and conditions.
 */

#include <sys/types.h>
#include <sys/modctl.h>
#include <sys/kmem.h>
#include <sys/conf.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/pci.h>
#include <sys/note.h>
#include <sys/audio/audio_driver.h>
#include <sys/audio/ac97.h>

#include "audiop16x.h"

/*
 * These boards use an AC'97 codec, but don't have all of the
 * various outputs that the AC'97 codec can offer.  We just
 * suppress them for now.
 */
static char *p16x_remove_ac97[] = {
        AUDIO_CTRL_ID_BEEP,
        AUDIO_CTRL_ID_VIDEO,
        AUDIO_CTRL_ID_MICSRC,
        AUDIO_CTRL_ID_SPEAKER,
        AUDIO_CTRL_ID_SPKSRC,
        NULL
};

static struct ddi_device_acc_attr dev_attr = {
        DDI_DEVICE_ATTR_V0,
        DDI_STRUCTURE_LE_ACC,
        DDI_STRICTORDER_ACC
};

static struct ddi_device_acc_attr buf_attr = {
        DDI_DEVICE_ATTR_V0,
        DDI_NEVERSWAP_ACC,
        DDI_STRICTORDER_ACC
};

static ddi_dma_attr_t dma_attr_buf = {
        DMA_ATTR_V0,            /* version number */
        0x00000000,             /* low DMA address range */
        0xffffffff,             /* high DMA address range */
        0xfffffffe,             /* DMA counter register */
        4,                      /* DMA address alignment */
        0x3c,                   /* DMA burstsizes */
        4,                      /* min effective DMA size */
        0xffffffff,             /* max DMA xfer size */
        0xffffffff,             /* segment boundary */
        1,                      /* s/g length */
        4,                      /* granularity of device */
        0                       /* Bus specific DMA flags */
};

static int p16x_attach(dev_info_t *);
static int p16x_resume(dev_info_t *);
static int p16x_detach(p16x_dev_t *);
static int p16x_suspend(p16x_dev_t *);

static int p16x_open(void *, int, unsigned *, caddr_t *);
static void p16x_close(void *);
static int p16x_start(void *);
static void p16x_stop(void *);
static int p16x_format(void *);
static int p16x_channels(void *);
static int p16x_rate(void *);
static uint64_t p16x_count(void *);
static void p16x_sync(void *, unsigned);
static void p16x_chinfo(void *, int, unsigned *, unsigned *);

static uint16_t p16x_read_ac97(void *, uint8_t);
static void p16x_write_ac97(void *, uint8_t, uint16_t);
static int p16x_alloc_port(p16x_dev_t *, int);
static void p16x_destroy(p16x_dev_t *);
static void p16x_hwinit(p16x_dev_t *);

static audio_engine_ops_t p16x_engine_ops = {
        AUDIO_ENGINE_VERSION,
        p16x_open,
        p16x_close,
        p16x_start,
        p16x_stop,
        p16x_count,
        p16x_format,
        p16x_channels,
        p16x_rate,
        p16x_sync,
        NULL,
        p16x_chinfo,
        NULL
};

static unsigned int
read_reg(p16x_dev_t *dev, int reg, int chn)
{
        unsigned int val;

        mutex_enter(&dev->mutex);
        OUTL(dev, (reg << 16) | (chn & 0xffff), PTR);   /* Pointer */
        val = INL(dev, DR);     /* Data */
        mutex_exit(&dev->mutex);

        return (val);
}

static void
write_reg(p16x_dev_t *dev, int reg, int chn, unsigned int value)
{

        mutex_enter(&dev->mutex);
        OUTL(dev, (reg << 16) | (chn & 0xffff), PTR);   /* Pointer */
        OUTL(dev, value, DR);   /* Data */
        mutex_exit(&dev->mutex);
}

static void
set_reg_bits(p16x_dev_t *dev, int reg, int chn, unsigned int mask)
{
        unsigned int    val;
        mutex_enter(&dev->mutex);
        OUTL(dev, (reg << 16) | (chn & 0xffff), PTR);   /* Pointer */
        val = INL(dev, DR);     /* Data */
        val |= mask;
        OUTL(dev, val, DR);     /* Data */
        mutex_exit(&dev->mutex);
}

static void
clear_reg_bits(p16x_dev_t *dev, int reg, int chn, unsigned int mask)
{
        unsigned int    val;
        mutex_enter(&dev->mutex);
        OUTL(dev, (reg << 16) | (chn & 0xffff), PTR);   /* Pointer */
        val = INL(dev, DR);     /* Data */
        val &= ~(mask);
        OUTL(dev, val, DR);     /* Data */
        mutex_exit(&dev->mutex);
}

static uint16_t
p16x_read_ac97(void *arg, uint8_t index)
{
        p16x_dev_t *dev = arg;
        uint16_t value;
        int i;

        OUTB(dev, index, AC97A);
        for (i = 0; i < 10000; i++)
                if (INB(dev, AC97A) & 0x80)
                        break;
        value = INW(dev, AC97D);
        return (value);
}

static void
p16x_write_ac97(void *arg, uint8_t index, uint16_t data)
{
        p16x_dev_t *dev = arg;
        unsigned int i;

        OUTB(dev, index, AC97A);
        for (i = 0; i < 10000; i++)
                if (INB(dev, AC97A) & 0x80)
                        break;
        OUTW(dev, data, AC97D);
}

/*
 * Audio routines
 */

int
p16x_open(void *arg, int flag, uint_t *nframes, caddr_t *bufp)
{
        p16x_port_t     *port = arg;

        _NOTE(ARGUNUSED(flag));

        port->count = 0;
        *nframes = port->buf_frames;
        *bufp = port->buf_kaddr;

        return (0);
}

void
p16x_close(void *arg)
{
        _NOTE(ARGUNUSED(arg));
}

int
p16x_start(void *arg)
{
        p16x_port_t     *port = arg;
        p16x_dev_t      *dev = port->dev;

        port->offset = 0;

        if (port->port_num == P16X_REC) {
                write_reg(dev, CRFA, 0, 0);
                write_reg(dev, CRCAV, 0, 0);

                /* Enable rec channel */
                set_reg_bits(dev, SA, 0, 0x100);
        } else {
                for (int i = 0; i < 3; i++) {
                        write_reg(dev, PTBA, i, 0);
                        write_reg(dev, PTBS, i, 0);
                        write_reg(dev, PTCA, i, 0);
                        write_reg(dev, PFEA, i, 0);
                        write_reg(dev, CPFA, i, 0);
                        write_reg(dev, CPCAV, i, 0);
                }

                /* Enable play channel */
                set_reg_bits(dev, SA, 0, 0x7);
        }

        return (0);
}

void
p16x_stop(void *arg)
{
        p16x_port_t     *port = arg;
        p16x_dev_t      *dev = port->dev;

        if (port->port_num == P16X_REC) {
                /* Disable rec channel */
                clear_reg_bits(dev, SA, 0, 0x100);

        } else {
                /* Disable Play channel */
                clear_reg_bits(dev, SA, 0, 0x7);
        }
}

int
p16x_format(void *arg)
{
        _NOTE(ARGUNUSED(arg));

        return (AUDIO_FORMAT_S16_LE);
}

int
p16x_channels(void *arg)
{
        p16x_port_t *port = arg;

        return (port->nchan);
}

int
p16x_rate(void *arg)
{
        _NOTE(ARGUNUSED(arg));

        return (48000);
}

void
p16x_sync(void *arg, unsigned nframes)
{
        p16x_port_t *port = arg;
        _NOTE(ARGUNUSED(nframes));

        (void) ddi_dma_sync(port->buf_dmah, 0, 0, port->syncdir);
}

uint64_t
p16x_count(void *arg)
{
        p16x_port_t     *port = arg;
        p16x_dev_t      *dev = port->dev;
        uint64_t        val;
        uint32_t        offset, n;

        if (port->port_num == P16X_PLAY) {
                offset = read_reg(dev, CPFA, 0);
        } else {
                offset = read_reg(dev, CRFA, 0);
        }

        /* get the offset, and switch to frames */
        offset /= (2 * sizeof (uint16_t));

        if (offset >= port->offset) {
                n = offset - port->offset;
        } else {
                n = offset + (port->buf_frames - port->offset);
        }
        port->offset = offset;
        port->count += n;
        val = port->count;

        return (val);
}

static void
p16x_chinfo(void *arg, int chan, unsigned *offset, unsigned *incr)
{
        p16x_port_t *port = arg;
        unsigned mult;

        if (port->port_num == P16X_PLAY) {
                switch (chan) {
                case 0: /* left front */
                case 1: /* right front */
                        mult = 0;
                        break;
                case 2: /* center */
                case 3: /* lfe */
                        mult = 2;
                        break;
                case 4: /* left surround */
                case 5: /* right surround */
                        mult = 1;
                        break;
                }
                *offset = (port->buf_frames * 2 * mult) + (chan % 2);
                *incr = 2;
        } else {
                *offset = chan;
                *incr = 2;
        }
}

/* private implementation bits */

int
p16x_alloc_port(p16x_dev_t *dev, int num)
{
        p16x_port_t             *port;
        size_t                  len;
        ddi_dma_cookie_t        cookie;
        uint_t                  count;
        int                     dir;
        unsigned                caps;
        audio_dev_t             *adev;

        adev = dev->adev;
        port = kmem_zalloc(sizeof (*port), KM_SLEEP);
        dev->port[num] = port;
        port->dev = dev;

        switch (num) {
        case P16X_REC:
                port->syncdir = DDI_DMA_SYNC_FORKERNEL;
                caps = ENGINE_INPUT_CAP;
                dir = DDI_DMA_READ;
                port->port_num = P16X_REC;
                port->nchan = 2;
                break;
        case P16X_PLAY:
                port->syncdir = DDI_DMA_SYNC_FORDEV;
                caps = ENGINE_OUTPUT_CAP;
                dir = DDI_DMA_WRITE;
                port->port_num = P16X_PLAY;
                port->nchan = 6;
                break;
        default:
                return (DDI_FAILURE);
        }

        /*
         * NB: The device operates in pairs of dwords at a time, for
         * performance reasons.  So make sure that our buffer is
         * arranged as a whole number of these.  The value below gives
         * a reasonably large buffer so we can support a deep
         * playahead if we need to (and we should avoid input
         * overruns.)
         */
        port->buf_frames = 4096;
        port->buf_size = port->buf_frames * port->nchan * sizeof (uint16_t);

        /* now allocate buffers */
        if (ddi_dma_alloc_handle(dev->dip, &dma_attr_buf, DDI_DMA_SLEEP, NULL,
            &port->buf_dmah) != DDI_SUCCESS) {
                audio_dev_warn(adev, "failed to allocate BUF handle");
                return (DDI_FAILURE);
        }

        if (ddi_dma_mem_alloc(port->buf_dmah, port->buf_size,
            &buf_attr, DDI_DMA_CONSISTENT, DDI_DMA_SLEEP, NULL,
            &port->buf_kaddr, &len, &port->buf_acch) != DDI_SUCCESS) {
                audio_dev_warn(adev, "failed to allocate BUF memory");
                return (DDI_FAILURE);
        }

        if (ddi_dma_addr_bind_handle(port->buf_dmah, NULL, port->buf_kaddr,
            len, DDI_DMA_CONSISTENT | dir, DDI_DMA_SLEEP, NULL, &cookie,
            &count) != DDI_SUCCESS) {
                audio_dev_warn(adev, "failed binding BUF DMA handle");
                return (DDI_FAILURE);
        }
        port->buf_paddr = cookie.dmac_address;

        port->engine = audio_engine_alloc(&p16x_engine_ops, caps);
        if (port->engine == NULL) {
                audio_dev_warn(adev, "audio_engine_alloc failed");
                return (DDI_FAILURE);
        }

        audio_engine_set_private(port->engine, port);
        audio_dev_add_engine(adev, port->engine);

        return (DDI_SUCCESS);
}

void
p16x_destroy(p16x_dev_t *dev)
{
        mutex_destroy(&dev->mutex);

        for (int i = 0; i < P16X_NUM_PORT; i++) {
                p16x_port_t *port = dev->port[i];
                if (!port)
                        continue;
                if (port->engine) {
                        audio_dev_remove_engine(dev->adev, port->engine);
                        audio_engine_free(port->engine);
                }
                if (port->buf_paddr) {
                        (void) ddi_dma_unbind_handle(port->buf_dmah);
                }
                if (port->buf_acch) {
                        ddi_dma_mem_free(&port->buf_acch);
                }
                if (port->buf_dmah) {
                        ddi_dma_free_handle(&port->buf_dmah);
                }
                kmem_free(port, sizeof (*port));
        }

        if (dev->ac97 != NULL) {
                ac97_free(dev->ac97);
        }
        if (dev->adev != NULL) {
                audio_dev_free(dev->adev);
        }
        if (dev->regsh != NULL) {
                ddi_regs_map_free(&dev->regsh);
        }
        if (dev->pcih != NULL) {
                pci_config_teardown(&dev->pcih);
        }
        kmem_free(dev, sizeof (*dev));
}

void
p16x_hwinit(p16x_dev_t *dev)
{
        p16x_port_t             *port;
        uint32_t                paddr;
        uint32_t                chunksz;
        int i;

        for (i = 0; i < 3; i++) {
                write_reg(dev, PTBA, i, 0);
                write_reg(dev, PTBS, i, 0);
                write_reg(dev, PTCA, i, 0);
                write_reg(dev, PFEA, i, 0);
                write_reg(dev, CPFA, i, 0);
                write_reg(dev, CPCAV, i, 0);
                write_reg(dev, CRFA, i, 0);
                write_reg(dev, CRCAV, i, 0);
        }
        write_reg(dev, SCS0, 0, 0x02108504);
        write_reg(dev, SCS1, 0, 0x02108504);
        write_reg(dev, SCS2, 0, 0x02108504);

        /* set the spdif/analog combo jack to analog out */
        write_reg(dev, SPC, 0, 0x00000700);
        write_reg(dev, EA_aux, 0, 0x0001003f);

        port = dev->port[P16X_REC];
        /* Set physical address of the DMA buffer */
        write_reg(dev, RFBA, 0, port->buf_paddr);
        write_reg(dev, RFBS, 0, (port->buf_size) << 16);

        /* Set physical address of the DMA buffer */
        port = dev->port[P16X_PLAY];
        paddr = port->buf_paddr;
        chunksz = port->buf_frames * 4;
        write_reg(dev, PFBA, 0, paddr);
        write_reg(dev, PFBS, 0, chunksz << 16);
        paddr += chunksz;
        write_reg(dev, PFBA, 1, paddr);
        write_reg(dev, PFBS, 1, chunksz << 16);
        paddr += chunksz;
        write_reg(dev, PFBA, 2, paddr);
        write_reg(dev, PFBS, 2, chunksz << 16);

        OUTL(dev, 0x1080, GPIO);        /* GPIO */
        /* Clear any pending interrupts */
        OUTL(dev, INTR_ALL, IP);
        OUTL(dev, 0, IE);
        OUTL(dev, 0x9, HC);     /* Enable audio */
}

int
p16x_attach(dev_info_t *dip)
{
        uint16_t        vendor, device;
        p16x_dev_t      *dev;
        ddi_acc_handle_t pcih;

        dev = kmem_zalloc(sizeof (*dev), KM_SLEEP);
        dev->dip = dip;
        ddi_set_driver_private(dip, dev);

        mutex_init(&dev->mutex, NULL, MUTEX_DRIVER, NULL);

        if ((dev->adev = audio_dev_alloc(dip, 0)) == NULL) {
                cmn_err(CE_WARN, "audio_dev_alloc failed");
                goto error;
        }

        if (pci_config_setup(dip, &pcih) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev, "pci_config_setup failed");
                goto error;
        }
        dev->pcih = pcih;

        vendor = pci_config_get16(pcih, PCI_CONF_VENID);
        device = pci_config_get16(pcih, PCI_CONF_DEVID);
        if (vendor != CREATIVE_VENDOR_ID ||
            device != SB_P16X_ID) {
                audio_dev_warn(dev->adev, "Hardware not recognized "
                    "(vendor=%x, dev=%x)", vendor, device);
                goto error;
        }

        /* set PCI command register */
        pci_config_put16(pcih, PCI_CONF_COMM,
            pci_config_get16(pcih, PCI_CONF_COMM) |
            PCI_COMM_MAE | PCI_COMM_IO);


        if ((ddi_regs_map_setup(dip, 1, &dev->base, 0, 0, &dev_attr,
            &dev->regsh)) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev, "failed to map registers");
                goto error;
        }

        audio_dev_set_description(dev->adev, "Creative Sound Blaster Live!");
        audio_dev_set_version(dev->adev, "SBO200");

        if ((p16x_alloc_port(dev, P16X_PLAY) != DDI_SUCCESS) ||
            (p16x_alloc_port(dev, P16X_REC) != DDI_SUCCESS)) {
                goto error;
        }

        p16x_hwinit(dev);

        dev->ac97 = ac97_allocate(dev->adev, dip,
            p16x_read_ac97, p16x_write_ac97, dev);
        if (dev->ac97 == NULL) {
                audio_dev_warn(dev->adev, "failed to allocate ac97 handle");
                goto error;
        }

        ac97_probe_controls(dev->ac97);

        /* remove the AC'97 controls we don't want to expose */
        for (int i = 0; p16x_remove_ac97[i]; i++) {
                ac97_ctrl_t *ctrl;
                ctrl = ac97_control_find(dev->ac97, p16x_remove_ac97[i]);
                if (ctrl != NULL) {
                        ac97_control_unregister(ctrl);
                }
        }

        ac97_register_controls(dev->ac97);

        if (audio_dev_register(dev->adev) != DDI_SUCCESS) {
                audio_dev_warn(dev->adev, "unable to register with framework");
                goto error;
        }

        ddi_report_dev(dip);

        return (DDI_SUCCESS);

error:
        p16x_destroy(dev);
        return (DDI_FAILURE);
}

int
p16x_resume(dev_info_t *dip)
{
        p16x_dev_t *dev;

        dev = ddi_get_driver_private(dip);

        p16x_hwinit(dev);

        ac97_reset(dev->ac97);

        audio_dev_resume(dev->adev);

        return (DDI_SUCCESS);
}

int
p16x_detach(p16x_dev_t *dev)
{
        if (audio_dev_unregister(dev->adev) != DDI_SUCCESS)
                return (DDI_FAILURE);

        p16x_destroy(dev);
        return (DDI_SUCCESS);
}

int
p16x_suspend(p16x_dev_t *dev)
{
        audio_dev_suspend(dev->adev);

        return (DDI_SUCCESS);
}

static int p16x_ddi_attach(dev_info_t *, ddi_attach_cmd_t);
static int p16x_ddi_detach(dev_info_t *, ddi_detach_cmd_t);
static int p16x_ddi_quiesce(dev_info_t *);

static struct dev_ops p16x_dev_ops = {
        DEVO_REV,               /* rev */
        0,                      /* refcnt */
        NULL,                   /* getinfo */
        nulldev,                /* identify */
        nulldev,                /* probe */
        p16x_ddi_attach,        /* attach */
        p16x_ddi_detach,        /* detach */
        nodev,                  /* reset */
        NULL,                   /* cb_ops */
        NULL,                   /* bus_ops */
        NULL,                   /* power */
        p16x_ddi_quiesce,       /* quiesce */
};

static struct modldrv p16x_modldrv = {
        &mod_driverops,         /* drv_modops */
        "Creative P16X Audio",  /* linkinfo */
        &p16x_dev_ops,          /* dev_ops */
};

static struct modlinkage modlinkage = {
        MODREV_1,
        { &p16x_modldrv, NULL }
};

int
_init(void)
{
        int     rv;

        audio_init_ops(&p16x_dev_ops, P16X_NAME);
        if ((rv = mod_install(&modlinkage)) != 0) {
                audio_fini_ops(&p16x_dev_ops);
        }
        return (rv);
}

int
_fini(void)
{
        int     rv;

        if ((rv = mod_remove(&modlinkage)) == 0) {
                audio_fini_ops(&p16x_dev_ops);
        }
        return (rv);
}

int
_info(struct modinfo *modinfop)
{
        return (mod_info(&modlinkage, modinfop));
}

int
p16x_ddi_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
        switch (cmd) {
        case DDI_ATTACH:
                return (p16x_attach(dip));

        case DDI_RESUME:
                return (p16x_resume(dip));

        default:
                return (DDI_FAILURE);
        }
}

int
p16x_ddi_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
        p16x_dev_t *dev;

        dev = ddi_get_driver_private(dip);

        switch (cmd) {
        case DDI_DETACH:
                return (p16x_detach(dev));

        case DDI_SUSPEND:
                return (p16x_suspend(dev));

        default:
                return (DDI_FAILURE);
        }
}

int
p16x_ddi_quiesce(dev_info_t *dip)
{
        p16x_dev_t      *dev;

        dev = ddi_get_driver_private(dip);

        write_reg(dev, SA, 0, 0);
        OUTL(dev, 0x01, HC);

        return (DDI_SUCCESS);
}