audiols.c revision 4a20ca87c19352fb04bb75918ba0231663faa6a8
/*
* 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 2009 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
/*
* Purpose: Driver for the Creative Audigy LS sound card
*/
/*
*
* Copyright (C) 4Front Technologies 1996-2009.
*/
#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 "audiols.h"
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 */
0x000fffff, /* DMA counter (16 bits only in Audigy LS) */
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 audigyls_attach(dev_info_t *);
static int audigyls_resume(dev_info_t *);
static int audigyls_detach(audigyls_dev_t *);
static int audigyls_suspend(audigyls_dev_t *);
static int audigyls_open(void *, int, unsigned *, unsigned *, caddr_t *);
static void audigyls_close(void *);
static int audigyls_start(void *);
static void audigyls_stop(void *);
static int audigyls_format(void *);
static int audigyls_channels(void *);
static int audigyls_rate(void *);
static uint64_t audigyls_count(void *);
static void audigyls_sync(void *, unsigned);
static size_t audigyls_qlen(void *);
static void audigyls_chinfo(void *, int, unsigned *, unsigned *);
static uint16_t audigyls_read_ac97(void *, uint8_t);
static void audigyls_write_ac97(void *, uint8_t, uint16_t);
static int audigyls_alloc_port(audigyls_dev_t *, int);
static void audigyls_start_port(audigyls_port_t *);
static void audigyls_stop_port(audigyls_port_t *);
static void audigyls_update_port(audigyls_port_t *);
static void audigyls_reset_port(audigyls_port_t *);
static void audigyls_destroy(audigyls_dev_t *);
static int audigyls_setup_intrs(audigyls_dev_t *);
static void audigyls_hwinit(audigyls_dev_t *);
static uint_t audigyls_intr(caddr_t, caddr_t);
static void audigyls_configure_mixer(audigyls_dev_t *dev);
static audio_engine_ops_t audigyls_engine_ops = {
AUDIO_ENGINE_VERSION,
audigyls_open,
audigyls_close,
audigyls_start,
audigyls_stop,
audigyls_count,
audigyls_format,
audigyls_channels,
audigyls_rate,
audigyls_sync,
audigyls_qlen,
audigyls_chinfo
};
/*
* Audigy LS uses AC'97 strictly for the recording side of things.
* While the chip can supposedly route output to AC'97 for playback,
* the PCI devices use a separate I2S DAC instead. As a result we
* need to suppress controls that the AC'97 codec registers.
*
* Furthermore, even then the AC'97 codec offers inputs that we just
* aren't interested in.
*/
const char *audigyls_remove_ac97[] = {
AUDIO_CTRL_ID_VOLUME,
AUDIO_CTRL_ID_LINEOUT,
AUDIO_CTRL_ID_HEADPHONE,
AUDIO_CTRL_ID_CD,
AUDIO_CTRL_ID_VIDEO,
AUDIO_CTRL_ID_3DDEPTH,
AUDIO_CTRL_ID_3DENHANCE,
AUDIO_CTRL_ID_BEEP,
AUDIO_CTRL_ID_RECGAIN,
AUDIO_CTRL_ID_RECSRC,
AUDIO_CTRL_ID_LOOPBACK,
NULL,
};
/*
* AC'97 sources we don't want to expose.
*/
const char *audigyls_badsrcs[] = {
AUDIO_PORT_VIDEO,
AUDIO_PORT_CD,
AUDIO_PORT_STEREOMIX,
AUDIO_PORT_MONOMIX,
NULL,
};
static unsigned int
read_chan(audigyls_dev_t *dev, int reg, int chn)
{
uint32_t val;
mutex_enter(&dev->low_mutex);
/* Pointer */
OUTL(dev, PR, (reg << 16) | (chn & 0xffff));
/* Data */
val = INL(dev, DR);
mutex_exit(&dev->low_mutex);
return (val);
}
static void
write_chan(audigyls_dev_t *dev, int reg, int chn, uint32_t value)
{
mutex_enter(&dev->low_mutex);
/* Pointer */
OUTL(dev, PR, (reg << 16) | (chn & 0x7));
/* Data */
OUTL(dev, DR, value);
mutex_exit(&dev->low_mutex);
}
static unsigned int
read_reg(audigyls_dev_t *dev, int reg)
{
return (read_chan(dev, reg, 0));
}
static void
write_reg(audigyls_dev_t *dev, int reg, uint32_t value)
{
write_chan(dev, reg, 0, value);
}
static uint16_t
audigyls_read_ac97(void *arg, uint8_t index)
{
audigyls_dev_t *dev = arg;
uint16_t dtemp = 0;
int i;
mutex_enter(&dev->low_mutex);
OUTB(dev, AC97A, index);
for (i = 0; i < 10000; i++) {
if (INB(dev, AC97A) & 0x80)
break;
}
if (i == 10000) { /* Timeout */
mutex_exit(&dev->low_mutex);
return (0xffff);
}
dtemp = INW(dev, AC97D);
mutex_exit(&dev->low_mutex);
return (dtemp);
}
static void
audigyls_write_ac97(void *arg, uint8_t index, uint16_t data)
{
audigyls_dev_t *dev = arg;
int i;
mutex_enter(&dev->low_mutex);
OUTB(dev, AC97A, index);
for (i = 0; i < 50000; i++) {
if (INB(dev, AC97A) & 0x80)
break;
}
if (i == 50000) {
mutex_exit(&dev->low_mutex);
return;
}
OUTW(dev, AC97D, data);
mutex_exit(&dev->low_mutex);
}
static void
select_digital_enable(audigyls_dev_t *dev, int mode)
{
/*
* Set the out3/spdif combo jack format.
* mode0=analog rear/center, 1=spdif
*/
if (mode == 0) {
write_reg(dev, SPC, 0x00000f00);
} else {
write_reg(dev, SPC, 0x0000000f);
}
}
/* only for SBLive 7.1 */
void
audigyls_i2c_write(audigyls_dev_t *dev, int reg, int data)
{
int i, timeout, tmp;
tmp = (reg << 9 | data) << 16; /* set the upper 16 bits */
/* first write the command to the data reg */
write_reg(dev, I2C_1, tmp);
for (i = 0; i < 20; i++) {
tmp = read_reg(dev, I2C_A) & ~0x6fe;
/* see audigyls.pdf for bits */
tmp |= 0x400 | 0x100 | 0x34;
write_reg(dev, I2C_A, tmp);
/* now wait till controller sets valid bit (0x100) to 0 */
timeout = 0;
for (;;) {
tmp = read_reg(dev, I2C_A);
if ((tmp & 0x100) == 0)
break;
if (timeout > 100)
break;
timeout++;
}
/* transaction aborted */
if (tmp & 0x200)
break;
}
}
int
audigyls_spi_write(audigyls_dev_t *dev, int data)
{
unsigned int orig;
unsigned int tmp;
int i, valid;
tmp = read_reg(dev, SPI);
orig = (tmp & ~0x3ffff) | 0x30000;
write_reg(dev, SPI, orig | data);
valid = 0;
/* Wait for status bit to return to 0 */
for (i = 0; i < 1000; i++) {
drv_usecwait(100);
tmp = read_reg(dev, SPI);
if (!(tmp & 0x10000)) {
valid = 1;
break;
}
}
if (!valid) /* Timed out */
return (0);
return (1);
}
static void
audigyls_update_port(audigyls_port_t *port)
{
audigyls_dev_t *dev = port->dev;
uint32_t offset, n;
if (dev->suspended)
return;
if (port->direction == AUDIGYLS_PLAY_PORT) {
offset = read_chan(dev, CPFA, 0);
} else {
offset = read_chan(dev, CRFA, 2);
}
/* 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;
}
static void
check_play_intr(audigyls_dev_t *dev)
{
audigyls_port_t *port = dev->port[AUDIGYLS_PLAY_PORT];
if (!port->started)
return;
audio_engine_consume(port->engine);
}
static void
check_rec_intr(audigyls_dev_t *dev)
{
audigyls_port_t *port = dev->port[AUDIGYLS_REC_PORT];
if (!port->started)
return;
audio_engine_produce(port->engine);
}
static uint_t
audigyls_intr(caddr_t argp, caddr_t nocare)
{
audigyls_dev_t *dev = (void *)argp;
uint32_t status;
_NOTE(ARGUNUSED(nocare));
status = INL(dev, IPR);
if (dev->suspended) {
OUTL(dev, IPR, status); /* Acknowledge */
return (DDI_INTR_UNCLAIMED);
}
if (!(status & (INTR_IT1 | INTR_PCI))) {
/* No audio interrupts pending */
OUTL(dev, IPR, status); /* Acknowledge */
return (DDI_INTR_UNCLAIMED);
}
check_play_intr(dev);
check_rec_intr(dev);
if (dev->ksp) {
AUDIGYLS_KIOP(dev)->intrs[KSTAT_INTR_HARD]++;
}
OUTL(dev, IPR, status); /* Acknowledge */
return (DDI_INTR_CLAIMED);
}
/*
* Audio routines
*/
int
audigyls_open(void *arg, int flag,
unsigned *fragfrp, unsigned *nfragsp, caddr_t *bufp)
{
audigyls_port_t *port = arg;
audigyls_dev_t *dev = port->dev;
_NOTE(ARGUNUSED(flag));
mutex_enter(&dev->mutex);
port->started = B_FALSE;
port->count = 0;
*fragfrp = port->fragfr;
*nfragsp = AUDIGYLS_NUM_FRAGS;
*bufp = port->buf_kaddr;
audigyls_reset_port(port);
mutex_exit(&dev->mutex);
return (0);
}
void
audigyls_close(void *arg)
{
audigyls_port_t *port = arg;
audigyls_dev_t *dev = port->dev;
mutex_enter(&dev->mutex);
audigyls_stop_port(port);
port->started = B_FALSE;
mutex_exit(&dev->mutex);
}
int
audigyls_start(void *arg)
{
audigyls_port_t *port = arg;
audigyls_dev_t *dev = port->dev;
mutex_enter(&dev->mutex);
if (!port->started) {
audigyls_start_port(port);
port->started = B_TRUE;
}
mutex_exit(&dev->mutex);
return (0);
}
void
audigyls_stop(void *arg)
{
audigyls_port_t *port = arg;
audigyls_dev_t *dev = port->dev;
mutex_enter(&dev->mutex);
if (port->started) {
audigyls_stop_port(port);
port->started = B_FALSE;
}
mutex_exit(&dev->mutex);
}
int
audigyls_format(void *arg)
{
_NOTE(ARGUNUSED(arg));
return (AUDIO_FORMAT_S16_LE);
}
int
audigyls_channels(void *arg)
{
audigyls_port_t *port = arg;
return (port->nchan);
}
int
audigyls_rate(void *arg)
{
_NOTE(ARGUNUSED(arg));
return (48000);
}
void
audigyls_sync(void *arg, unsigned nframes)
{
audigyls_port_t *port = arg;
_NOTE(ARGUNUSED(nframes));
(void) ddi_dma_sync(port->buf_dmah, 0, 0, port->syncdir);
}
size_t
audigyls_qlen(void *arg)
{
_NOTE(ARGUNUSED(arg));
return (0);
}
uint64_t
audigyls_count(void *arg)
{
audigyls_port_t *port = arg;
audigyls_dev_t *dev = port->dev;
uint64_t count;
mutex_enter(&dev->mutex);
if (!dev->suspended)
audigyls_update_port(port);
count = port->count;
mutex_exit(&dev->mutex);
return (count);
}
static void
audigyls_chinfo(void *arg, int chan, unsigned *offset, unsigned *incr)
{
audigyls_port_t *port = arg;
if (port->direction == AUDIGYLS_PLAY_PORT) {
*offset = (port->buf_frames * 2 * (chan / 2)) + (chan % 2);
*incr = 2;
} else {
*offset = chan;
*incr = 2;
}
}
/* private implementation bits */
void
audigyls_start_port(audigyls_port_t *port)
{
audigyls_dev_t *dev = port->dev;
uint32_t tmp;
ASSERT(mutex_owned(&dev->mutex));
if (dev->suspended || port->active)
return;
port->active = B_TRUE;
port->offset = 0;
dev->nactive++;
if (dev->nactive == 1) {
write_reg(dev, IT, dev->timer);
OUTL(dev, IER, INL(dev, IER) | INTR_IT1);
}
switch (port->direction) {
case AUDIGYLS_PLAY_PORT:
tmp = read_reg(dev, SA);
tmp |= SA_SPA(0);
tmp |= SA_SPA(1);
tmp |= SA_SPA(3);
write_reg(dev, SA, tmp);
break;
case AUDIGYLS_REC_PORT:
tmp = read_reg(dev, SA);
tmp |= SA_SRA(2);
write_reg(dev, SA, tmp);
break;
}
}
void
audigyls_stop_port(audigyls_port_t *port)
{
audigyls_dev_t *dev = port->dev;
uint32_t tmp;
if (dev->suspended || !port->active)
return;
port->active = B_FALSE;
dev->nactive--;
switch (port->direction) {
case AUDIGYLS_PLAY_PORT:
tmp = read_reg(dev, SA);
tmp &= ~SA_SPA(0);
tmp &= ~SA_SPA(1);
tmp &= ~SA_SPA(3);
write_reg(dev, SA, tmp);
break;
case AUDIGYLS_REC_PORT:
tmp = read_reg(dev, SA);
tmp &= ~SA_SRA(2);
write_reg(dev, SA, tmp);
break;
}
if (dev->nactive == 0) {
OUTL(dev, IER, INL(dev, IER) & ~INTR_IT1);
}
}
void
audigyls_reset_port(audigyls_port_t *port)
{
audigyls_dev_t *dev = port->dev;
ASSERT(mutex_owned(&dev->mutex));
if (dev->suspended)
return;
switch (port->direction) {
case AUDIGYLS_PLAY_PORT:
write_chan(dev, PTCA, 0, 0);
write_chan(dev, CPFA, 0, 0);
write_chan(dev, CPCAV, 0, 0);
write_chan(dev, PTCA, 1, 0);
write_chan(dev, CPFA, 1, 0);
write_chan(dev, CPCAV, 1, 0);
write_chan(dev, PTCA, 3, 0);
write_chan(dev, CPFA, 3, 0);
write_chan(dev, CPCAV, 3, 0);
break;
case AUDIGYLS_REC_PORT:
write_chan(dev, CRFA, 2, 0);
write_chan(dev, CRCAV, 2, 0);
break;
}
}
int
audigyls_alloc_port(audigyls_dev_t *dev, int num)
{
audigyls_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;
port->started = B_FALSE;
port->direction = num;
switch (num) {
case AUDIGYLS_REC_PORT:
port->syncdir = DDI_DMA_SYNC_FORKERNEL;
caps = ENGINE_INPUT_CAP;
dir = DDI_DMA_READ;
port->nchan = 2;
break;
case AUDIGYLS_PLAY_PORT:
port->syncdir = DDI_DMA_SYNC_FORDEV;
caps = ENGINE_OUTPUT_CAP;
dir = DDI_DMA_WRITE;
port->nchan = 6;
break;
default:
return (DDI_FAILURE);
}
/* figure out fragment configuration */
port->fragfr = 48000 / dev->intrs;
/* we want to make sure that we are aligning on reasonable chunks */
port->fragfr = (port->fragfr + 63) & ~(63);
/* 16 bit frames */
port->fragsz = port->fragfr * 2 * port->nchan;
port->buf_size = port->fragsz * AUDIGYLS_NUM_FRAGS;
port->buf_frames = port->fragfr * AUDIGYLS_NUM_FRAGS;
/* Alloc 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(&audigyls_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);
}
int
audigyls_setup_intrs(audigyls_dev_t *dev)
{
uint_t ipri;
int actual;
int rv;
ddi_intr_handle_t ih[1];
rv = ddi_intr_alloc(dev->dip, ih, DDI_INTR_TYPE_FIXED,
0, 1, &actual, DDI_INTR_ALLOC_STRICT);
if ((rv != DDI_SUCCESS) || (actual != 1)) {
audio_dev_warn(dev->adev,
"Can't alloc interrupt handle (rv %d actual %d)",
rv, actual);
return (DDI_FAILURE);
}
if (ddi_intr_get_pri(ih[0], &ipri) != DDI_SUCCESS) {
audio_dev_warn(dev->adev, "Can't get interrupt priority");
(void) ddi_intr_free(ih[0]);
return (DDI_FAILURE);
}
if (ddi_intr_add_handler(ih[0], audigyls_intr, dev, NULL) !=
DDI_SUCCESS) {
audio_dev_warn(dev->adev, "Can't add interrupt handler");
(void) ddi_intr_free(ih[0]);
return (DDI_FAILURE);
}
dev->ih = ih[0];
mutex_init(&dev->mutex, NULL, MUTEX_DRIVER, DDI_INTR_PRI(ipri));
mutex_init(&dev->low_mutex, NULL, MUTEX_DRIVER, DDI_INTR_PRI(ipri));
return (DDI_SUCCESS);
}
void
audigyls_del_controls(audigyls_dev_t *dev)
{
for (int i = 0; i < CTL_NUM; i++) {
if (dev->controls[i].ctrl) {
audio_dev_del_control(dev->controls[i].ctrl);
dev->controls[i].ctrl = NULL;
}
}
}
void
audigyls_destroy(audigyls_dev_t *dev)
{
if (dev->ih != NULL) {
(void) ddi_intr_disable(dev->ih);
(void) ddi_intr_remove_handler(dev->ih);
(void) ddi_intr_free(dev->ih);
mutex_destroy(&dev->mutex);
mutex_destroy(&dev->low_mutex);
}
if (dev->ksp) {
kstat_delete(dev->ksp);
}
for (int i = 0; i < AUDIGYLS_NUM_PORT; i++) {
audigyls_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);
}
audigyls_del_controls(dev);
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
audigyls_hwinit(audigyls_dev_t *dev)
{
static unsigned int spi_dac[] = {
0x00ff, 0x02ff, 0x0400, 0x520, 0x0620, 0x08ff, 0x0aff, 0x0cff,
0x0eff, 0x10ff, 0x1200, 0x1400, 0x1800, 0x1aff, 0x1cff,
0x1e00, 0x0530, 0x0602, 0x0622, 0x1400,
};
uint32_t tmp;
int i, tries;
uint32_t paddr;
uint32_t chunksz;
audigyls_port_t *port;
/* Set the orange jack to be analog out or S/PDIF */
select_digital_enable(dev, dev->digital_enable);
/*
* In P17, there's 8 GPIO pins.
* GPIO register: 0x00XXYYZZ
* XX: Configure GPIO to be either GPI (0) or GPO (1).
* YY: GPO values, applicable if the pin is configure to be GPO.
* ZZ: GPI values, applicable if the pin is configure to be GPI.
*
* in SB570, pin 0-4 and 6 is used as GPO and pin 5 and 7 is
* used as GPI.
*
* GPO0:
* 1 ==> Analog output
* 0 ==> Digital output
* GPO1:
* 1 ==> Enable output on card
* 0 ==> Disable output on card
* GPO2:
* 1 ==> Enable Mic Bias and Mic Path
* 0 ==> Disable Mic Bias and Mic Path
* GPO3:
* 1 ==> Disable SPDIF-IO output
* 0 ==> Enable SPDIF-IO output
* GPO4 and GPO6:
* DAC sampling rate selection:
* Not applicable to SB570 since DAC is controlled through SPI
* GPI5:
* 1 ==> Front Panel is not connected
* 0 ==> Front Panel is connected
* GPI7:
* 1 ==> Front Panel Headphone is not connected
* 0 ==> Front Panel Headphone is connected
*/
if (dev->ac97)
OUTL(dev, GPIO, 0x005f03a3);
else {
/* for SBLive 7.1 */
OUTL(dev, GPIO, 0x005f4301);
audigyls_i2c_write(dev, 0x15, 0x2);
tries = 0;
again:
for (i = 0; i < sizeof (spi_dac); i++) {
if (!audigyls_spi_write(dev, spi_dac[i]) &&
tries < 100) {
tries++;
goto again;
}
}
}
OUTL(dev, IER, INTR_PCI);
OUTL(dev, HC, 0x00000009); /* Enable audio, use 48 kHz */
tmp = read_chan(dev, SRCTL, 0);
if (dev->ac97)
tmp |= 0xf0c81000; /* Record src0/src1 from ac97 */
else
tmp |= 0x50c81000; /* Record src0/src1 from I2SIN */
tmp &= ~0x0303c00f; /* Set sample rates to 48 kHz */
write_chan(dev, SRCTL, 0, tmp);
write_reg(dev, HMIXMAP_I2S, 0x76543210); /* Default out route */
write_reg(dev, AUDCTL, 0x0f0f003f); /* Enable all outputs */
/* All audio stopped! */
write_reg(dev, SA, 0);
for (i = 0; i < 4; i++) {
/*
* Reset DMA pointers and counters. Note that we do
* not use scatter/gather.
*/
write_chan(dev, PTBA, i, 0);
write_chan(dev, PTBS, i, 0);
write_chan(dev, PTCA, i, 0);
write_chan(dev, CPFA, i, 0);
write_chan(dev, PFEA, i, 0);
write_chan(dev, CPCAV, i, 0);
write_chan(dev, CRFA, i, 0);
write_chan(dev, CRCAV, i, 0);
}
/*
* The 5.1 play port made up channels 0, 1, and 3. The record
* port is channel 2.
*/
port = dev->port[AUDIGYLS_PLAY_PORT];
paddr = port->buf_paddr;
chunksz = port->buf_frames * 4;
write_chan(dev, PFBA, 0, paddr);
write_chan(dev, PFBS, 0, chunksz << 16);
paddr += chunksz;
write_chan(dev, PFBA, 1, paddr);
write_chan(dev, PFBS, 1, chunksz << 16);
paddr += chunksz;
write_chan(dev, PFBA, 3, paddr);
write_chan(dev, PFBS, 3, chunksz << 16);
/* Record */
port = dev->port[AUDIGYLS_REC_PORT];
paddr = port->buf_paddr;
chunksz = port->buf_frames * 4;
write_chan(dev, RFBA, 2, paddr);
write_chan(dev, RFBS, 2, chunksz << 16);
/* Set sample rates to 48 kHz. */
tmp = read_chan(dev, SRCTL, 0) & ~0x0303c00f;
write_chan(dev, SRCTL, 0, tmp);
write_reg(dev, SCS0, 0x02108004); /* Audio */
write_reg(dev, SCS1, 0x02108004); /* Audio */
write_reg(dev, SCS2, 0x02108004); /* Audio */
write_reg(dev, SCS3, 0x02108004); /* Audio */
}
#define PLAYCTL (AUDIO_CTRL_FLAG_RW | AUDIO_CTRL_FLAG_PLAY)
#define RECCTL (AUDIO_CTRL_FLAG_RW | AUDIO_CTRL_FLAG_REC)
#define MONCTL (AUDIO_CTRL_FLAG_RW | AUDIO_CTRL_FLAG_MONITOR)
#define PCMVOL (PLAYCTL | AUDIO_CTRL_FLAG_PCMVOL)
#define MAINVOL (PLAYCTL | AUDIO_CTRL_FLAG_MAINVOL)
#define RECVOL (RECCTL | AUDIO_CTRL_FLAG_RECVOL)
#define MONVOL (MONCTL | AUDIO_CTRL_FLAG_MONVOL)
#define MASK(nbits) ((1 << (nbits)) - 1)
#define SCALE(val, nbits) \
((uint8_t)((((val) * MASK(nbits)) / 100)) << (8 - (nbits)))
static uint32_t
audigyls_stereo_scale(uint32_t value, uint8_t bits)
{
uint8_t left, right;
uint32_t val;
left = (value >> 8) & 0xff;
right = value & 0xff;
val = (((left * ((1 << bits) - 1) / 100) << 8) |
(right * ((1 << bits) - 1) / 100));
return (val);
}
static void
audigyls_configure_mixer(audigyls_dev_t *dev)
{
unsigned int r, v1, v2;
/* output items */
/* front */
r = 0xffff - audigyls_stereo_scale(dev->controls[CTL_FRONT].val, 8);
r = (r << 16) | r;
write_chan(dev, MIXVOL_I2S, 0, r);
/* surround */
r = 0xffff - audigyls_stereo_scale(dev->controls[CTL_SURROUND].val, 8);
r = (r << 16) | r;
write_chan(dev, MIXVOL_I2S, 3, r);
/* center/lfe */
v1 = 255 - SCALE(dev->controls[CTL_CENTER].val, 8);
v2 = 255 - SCALE(dev->controls[CTL_LFE].val, 8);
r = (v1 << 8) | v2;
r = (r << 16) | r;
write_chan(dev, MIXVOL_I2S, 1, r);
/* spread */
r = dev->controls[CTL_SPREAD].val ? 0x10101010 : 0x76543210;
write_reg(dev, HMIXMAP_I2S, r);
/* input items */
/* recgain */
v1 = dev->controls[CTL_RECORDVOL].val;
if (dev->ac97_recgain && !dev->controls[CTL_LOOP].val) {
/*
* For AC'97, we use the AC'97 record gain, unless we are
* in loopback.
*/
(void) ac97_control_set(dev->ac97_recgain, v1);
write_reg(dev, P17RECVOLL, 0x30303030);
write_reg(dev, P17RECVOLH, 0x30303030);
} else {
/*
* Otherwise we set the P17 gain.
*/
r = 0xffff - audigyls_stereo_scale(v1, 8);
r = r << 16 | r;
write_reg(dev, P17RECVOLL, r);
write_reg(dev, P17RECVOLH, r);
}
/* monitor gain */
if (dev->ac97) {
/* AC'97 monitor gain is done by the AC'97 codec */
write_chan(dev, SRCTL, 1, 0x30303030);
write_reg(dev, SMIXMAP_I2S, 0x10101076);
} else {
/* For non-AC'97 devices, just a single master monitor gain */
r = 255 - SCALE(dev->controls[CTL_MONGAIN].val, 8);
write_chan(dev, SRCTL, 1, 0xffff0000 | r << 8 | r);
if (r != 0xff) {
write_reg(dev, SMIXMAP_I2S, 0x10101076);
} else {
write_reg(dev, SMIXMAP_I2S, 0x10101010);
}
}
/* record source */
if (dev->ac97_recsrc != NULL) {
(void) ac97_control_set(dev->ac97_recsrc,
dev->controls[CTL_RECSRC].val);
v1 = RECSEL_AC97; /* Audigy LS */
} else {
switch (dev->controls[CTL_RECSRC].val) {
case 1:
audigyls_i2c_write(dev, 0x15, 0x2); /* Mic */
OUTL(dev, GPIO, INL(dev, GPIO) | 0x400);
break;
case 2:
audigyls_i2c_write(dev, 0x15, 0x4); /* Line */
OUTL(dev, GPIO, INL(dev, GPIO) & ~0x400);
break;
}
v1 = RECSEL_I2SIN; /* SB 7.1 value */
}
/* If loopback, record what you hear instead */
if (dev->controls[CTL_LOOP].val) {
r = 0;
v1 = RECSEL_I2SOUT;
r |= (v1 << 28) | (v1 << 24) | (v1 << 20) | (v1 << 16) | v2;
} else {
/*
* You'd think this would be the same as the logic
* above, but experience shows that what you need for
* loopback is different. This whole thing looks
* particularly fishy to me. I suspect someone has
* made a mistake somewhere. But I can't seem to
* figure out where it lies.
*/
r = 0xe4;
for (int i = 0; i < 4; i++)
r |= v1 << (16 + i * 3); /* Select input */
}
write_reg(dev, P17RECSEL, r);
}
static int
audigyls_set_control(void *arg, uint64_t val)
{
audigyls_ctrl_t *pc = arg;
audigyls_dev_t *dev = pc->dev;
switch (pc->num) {
case CTL_FRONT:
case CTL_SURROUND:
case CTL_RECORDVOL:
if (((val & 0xff) > 100) ||
(((val & 0xff00) >> 8) > 100) ||
((val & ~0xffff) != 0)) {
return (EINVAL);
}
break;
case CTL_CENTER:
case CTL_LFE:
case CTL_MONGAIN:
if (val > 100) {
return (EINVAL);
}
break;
case CTL_RECSRC:
if (((1U << val) & (dev->recmask)) == 0) {
return (EINVAL);
}
break;
case CTL_SPREAD:
case CTL_LOOP:
switch (val) {
case 0:
case 1:
break;
default:
return (EINVAL);
}
}
mutex_enter(&dev->mutex);
pc->val = val;
if (!dev->suspended) {
audigyls_configure_mixer(dev);
}
mutex_exit(&dev->mutex);
return (0);
}
static int
audigyls_get_control(void *arg, uint64_t *val)
{
audigyls_ctrl_t *pc = arg;
audigyls_dev_t *dev = pc->dev;
mutex_enter(&dev->mutex);
*val = pc->val;
mutex_exit(&dev->mutex);
return (0);
}
static void
audigyls_alloc_ctrl(audigyls_dev_t *dev, uint32_t num, uint64_t val)
{
audio_ctrl_desc_t desc;
audigyls_ctrl_t *pc;
bzero(&desc, sizeof (desc));
pc = &dev->controls[num];
pc->num = num;
pc->dev = dev;
switch (num) {
case CTL_FRONT:
desc.acd_name = AUDIO_CTRL_ID_FRONT;
desc.acd_type = AUDIO_CTRL_TYPE_STEREO;
desc.acd_minvalue = 0;
desc.acd_maxvalue = 100;
desc.acd_flags = MAINVOL;
break;
case CTL_SURROUND:
desc.acd_name = AUDIO_CTRL_ID_SURROUND;
desc.acd_type = AUDIO_CTRL_TYPE_STEREO;
desc.acd_minvalue = 0;
desc.acd_maxvalue = 100;
desc.acd_flags = MAINVOL;
break;
case CTL_CENTER:
desc.acd_name = AUDIO_CTRL_ID_CENTER;
desc.acd_type = AUDIO_CTRL_TYPE_MONO;
desc.acd_minvalue = 0;
desc.acd_maxvalue = 100;
desc.acd_flags = MAINVOL;
break;
case CTL_LFE:
desc.acd_name = AUDIO_CTRL_ID_LFE;
desc.acd_type = AUDIO_CTRL_TYPE_MONO;
desc.acd_minvalue = 0;
desc.acd_maxvalue = 100;
desc.acd_flags = MAINVOL;
break;
case CTL_RECORDVOL:
desc.acd_name = AUDIO_CTRL_ID_RECGAIN;
desc.acd_type = AUDIO_CTRL_TYPE_STEREO;
desc.acd_minvalue = 0;
desc.acd_maxvalue = 100;
desc.acd_flags = RECVOL;
break;
case CTL_RECSRC:
desc.acd_name = AUDIO_CTRL_ID_RECSRC;
desc.acd_type = AUDIO_CTRL_TYPE_ENUM;
desc.acd_flags = RECCTL;
/*
* For AC'97 devices, we want to expose the reasonable
* AC'97 input sources, but suppress the stereomix,
* because we use loopback instead.
*/
if (dev->ac97_recsrc) {
int i, j;
const char *n;
const audio_ctrl_desc_t *adp;
adp = ac97_control_desc(dev->ac97_recsrc);
for (i = 0; i < 64; i++) {
n = adp->acd_enum[i];
if (((adp->acd_minvalue & (1 << i)) == 0) ||
(n == NULL)) {
continue;
}
for (j = 0; audigyls_badsrcs[j]; j++) {
if (strcmp(n, audigyls_badsrcs[j])
== 0) {
n = NULL;
break;
}
}
if (n) {
desc.acd_enum[i] = n;
dev->recmask |= (1 << i);
}
}
desc.acd_minvalue = desc.acd_maxvalue = dev->recmask;
} else {
dev->recmask = 3;
desc.acd_minvalue = 3;
desc.acd_maxvalue = 3;
desc.acd_enum[0] = AUDIO_PORT_MIC;
desc.acd_enum[1] = AUDIO_PORT_LINEIN;
}
break;
case CTL_MONGAIN:
ASSERT(!dev->ac97);
desc.acd_name = AUDIO_CTRL_ID_MONGAIN;
desc.acd_type = AUDIO_CTRL_TYPE_MONO;
desc.acd_minvalue = 0;
desc.acd_maxvalue = 100;
desc.acd_flags = MONVOL;
break;
case CTL_SPREAD:
desc.acd_name = AUDIO_CTRL_ID_SPREAD;
desc.acd_type = AUDIO_CTRL_TYPE_BOOLEAN;
desc.acd_minvalue = 0;
desc.acd_maxvalue = 1;
desc.acd_flags = PLAYCTL;
break;
case CTL_LOOP:
desc.acd_name = AUDIO_CTRL_ID_LOOPBACK;
desc.acd_type = AUDIO_CTRL_TYPE_BOOLEAN;
desc.acd_minvalue = 0;
desc.acd_maxvalue = 1;
desc.acd_flags = RECCTL;
break;
}
pc->val = val;
pc->ctrl = audio_dev_add_control(dev->adev, &desc,
audigyls_get_control, audigyls_set_control, pc);
}
static void
audigyls_add_controls(audigyls_dev_t *dev)
{
(void) audio_dev_add_soft_volume(dev->adev);
audigyls_alloc_ctrl(dev, CTL_FRONT, 75 | (75 << 8));
audigyls_alloc_ctrl(dev, CTL_SURROUND, 75 | (75 << 8));
audigyls_alloc_ctrl(dev, CTL_CENTER, 75);
audigyls_alloc_ctrl(dev, CTL_LFE, 75);
audigyls_alloc_ctrl(dev, CTL_RECORDVOL, 75 | (75 << 8));
audigyls_alloc_ctrl(dev, CTL_RECSRC, 1);
audigyls_alloc_ctrl(dev, CTL_SPREAD, 0);
audigyls_alloc_ctrl(dev, CTL_LOOP, 0);
if (!dev->ac97) {
audigyls_alloc_ctrl(dev, CTL_MONGAIN, 0);
}
}
int
audigyls_attach(dev_info_t *dip)
{
uint16_t pci_command, vendor, device;
uint32_t subdevice;
audigyls_dev_t *dev;
ddi_acc_handle_t pcih;
const char *name, *version;
boolean_t ac97 = B_FALSE;
dev = kmem_zalloc(sizeof (*dev), KM_SLEEP);
dev->dip = dip;
ddi_set_driver_private(dip, dev);
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);
subdevice = pci_config_get16(pcih, PCI_CONF_SUBVENID);
subdevice <<= 16;
subdevice |= pci_config_get16(pcih, PCI_CONF_SUBSYSID);
if (vendor != PCI_VENDOR_ID_CREATIVE ||
device != PCI_DEVICE_ID_CREATIVE_AUDIGYLS) {
audio_dev_warn(dev->adev, "Hardware not recognized "
"(vendor=%x, dev=%x)", vendor, device);
goto error;
}
pci_command = pci_config_get16(pcih, PCI_CONF_COMM);
pci_command |= PCI_COMM_ME | PCI_COMM_IO;
pci_config_put16(pcih, PCI_CONF_COMM, pci_command);
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;
}
/* Function of the orange jack: 0=analog, 1=digital */
dev->digital_enable = ddi_prop_get_int(DDI_DEV_T_ANY, dev->dip,
DDI_PROP_DONTPASS, "digital-enable", 0);
if (audigyls_setup_intrs(dev) != DDI_SUCCESS)
goto error;
dev->intrs = ddi_prop_get_int(DDI_DEV_T_ANY, dev->dip,
DDI_PROP_DONTPASS, "interrupt-rate", AUDIGYLS_INTRS);
/* make sure the values are good */
if (dev->intrs < AUDIGYLS_MIN_INTRS) {
audio_dev_warn(dev->adev,
"interrupt-rate too low, %d, reset to %d",
dev->intrs, AUDIGYLS_INTRS);
dev->intrs = AUDIGYLS_INTRS;
} else if (dev->intrs > AUDIGYLS_MAX_INTRS) {
audio_dev_warn(dev->adev,
"interrupt-rate too high, %d, reset to %d",
dev->intrs, AUDIGYLS_INTRS);
dev->intrs = AUDIGYLS_INTRS;
}
dev->timer = (192000 / dev->intrs) << 16;
switch (subdevice) {
case 0x11021001: /* SB0310 */
case 0x11021002: /* SB0310 */
case 0x11021005: /* SB0310b */
name = "Creative Audigy LS";
version = "SB0310"; /* could also be SB0312 */
ac97 = B_TRUE;
break;
case 0x11021006:
name = "Creative Sound Blaster Live! 24 bit";
version = "SB0410";
break;
case 0x11021007: /* Dell OEM version */
name = "Creative Sound Blaster Live! 24 bit";
version = "SB0413";
break;
case 0x1102100a:
name = "Creative Audigy SE";
version = "SB0570";
break;
case 0x11021011:
name = "Creative Audigy SE OEM";
version = "SB0570a";
break;
case 0x11021012:
name = "Creative X-Fi Extreme Audio";
version = "SB0790";
break;
case 0x14621009:
name = "MSI K8N Diamond MB";
version = "SB0438";
break;
case 0x12973038:
name = "Shuttle XPC SD31P";
version = "SD31P";
break;
case 0x12973041:
name = "Shuttle XPC SD11G5";
version = "SD11G5";
break;
default:
name = "Creative Audigy LS";
version = NULL;
break;
}
audio_dev_set_description(dev->adev, name);
if (version)
audio_dev_set_version(dev->adev, version);
if (ac97) {
ac97_ctrl_t *ctrl;
/* Original Audigy LS revision (AC97 based) */
dev->ac97 = ac97_allocate(dev->adev, dip,
audigyls_read_ac97, audigyls_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; audigyls_remove_ac97[i]; i++) {
ctrl = ac97_control_find(dev->ac97,
audigyls_remove_ac97[i]);
if (ctrl != NULL) {
ac97_control_unregister(ctrl);
}
}
dev->ac97_recgain = ac97_control_find(dev->ac97,
AUDIO_CTRL_ID_RECGAIN);
dev->ac97_recsrc = ac97_control_find(dev->ac97,
AUDIO_CTRL_ID_RECSRC);
}
audigyls_add_controls(dev);
if (dev->ac97) {
ac97_register_controls(dev->ac97);
}
if (audigyls_alloc_port(dev, AUDIGYLS_PLAY_PORT) != DDI_SUCCESS)
goto error;
if (audigyls_alloc_port(dev, AUDIGYLS_REC_PORT) != DDI_SUCCESS)
goto error;
audigyls_hwinit(dev);
audigyls_configure_mixer(dev);
/* set up kernel statistics */
if ((dev->ksp = kstat_create(AUDIGYLS_NAME, ddi_get_instance(dip),
AUDIGYLS_NAME, "controller", KSTAT_TYPE_INTR, 1,
KSTAT_FLAG_PERSISTENT)) != NULL) {
kstat_install(dev->ksp);
}
if (audio_dev_register(dev->adev) != DDI_SUCCESS) {
audio_dev_warn(dev->adev, "unable to register with framework");
goto error;
}
(void) ddi_intr_enable(dev->ih);
ddi_report_dev(dip);
return (DDI_SUCCESS);
error:
audigyls_destroy(dev);
return (DDI_FAILURE);
}
int
audigyls_resume(dev_info_t *dip)
{
audigyls_dev_t *dev;
audigyls_port_t *port;
dev = ddi_get_driver_private(dip);
for (int i = 0; i < AUDIGYLS_NUM_PORT; i++) {
port = dev->port[i];
audio_engine_reset(port->engine);
}
audigyls_hwinit(dev);
/* allow ac97 operations again */
if (dev->ac97)
ac97_resume(dev->ac97);
audigyls_configure_mixer(dev);
mutex_enter(&dev->mutex);
dev->suspended = B_FALSE;
for (int i = 0; i < AUDIGYLS_NUM_PORT; i++) {
port = dev->port[i];
audigyls_reset_port(port);
if (port->started) {
audigyls_start_port(port);
}
}
mutex_exit(&dev->mutex);
return (DDI_SUCCESS);
}
int
audigyls_detach(audigyls_dev_t *dev)
{
if (audio_dev_unregister(dev->adev) != DDI_SUCCESS)
return (DDI_FAILURE);
audigyls_destroy(dev);
return (DDI_SUCCESS);
}
int
audigyls_suspend(audigyls_dev_t *dev)
{
if (dev->ac97)
ac97_suspend(dev->ac97);
mutex_enter(&dev->mutex);
for (int i = 0; i < AUDIGYLS_NUM_PORT; i++) {
audigyls_port_t *port = dev->port[i];
audigyls_stop_port(port);
}
dev->suspended = B_TRUE;
mutex_exit(&dev->mutex);
return (DDI_SUCCESS);
}
static int audigyls_ddi_attach(dev_info_t *, ddi_attach_cmd_t);
static int audigyls_ddi_detach(dev_info_t *, ddi_detach_cmd_t);
static int audigyls_ddi_quiesce(dev_info_t *);
static struct dev_ops audigyls_dev_ops = {
DEVO_REV, /* rev */
0, /* refcnt */
NULL, /* getinfo */
nulldev, /* identify */
nulldev, /* probe */
audigyls_ddi_attach, /* attach */
audigyls_ddi_detach, /* detach */
nodev, /* reset */
NULL, /* cb_ops */
NULL, /* bus_ops */
NULL, /* power */
audigyls_ddi_quiesce, /* quiesce */
};
static struct modldrv audigyls_modldrv = {
&mod_driverops, /* drv_modops */
"Creative Audigy LS Audio", /* linkinfo */
&audigyls_dev_ops, /* dev_ops */
};
static struct modlinkage modlinkage = {
MODREV_1,
{ &audigyls_modldrv, NULL }
};
int
_init(void)
{
int rv;
audio_init_ops(&audigyls_dev_ops, AUDIGYLS_NAME);
if ((rv = mod_install(&modlinkage)) != 0) {
audio_fini_ops(&audigyls_dev_ops);
}
return (rv);
}
int
_fini(void)
{
int rv;
if ((rv = mod_remove(&modlinkage)) == 0) {
audio_fini_ops(&audigyls_dev_ops);
}
return (rv);
}
int
_info(struct modinfo *modinfop)
{
return (mod_info(&modlinkage, modinfop));
}
int
audigyls_ddi_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
switch (cmd) {
case DDI_ATTACH:
return (audigyls_attach(dip));
case DDI_RESUME:
return (audigyls_resume(dip));
default:
return (DDI_FAILURE);
}
}
int
audigyls_ddi_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
audigyls_dev_t *dev;
dev = ddi_get_driver_private(dip);
switch (cmd) {
case DDI_DETACH:
return (audigyls_detach(dev));
case DDI_SUSPEND:
return (audigyls_suspend(dev));
default:
return (DDI_FAILURE);
}
}
int
audigyls_ddi_quiesce(dev_info_t *dip)
{
audigyls_dev_t *dev;
uint32_t status;
dev = ddi_get_driver_private(dip);
for (int i = 0; i < AUDIGYLS_NUM_PORT; i++) {
audigyls_port_t *port = dev->port[i];
audigyls_stop_port(port);
}
/*
* Turn off the hardware
*/
write_reg(dev, SA, 0);
OUTL(dev, IER, 0); /* Interrupt disable */
write_reg(dev, AIE, 0); /* Disable audio interrupts */
status = INL(dev, IPR);
OUTL(dev, IPR, status); /* Acknowledge */
return (DDI_SUCCESS);
}